附录 A.Go 语法与惯用法速查

本附录把全书用到的 Go 语法与惯用法压缩成一组速查表:类型与零值、变量与常量、控制流、函数与 defer、切片与 map、字符串与 rune、结构体与方法、接口、错误处理、泛型与并发原语,每段片段都按「想做什么 → 怎么写」组织,并在 go1.27.0 上实测输出。

前面 18 章把「为什么这样设计」讲透了,这一页只做一件事:把写代码时真正会去翻的语法与惯用法压成表。建议第一次通读建立索引感,之后把它当案头卡片——写的时候不必回忆章节号,先在这里对号入座,再回正文看完整推导。

本附录所有代码片段都在本机 go1.27.0 上实际运行过,输出为真实结果;标注了版本下限的语法低于该版本会直接报错。

类型与零值

类型零值备注
数值(int / float64 等)0整数与浮点都是 0
string""空串,不是 nil
boolfalse
指针 / 接口 / map / chan / funcnil用前要判断
切片 []Tnilnil 切片可直接 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.Contains1.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) []U1.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.WaitGroupAdd / 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。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 《Go 语言编程实战》目录
  2. 《Go 语言编程实战》18.3 上线、观测与迭代
  3. 《Go 语言编程实战》18.2 故障演练