package main 程序入口
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}所有可执行 Go 程序都从 package main 的 func main() 启动。同 package 下的文件可以互相访问小写开头的未导出标识符。
Go (Golang) 速查表,100+ 段地道 Go 代码,语法/goroutine/channel/泛型/错误/标准库。
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}所有可执行 Go 程序都从 package main 的 func main() 启动。同 package 下的文件可以互相访问小写开头的未导出标识符。
import "fmt"
import (
"fmt"
"os"
"strings"
_ "embed" // 仅副作用导入
str "strings" // 别名
)分组用括号包起来。前面写下划线表示只要副作用(注册 driver 之类);左边写名字就是起别名。
var name string = "Lei"
var age int = 30
var ok bool // 零值 false
var xs []int // 零值 nil
var m map[string]int // 零值 nil
var (
host = "localhost"
port = 8080
)var 声明变量,可选初始化。没初始值就用零值:数字 0,字符串空串,bool 是 false,slice/map/指针/接口/chan/func 都是 nil。
name := "Lei" count := 42 ok, err := tryParse(input) // 多返回值 // ❌ 包级别不能用 // := 只能在函数内部用
:= 一步声明 + 赋值,类型自动推断。只能在函数体内用,左边至少要有一个新变量。
const Pi = 3.14159
const Greeting = "hello"
const (
StatusOK = 200
StatusFound = 302
)
const MaxBuf = 1 << 20 // 1 MiBconst 在编译期求值,可以是无类型(上面的 Pi)也可以带类型。无类型常量能自动转成任何兼容的数值类型。
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
)
// 位掩码常用 iota
const (
FlagRead = 1 << iota // 1
FlagWrite // 2
FlagExec // 4
)iota 在每个 const 块起始处归零,每行 +1。配合左移运算符做位掩码枚举特别顺手。
var a int = 42 // 平台位宽 var b int64 = 9_223_372_036_854_775_807 var c uint32 = 4_000_000_000 var d float64 = 3.14 var e bool = true var f string = "中文 ok" var g byte = 'A' // byte = uint8 var h rune = '中' // rune = int32
int 跟平台位宽走(32 位系统是 32 位,64 位系统是 64 位)。要固定位宽就用 intN。byte 等价于 uint8,rune 等价于 int32(一个 Unicode 码点)。
s := "hello 你好"
len(s) // 12 字节数,不是字符数
s[0] // 104 (byte 'h')
// 按 rune 遍历
for i, r := range s {
fmt.Printf("%d=%c ", i, r) // 0=h 1=e ... 6=你 9=好
}
// 转成 []rune 取字符长度
runes := []rune(s)
len(runes) // 8string 是不可变的 UTF-8 字节序列。len(s) 拿到的是字节数,不是字符数。range 遍历给的是 (字节下标, rune) 对。要按字符处理就转 []rune。
var a [3]int // [0, 0, 0]
b := [3]int{1, 2, 3}
c := [...]int{10, 20, 30} // 编译器推 3
d := [3]int{0: 1, 2: 9} // [1, 0, 9]
// 数组是值类型,赋值会复制
e := b // e 是 b 的拷贝
e[0] = 99 // b[0] 不变数组长度是类型的一部分,[3]int 和 [4]int 是两个不同的类型。数组是值类型,赋值会整个复制。日常很少直接用数组,slice 才是常用选择。
s := []int{1, 2, 3}
s = append(s, 4) // [1 2 3 4]
s = append(s, 5, 6, 7) // [1 2 3 4 5 6 7]
// 切片表达式 s[low:high:max]
sub := s[1:4] // [2 3 4]
cap(sub) // 6 共享底层数组
// 三索引切片,限制 cap
safe := s[1:4:4]
cap(safe) // 3slice 是 (指针, len, cap) 的三元组,背后指向一段数组。append 可能扩容,返回值一定要赋回去。三索引切片 s[low:high:max] 可以限定 cap,后面 append 就不会踩到原数据。
s := make([]int, 5) // len=5 cap=5 全是 0 s = make([]int, 0, 100) // len=0 cap=100 预分配 m := make(map[string]int) m2 := make(map[string]int, 1000) // 预估大小,省 rehash ch := make(chan int) // 无缓冲 buf := make(chan int, 10) // 缓冲 10
make 用来初始化 slice、map、chan。slice 的三参数形式 make([]T, len, cap) 预分配 cap,append 不再扩容。map 给 size hint 可以避免反复 rehash。
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src) // n=3, dst=[1 2 3]
// 复制重叠区间也安全
buf := []byte("hello")
copy(buf[1:], buf[:4]) // hhellcopy(dst, src) 复制 min(len(dst), len(src)) 个元素并返回数量。重叠内存也能正确处理,相当于 C 的 memmove。
m := map[string]int{
"alice": 30,
"bob": 25,
}
m["carol"] = 40
// 双返回值判断 key 是否存在
v, ok := m["dave"] // v=0 ok=false
if !ok {
fmt.Println("not found")
}
delete(m, "bob") // 删除 key
len(m) // 2map 取不到 key 时返回零值,用 v, ok := m[k] 才能区分"没这个 key"和"值就是零"。删除用 delete(m, k)。
type User struct {
ID int
Name string
Email string
}
u := User{1, "Lei", "lei@x.com"} // 按位置
u2 := User{ID: 2, Name: "Wang"} // 命名(推荐)
// 匿名 struct
opts := struct {
Host string
Port int
}{Host: "127.0.0.1", Port: 8080}struct 是一组固定的命名字段。建议用字段名初始化,以后加字段不会编译失败。匿名 struct 适合一次性的配置或本地形状。
type User struct {
ID int `json:"id"`
Name string `json:"name" db:"user_name"`
Email string `json:"email,omitempty" validate:"required,email"`
secret string // 小写不导出,json 也忽略
}
// json.Marshal 按 tag 输出
b, _ := json.Marshal(User{ID: 1, Name: "Lei"})
// {"id":1,"name":"Lei"}struct tag 是反引号里的 key:"value" 元数据,encoder、ORM、validator 都靠它。omitempty 让零值字段不出现在 JSON 里。小写字段不导出,encoding/json 直接跳过。
x := 42 p := &x // p 是 *int fmt.Println(*p) // 42 *p = 100 // 通过指针改值 fmt.Println(x) // 100 // new 分配并返回指针 q := new(int) // *int,指向 0 *q = 7
Go 指针很简单:& 取地址,* 解引用。没有指针运算(一刀切掉一大类 bug)。new(T) 分配零值并返回 *T,实际上很少用,更地道的写法是结构体字面量加 &。
i := 42
f := float64(i) // int → float64
u := uint(f) // float64 → uint
s := "hello"
b := []byte(s) // string ↔ []byte
back := string(b)
// rune 切片
rs := []rune("中文") // [20013 25991]Go 没有隐式数值转换,int + float64 直接编译报错。T(v) 显式转。string ↔ []byte 和 string ↔ []rune 最常用。
var i int // 0
var f float64 // 0.0
var s string // ""
var b bool // false
var p *int // nil
var sl []int // nil len=0
var m map[string]int // nil
var fn func() // nil
var ch chan int // nil
var iface interface{} // nil
// struct 的零值是每个字段的零值
var u User // {ID:0 Name:"" Email:""}每个类型都有一个有意义的零值,var 不带初始化就用它。这样就杜绝了"未初始化变量"那一类 bug,但 nil 的 map / chan / func 使用会 panic。
a, b := 1, 2 a, b = b, a // 交换,无需临时变量 x, y, z := 1, "two", 3.0 // 一次声明多个不同类型 // 函数返回值直接解构 q, r := divmod(17, 5)
Go 先把整个右边算完再赋值,所以 a, b = b, a 直接交换不用临时变量。一行可以声明或赋值多个不同类型的变量。
type Celsius float64 // 新类型,方法可挂,需显式转换
type Fahrenheit float64
func (c Celsius) ToF() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
type Byte = uint8 // 别名,完全等同 uint8type T U 定义全新的命名类型,有自己的方法集,跟 U 互转要显式转换。type T = U 是别名,T 和 U 完全等同,主要用于渐进式重构(byte = uint8)。
const Big = 1 << 62 // 无类型,编译期任意精度 const Small = Big >> 60 // 4 var x int64 = Big // 赋给变量时才定型 // var y int8 = Big // ❌ 溢出,编译报错 const Third = 1.0 / 3.0 // 高精度,不是 float64 截断
无类型常量在赋给有类型变量之前保持任意精度,赋值时必须放得下。1<<62 当常量没问题,赋给 int8 就溢出报错。1.0/3.0 这种无类型浮点常量精度比 float64 还高。
grid := make([][]int, 3)
for i := range grid {
grid[i] = make([]int, 4) // 每行单独分配
}
grid[1][2] = 9
// 字面量
m := [][]int{
{1, 2, 3},
{4, 5, 6},
}Go 没有内置二维切片,用切片的切片拼。每行单独分配,所以各行长度可以不同(锯齿)。循环里一行一行分配,或者用嵌套字面量。
a := []int{1, 2}
b := []int{3, 4}
c := append(a, b...) // [1 2 3 4] 展开 b
// 头插(注意会分配新底层)
d := append([]int{0}, a...) // [0 1 2]
// 删除下标 i(不保序,最快)
s := []int{1, 2, 3, 4}
s[2] = s[len(s)-1]
s = s[:len(s)-1] // [1 2 4]append(a, b...) 把切片 b 展开接到 a 后面。头插就是往一个新切片上 append。删除下标 i 又不在乎顺序时,用末尾元素覆盖它再缩短,O(1) 而不是 O(n)。
groups := map[string][]int{}
groups["even"] = append(groups["even"], 2) // nil slice 也能 append
// ❌ map 里的 struct 字段不能直接改
type P struct{ X int }
m := map[string]P{"a": {1}}
// m["a"].X = 2 // 编译报错:不可寻址
// ✅ 取出改回去,或存指针
p := m["a"]; p.X = 2; m["a"] = p往不存在的 map key 上 append 是可以的,因为零值是 nil 切片,append 能处理 nil。但 map 的值不可寻址:m["a"].X = 2 编译不过。取出来改完写回去,或者存 *T。
s := "héllo" // é 是 2 字节
len(s) // 6 字节数
// byte 遍历(下标 0..len-1)
for i := 0; i < len(s); i++ {
_ = s[i] // byte,会拆开多字节字符
}
// rune 遍历(按字符)
for _, r := range s {
fmt.Printf("%c", r) // h é l l o
}s[i] 取到的是 byte,会把多字节 UTF-8 字符拆开。range 遍历字符串解码出 rune(完整字符)。纯 ASCII 才用下标循环,否则用 range 或 []rune。
type ByteSize float64
const (
_ = iota // 跳过 0
KB ByteSize = 1 << (10 * iota) // 1<<10
MB // 1<<20
GB // 1<<30
TB // 1<<40
)
fmt.Println(MB) // 1.048576e+06iota 能驱动整个 const 块里重复的表达式:每行重新求值 1 << (10*iota)。开头用 _ = iota 跳过零值。这是定义 KB/MB/GB 尺寸常量的经典写法。
if x := compute(); x > 10 {
fmt.Println("big:", x)
} else if x > 0 {
fmt.Println("small:", x)
} else {
fmt.Println("zero or negative")
}
// x 出了 if/else 就消失if 可以在条件前声明只在 if/else 链里可见的变量,"算一个值再判断" 的常用写法。条件不需要括号,花括号必须有。
data, err := os.ReadFile("config.yaml")
if err != nil {
return fmt.Errorf("read config: %w", err)
}
// 用 data ...
// 紧贴的初始化 + 判断
if err := db.Ping(); err != nil {
log.Fatal(err)
}Go 最高频的代码片段:每个返回 error 的调用后面立刻检查。用 %w 包一层保留错误链。千万别跳,默默吞错是神秘 bug 的头号来源。
switch day {
case "Mon", "Tue", "Wed", "Thu", "Fri":
fmt.Println("weekday")
case "Sat", "Sun":
fmt.Println("weekend")
default:
fmt.Println("?")
}
// 显式穿透
switch n {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("one or two")
}Go 的 switch 默认不穿透,要穿透必须显式写 fallthrough。case 可以用逗号列多个值。不需要写 break。
switch {
case score >= 90:
grade = "A"
case score >= 80:
grade = "B"
case score >= 60:
grade = "C"
default:
grade = "F"
}不写表达式的 switch 等价于 if-else-if 链,每个 case 都是布尔表达式,自上而下匹配。比一长串 if-else 干净。
func describe(i interface{}) string {
switch v := i.(type) {
case int:
return fmt.Sprintf("int %d", v)
case string:
return fmt.Sprintf("string %q", v)
case []byte:
return fmt.Sprintf("bytes len=%d", len(v))
case nil:
return "nil"
default:
return fmt.Sprintf("unknown %T", v)
}
}switch x.(type) 检查接口值的动态类型,每个 case 里 v 已经是具体类型。从接口边界进入具体类型逻辑时离不开它。
// 三段式 (C 风格)
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// while 风格
i := 0
for i < 10 {
i++
}
// 死循环
for {
if done() { break }
}Go 只有 for 一个循环关键字,覆盖 C 三段式、while 风格、死循环三种用法。没有 do-while。
// slice / array: index + value
for i, v := range []int{10, 20, 30} {
fmt.Println(i, v)
}
// map: key + value (顺序随机)
for k, v := range m {
fmt.Println(k, v)
}
// string: byte index + rune
for i, r := range "中文" {
fmt.Printf("%d=%c\n", i, r)
}
// channel: 收到 close 才停
for v := range ch {
fmt.Println(v)
}range 对 slice、array、string、map、chan 都能用。map 遍历顺序是故意随机的,不要依赖。string 上 range 给的是 (字节下标, rune),不是 (i, byte)。
outer:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i*j > 6 {
break outer // 跳出外层
}
if j == 2 {
continue // 内层下一轮
}
fmt.Println(i, j)
}
}break 和 continue 默认作用于最内层循环。要跳出外层循环就给外层 for 加一个标签,这是 Go 里基本上唯一可接受的 label 用法。
func readConfig(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close() // 函数退出时关,无论哪条路径
data, err := io.ReadAll(f)
if err != nil {
return "", err
}
return string(data), nil
}defer 把一次调用挂到函数返回时执行,panic、return、正常流程都触发。最经典的用法就是开了资源紧跟一个 defer Close。
func main() {
for i := 0; i < 3; i++ {
defer fmt.Println(i) // 打印 2, 1, 0
}
}
// 参数在 defer 那一刻就求值
i := 10
defer fmt.Println(i) // 输出 10
i = 20defer 按 LIFO 顺序执行。关键:参数在 defer 那一行就立刻求值,不是真正调用的时候,所以上面循环打印的是 2、1、0,不是循环结束后的值打三遍。
func mustParse(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic(fmt.Sprintf("mustParse %q: %v", s, err))
}
return n
}
// panic 触发 defer 后再向上传播panic 立刻中断正常流程,执行 defer 链,然后向上抛栈。仅在真的没法继续(不变量被违反、程序员错误)时用,普通错误处理不要用。
func safeCall(f func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
f()
return nil
}recover 只能在 defer 的函数里调用。它返回 panic 传进来的值(没 panic 时返回 nil)。常用于服务端边界,防止一个坏请求把整个进程拖死。
// 几乎只在生成代码里用
for i := 0; i < 10; i++ {
if shouldSkip(i) {
goto next
}
process(i)
next:
}goto 存在主要是为了生成的代码偶尔需要。手写 Go 不要用它,用 break/continue/提前 return 重构。
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("macOS")
case "linux":
fmt.Println("Linux")
default:
fmt.Printf("%s\n", os)
}
// os 只在 switch 内可见和 if 一样,switch 可以在条件前跑一句初始化,作用域限在 switch 内。适合"算一个值再分发"又不想让变量泄漏出去。
func double(n int) (result int) {
defer func() {
result *= 2 // 改最终返回值
}()
result = n + 1
return result // 实际返回 (n+1)*2
}
double(3) // 8defer 的闭包能在 return 设好命名返回值之后、调用方拿到之前读改它们。recover() 往命名 (err error) 返回里塞错误就是靠这个机制。
// ✅ 守卫式:错误先退,主逻辑不缩进
func handle(r *Req) error {
if r == nil {
return errors.New("nil request")
}
if !r.Valid() {
return errors.New("invalid")
}
// 主逻辑在最外层,读起来顺
return process(r)
}地道的 Go 在错误和边界情况上提前 return(守卫子句),让主流程留在最外层缩进。别堆深 if-else 金字塔,扁平才好读。
// Go 1.22+
for i := range 5 {
fmt.Println(i) // 0 1 2 3 4
}
// 不需要下标时
for range 3 {
doOnce()
}Go 1.22 新增对整数 n 的 range,遍历 0..n-1。只要计数时比 for i := 0; i < n; i++ 干净。不需要下标就 for range n 重复 N 次。
panic("string message")
panic(fmt.Errorf("wrapped: %w", err)) // 传 error 最常用
panic(errBadState) // 哨兵 error
// recover 后按类型还原
defer func() {
switch x := recover().(type) {
case error:
log.Println("err panic:", x)
case string:
log.Println("msg panic:", x)
case nil: // 没 panic
}
}()panic 接受任意值,不限字符串。传 error 最实用,因为 recover() 能 type-switch 后接着处理。没有 panic 在传播时 recover() 返回 nil。
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer // 跳到外层下一轮,跳过本行 i 的剩余 j
}
fmt.Println(i, j) // 只打印 j==0
}
}continue 带标签会跳到被标记的外层循环的下一轮,跳过内层循环剩余部分。比带标签的 break 少见,但同样是多层控制流的干净写法。
func add(a, b int) int {
return a + b
}
// 多个相同类型参数可以合并
func clamp(v, lo, hi int) int {
if v < lo { return lo }
if v > hi { return hi }
return v
}参数类型写在名字后面。连续相同类型的参数可以共用一次类型标注。返回值类型写在参数列表后面。
func parseInts(s string) ([]int, error) {
parts := strings.Split(s, ",")
out := make([]int, 0, len(parts))
for _, p := range parts {
n, err := strconv.Atoi(strings.TrimSpace(p))
if err != nil {
return nil, fmt.Errorf("parse %q: %w", p, err)
}
out = append(out, n)
}
return out, nil
}
xs, err := parseInts("1, 2, 3")Go 函数可以返回多个值。可能失败的操作约定返回 (T, error)。要么返回 nil 值 + 实 error,要么返回实值 + nil error,不要两边都非 nil。
func divmod(a, b int) (q, r int, err error) {
if b == 0 {
err = errors.New("divide by zero")
return
}
q = a / b
r = a % b
return
}命名返回值预声明了结果变量(已置零值),可以用裸 return。适合短小的辅助函数,也是 defer + recover 设置 error 时的必备写法。
func sum(xs ...int) int {
total := 0
for _, x := range xs {
total += x
}
return total
}
sum(1, 2, 3) // 6
sum() // 0 允许零个
nums := []int{1, 2, 3}
sum(nums...) // 展开 slice最后一个参数写 ...T,会把多余参数收成 []T。调用时可以传任意个(包括零个),也可以用 slice... 把切片展开传进去。fmt.Printf 就是这么做的。
func counter() func() int {
n := 0
return func() int {
n++
return n
}
}
c := counter()
c() // 1
c() // 2
c() // 3闭包是引用了外层作用域变量的函数值。被捕获的变量活得跟闭包一样久,适合做计数器、备忘缓存、小型状态机。
// func 类型可以赋给变量、参数、字段
type Handler func(req string) string
func apply(h Handler, req string) string {
return h(req)
}
upper := func(s string) string {
return strings.ToUpper(s)
}
apply(upper, "hi") // "HI"func 是一等公民,可以作参数、存变量、做返回值。给签名起一个类型别名让代码更可读。
package config
var Settings = make(map[string]string)
func init() {
Settings["host"] = "localhost"
Settings["port"] = "8080"
// 加载默认配置 / 注册驱动 ...
}每个文件可以有一个或多个 init() 函数。包被 import 时自动跑,跑在包级变量初始化之后、main 之前。常用于注册 driver、设默认值。
_, err := fmt.Println("hi") // 忽略写入字节数
v, _ := strconv.Atoi("42") // 忽略 error (危险,少用)
// 仅副作用导入也用 _
import _ "github.com/lib/pq"_ 把不需要的值丢掉。常见于:忽略某个返回、忽略 range 的下标(for _, v := range xs)、副作用导入。用 _ 忽略 error 要有意识地决定,不要图省事。
type T struct{ n int }
func (t T) Add(x int) int { return t.n + x }
t := T{10}
// 方法值:绑定了接收者
f := t.Add
f(5) // 15
// 方法表达式:接收者变成第一个参数
g := T.Add
g(t, 5) // 15方法值 t.Add 绑定了接收者,得到 func(x int) int。方法表达式 T.Add 不绑定,得到 func(T, int) int,接收者变成第一个参数。写高阶代码时有用。
type Server struct {
port int
tls bool
}
type Option func(*Server)
func WithPort(p int) Option { return func(s *Server) { s.port = p } }
func WithTLS() Option { return func(s *Server) { s.tls = true } }
func New(opts ...Option) *Server {
s := &Server{port: 8080} // 默认值
for _, opt := range opts {
opt(s)
}
return s
}
srv := New(WithPort(9000), WithTLS())函数选项模式让构造函数保持向后兼容:每个选项是一个 func(*T) 改配置。调用方只传想要的,新增选项不会破坏已有调用点。gRPC、zap 等大量库都用它。
func fact(n int) int {
if n <= 1 {
return 1
}
return n * fact(n-1)
}
// 遍历目录树
func walk(dir string) {
entries, _ := os.ReadDir(dir)
for _, e := range entries {
if e.IsDir() {
walk(filepath.Join(dir, e.Name()))
}
}
}Go 没有尾递归优化,太深的递归会爆栈,但 goroutine 栈会动态增长,实际可用深度很高。递归适合树和分治;线性迭代用循环。
func slow() {
defer func(start time.Time) {
log.Printf("slow took %v", time.Since(start))
}(time.Now()) // ⚠ 参数立即求值,记下起点
time.Sleep(100 * time.Millisecond)
}把 time.Now() 作为 defer 的参数,在函数进入时就记下起点;defer 的闭包在退出时打印 time.Since。因为 defer 参数立即求值,无论从哪条路径返回起点都对。
// 想让 defer 看到每轮的值,用参数传进去
for i, name := range files {
defer func(idx int, n string) {
log.Printf("closed %d:%s", idx, n)
}(i, name) // 立即求值,每个 defer 记下当轮值
}把循环变量作为 defer 的参数传进去,每个 defer 调用就捕获了当轮的值(参数立即求值)。1.22 之前这是 defer 里循环变量捕获坑的唯一解法。
type Rectangle struct {
W, H float64
}
// 值接收者
func (r Rectangle) Area() float64 {
return r.W * r.H
}
// 指针接收者
func (r *Rectangle) Scale(k float64) {
r.W *= k
r.H *= k
}
r := Rectangle{3, 4}
r.Area() // 12
r.Scale(2) // r 变成 {6, 8}method 就是带接收者的函数。不修改且 struct 小 → 值接收者;要修改接收者或拷贝代价大 → 指针接收者。
// ✅ 全部用指针接收者
func (u *User) Greet() {}
func (u *User) Rename(s string) { u.Name = s }
// ❌ 混着用,迷惑:哪些方法能改值?
// func (u User) Greet() {}
// func (u *User) Rename(s string) { u.Name = s }同一个类型的方法要统一用值或指针接收者,混用既迷惑读者也容易在接口匹配时出隐蔽 bug。stdlib 习惯:只要有一个方法需要指针,其他全部用指针。
type Stringer interface {
String() string
}
type User struct {
Name string
Age int
}
func (u User) String() string {
return fmt.Sprintf("%s(%d)", u.Name, u.Age)
}
// User 自动满足 Stringer,无需 implements 关键字
var s Stringer = User{"Lei", 30}
fmt.Println(s.String())interface 就是一组方法签名。任何拥有匹配方法的类型都自动满足它,不需要 implements 关键字(结构化类型)。接口定义在"使用方",不是在"类型定义方"。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
// 等价于
// type ReadWriter interface {
// Read(...) ...
// Write(...) ...
// }接口通过嵌入其他接口来组合,方法集是并集。io.ReadWriter、io.ReadCloser、io.ReadWriteCloser 都是 stdlib 的现成例子。
// Go 1.18+ 推荐用 any (别名)
var x any = 42
x = "hello"
x = []int{1, 2, 3}
// 1.18 之前
// var x interface{} = 42
func dump(args ...any) {
for _, a := range args {
fmt.Printf("%T: %v\n", a, a)
}
}interface{}(Go 1.18+ 起的别名 any)能装任何值。少用,一用就失去静态类型检查。fmt.Println、json.Marshal 这种真正需要变参泛型的 API 才用。
var i any = "hello"
s := i.(string) // 不安全:失败 panic
s, ok := i.(string) // 安全:失败 ok=false
if n, ok := i.(int); ok {
fmt.Println("got int:", n)
} else {
fmt.Println("not int")
}i.(T) 把接口值断言成具体类型。单返回值版本失败会 panic;除非你能证明一定不会失败,否则一律用双返回值的 v, ok := i.(T)。
type Engine struct {
Power int
}
func (e Engine) Start() {
fmt.Println("brrr", e.Power, "hp")
}
type Car struct {
Engine // 匿名字段 = embed
Brand string
}
c := Car{Engine{200}, "Honda"}
c.Start() // 直接调 Engine 的方法
c.Power // 直接访问 Engine 的字段Go 没有继承,只有嵌入。匿名字段让外层类型直接拥有内层类型的字段和方法,相当于组合,比继承层级更清晰、更灵活。
// 编译期断言:MyHandler 必须实现 http.Handler
var _ http.Handler = (*MyHandler)(nil)
type MyHandler struct{}
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}惯用写法 var _ Interface = (*T)(nil) 强制让编译器检查 T 是否满足 Interface,缺方法当场报错。常写在声明接口实现的文件末尾。
type Color struct{ R, G, B uint8 }
func (c Color) String() string {
return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
}
c := Color{255, 0, 128}
fmt.Println(c) // #FF0080
fmt.Printf("%v", c) // #FF0080实现 String() string(即 fmt.Stringer 接口),fmt 的 %v / %s 会自动调它。枚举、ID、常打印的值类型很合适。别在 *T 接收者的 String() 里对自己用 %v,会无限递归。
type Speaker interface{ Speak() string }
type Dog struct{}
func (d *Dog) Speak() string { return "woof" } // 指针接收者
var s Speaker = &Dog{} // ✅ 必须取地址
// var s Speaker = Dog{} // ❌ Dog 不满足 Speaker
// 值接收者两种都行
type Cat struct{}
func (c Cat) Speak() string { return "meow" }
var s2 Speaker = Cat{} // ✅
var s3 Speaker = &Cat{} // ✅ 也行方法是指针接收者时,只有 *T 满足接口,值 T 不满足。值接收者方法同时属于 T 和 *T 的方法集。这就是为什么常给接口变量赋 &Type{}。
type Logger interface{ Log(string) }
type Service struct {
Logger // 嵌入接口
name string
}
s := Service{Logger: stdLogger{}, name: "api"}
s.Log("started") // 直接转发给嵌入的 Logger
// 只覆盖部分方法的装饰器套路
type quietService struct{ Service }
func (q quietService) Log(string) {} // 屏蔽日志struct 嵌入接口会提升它的方法,并且可以通过这个字段替换实现。常见装饰器套路:嵌入一个类型只覆盖其中一个方法,其余自动转发。
// 接口现在能列具体类型集(用于约束)
type Ordered interface {
~int | ~int64 | ~float64 | ~string
}
// 既能当约束,也限定底层类型
func Max[T Ordered](a, b T) T {
if a > b {
return a
}
return b
}1.18 起接口可以列类型集(T1 | T2 | ~T3)而不是方法,用作泛型约束。~ 前缀包含底层类型匹配的任意类型。这种接口只能当约束,不能作变量类型。
type Animal interface{ Sound() string }
type Dog struct{}
func (d *Dog) Sound() string { return "woof" }
var d *Dog = nil
var a Animal = d // a 非 nil(类型是 *Dog)
a == nil // false
// a.Sound() // 这里才真 nil 解引用 panic把类型化的 nil 指针赋给接口,得到的是非 nil 接口(带着 *Dog 类型)。a == nil 为 false,但调用解引用接收者的方法仍会 nil 指针 panic。这是著名 nil 接口坑的根因。
go fmt.Println("hi from goroutine")
go func() {
time.Sleep(100 * time.Millisecond)
fmt.Println("delayed")
}()
// main 返回会杀掉所有 goroutine
time.Sleep(200 * time.Millisecond)go 关键字启动一个 goroutine,Go 运行时管理的轻量线程,初始栈约 2KB,开几千个没问题。main 返回会杀掉所有 goroutine。
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done() // 退出时 -1
process(id)
}(i) // 显式传 id,避免闭包坑
}
wg.Wait() // 阻塞到计数归零WaitGroup 就是个计数器。Add(n) 加 n,Done() 减一,Wait() 阻塞到归零。Add 一定要写在 go 之前,goroutine 里第一行 defer wg.Done()。
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}sync.Mutex 保护共享可变状态。固定写法:Lock、defer Unlock、修改、return。Mutex 不要复制,嵌入一次后只传 *Mutex。
type Cache struct {
mu sync.RWMutex
m map[string]string
}
func (c *Cache) Get(k string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.m[k]
}
func (c *Cache) Set(k, v string) {
c.mu.Lock()
defer c.mu.Unlock()
c.m[k] = v
}RWMutex 允许多个读者并发持有 RLock,写者 Lock 会等所有读者释放并阻塞它们。读远多于写时用,否则普通 Mutex 更快。
var (
once sync.Once
config *Config
)
func GetConfig() *Config {
once.Do(func() {
config = loadConfig() // 只跑一次
})
return config
}sync.Once.Do(f) 保证 f 最多执行一次,即便多个 goroutine 同时调用 Do。延迟初始化单例(config、DB 连接池、编译好的 regex)的首选。
import "sync/atomic"
var n atomic.Int64
go func() {
for i := 0; i < 1000; i++ {
n.Add(1)
}
}()
fmt.Println(n.Load())
// CAS
var v atomic.Int32
v.CompareAndSwap(0, 1) // 0 → 1,成功sync/atomic 提供无锁的整数和指针操作。单字更新比 Mutex 快得多。Go 1.19+ 提供 atomic.Int64、atomic.Pointer[T] 等带类型包装,优先用包装,别用裸 int64 函数。
import "golang.org/x/sync/errgroup"
g, ctx := errgroup.WithContext(ctx)
for _, url := range urls {
url := url // 1.22 前需要
g.Go(func() error {
return fetch(ctx, url) // 任一返回非 nil 即取消其余
})
}
if err := g.Wait(); err != nil {
return err // 第一个错误
}errgroup.Group 是会收集第一个非 nil 错误的 WaitGroup。配 WithContext,第一个出错的 goroutine 会取消共享 ctx,让其他兄弟提前退出。并发跑一组可能失败任务的标准做法。
sem := make(chan struct{}, 3) // 最多 3 个并发
var wg sync.WaitGroup
for _, job := range jobs {
wg.Add(1)
sem <- struct{}{} // 占一个槽,满了就阻塞
go func(j Job) {
defer wg.Done()
defer func() { <-sem }() // 让出槽
process(j)
}(job)
}
wg.Wait()大小为 N 的带缓冲 channel 就是计数信号量:发送占用,接收释放。不用第三方库就能限并发,每个 goroutine 占用稀缺资源(DB 连接、文件句柄、API 配额)时必备。
func worker(ctx context.Context, in <-chan int) {
for {
select {
case <-ctx.Done():
return // 收到取消信号,干净退出
case v, ok := <-in:
if !ok {
return
}
process(v)
}
}
}长跑的 goroutine 应该 select 上 ctx.Done(),这样 context 取消时能干净退出而不是泄漏。永远给 goroutine 留一条退路,没有退出 case 的无限循环就是个等着发生的泄漏。
var bufPool = sync.Pool{
New: func() any { return new(bytes.Buffer) },
}
func process(data []byte) {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset() // ⚠ 取出来先清空
defer bufPool.Put(buf)
buf.Write(data)
// ... 用 buf ...
}sync.Pool 缓存临时对象,给热点路径减轻 GC 压力。Get 可能返回新建(走 New)或回收的对象,用前一定先 Reset。GC 随时可能清空池,所以别指望对象长期留存。
// 编译时插桩,运行时报告数据竞争 go test -race ./... go run -race main.go go build -race // 报告示例 // WARNING: DATA RACE // Write at 0x... by goroutine 7 // Previous read at 0x... by goroutine 6
-race 标志给内存访问插桩,运行时捕获数据竞争,并发且至少一方是写的未同步访问。它会让执行慢约 10 倍,所以在 CI/测试里跑,不上生产。它只报告本次运行真实发生的竞争。
// Go 1.25+:Go 方法自带 Add(1)+Done()
var wg sync.WaitGroup
for _, task := range tasks {
wg.Go(func() {
process(task) // 1.22+ 循环变量已每轮新建
})
}
wg.Wait()Go 1.25 新增 WaitGroup.Go(f),它自动 Add(1)、在新 goroutine 跑 f、再 Done(),省掉容易忘的 Add/Done 样板。配合 1.22 起每轮新建的循环变量,函数体也不用手动捕获。
ch := make(chan int)
go func() {
ch <- 42 // 阻塞到有接收者
}()
v := <-ch // 阻塞到有数据
fmt.Println(v) // 42无缓冲 channel 让收发双方同步,任一边没准备好都阻塞。发送 happens-before 接收返回,等于免费送一个同步原语。
ch := make(chan int, 3) ch <- 1 // 不阻塞 ch <- 2 ch <- 3 // ch <- 4 // 阻塞,缓冲满了 fmt.Println(<-ch, <-ch, <-ch) // 1 2 3
带缓冲 channel 不需要接收者就能存最多 N 个值。发送只有满了才阻塞,接收只有空了才阻塞。用于有界任务队列、削峰填谷。
ch := make(chan int, 3)
ch <- 1; ch <- 2
close(ch)
for {
v, ok := <-ch
if !ok { // 关了且抽空了
break
}
fmt.Println(v)
}close(ch) 标志"再也不会有新值"。缓冲清空后从已关闭 channel 接收会返回零值和 ok=false。只有发送方应该关,接收方千万别关,更不能重复关。
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // 不关,consumer 永远阻塞
}
ch := make(chan int)
go producer(ch)
for v := range ch {
fmt.Println(v)
}for v := range ch 会一直收到 channel 关闭且抽空才停。忘记 close 就会永远阻塞,channel 死锁最常见的姿势。
select {
case v := <-ch1:
fmt.Println("from ch1:", v)
case v := <-ch2:
fmt.Println("from ch2:", v)
case ch3 <- 42:
fmt.Println("sent to ch3")
case <-time.After(time.Second):
fmt.Println("timeout")
}select 同时等多个 channel 操作,谁先就绪谁跑。多个同时就绪时随机挑一个。time.After 直接给你一条超时 case。
select {
case v := <-ch:
fmt.Println("got:", v)
default:
fmt.Println("no value ready")
}
// 非阻塞发送
select {
case ch <- 42:
// 发出去了
default:
// 缓冲满了,丢弃
}加 default 让 select 变成非阻塞,没别的 case 就绪就立刻跑 default。少用:在 select+default 上忙轮询会吃 CPU。
// 只发送
func produce(out chan<- int) {
for i := 0; i < 10; i++ {
out <- i
}
close(out)
}
// 只接收
func consume(in <-chan int) {
for v := range in {
process(v)
}
}chan<- T 是只发送通道,<-chan T 是只接收通道。写到函数参数里能表达意图,编译器也会替你强制检查。
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * j // 模拟工作
}
}
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 4; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 10; j++ {
jobs <- j
}
close(jobs)
for r := 1; r <= 10; r++ {
fmt.Println(<-results)
}worker pool:N 个 goroutine 从共享 jobs channel 取活、写到 results。关闭 jobs 通知 worker 抽完退出;编排端按数量收 results。有界并发,不用手写 semaphore。
// fan-out: 一个 source 喂多个 worker
// fan-in: 多个 source 合并到一个 sink
func merge(cs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(cs))
for _, c := range cs {
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(c)
}
go func() { wg.Wait(); close(out) }()
return out
}fan-out 把任务分给多个 goroutine;fan-in 把多个 channel 合到一个。上面的 merge 是经典 fan-in:每个输入起一个 goroutine,WaitGroup 等全部完成后关 output。
// 阶段链:每个阶段一个 goroutine
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for v := range in {
out <- v * v
}
}()
return out
}
for v := range sq(gen(1, 2, 3, 4)) {
fmt.Println(v) // 1 4 9 16
}pipeline 把多个阶段串起来,每个阶段一个 goroutine,从入口 channel 读、写到出口 channel。入口抽空后必须关出口,这样关闭信号才能沿管道传下去。
done := make(chan struct{})
go func() {
// ... 做事 ...
close(done) // 通知完成
}()
<-done // 等通知
fmt.Println("worker finished")chan struct{} 不带任何数据,纯信号。每次发送零内存,空 struct 也表达了"我只关心时机"。close 可以一次广播给所有等待者。
var ch chan int // nil
// <-ch // 永久阻塞
// ch <- 1 // 永久阻塞
// 巧用:在 select 里禁用某个 case
var in chan int = source
for {
select {
case v, ok := <-in:
if !ok {
in = nil // 关闭后置 nil,这条 case 永不再触发
} else {
process(v)
}
}
}对 nil channel 收发会永久阻塞(不会 panic)。这是个特性:在 select 里把某个 channel 变量置 nil 就能禁用那条 case,是抽空输入后"关掉"它的干净写法。
select {
case v := <-ch:
return v, nil
case <-time.After(2 * time.Second):
return 0, errors.New("timeout")
}
// ⚠ time.After 每次都新建 Timer,循环里用 NewTimer 复用
t := time.NewTimer(2 * time.Second)
defer t.Stop()
select {
case v := <-ch:
return v, nil
case <-t.C:
return 0, errors.New("timeout")
}select 加一条 time.After 给等待 channel 设上限。注意 time.After 在触发前会留一个 Timer,一次性没事,热循环里要用 time.NewTimer + Stop 防止 Timer 堆积。
done := make(chan struct{})
for i := 0; i < 3; i++ {
go func(id int) {
select {
case <-done: // 所有 goroutine 同时收到
fmt.Println(id, "stopping")
}
}(i)
}
close(done) // 一次广播给全部等待者close 一个 channel 会唤醒所有阻塞在它上面接收的 goroutine,一对多广播。经典关停信号:一个 done chan struct{},close 一次告诉所有 worker 停下。
limiter := time.NewTicker(200 * time.Millisecond)
defer limiter.Stop()
for req := range requests {
<-limiter.C // 每 200ms 放行一个,5 req/s
handle(req)
}
// 突发容量:先填满缓冲
burst := make(chan struct{}, 3)time.Ticker 按固定间隔触发;从它的 C channel 接收能把循环节流到稳定速率(这里 5/s)。要允许突发就再配一个带缓冲的令牌 channel。更完整的令牌桶用 golang.org/x/time/rate。
// 预填满,每次借一个用完归还
pool := make(chan *bytes.Buffer, 4)
for i := 0; i < 4; i++ {
pool <- new(bytes.Buffer)
}
buf := <-pool // 借
buf.Reset()
// ... 用 buf ...
pool <- buf // 还预填 N 个对象的带缓冲 channel 就是固定大小的资源池:接收借出,发送归还。和 sync.Pool 不同,数量有界且对象不会消失,适合 DB 连接这种固定数量的昂贵资源。
ctx := context.Background() // 根 context,永不取消 // 不确定该用什么时占位 ctx := context.TODO()
Background 是永不取消的空根 context,main、init、测试里用它。TODO 表示"还没想好",方便静态检查工具标出位置。所有 context 都必须能追溯到这两者之一。
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // ⚠ 必须 cancel,否则泄漏
go func() {
time.Sleep(time.Second)
cancel() // 通知所有用 ctx 的 goroutine
}()
select {
case <-ctx.Done():
fmt.Println("cancelled:", ctx.Err())
}WithCancel 返回一个可取消的子 context。一定要 defer cancel,不调用就会泄漏负责传播信号的 goroutine。ctx.Done() 是取消时会关闭的 channel,ctx.Err() 告诉你原因。
ctx, cancel := context.WithTimeout(
context.Background(), 2*time.Second)
defer cancel() // 即便超时也调用,无害
req, _ := http.NewRequestWithContext(
ctx, "GET", "https://api.example.com", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
// 超时 / 取消 / 网络错
log.Fatal(err)
}
defer resp.Body.Close()WithTimeout 超时自动取消。配合 http.NewRequestWithContext 给外发 HTTP 加墙钟超时。还是要 defer cancel,已经触发也无害。
deadline := time.Now().Add(5 * time.Minute) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel()
WithDeadline 跟 WithTimeout 类似但传绝对时间。看哪个更能表达意图:"30 秒后"用 WithTimeout,"14:00 之前"用 WithDeadline。
type ctxKey string
const userIDKey ctxKey = "userID"
ctx := context.WithValue(parent, userIDKey, 42)
// 在下游取
if uid, ok := ctx.Value(userIDKey).(int); ok {
fmt.Println("user:", uid)
}WithValue 携带请求作用域数据(trace ID、auth token)。key 一定用包内私有类型(不能裸 string),防止跨包冲撞。普通函数参数不要塞进 context。
// 约定:ctx 始终是第一个参数,名字 ctx
func FetchUser(ctx context.Context, id int) (*User, error) {
req, err := http.NewRequestWithContext(
ctx, "GET", url(id), nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
// ...
}约定:ctx 永远是第一个参数,名字就叫 ctx。要往下传,别存到 struct 里,别在中间换成 Background。这样取消和截止时间才能贯穿整个调用树。
<-ctx.Done()
switch ctx.Err() {
case context.Canceled:
fmt.Println("手动取消")
case context.DeadlineExceeded:
fmt.Println("超时")
}
// 在循环里主动检查
if err := ctx.Err(); err != nil {
return err // 提前退出
}ctx.Done() 触发后,ctx.Err() 返回 context.Canceled(手动取消)或 context.DeadlineExceeded(超时/截止)。在长循环里检查 ctx.Err() 能及时退出,不用等下一个阻塞操作。
// Go 1.21+:ctx 结束时自动跑回调
stop := context.AfterFunc(ctx, func() {
cleanup() // 取消或超时后触发
})
defer stop() // 返回 false 表示回调已跑过
// 比手动起 goroutine 监听 Done() 干净context.AfterFunc(Go 1.21+)注册一个回调,ctx 结束后在自己的 goroutine 里跑一次。比起一个只 select ctx.Done() 的 goroutine 更干净。返回的 stop() 在未触发时可注销它。
// ❌ 反模式:context 存字段
type Service struct {
ctx context.Context // 别这样
}
// ✅ 每个方法显式接收
type Service struct{ /* 其他字段 */ }
func (s *Service) Do(ctx context.Context) error {
return s.call(ctx)
}context 每次调用作为第一个参数传,别存到 struct 字段。存起来的 context 生命周期不对,反映不了某次请求的截止/取消,还容易活得比该有的作用域长。stdlib 和 go vet 都不建议。
// Go 1.21+:保留值,但切断取消传播
func handle(ctx context.Context) {
// 请求结束后还要跑的后台任务
bg := context.WithoutCancel(ctx)
go audit(bg) // 不随请求 ctx 取消而中断
}context.WithoutCancel(Go 1.21+)返回一个保留父 context 的值、但父被取消时它不取消的 context。用于必须活得比发起请求更久的后台任务(审计日志、清理),同时还带着 trace ID。
import "errors"
var ErrNotFound = errors.New("not found")
func find(id int) (*User, error) {
if id < 0 {
return nil, ErrNotFound
}
// ...
}errors.New 创建一个固定消息的 error。哨兵错误放包级变量,调用方用 errors.Is 比较。命名约定 ErrXxx。
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("parse %q: %v", s, err)
}fmt.Errorf 用 printf 风格拼错误消息。%v 是普通插值,%w 用来包装(下条)。
data, err := os.ReadFile("config.yaml")
if err != nil {
return fmt.Errorf("load config: %w", err)
}
// 调用方可以 unwrap 还原
// errors.Is(err, os.ErrNotExist)
// errors.As(err, &pathErr)%w 把内层 error 包起来,调用方能用 errors.Is / errors.As 看穿。一个 Errorf 用一个 %w(Go 1.20 起允许多个,少用)。用 %v 会断掉错误链。
_, err := os.ReadFile("missing.txt")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file gone, that's fine")
} else if err != nil {
return err
}errors.Is 会拆开错误链查找哨兵错误。比 == 强,一旦中间有人用 %w 包过,直接比较就匹配不上了。
_, err := os.Open("missing.txt")
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
fmt.Println("op:", pathErr.Op, "path:", pathErr.Path)
}errors.As 沿错误链找第一个匹配类型的 error,写入 target。需要拿结构化错误信息(文件路径、HTTP 状态、SQL 错误码)时用它。
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s: %s", e.Field, e.Msg)
}
// 用法
err := &ValidationError{Field: "email", Msg: "missing @"}
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println(ve.Field)
}任何有 Error() string 方法的类型都是 error。接收者写成指针,errors.As 才能写回去。字段按调用方可能需要的结构化信息加。
// 哨兵:调用方只需要"是 / 不是"
var ErrNotFound = errors.New("not found")
if errors.Is(err, ErrNotFound) { /* ... */ }
// 类型:调用方需要结构化数据
type RetryableError struct{ After time.Duration }
func (e *RetryableError) Error() string { /* ... */ }
var re *RetryableError
if errors.As(err, &re) {
time.Sleep(re.After)
retry()
}调用方只关心"是不是这个"用哨兵;要读字段(重试间隔、校验字段、状态码)用类型化 error。两者可以混用。
// Go 1.20+
var errs []error
for _, item := range items {
if err := validate(item); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...) // nil 自动忽略errors.Join(Go 1.20+)把多个错误合成一个。errors.Is / errors.As 都会下钻到合并的错误。比自己写多错误类型干净,校验和批量操作都好用。
func readFile(path string) (err error) {
f, err := os.Open(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr // Close 出错且主逻辑没错时才上报
}
}()
// ... 读文件,err 可能在这里被设
return err
}defer 闭包配命名 (err error) 返回,既能给函数错误补上下文,又能上报 Close() 的错误,但只在 err 还是 nil 时覆盖,避免真正的处理错误被关闭失败盖掉。
// ✅ 可预期的失败 → 返回 error
func parse(s string) (int, error) {
return strconv.Atoi(s)
}
// ✅ 程序员错误 / 不变量违反 → panic
func mustCompile(expr string) *regexp.Regexp {
re, err := regexp.Compile(expr)
if err != nil {
panic("invalid regexp: " + expr) // 编译期常量,绝不该失败
}
return re
}调用方能合理预期并处理的失败(坏输入、文件不存在、网络挂)一律返回 error。panic 只留给程序员错误和不变量被破坏,继续执行毫无意义的情况。regexp.MustCompile 就是这个原则。
type HTTPError struct{ Code int }
func (e *HTTPError) Error() string {
return fmt.Sprintf("http %d", e.Code)
}
// 让 4xx 都匹配同一个哨兵
func (e *HTTPError) Is(target error) bool {
return target == ErrClient && e.Code >= 400 && e.Code < 500
}
var ErrClient = errors.New("client error")
// errors.Is(&HTTPError{404}, ErrClient) → true自定义 error 可以实现 Is(target error) bool 来定制 errors.Is 的匹配逻辑。这里任意 4xx 的 HTTPError 都匹配 ErrClient 哨兵。同理 As(any) bool 方法能定制 errors.As。
err := fmt.Errorf("outer: %w",
fmt.Errorf("inner: %w", io.EOF))
errors.Unwrap(err) // inner: EOF
errors.Unwrap(errors.Unwrap(err)) // io.EOF
// 通常用 errors.Is/As,少手动 Unwraperrors.Unwrap 返回错误链里的下一层(被 %w 包的那个),到底层返回 nil。一般不直接调它,errors.Is 和 errors.As 会替你遍历链,但它是这两者的底层原语。
// Go 1.18+
func First[T any](xs []T) (T, bool) {
var zero T
if len(xs) == 0 {
return zero, false
}
return xs[0], true
}
s, ok := First([]string{"a", "b"}) // 类型自动推断
n, ok := First([]int{1, 2, 3})类型参数写在函数名后的方括号里。any(interface{} 的别名)接受任意类型。绝大多数调用不需要显式写类型参数,编译器从参数推断。
// comparable: 支持 == 和 != 的类型
func Index[T comparable](xs []T, target T) int {
for i, v := range xs {
if v == target {
return i
}
}
return -1
}
Index([]int{1, 2, 3}, 2) // 1
Index([]string{"a", "b"}, "b") // 1comparable 是内置约束,表示支持 == 和 != 的类型。泛型代码里要比较值就用它。注意 slice、map、func 都不是 comparable。
type Numeric interface {
~int | ~int64 | ~float64
}
func Sum[T Numeric](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}
Sum([]int{1, 2, 3}) // 6
Sum([]float64{1.1, 2.2, 3.3})约束就是把允许的类型用 | 列出来的接口。~ 前缀表示"该类型本身或底层类型为它的命名类型",这样 type Cents int 这种命名类型也算。
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
n := len(s.items) - 1
v := s.items[n]
s.items = s.items[:n]
return v, true
}
s := &Stack[int]{}
s.Push(1); s.Push(2)泛型 struct 把字段参数化。方法通过接收者继承类型参数:func (s *Stack[T]) ...,func 关键字后不再写方括号。
func Map[T, U any](xs []T, fn func(T) U) []U {
out := make([]U, len(xs))
for i, x := range xs {
out[i] = fn(x)
}
return out
}
func Filter[T any](xs []T, ok func(T) bool) []T {
out := xs[:0]
for _, x := range xs {
if ok(x) {
out = append(out, x)
}
}
return out
}两个类型参数能写出 Map 这种函数式 helper。Map[T, U] 保留输入的顺序和长度。真实项目里能用 golang.org/x/exp/slices、maps 就优先用官方的。
import "slices"
xs := []int{3, 1, 2}
slices.Sort(xs) // [1 2 3]
slices.Contains(xs, 2) // true
i, ok := slices.BinarySearch(xs, 2) // 1, true
slices.Max(xs) // 3
slices.Reverse(xs)
ys := slices.Clone(xs) // 浅拷贝
slices.Equal(xs, ys) // trueslices 包(Go 1.21+,标准库)提供泛型的 Sort、Contains、Index、Max/Min、BinarySearch、Reverse、Clone、Equal 等,再也不用手写。SortFunc 传比较函数做自定义排序。
import "maps"
m := map[string]int{"a": 1, "b": 2}
m2 := maps.Clone(m) // 浅拷贝
maps.Equal(m, m2) // true
maps.Copy(dst, src) // 合并 src 进 dst
// 1.23+:迭代器
for k := range maps.Keys(m) { _ = k }
for v := range maps.Values(m) { _ = v }maps 包(Go 1.21+)提供泛型的 Clone、Equal、Copy、DeleteFunc。Go 1.23 新增返回迭代器(iter.Seq)的 Keys/Values。这些取代了过去人人复制粘贴的样板循环。
import "cmp"
// 内置约束,无需自己写 Ordered
func Min[T cmp.Ordered](a, b T) T {
if a < b { return a }
return b
}
cmp.Compare(1, 2) // -1
cmp.Less("a", "b") // true
cmp.Or("", "fallback") // "fallback" 返回首个非零值cmp.Ordered(Go 1.21+)是支持 < <= >= > 所有类型的内置约束,不用自己手写类型集。cmp.Compare 返回 -1/0/+1(配 slices.SortFunc 正好),cmp.Or 返回第一个非零参数。
func Pop[T any](s []T) (T, []T) {
var zero T // T 的零值,编译期不知道具体类型也能写
if len(s) == 0 {
return zero, s
}
last := s[len(s)-1]
return last, s[:len(s)-1]
}泛型代码里没法给未知的 T 写字面量零值,所以声明 var zero T,无论 T 最终是什么都能拿到它的零值。泛型函数里"返回空"分支的标准写法。
func Make[T any]() []T {
return make([]T, 0)
}
// 无参数可推断时必须显式写
xs := Make[int]() // []int
ys := Make[string]() // []string
// 通常能推断就让编译器推
First([]int{1, 2}) // 不用写 First[int]编译器无法从参数推断类型参数时(比如函数没有 T 类型的参数),用方括号显式给出:Make[int]()。能推断时就省略,显式写反而是噪音。
fmt.Println("a", "b") // 写到 stdout
fmt.Printf("name=%s age=%d\n", "Lei", 30)
s := fmt.Sprintf("user=%q", "Lei") // 返回字符串
fmt.Fprintln(os.Stderr, "boom") // 写到任意 io.Writer
// 常用动词
// %v 默认 %+v 带字段名 %#v Go 字面量
// %d int %f float %s string %q 加引号
// %t bool %T 类型 %p 指针 %x 十六进制fmt 是 Go 的 printf。Println 自动加空格和换行;Printf 带格式;Sprintf 返回字符串。最常用动词:%v %+v %d %s %q %t %T。
os.Args // []string 程序参数
os.Getenv("HOME") // 取环境变量
os.Setenv("FOO", "bar")
os.Exit(1) // ⚠ 不跑 defer
data, err := os.ReadFile("a.txt")
err = os.WriteFile("b.txt", data, 0o644)os 装着进程和文件系统的基本能力。注意 os.Exit 不会跑 defer,优先从 main 返回。小文件用 os.ReadFile / WriteFile 一行搞定。
// 把 src 全部抄到 dst
n, err := io.Copy(dst, src) // 流式,常量内存
// 读全部到内存
data, err := io.ReadAll(r) // ⚠ 大文件会炸内存
// EOF 不是错误
for {
n, err := r.Read(buf)
if err == io.EOF { break }
if err != nil { return err }
// ... 用 buf[:n]
}io.Reader 和 io.Writer 是 stdlib 最重要的接口,几乎所有流类型都实现它们。io.Copy 流式复制不缓存全部;io.ReadAll 全读入内存(要慎用)。io.EOF 表示流结束,不算真正错误。
strings.Contains("hello", "ll") // true
strings.HasPrefix("README.md", "README")
strings.HasSuffix("a.txt", ".txt")
strings.Split("a,b,c", ",") // [a b c]
strings.Join([]string{"a","b"}, "-") // "a-b"
strings.TrimSpace(" hi ") // "hi"
strings.ReplaceAll("a-b-c", "-", "_")
strings.ToUpper("hi")
strings.Fields(" a b\tc ") // [a b c] 按空白拆strings 包覆盖了日常字符串处理:拆 / 拼 / 去空白 / 替换 / 前后缀检查 / 大小写。循环里高效拼字符串用 strings.Builder。
var b strings.Builder
for i := 0; i < 1000; i++ {
b.WriteString("x")
}
result := b.String() // O(n),单次分配
// ❌ 不要在循环里用 + 拼
// s := ""
// for ... { s += "x" } // O(n²)strings.Builder 用类似 vector 的扩容策略,n 次写入总成本 O(n)。循环里 += 拼是 O(n²),每次都要分配和复制。Go 最常见的性能坑之一。
n, err := strconv.Atoi("42") // string → int
s := strconv.Itoa(42) // int → string
f, err := strconv.ParseFloat("3.14", 64)
b, err := strconv.ParseBool("true")
n2, err := strconv.ParseInt("-100", 10, 64)
s2 := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"strconv 负责基本类型与字符串互转。Atoi / Itoa 是 int 简写;ParseFloat / ParseBool / ParseInt 要传位宽和进制。fmt.Sprintf 也行但更慢。
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
}
// struct → JSON
b, err := json.Marshal(User{Name: "Lei"})
// {"name":"Lei"}
// JSON → struct
var u User
err = json.Unmarshal(b, &u) // ⚠ 必须传指针
// 流式
json.NewEncoder(w).Encode(u)
json.NewDecoder(r).Decode(&u)encoding/json 按 struct tag 序列化导出字段。Marshal 返回字节;Unmarshal 必须传指针。流式用 Encoder / Decoder 不一次性加载整个文档。omitempty 去掉零值。
now := time.Now()
later := now.Add(2 * time.Hour)
d := later.Sub(now) // 2h0m0s
// 解析 / 格式化 (固定参考时间)
t, err := time.Parse("2006-01-02", "2026-05-26")
s := now.Format("2006-01-02 15:04:05") // ⚠ 这个时间数字不能改
time.Sleep(500 * time.Millisecond)
// 定时器
<-time.After(time.Second) // 一次性
tk := time.NewTicker(time.Second); defer tk.Stop()
for range tk.C { /* 每秒 */ }Go 用魔法参考时间 "Mon Jan 2 15:04:05 MST 2006" 做格式串,记住 01/02 03:04:05 PM '06。时长是带类型的:2 * time.Hour,不要写裸秒数。Ticker.Stop() 永远 defer。
mux := http.NewServeMux()
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "hello, %s\n", r.URL.Query().Get("name"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
}
log.Fatal(srv.ListenAndServe())net/http 十几行写出能上生产的服务端。一定要给 http.Server 显式设超时,默认是不超时,慢客户端能把 goroutine 拖爆。路由用 ServeMux 或 chi 等第三方。
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(
ctx, "GET", "https://api.example.com/users/42", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close() // ⚠ 一定要 Close
var u User
err = json.NewDecoder(resp.Body).Decode(&u)自己 new 一个带 Timeout 的 http.Client,http.DefaultClient 默认不超时。NewRequestWithContext 让请求受 context 控制。检查 err 后立刻 defer resp.Body.Close()。
log.Println("starting") // 老式
log.Fatalf("boom: %v", err) // 打印 + os.Exit(1)
// Go 1.21+: 结构化日志 slog
import "log/slog"
slog.Info("user login", "id", 42, "ip", "1.2.3.4")
slog.Error("db error", slog.String("op", "query"), slog.Any("err", err))脚本用普通 log 就行。服务端用 log/slog(Go 1.21+),结构化键值对,能干净地输出到 JSON、Loki、Datadog。log.Fatal 会调 os.Exit(1) 并跳过 defer。
var (
port = flag.Int("port", 8080, "listen port")
host = flag.String("host", "0.0.0.0", "bind host")
dbg = flag.Bool("debug", false, "enable debug logs")
)
func main() {
flag.Parse()
fmt.Printf("listen %s:%d\n", *host, *port)
}
// ./app -port=9000 -debugflag 是内置命令行解析。包级变量定义 flag,main 里 flag.Parse。返回指针的写法有点别扭但是惯例。要子命令、别名等更丰富体验用 cobra 或 urfave/cli。
f, _ := os.Open("big.log")
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
line := sc.Text() // 不含换行符
process(line)
}
if err := sc.Err(); err != nil { // ⚠ 循环后检查错误
log.Fatal(err)
}
// 超长行:调大 buffer
sc.Buffer(make([]byte, 1024*1024), 1024*1024)bufio.Scanner 按行(或按 token)流式读取,常量内存,大文件理想选择。循环后一定检查 sc.Err();Scan 返回 false 既可能是 EOF 也可能是真错误。默认单行上限 64KB,用 Buffer 调大。
re := regexp.MustCompile(`(\w+)@(\w+\.\w+)`)
re.MatchString("a@b.com") // true
re.FindString("hi a@b.com") // "a@b.com"
re.FindStringSubmatch("a@b.com") // ["a@b.com" "a" "b.com"]
re.ReplaceAllString("a@b.com", "$1") // "a"
// 编译一次复用,别在循环里 Compile用 regexp.MustCompile 编译一次(坏模式会 panic,常量没问题)然后复用。FindStringSubmatch 返回整体匹配加捕获组。Go 用 RE2,线性时间、无灾难性回溯,但不支持反向引用。
people := []Person{{"Lei", 30}, {"Wang", 25}}
// 按年龄升序
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
// 稳定排序(保持相等元素原顺序)
sort.SliceStable(people, func(i, j int) bool {
return people[i].Name < people[j].Name
})sort.Slice 原地排序,传一个按下标比较的 less(i, j) 回调。相等元素要保持原顺序就用 sort.SliceStable。Go 1.21+ 用泛型且类型安全的 slices.SortFunc 替代。
var buf bytes.Buffer
buf.WriteString("Content-Type: ")
buf.WriteString("application/json\n")
fmt.Fprintf(&buf, "Length: %d\n", 42)
data := buf.Bytes() // 底层切片,别长期持有
s := buf.String()
// 也实现了 io.Reader / io.Writer
io.Copy(dst, &buf)bytes.Buffer 是可增长的字节缓冲,同时实现 io.Reader 和 io.Writer,能接入任何流式 API。拼字节输出很高效。Buffer.Bytes() 暴露底层切片,要在下次写入后还留着就拷一份。
filepath.Join("a", "b", "c.txt") // a/b/c.txt(Windows 用 \)
filepath.Base("/x/y/z.go") // z.go
filepath.Dir("/x/y/z.go") // /x/y
filepath.Ext("z.go") // .go
abs, _ := filepath.Abs("rel.txt")
// 遍历目录树
filepath.WalkDir("/x", func(p string, d fs.DirEntry, err error) error {
return nil
})filepath 用操作系统分隔符拼接和拆分路径,代码在 Linux/Windows 间可移植。永远用 filepath.Join 而不是字符串拼接,它会规范化分隔符并清理路径。WalkDir(1.16+)高效遍历目录树。
import "math/rand/v2"
rand.IntN(100) // [0, 100)
rand.Float64() // [0.0, 1.0)
rand.Shuffle(len(s), func(i, j int) {
s[i], s[j] = s[j], s[i]
})
// v2 默认已随机种子,无需 Seed
// ⚠ 不是加密安全,密钥/token 用 crypto/randmath/rand/v2(Go 1.22+)自动随机种子,不用再写 rand.Seed 样板,命名改为 IntN/Int64N。它不是加密安全的,token、盐、密钥要用 crypto/rand。
import "crypto/rand"
// 安全随机字节(token / salt / key)
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
log.Fatal(err)
}
token := hex.EncodeToString(b)
// 安全范围随机数
n, _ := rand.Int(rand.Reader, big.NewInt(100)) // [0, 100)crypto/rand 从操作系统 CSPRNG 取数,一切安全敏感的东西都用它:会话 token、盐、密钥、nonce。rand.Read 填充字节切片;rand.Int 给 [0, max) 均匀值。密钥绝不用 math/rand。
// 延迟解析某个字段
type Envelope struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"` // 先不解
}
var e Envelope
json.Unmarshal(raw, &e)
switch e.Type {
case "user":
var u User
json.Unmarshal(e.Data, &u) // 按 type 再解
}json.RawMessage 保留未解析的原始字节,让你把某字段的解码推迟到知道它具体形状之后,处理多态"标签联合" JSON 的标准做法。只想原样转发一段时也避免了重新编码。
start := time.Now()
// ... 做事 ...
elapsed := time.Since(start) // = time.Now().Sub(start)
t := time.NewTimer(time.Second)
defer t.Stop() // 不用了要 Stop,否则泄漏
<-t.C // 一次性触发
// 重置复用
if !t.Stop() { <-t.C } // 排空旧值
t.Reset(2 * time.Second)time.Since(start) 是测量耗时的干净写法。Timer 在 C channel 上触发一次;Ticker 反复触发。不用的 Timer/Ticker 一定要 Stop。安全 Reset 一个已触发的 Timer 前要先排空 C。
import ("encoding/base64"; "encoding/hex")
raw := []byte("hello")
base64.StdEncoding.EncodeToString(raw) // aGVsbG8=
base64.URLEncoding.EncodeToString(raw) // URL 安全变体
hex.EncodeToString(raw) // 68656c6c6f
dec, _ := base64.StdEncoding.DecodeString("aGVsbG8=")base64.StdEncoding 是标准 base64;URLEncoding 把 +/ 换成 -_,输出在 URL 和文件名里安全。hex.EncodeToString 给小写十六进制,适合哈希和二进制 ID。两者都有流式的 Encoder/Decoder 变体。
import "crypto/sha256"
// 一次性
sum := sha256.Sum256([]byte("hello")) // [32]byte
fmt.Printf("%x\n", sum)
// 流式(大文件不占内存)
h := sha256.New()
io.Copy(h, file)
digest := h.Sum(nil)sha256.Sum256 一次性哈希字节切片,返回 [32]byte 数组。大输入用 sha256.New() 拿到 hash.Hash,io.Copy 把流灌进去,再 Sum(nil)。md5、sha1、sha512 同样的写法。
// file: math_test.go 同包
package math
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2, 3) = %d, want %d", got, want)
}
}
// 命令行: go test ./...测试文件以 _test.go 结尾,跟源码同目录。测试函数命名 TestXxx(t *testing.T)。断言失败用 t.Errorf(测试继续),用 t.Fatalf 立刻中断。
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b int
want int
}{
{"positive", 2, 3, 5},
{"negative", -1, -1, -2},
{"zero", 0, 0, 0},
{"mixed", -1, 5, 4},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := Add(tc.a, tc.b)
if got != tc.want {
t.Errorf("got %d, want %d", got, tc.want)
}
})
}
}Go 最地道的测试写法:一个 case 切片配 t.Run(name, ...) 命名子测试。加一条 case 就一行。单跑某条用 go test -run TestAdd/positive。
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Add(2, 3)
}
}
// go test -bench=. -benchmem
// BenchmarkAdd-8 1000000000 0.31 ns/op 0 B/op 0 allocs/opBenchmarkXxx(b *testing.B) 跑 b.N 次循环,框架按目标耗时(约 1s)算 N。加 -benchmem 还报内存分配。防止结果被死代码消除:赋给一个包级 sink 变量。
// Go 1.18+
func FuzzReverse(f *testing.F) {
f.Add("hello") // seed
f.Add("a")
f.Fuzz(func(t *testing.T, s string) {
rev := Reverse(s)
if Reverse(rev) != s {
t.Errorf("Reverse(Reverse(%q)) = %q", s, Reverse(rev))
}
})
}
// go test -fuzz=FuzzReverseFuzzXxx(f *testing.F) 用随机或种子语料找边界用例。用 f.Add 加种子。像 "Reverse(Reverse(x)) == x" 这种性质很容易 fuzz。语料存在 testdata/fuzz/ 下。
func mustOpen(t *testing.T, path string) *os.File {
t.Helper() // 错误定位指到调用处
f, err := os.Open(path)
if err != nil {
t.Fatalf("open %s: %v", path, err)
}
t.Cleanup(func() { f.Close() }) // 测试结束时执行
return f
}t.Helper() 把函数标为测试 helper,失败定位指向调用方而不是 helper 内部。t.Cleanup 注册清理,像 defer 但跟测试生命周期挂钩,子测试结束后才跑。
func TestEndpoints(t *testing.T) {
for _, tc := range cases {
tc := tc // 1.22 前必须
t.Run(tc.name, func(t *testing.T) {
t.Parallel() // 标记可并行
got := call(tc.input)
if got != tc.want {
t.Errorf("got %v want %v", got, tc.want)
}
})
}
}t.Parallel() 让子测试暂停到父测试的串行代码跑完,然后把标记的子测试并发跑,加速 I/O 密集的测试集。1.22 之前要捕获循环变量(tc := tc),否则所有子测试都看到最后一条 case。
func TestHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/users/42", nil)
w := httptest.NewRecorder()
myHandler(w, req)
resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
// 断言 body ...
}httptest.NewRecorder 不走真实网络就能捕获 handler 的响应。用假请求直接调 handler,检查状态码/头/body。要测客户端时用 httptest.NewServer 起一个真实的本地服务器。
func TestMain(m *testing.M) {
// 全局 setup:连数据库、起容器
db := setupDB()
code := m.Run() // 跑所有测试
// 全局 teardown
db.Close()
os.Exit(code) // ⚠ 必须用 m.Run 的返回码
}TestMain(m *testing.M) 包裹整个包的测试,给昂贵的共享 setup(DB、fixture、容器)和 teardown 一个统一入口。必须调 m.Run() 并 os.Exit 它的返回码,否则测试不跑或退出码不对。
var update = flag.Bool("update", false, "update golden files")
func TestRender(t *testing.T) {
got := render(input)
golden := "testdata/render.golden"
if *update {
os.WriteFile(golden, got, 0o644) // -update 时重写
}
want, _ := os.ReadFile(golden)
if !bytes.Equal(got, want) {
t.Errorf("output differs from %s", golden)
}
}golden 文件测试把输出跟 testdata/ 里签入的参考文件比对。加一个 -update 标志在预期输出确实变了时重新生成,差异在 code review 里审。大块结构化输出(渲染 HTML、代码生成)很合适。
func BenchmarkParse(b *testing.B) {
data := loadFixture() // 昂贵的准备工作
b.ResetTimer() // 不把准备时间算进去
b.ReportAllocs() // 报告每次操作的分配
for i := 0; i < b.N; i++ {
_ = parse(data)
}
}b.ResetTimer() 在昂贵的准备工作后归零计时器,让基准只测量热循环。b.ReportAllocs() 给输出加上 B/op 和 allocs/op(等于 -benchmem 但按基准单独开)。两者一起给出准确且关注分配的数据。
func returnsError() error {
var p *MyError = nil
return p // 返回了 (*MyError, nil) ≠ nil
}
err := returnsError()
fmt.Println(err == nil) // ❌ false!
// ✅ 显式返回裸 nil
func returnsErrorFixed() error {
var p *MyError = nil
if p == nil {
return nil
}
return p
}接口值是 (类型, 值) 二元组,只有两者都为 nil 才等于 nil。返回一个类型化的 nil 指针得到的是非 nil 的接口(值是 nil 而已)。Go 最经典坑;没错时一律 return nil(裸的)。
// Go 1.22 之前的坑 (1.22 之后修了)
xs := []int{1, 2, 3}
var fns []func()
for _, v := range xs {
fns = append(fns, func() {
fmt.Println(v) // 1.22 前都打印 3
})
}
// ✅ 显式拷贝
for _, v := range xs {
v := v // shadow
fns = append(fns, func() { fmt.Println(v) })
}Go 1.22 之前,range 循环变量被复用,闭包捕获的是同一个变量,最终都看到末值。1.22+ 每轮新建变量已修。还要兼容老版本就用 v := v 显式 shadow。
m := map[string]int{}
// ❌ 并发写直接 panic
go func() { m["a"] = 1 }()
go func() { m["b"] = 2 }()
// ✅ 加锁
var mu sync.Mutex
mu.Lock(); m["a"] = 1; mu.Unlock()
// ✅ 或者用 sync.Map(少数场景)
var sm sync.Map
sm.Store("a", 1)
v, ok := sm.Load("a")内置 map 并发写会直接 panic "concurrent map writes"。要么用 sync.Mutex 包,要么用 sync.Map(读多写少且 key 重叠少的场景)。不要侥幸"应该没事"。
// ❌ 主 goroutine 自己等自己 ch := make(chan int) ch <- 1 // 死锁:没人收 v := <-ch // ✅ 起 goroutine 或用缓冲 ch := make(chan int, 1) ch <- 1 v := <-ch // OK
无缓冲的发送 / 接收必须跨 goroutine 配对。在打算自己接收的 goroutine 里发送 = 立刻死锁。要么开 goroutine,要么用至少 1 大小的缓冲 channel。
// ❌ 忘记 cancel ctx, _ := context.WithTimeout(context.Background(), time.Second) doWork(ctx) // 内部的定时器 goroutine 直到超时才退出,资源泄漏 // ✅ 永远 defer cancel ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() doWork(ctx)
每个 Withxxx 都返回 cancel 函数,永远不要扔。即便 WithTimeout 也要 cancel,工作提前完成时不调用,内部定时器 goroutine 就一直在。永远在下一行 defer cancel()。
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // [2 3] cap=4
b = append(b, 99) // 写到了 a 的底层数组!
fmt.Println(a) // [1 2 3 99 5] ❌ a[3] 被改
// ✅ 用三索引切片限制 cap
b := a[1:3:3]
b = append(b, 99) // 这次另起底层数组,a 不变slice 共用底层数组。在还有 cap 的 slice 上 append 会写穿到底层,悄悄改到其他指向同一数组的 slice。用三索引切片 s[low:high:max] 限定 cap,append 就会另起新数组。
// ❌ 1000 个 defer 全在函数返回时才跑
for _, path := range paths {
f, _ := os.Open(path)
defer f.Close() // 全堆到函数结束
// ...
}
// ✅ 抽小函数包住
for _, path := range paths {
func() {
f, _ := os.Open(path)
defer f.Close() // 每轮立刻 Close
// ...
}()
}defer 在函数返回时跑,不是循环每轮。循环里 defer Close 等于把所有句柄攒到函数结束,FD 上限直接炸。每轮包一个匿名函数,让 defer 在每轮内立刻触发。
resp, err := http.Get(url)
if err != nil {
return err
}
// ❌ 忘 Close → TCP 连接不复用,连接泄漏
defer resp.Body.Close()
// 即便不读 body 也要 Close
// 想复用 keep-alive 还要把 body 抽空
io.Copy(io.Discard, resp.Body)
resp.Body.Close()忘了 resp.Body.Close() 会泄漏 TCP 连接,keep-alive 也废了。检查 err 后立刻 defer Close。要复用连接还得先把 body 抽空(io.Copy(io.Discard, body))。
// ❌ 这个 select 永远走 default,busy loop
for {
select {
case v := <-ch:
process(v)
default:
// 啥都不干
}
} // CPU 100%
// ✅ 没事干就别加 default
for v := range ch {
process(v)
}select 加 default 但 default 啥都不干,channel 空时就是 CPU 100% 的死循环。要么去掉 default 让它阻塞,要么加 time.After,要么 sleep 一下。纯忙轮询几乎都是错的。
var err error
data, err := os.ReadFile("a.txt") // ✅ 复用外层 err
if err != nil { return err }
// ❌ 内层 := 偷偷新建 err
if cond {
data, err := os.ReadFile("b.txt") // 这个 err 是新的,外层那个不变
_ = data
_ = err
} // 外层 err 永远是上一次的值:= 只要左边有一个名字在当前作用域是新的就建新变量。在 if / for 块里就 shadow 了外层同名变量,读代码很容易漏掉。go vet -shadow 能查出来。
var x int8 = 127
x++
fmt.Println(x) // -128,没有 panic
// ✅ 担心溢出就用更宽类型 / math/bits
import "math"
if a > math.MaxInt32-b {
return errors.New("overflow")
}
sum := a + bGo 整数溢出静默回绕,不 panic 不报错。不放心就主动用更宽类型,或者输入不可信时先做边界检查。math/bits 提供 64 位带检查算术。
var u User
err := json.Unmarshal(data, u) // ❌ 编译过,运行什么都没填
err = json.Unmarshal(data, &u) // ✅ 传指针
// 大小写敏感:字段必须导出(首字母大写)
type User struct {
name string // ❌ 小写,json 看不见
Name string // ✅ 大写
}json.Unmarshal 要传指针,传值能编过但啥都没写。json 包只能看见导出字段(首字母大写)。两个"差一点"的坑都很烧时间,一起记住。
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3) // ⚠ len=3
n := copy(dst, src) // n=3,只复制了前 3 个!
fmt.Println(dst) // [1 2 3]
// ✅ 要全复制,dst 长度要够
dst = make([]int, len(src))
copy(dst, src) // [1 2 3 4 5]copy 复制 min(len(dst), len(src)) 个元素,它不会扩大 dst。常见 bug:make 一个有 cap 但 len 为 0 的 dst,copy 什么都没复制。让 dst 的 len 等于 len(src),或用 append(nil, src...) 克隆。
// ❌ 浮点累积误差
fmt.Println(0.1+0.2 == 0.3) // false!
// ✅ 比较容差
import "math"
func almostEqual(a, b, eps float64) bool {
return math.Abs(a-b) <= eps
}
almostEqual(0.1+0.2, 0.3, 1e-9) // true
// NaN 跟自己都不相等
math.IsNaN(0.0 / 0.0) // 用这个判断浮点运算会累积舍入误差,所以对算出来的浮点用 == 基本都是错的(0.1+0.2 != 0.3)。用容差比较。NaN 跟任何值(包括自己)都不相等,用 math.IsNaN 判断。
m := map[string]int{"a": 1, "b": 2, "c": 3}
// ❌ 顺序每次运行都可能不同
for k := range m {
fmt.Println(k)
}
// ✅ 要稳定顺序:取 key 排序后遍历
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}map 遍历顺序是故意随机的,就为了防止代码依赖它。必须确定的输出(测试、JSON、日志)要先排序 key,把 key 收进切片排序再遍历。这坑常让本地过、CI 挂的测试浮现。
// ❌ string(65) 是 rune 转换,不是数字转字符串 fmt.Println(string(rune(65))) // "A",码点 65 // string(65) 现在 vet 会警告 // ✅ 数字转字符串用 strconv fmt.Println(strconv.Itoa(65)) // "65" fmt.Println(fmt.Sprint(65)) // "65"
string(someInt) 是把整数当 Unicode 码点转换,不是十进制文本,string(65) 是 "A" 不是 "65"。经典新手坑,go vet 现在会警告。要十进制字符串用 strconv.Itoa 或 fmt.Sprint。
// ❌ 每轮新建一个 Timer,没触发的留到 GC
for {
select {
case v := <-ch:
process(v)
case <-time.After(time.Minute): // 每轮新建!
return
}
}
// ✅ 复用一个 Timer
t := time.NewTimer(time.Minute)
defer t.Stop()
for {
select {
case v := <-ch:
process(v)
if !t.Stop() { <-t.C }
t.Reset(time.Minute)
case <-t.C:
return
}
}time.After 每次调用都新建一个 Timer;热循环里没触发的会留到到期才回收,内存堆积。复用一个 time.NewTimer 配 Stop/Reset,或把 time.After 移出循环。长跑服务里很隐蔽的泄漏。
// ❌ 接收方提前 return,发送方永远阻塞 → goroutine 泄漏
func leak() <-chan int {
ch := make(chan int) // 无缓冲
go func() {
ch <- compute() // 没人收就永久阻塞
}()
return ch // 调用方可能根本不读
}
// ✅ 给缓冲 1,发送方发完就能退出
func noLeak() <-chan int {
ch := make(chan int, 1)
go func() { ch <- compute() }()
return ch
}goroutine 往无缓冲 channel 发送,如果接收方提前放弃(比如 context 超时还没读就返回了),就永久阻塞,静默的 goroutine 泄漏。给 channel 缓冲 1,即便没人读发送方也能发完退出。
a := make([]int, 2, 4) // len=2 cap=4 b := append(a, 9) // 有 cap,b 和 a 共享底层 b[0] = 100 // a[0] 也变成 100! c := make([]int, 2, 2) // cap 满 d := append(c, 9) // 扩容,d 是新底层 d[0] = 100 // c[0] 不变 // 结论:append 后别再依赖原 slice 的内容
append 是否共享原底层数组取决于容量:还有富余 cap 就原地写(与源别名),否则分配新数组。这让 append 的别名行为不确定,永远别假设,也别在用 append 派生出另一个 slice 后再改原 slice。
可搜索的 Go (Golang) 速查表,覆盖日常真在撸的 100+ 段地道代码,不是凑数的 hello-world。十二大分类:基 础(package、var / const / iota、基本类型、array / slice / map / struct、struct tag、指针、零值、make / copy、显式类型转换),控制流(if 带初始化、switch 含 type switch、for / range 遍历四种可迭代、带标签 break、 defer 的 LIFO 和参数求值时机、panic、recover),函数 (多返回值 + error、命名返回值、变参、闭包、func 一等 公民、init、空标识符 _),方法与接口(值 vs 指针接 收者、接收者一致性、接口组合、空接口 / any、type assertion 双返回值、embedding 组合、编译期接口满足断 言),goroutine(go 关键字、sync.WaitGroup、 sync.Mutex / RWMutex、sync.Once、sync/atomic 带类型 包装),channel(无缓冲 vs 带缓冲、close + comma-ok、 for range、select + default、方向 channel、worker pool / fan-out fan-in / pipeline / 信号 channel 四种 模式),context(Background / TODO、WithCancel / WithTimeout / WithDeadline / WithValue、ctx 第一个参 数传播),错误(errors.New、fmt.Errorf %w、errors.Is / errors.As、自定义 error、哨兵 vs 类型化、 errors.Join),泛型 1.18+(类型参数、any、comparable、 含 ~ 和 | 的自定义约束、泛型 struct、Map / Filter), 标准库(fmt、os、io.Reader / Writer、strings 与 strings.Builder、strconv、encoding/json、time 含魔法 参考时间、net/http server / client 带 timeout、log / slog、flag),测试(testing.T、表驱动、t.Run 子测试、 benchmark 加 -benchmem、fuzz 1.18+、t.Helper + t.Cleanup),以及 12 个真烧时间的坑(nil 接口不等于 nil、1.22 前 for range 循环变量捕获、map 并发写 panic、无缓冲 channel 自死锁、忘 cancel 导致 goroutine 泄漏、slice 共享底层数组被 append 写穿、循环里 defer Close 把 FD 攒爆、忘了 resp.Body.Close、select default 写成忙轮询、:= 偷偷 shadow 外层 err、整数溢 出静默回绕、json.Unmarshal 必须传指针)。每条都带: 双语标题、可直接复制的真实代码、双语说明。搜索框跨 标题 / 代码 / 中英说明一起过滤,分类胶囊缩范围。中 英文都是为对应语言用户独立撰写,不是机翻。配 Python / TypeScript / Redis / PostgreSQL 速查覆盖整条技术 栈,搭 JSON Formatter 处理数据。
把内容粘贴或拖入工具面板。
点击按钮,在浏览器内本地处理,文件不上传。
一键复制结果或下载到本地。
适合穿插在写代码、查问题、做 Review、上线前的小任务里。
这些入口会把当前任务接到更完整的工具链里。
你把一个 4 千行的 Node API 迁到 Go,突然每个调用点都要写 if err != nil。筛到「错误」和「控制流」,把 fmt.Errorf %w 包装和 errors.Is 判断直接抄走,一遍过给 db 层和 http 层套上。那条 nil 接口不等于 nil 的坑,帮你省掉一下午,不然你会卡在一个读起来非 nil 的类型化 nil 返回上。
手上有 5 万张图要缩放,单循环跑太慢。打开「channel」分类,把带 close jobs 和 WaitGroup 的 worker pool 抄下来,worker 数设成 8 对齐核数。fan-out fan-in 那条加上「忘 cancel」那条坑,帮你避开两种最经典的泄漏:channel 一直不 close,以及把 context 的 cancel 丢了。
一次故障里,某个没设超时的下游调用钉住了 200 个 goroutine。筛到「context」和「标准库」,拿 context.WithTimeout 配 defer cancel,再拿带显式 Timeout 字段的 http.Client。一小时内你就给所有出站调用统一加上 3 秒超时,而「永远别扔掉 cancel 函数」那条,刚好拦住你差点又埋回去的泄漏。
组里一半人没写过类型参数。筛到「泛型」,当场过一遍 any、comparable、~ 和 | 约束写法,还有泛型版 Map 和 Filter。讲到一半搜 channel 或 errors 就能跳到相关写法,不用离开页面,20 分钟的小会一直停在具体代码上,不是空泛的幻灯片。
在签名写 error 的地方返回类型化的 nil 指针:接口里类型非 nil,调用方就看到 err != nil。没错的路径一律 return 裸的 nil 关键字,别 return var p *MyError。
用 context.WithTimeout 起 goroutine 却忘了 defer cancel():就算走成功路径,那个计时器 goroutine 也会一直挂着;创建 context 的下一行就把 defer cancel() 补上。
在热循环里给 select 写个裸 default,CPU 直接打满 100%。改用 time.After 分支或一个会阻塞的接收,default 只留给真正需要非阻塞轮询的场景。
全部在你浏览器里跑。这份速查是单个静态页,搜索只是对内存里的片段数组做过滤,你输入的任何内容都不会被发走、记录,也不会进 URL。没有 Go 执行,也没有上传。公司代理后面、连 go install 都装不了的气隙网络里,它都能照常用。
做你这行的人, 还会一起用这些。