Go 的设计哲学一句话可以概括:少即是多。没有继承、没有泛型滥用(直到 1.18 才谨慎引入)、没有异常机制,但用 goroutine 和 channel 把并发做成了语言级别的第一公民。这篇文章面向有编程基础的读者,用最短的路径带你从语法走到并发。
安装与第一个程序
# 下载安装:https://go.dev/dl/ (Windows/macOS/Linux 都有安装包)
go version # 确认安装成功,如 go version go1.23.0 windows/amd64
mkdir hello && cd hello
go mod init example.com/hello # 初始化模块
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
go run . # 直接运行
go build -o hello . # 编译成二进制(无运行时依赖,直接丢服务器就能跑)
「编译出静态单文件二进制」是 Go 在部署上最大的卖点之一——不需要目标机器装任何运行时。
包与模块
go.mod:模块定义文件,记录模块名和依赖版本;- 大写开头 = 导出:语言层面用大小写控制可见性,不需要 public/private 关键字;
go mod tidy:整理依赖(增删 + 校验),提交代码前必跑。
// 目录即包:util/strings.go 的包名是 util
package util
// Exported:大写开头,包外可访问
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
// unexported:小写开头,仅包内可见
func normalize(s string) string { return s }
变量、类型与「零值」哲学
var a int // 零值 0
var s string // 零值 ""(不是 nil,Go 里字符串永远有值)
var p *int // 零值 nil
// 短变量声明(函数内使用,最常用)
count := 0
name := "Fadey"
// 多重赋值与交换
x, y := 1, 2
x, y = y, x
// 常量与 iota 枚举
type Status int
const (
StatusTodo Status = iota // 0
StatusDoing // 1
StatusDone // 2
)
零值可用是 Go 的一个核心设计:var buf bytes.Buffer 拿来就能用,不需要构造函数。写类型时尽量让自己的结构体也满足这个性质,调用方会感谢你。
切片、map 与遍历
// 切片:长度可变的数组视图(底层 = 指针 + 长度 + 容量)
nums := []int{1, 2, 3}
nums = append(nums, 4) // 容量不够时自动扩容
sub := nums[1:3] // [2 3],共享底层数组要小心
// map:键值对
scores := map[string]int{"alice": 90}
scores["bob"] = 85
v, ok := scores["carol"] // 逗号-ok 惯用法:判存在
if !ok {
fmt.Println("没找到 carol") // v 是 int 零值 0
}
for i, v := range nums { // 需要索引用 i,不需要用 _
fmt.Println(i, v)
}
for k := range scores { // map 遍历顺序是随机的(故意的)
fmt.Println(k)
}
注意 range 的经典坑:循环变量在 Go 1.22 之前是共享的,闭包里捕获会拿到最后一个值;新版本每轮迭代都是新变量,但这个坑在旧代码里仍然遍地都是。
函数、方法与接口
// 多返回值:错误作为第二个返回值是铁律
func div(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为 0")
}
return a / b, nil
}
// 值接收者 vs 指针接收者:需要修改状态用指针
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
func (c Counter) Value() int { return c.n }
// 接口:隐式实现——不需要 implements,方法集对上了就成立
type Stringer interface {
String() string
}
type User struct{ Name string }
func (u User) String() string { return "User(" + u.Name + ")" }
func printAll(items []Stringer) {
for _, it := range items {
fmt.Println(it.String())
}
}
func main() {
c := &Counter{}
c.Inc()
fmt.Println(c.Value()) // 1
printAll([]Stringer{User{Name: "Fadey"}}) // 无需显式声明 implements
}
隐式实现让接口可以「用完才定义」:你先写库,用户后来按需定义小接口(io.Reader 只有一个方法),组合自由度和测试易用性都极高。
错误处理:没有异常,只有 error
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found") // 哨兵错误:供 errors.Is 判断
func findUser(id int) (*User, error) {
if id <= 0 {
// %w:包装错误并保留错误链,调用方仍能用 errors.Is/As 判断
return nil, fmt.Errorf("findUser(id=%d): %w", id, ErrNotFound)
}
return &User{Name: "Fadey"}, nil
}
func main() {
_, err := findUser(-1)
if err != nil {
if errors.Is(err, ErrNotFound) { // 沿错误链匹配
fmt.Println("按业务处理:返回空页面")
} else {
fmt.Println("未知错误,上报:", err)
}
}
}
三件套记住就够:errors.New 造哨兵错误、fmt.Errorf("%w") 包装、errors.Is / errors.As 判断。不要用字符串比较错误,那是最容易在重构中碎掉的写法。
并发:goroutine 与 channel
go 关键字启动一个 goroutine(几 KB 栈、由运行时调度),channel 是它们之间通信的管道。格言:不要通过共享内存来通信,而要通过通信来共享内存。
import (
"fmt"
"sync"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs { // 通道关闭后循环自动结束
time.Sleep(100 * time.Millisecond) // 模拟耗时任务
results <- j * 2
}
}
func main() {
jobs := make(chan int, 10) // 带缓冲:发送方不被立刻阻塞
results := make(chan int, 10)
var wg sync.WaitGroup
for w := 1; w <= 3; w++ { // 3 个 worker 并发消费
wg.Add(1)
go worker(w, jobs, results, &wg)
}
for i := 1; i <= 6; i++ {
jobs <- i
}
close(jobs) // 关闭后 range 结束、worker 退出
wg.Wait() // 等全部 worker 完成
close(results)
for r := range results {
fmt.Println("结果:", r)
}
}
select 与 context:并发的控制面
import (
"context"
"fmt"
"net/http"
"time"
)
// select:等待多个通道中的任意一个就绪(哪个先来走哪个)
func or(chans ...chan string) <-chan string {
out := make(chan string, 1)
go func() {
for _, c := range chans {
select {
case v := <-c:
out <- v
}
}
}()
return out
}
// context:取消信号沿调用链传播,超时/关停的标准做法
func fetchWithTimeout(ctx context.Context, url string) (string, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel() // 必须调用,否则泄漏定时器
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("请求 %s: %w", url, err) // ctx 超时会体现在 err 里
}
defer resp.Body.Close()
return resp.Status, nil
}
工程上的铁律:每个耗时函数第一个参数是 ctx context.Context,取消和超时才能贯穿整条调用链;goroutine 之间用 defer + WaitGroup 管理生命周期,不要「发射后不管」。
值得先学的标准库
net/http:标准库自带生产级 HTTP 服务器,80 行能写一个 JSON API;encoding/json:结构体标签(`json:"name"`)做序列化,零依赖;log/slog(1.21+):结构化日志,直接输出 JSON 给日志系统;testing:go test ./...内置测试与基准,不需要第三方框架起步;database/sql+ 驱动:连接池、预处理、事务的标准接口。
Go 用「显式」换掉了很多语言的「聪明」:错误显式返回、并发显式通信、依赖显式声明。第一周嫌啰嗦,第三个月会感谢它。
总结
这次入门路线覆盖了 Go 的骨架:模块与包 → 变量类型零值 → 切片 map → 方法接口 → error 三件套 → goroutine/channel/select/context。建议的下一步:用 net/http + database/sql 写一个带增删改查的小服务,再用 go test 补上测试——写完这一个项目,Go 的思维方式就基本成型了。