Go 语言语法速查表 - Go 常用命令与并发编程大全
这套速查给用 Go 写接口服务、并发 worker 或运维工具的开发者,聚合最常见的语法骨架:变量声明、循环与分支、函数与 defer、结构体方法、接口,以及最容易写错的 goroutine/channel 并发。与单纯贴 API 文档不同,条目按"片断解决什么问题"给出可直接抄的写法。读完应能独立写出 goroutine + WaitGroup 的并发代码、用接口做依赖抽象,并按 Go 惯例用 err != nil 显式检查错误。
典型使用场景
面向用 Go 写后端服务与并发任务的工程师。Go 的四个必掌握点在本表都有对应分组:值类型与引用类型(map 取不存在键返回零值,这是 Go 最常见的逻辑 bug 来源)、显式错误处理(每个返回 error 的调用都要检查)、接口隐式实现(不需要 implements 关键字)、以及 goroutine 与 channel 的并发模型。本表按这四块加结构体方法组织,示例均可直接编译对照。
变量与常量 Variables & Constants 7
var name string = "value"name := "value"const Pi = 3.14var (name string; age int)var arr [5]intslice := []int{1, 2, 3}m := map[string]int{"a": 1}控制流 Control Flow 8
if condition { } else { }if err := fn(); err != nil { }switch x { case 1: case 2: default: }for i := 0; i < 10; i++ { }for condition { }for { }for i, v := range slice { }for k, v := range map { }函数 Functions 6
func add(a, b int) int { return a + b }func swap(a, b int) (int, int)func sum(nums ...int) intdefer fn()panic("error")recover()结构体与方法 Struct & Method 5
type User struct { Name string; Age int }u := User{Name: "tom", Age: 20}func (u User) String() stringfunc (u *User) SetName(n string)u := &User{}接口 Interface 5
type Reader interface { Read(p []byte) (int, error) }var r Reader = &File{}type Empty interface{}v, ok := i.(string)switch v := i.(type) { }并发 Concurrency 8
go fn()ch := make(chan int)ch := make(chan int, 10)ch <- valuevalue := <-chclose(ch)select { case <-ch1: case <-ch2: }var wg sync.WaitGroup错误处理 Error Handling 5
if err != nil { return err }type MyError struct { msg string }func (e *MyError) Error() stringerrors.New("message")fmt.Errorf("failed: %w", err)参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
:= | 短变量声明,类型由右侧推断(仅函数内) | name := "value" |
const | 编译期常量,仅限基本类型 | const Pi = 3.14 |
slice := []int{} | 切片,可变长,append 可能换底层数组 | slice := []int{1, 2, 3} |
v, ok := m[k] | 判断 map 中键是否存在 | v, ok := m["a"] |
defer | 函数退出时执行,参数此时求值 | defer f.Close() |
指针接收者 | 可修改原实例字段,也避免拷贝大结构体 | func (u *User) SetName(n string) |
值接收者 | 操作副本,改动不影响原实例 | func (u User) String() string |
v, ok := i.(string) | 类型断言(安全形式) | switch v := i.(type) {} |
interface{} | 空接口,可存任意类型 | var v interface{} |
go fn() | 启动 goroutine,调用方不等待 | go process() |
make(chan int, n) | 有缓冲 channel,容量未满不阻塞 | ch := make(chan int, 10) |
close(ch) | 发送方关闭,接收方遍历可据此退出 | close(ch) |
select | 同时等待多个 channel | select { case <-done: } |
fmt.Errorf %w | 包装错误并支持 errors.Is/As | fmt.Errorf("failed: %w", err) |
易错点与避坑指南
现象遍历 map 顺序每次都不一样,测试偶发失败。
原因Go 故意随机化 map 遍历顺序,不保证稳定。
处置需要有序输出时先把 key 收集到切片排序后再遍历。
现象写并发安全代码时出现数据竞争,go run -race 才报出来。
原因多个 goroutine 共享同一变量而无同步。
处置用 channel 传递数据,或用 sync.WaitGroup 等待收尾;提交前用 -race 跑一遍。
现象向已关闭的 channel 发送导致 panic。
原因channel 只能由发送方关闭一次,关闭后再发送会崩溃。
处置约定由发送方 close;多发送方场景用 sync.Once 保证只关一次。
现象goroutine 里的 panic 导致整个程序崩溃,无法被调用方感知。
原因没有在 goroutine 内部 recover。
处置在 goroutine 内的 defer 中调用 recover(),把 panic 转成日志或返回值。
现象map 读取不存在的键得到 0,被当成真实的零值继续计算。
原因Go 的 map 读取缺失键直接返回零值,不报错。
处置一律用 v, ok := m[k] 两值形式判断存在性,不要靠值是否为零判断。
现象用值接收者的方法修改字段,调用后原对象没变。
原因值接收者拿到的是副本,改动只作用于副本。
处置需要修改状态的方法改用指针接收者 (u *User);同一类型的接收者应保持一致。
现象defer 里的 os.Open 结果为 nil 时 Close 报错或掩盖真实错误。
原因Open 失败时返回 nil,defer 仍执行 Close。
处置先判断 err != nil 再返回,之后才注册 defer;或写成 if f != nil { defer f.Close() }。
现象循环里用 defer 导致文件直到函数结束才关闭。
原因defer 注册在函数退出时执行,不是循环迭代结束时。
处置把单次处理抽成独立函数,在该函数内 defer Close。
排障路径
1查数据竞争
go run -race main.gorace detector 能定位并发读写同一变量的位置,提交前必跑。
2确认错误被正确包装
fmt.Errorf("failed: %w", err)用 %w 包装,配合 errors.Is / errors.As 才能沿调用链判断。
3判断 map 中键是否存在
v, ok := m[k]不要用 v == 0 判断缺失,因为零值可能是合法数据。
4遍历 map 前先排序保证输出稳定
for _, k := range keys { v := m[k] }Go 的 map 顺序随机,测试与日志输出都需要显式排序。
5goroutine 内兜住 panic
go func() { defer func() { recover() }(); work() }()goroutine 内的 panic 不会被调用方 recover,必须就地处理。
命令示例
标准错误链:包装 + 判定 + recover 兜底
func load(id string) error {
if id == "" {
return fmt.Errorf("load: %w", errors.New("empty id"))
}
return nil
}
func handle() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
return load("")
}fmt.Errorf 的 %w 把底层错误包进链里,上层可以用 errors.Is/As 逐层判定;defer 里必须给返回值命名,才能在 recover 后替换要返回的 error。
并发写库:WaitGroup 等待 + 错误汇总
var wg sync.WaitGroup
errCh := make(chan error, 3)
for _, id := range ids {
wg.Add(1)
go func(id string) {
defer wg.Done()
if err := save(id); err != nil {
errCh <- fmt.Errorf("save %s: %w", id, err)
}
}(id)
}
wg.Wait()
close(errCh)
for err := range errCh {
return err
}Go 1.22 之前的循环变量是同一个,goroutine 里直接闭包捕获 id 会全部拿到最后一个值,必须把 id 作为参数传进去。errCh 必须有缓冲,否则全部 goroutine 都在等它、wg.Wait 永远不返回。
channel 关闭 + select 超时/多路复用
go func() {
defer close(ch)
for _, v := range items {
ch <- v
}
}()
for {
select {
case v, ok := <-ch:
if !ok {
return
}
use(v)
case <-time.After(2 * time.Second):
return errors.New("consume timeout")
}
}发送方负责 close,接收方用 v, ok := <-ch 判断是否排空;select 的多个 case 同时就绪时随机选一个,所以关闭信号要和数据竞争同一条 channel。
结构体 + 指针接收者 + 接口实现与类型断言
type User struct {
Name string
age int
}
func (u *User) SetAge(a int) { u.age = a }
type Reader interface {
Read(p []byte) (int, error)
}
var r Reader = &User{}
switch v := r.(type) {
case *User:
v.SetAge(18)
}值接收者改字段只改副本,要真正写入必须用指针接收者(并且通过指针调用)。接口实现是隐式的,任何满足方法集的指针类型都直接赋给 Reader。
常见坑与注意事项
- 向 nil map 写入直接 panic(assignment to entry in nil map):var m map[string]int 只可用于读取,声明处就得 make 或初始化字面量,否则运行到那一行才崩。
- defer 绑定的是函数不是循环:写在 for 循环体里的 defer 要等整个函数返回才执行,循环一万次就会攒一万个延迟调用(文件句柄/锁耗尽),需要即时释放应封装成小函数调用。
- goroutine 泄漏最难排查:向无缓冲 channel 发送后再无人接收,或 select 少了一个退出分支,goroutine 会永久阻塞且不报错,表现为内存单调上涨。
- 对已 close 的 channel 发送会 panic,重复 close 也会 panic;关闭责任必须唯一地交给发送方,否则典型报错是 send on closed channel。
- 把带类型的 nil 指针赋给接口后,接口本身不等于 nil:err != nil 仍为 true,这个陷阱会让错误被误判为成功,且没有任何编译期提示。
- append 与切片共享底层数组:sub := s[1:3] 之后对 sub 的 append 可能改到原数组尾部,超过容量时又悄悄换新数组,同一份代码加个元素就出现难查的偶发数据错乱。
提示
- Go 的错误处理是显式的,每个可能返回 error 的函数都要检查 err != nil。
- goroutine 非常轻量(几 KB),可以轻松创建数十万个。
- channel 是 goroutine 之间通信的主要方式,遵循 "不要通过共享内存通信,而要通过通信共享内存"。
常见问题
:= 和 var 有什么区别?
:= 是短变量声明,由右侧表达式自动推断类型,只能在函数内部使用;var 需要显式写类型,且可用于包级作用域。实际写业务代码优先用 :=,因为类型由编译器推导不易出错;var 更适合声明零值或类型无法从右侧推断的情况。另外 := 至少要有一个左侧变量是新的,否则就是编译错误。
值接收者和指针接收者的方法有什么区别?
值接收者(func (u User) String())操作的是结构体的副本,方法内改动字段不影响原实例;指针接收者(func (u *User) SetName(n))直接修改原实例,也能避免大结构体的拷贝开销。经验法则是:涉及修改就用指针接收者,只做格式化输出的 String 这类纯读方法用值接收者。同一个类型不要混用两种,否则接口实现会出现意外。
无缓冲 channel 和有缓冲 channel 有什么不同?
无缓冲 channel 的发送方会一直阻塞,直到有接收方取走,天然完成收发双方同步;make(chan int, 10) 创建的带缓冲 channel 在容量未满时发送不阻塞,能解耦生产与消费节奏。前者适合明确的一对一交接,后者适合生产者快于消费者的流水线。往已关闭的 channel 发送会 panic,接收方则会在排空后得到零值。
defer 的执行顺序是怎样的?
defer 把它挂到当前函数上,在函数返回前按后进先出(LIFO)的顺序执行,与代码书写顺序相反。所以在一段函数里先注册的 defer 会最后运行,常用于打开文件后紧跟注册的关闭操作。反过来,如果你希望清理最先执行,就要把它注册在其他 defer 之前。
error 应该怎么处理?
Go 的惯例是错误作为返回值而非抛出:if err := fn(); err != nil { return err }。需要保留原始错误链时用 fmt.Errorf("failed: %w", err) 包装,调用方就能用 errors.Is 和 errors.As 判断错误类型或取出结构化信息。只有真正无法恢复的编程错误才用 panic。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。