前面 18 章把「为什么这样设计」讲透了,这一页只做一件事:把写代码时真正会去翻的语法与惯用法压成表。建议第一次通读建立索引感,之后把它当案头卡片——写的时候不必回忆章节号,先在这里对号入座,再回正文看完整推导。
本附录所有代码片段都在本机 go1.27.0 上实际运行过,输出为真实结果;标注了版本下限的语法低于该版本会直接报错。
类型与零值
| 类型 | 零值 | 备注 |
|---|---|---|
数值(int / float64 等) | 0 | 整数与浮点都是 0 |
string | "" | 空串,不是 nil |
bool | false | |
指针 / 接口 / map / chan / func | nil | 用前要判断 |
切片 []T | nil | nil 切片可直接 append |
| 结构体 | 各字段取零值 | 零值结构体通常可直接用 |
注意 map 的零值是 nil,向 nil map 写入会 panic;读、len、range 则安全。需要写入时用 make(map[K]V) 或字面量 map[K]V{},详见附录 D。
变量与常量
| 想做什么 | 怎么写 | 备注 |
|---|---|---|
| 声明并推断类型 | x := 1 | 只能用在函数内 |
| 声明指定类型 | var x int = 1 | 可出现在包级 |
| 只声明不给值 | var x int | 取零值 |
| 一次多个 | a, b := 1, "s" | 类型可不同 |
| 常量 / 枚举 | const pi = 3.14 / const ( A = iota; B; C ) | iota 每行自增 |
编译器对「声明了却没用到的局部变量」是报错而非警告;包级变量、常量不受这条限制。
控制流
| 想做什么 | 怎么写 |
|---|---|
| 条件 / 带初始化 | if x > 0 { ... } / if v, ok := m[k]; ok { ... } |
| 三种循环 | for i := 0; i < n; i++ {} / for cond {} / for {} |
| 遍历容器 | for i, v := range xs {} / for k, v := range m {} |
| 多分支 / 无表达式 | switch x { case 1: ... } / switch { case x < 0: ... } |
package main
import "fmt"
const (
StatusTodo = iota
StatusDoing
StatusDone
)
func main() {
var zero []int
b := 2
const pi = 3.14
fmt.Println(zero == nil, b, pi, StatusTodo, StatusDone)
for i := 0; i < 3; i++ {
fmt.Print(i, " ")
}
fmt.Println()
if b%2 == 0 {
fmt.Println("even")
}
}
true 2 3.14 0 2
0 1 2
even
Go 只有一种循环关键字 for,while 与 do-while 都用它表达。switch 默认不贯穿(不需要 break),需要贯穿时显式写 fallthrough。
函数与 defer
| 想做什么 | 怎么写 | 备注 |
|---|---|---|
| 多返回值 | func f() (int, error) | 惯用「值 + error」 |
| 命名返回值 | func f() (n int) | 可裸 return |
| 可变参数 | func f(xs ...int) | xs 是切片 |
| 函数作为值 | var f func(int) int | 一等公民 |
| 延迟执行 | defer f() | 函数返回前逆序执行 |
| 立即求值、延迟执行 | defer trace("x")() | 参数在 defer 处求值 |
package main
import "fmt"
func divmod(a, b int) (q, r int) { return a / b, a % b }
func main() {
q, r := divmod(7, 3)
fmt.Println(q, r)
defer fmt.Println("exit")
fmt.Println("body")
}
defer 的三个要点:注册顺序与执行顺序相反(后进先出);参数在注册时求值;常用来保证资源释放与 recover 捕获 panic。
切片与数组
| 想做什么 | 怎么写 | 备注 |
|---|---|---|
| 数组(定长) | var a [3]int | 长度是类型的一部分 |
| 切片(变长) | xs := []int{1, 2, 3} | 更常用 |
| 预分配容量 | make([]int, 0, 10) | 减少扩容 |
| 追加 | xs = append(xs, 4) | 可能重新分配底层数组 |
| 切片 | xs[1:3] | 前闭后开,共享底层数组 |
| 复制 / 排序 / 包含 | slices.Clone / slices.Sort / slices.Contains | 1.21+ |
map
| 想做什么 | 怎么写 | 备注 |
|---|---|---|
| 创建 | m := map[string]int{} | 也可 make(map[string]int) |
| 读写 | m["k"] = 1 / v := m["k"] | 键不存在返回零值 |
| 判断存在 | v, ok := m["k"] | comma-ok 惯用法 |
| 删除 / 遍历 | delete(m, "k") / for k, v := range m {} | 遍历顺序随机 |
| 取键列表 | slices.Sorted(maps.Keys(m)) | 1.23+ 迭代器 |
package main
import (
"fmt"
"slices"
)
func main() {
xs := append([]int{}, 1, 2, 3)
sub := xs[1:]
sub[0] = 99
m := map[string]int{"a": 1}
v, ok := m["b"]
fmt.Println(xs, slices.Contains(xs, 99), v, ok)
}
[1 99 3] true 0 false
切片与 map 都是引用语义:子切片与原切片共享底层数组,map 传给函数不会复制内容。这是新手最常见的坑之一,详见附录 D。
字符串、rune 与字节
| 想做什么 | 怎么写 | 备注 |
|---|---|---|
| 字节长度 | len(s) | UTF-8 下中文占 3 字节 |
| 字符个数 | len([]rune(s)) | 按码点计 |
| 遍历字符 | for i, r := range s {} | r 是 rune |
| 取第 i 个字符 | []rune(s)[i] | 转成 rune 切片 |
| 拼接 / 大小写 | strings.Join(xs, ",") / strings.ToUpper(s) |
package main
import (
"fmt"
"strings"
)
func main() {
s := "中文abc"
fmt.Println(len(s), len([]rune(s)))
fmt.Println(strings.ToUpper("go"))
}
string 是只读的字节序列,s[i] 取的是字节而不是字符;rune 是 int32 的别名,代表一个 Unicode 码点。对中文按字节切片会切出乱码,见附录 D。
结构体与方法
| 想做什么 | 怎么写 | 备注 |
|---|---|---|
| 定义 | type Task struct { ID int64 } | |
| 字面量 | Task{ID: 1} | 指定字段名更清晰 |
| 指针接收者 | func (t *Task) Complete() | 可修改原对象 |
| 值接收者 | func (t Task) String() string | 只读、改的是副本 |
| 字段标签 | Title string `json:"title"` | 供序列化用 |
| 嵌入 | struct { Base; Extra int } | 组合复用 |
接口
| 想做什么 | 怎么写 | 备注 |
|---|---|---|
| 定义接口 | type Store interface { Get(id int64) (Task, error) } | 只放行为 |
| 隐式实现 | 方法集匹配即实现 | 无需 implements |
| 类型断言 | t, ok := v.(Task) | 带 ok 更安全 |
| 多类型分支 | switch v := x.(type) {} | type switch |
| 空接口 | any(即 interface{}) | 承载任意值 |
package main
import "fmt"
type Task struct {
ID int64
Done bool
}
func (t *Task) Complete() { t.Done = true }
type Animal interface{ Sound() string }
type Dog struct{}
func (Dog) Sound() string { return "woof" }
func main() {
t := Task{ID: 1}
t.Complete()
var a Animal = Dog{}
fmt.Println(t.Done, a.Sound())
}
判断接收者用值还是指针,看一句话:方法需不需要修改接收者。接口是隐式实现的,惯用做法是接受接口、返回结构体。
错误处理
| 想做什么 | 怎么写 | 备注 |
|---|---|---|
| 创建错误 | errors.New("not found") | 哨兵错误 |
| 格式化错误 | fmt.Errorf("load %q: %w", p, err) | %w 保留原因 |
| 判断错误类别 | errors.Is(err, ErrNotFound) | 会沿错误链查找 |
| 取回具体类型 | errors.As(err, &nf) | nf 是 *NotFoundError |
| 解包 / 合并 | errors.Unwrap(err) / errors.Join(e1, e2) |
错误是普通值,用 if err != nil 显式处理;包装用 %w,判定用 errors.Is / errors.As,不要用字符串比较。
泛型
| 想做什么 | 怎么写 | 备注 |
|---|---|---|
| 泛型函数 | func Map[T, U any](xs []T, f func(T) U) []U | 1.18+ |
| 泛型类型 | type Page[T any] struct { Items []T } | |
| 约束 | [T comparable] / [T cmp.Ordered] | 限定可用操作 |
| 实例化 | Page[string]{} | 显式或推断 |
package main
import "fmt"
type Page[T any] struct {
Items []T
Total int
}
func Map[T, U any](xs []T, f func(T) U) []U {
out := make([]U, 0, len(xs))
for _, x := range xs {
out = append(out, f(x))
}
return out
}
func main() {
p := Page[string]{Items: []string{"a", "b"}, Total: 2}
fmt.Println(p.Items, p.Total)
fmt.Println(Map([]int{1, 2, 3}, func(x int) string { return fmt.Sprint(x * 2) }))
}
泛型适合「逻辑相同、类型不同」的容器与算法。能用接口表达清楚的地方,优先用接口;取舍详见 9.3 泛型的取舍 。
并发原语
| 想做什么 | 怎么写 | 备注 |
|---|---|---|
| 起协程 | go f() | 轻量,数百万级 |
| 无缓冲 / 带缓冲通道 | make(chan int) / make(chan int, 10) | 前者收发同步 |
| 关闭通道 | close(ch) | 只由发送方关 |
| 多路等待 | select { case v := <-ch: } | 随机选就绪分支 |
| 等待一组 | var wg sync.WaitGroup | Add / Done / Wait |
| 互斥锁 / 原子计数 | var mu sync.Mutex / var n atomic.Int64 | 保护共享状态 |
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
ch := make(chan int, 3)
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(n int) { defer wg.Done(); ch <- n * n }(i)
}
wg.Wait()
close(ch)
sum := 0
for v := range ch {
sum += v
}
fmt.Println(sum)
}
14
并发安全、-race 与 context 的完整用法见第 10–12 章。一句话记忆:共享可变状态必须加锁或用原子操作,通道适合传递所有权。
小结
- 类型速查先记零值:
map/ 指针 / 接口的零值是nil,切片零值可直接append。 - 变量
:=只能用在函数内,且必须被使用;枚举用iota。 - 控制流只有
for一种循环,switch默认不贯穿。 defer逆序执行、参数在注册时求值,是资源清理与recover的主力。- 切片与
map都是引用语义,range顺序随机;需要独立副本用slices.Clone。 string按字节索引,处理中文要先转[]rune。- 错误是值,用
%w包装、errors.Is/errors.As判定。 - 接口隐式实现,惯用「接受接口、返回结构体」;泛型只用于真正通用的容器与算法。
- 并发三件套:
go起协程、channel传数据、sync保安全,用-race验证。
语法只是工具,真正决定代码质量的,是结构体、接口与错误这三样东西怎么组合。附录 B 会在这份语法表之上,继续整理标准库常用 API。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。