跳到主要内容

Go (Golang) 速查表:100+ 段地道 Go 代码,涵盖 goroutine、channel、泛型与常见坑

Go (Golang) 速查表,100+ 段地道 Go 代码,语法/goroutine/channel/泛型/错误/标准库。

  • 本地处理
  • 分类 开发运维
  • 适合 格式化、校验、压缩或检查和代码相关的文本。
分类:
183
基础 (26)

package main 程序入口

package main

import "fmt"

func main() {
    fmt.Println("hello, world")
}

所有可执行 Go 程序都从 package main 的 func main() 启动。同 package 下的文件可以互相访问小写开头的未导出标识符。

import 单条与分组

import "fmt"

import (
    "fmt"
    "os"
    "strings"
    _ "embed"          // 仅副作用导入
    str "strings"      // 别名
)

分组用括号包起来。前面写下划线表示只要副作用(注册 driver 之类);左边写名字就是起别名。

var 声明变量

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 编译期常量

const Pi = 3.14159
const Greeting = "hello"

const (
    StatusOK    = 200
    StatusFound = 302
)

const MaxBuf = 1 << 20   // 1 MiB

const 在编译期求值,可以是无类型(上面的 Pi)也可以带类型。无类型常量能自动转成任何兼容的数值类型。

iota 自增常量

const (
    Sunday = iota   // 0
    Monday          // 1
    Tuesday         // 2
    Wednesday       // 3
)

// 位掩码常用 iota
const (
    FlagRead  = 1 << iota   // 1
    FlagWrite               // 2
    FlagExec                // 4
)

iota 在每个 const 块起始处归零,每行 +1。配合左移运算符做位掩码枚举特别顺手。

基本类型 int / float / bool / string

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 码点)。

string 不可变字节,UTF-8

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)          // 8

string 是不可变的 UTF-8 字节序列。len(s) 拿到的是字节数,不是字符数。range 遍历给的是 (字节下标, rune) 对。要按字符处理就转 []rune。

array 定长序列

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 才是常用选择。

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)                   // 3

slice 是 (指针, len, cap) 的三元组,背后指向一段数组。append 可能扩容,返回值一定要赋回去。三索引切片 s[low:high:max] 可以限定 cap,后面 append 就不会踩到原数据。

make 创建带容量的 slice/map/chan

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。

copy 复制 slice 元素

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])          // hhell

copy(dst, src) 复制 min(len(dst), len(src)) 个元素并返回数量。重叠内存也能正确处理,相当于 C 的 memmove。

map 键值查找

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)                          // 2

map 取不到 key 时返回零值,用 v, ok := m[k] 才能区分"没这个 key"和"值就是零"。删除用 delete(m, k)。

struct 命名字段

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 适合一次性的配置或本地形状。

struct tag 用于 JSON/DB/校验

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 直接跳过。

pointer & 取地址 * 解引用

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 直接交换不用临时变量。一行可以声明或赋值多个不同类型的变量。

命名类型 vs 类型别名

type Celsius float64        // 新类型,方法可挂,需显式转换
type Fahrenheit float64

func (c Celsius) ToF() Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

type Byte = uint8           // 别名,完全等同 uint8

type 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 没有内置二维切片,用切片的切片拼。每行单独分配,所以各行长度可以不同(锯齿)。循环里一行一行分配,或者用嵌套字面量。

append 展开与头插

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)。

map 装切片 / 结构体值

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。

rune 与 byte 遍历区别

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。

const 块里的 iota 表达式

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+06

iota 能驱动整个 const 块里重复的表达式:每行重新求值 1 << (10*iota)。开头用 _ = iota 跳过零值。这是定义 KB/MB/GB 尺寸常量的经典写法。

控制流 (19)

if 带初始化语句

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 链里可见的变量,"算一个值再判断" 的常用写法。条件不需要括号,花括号必须有。

if err != nil Go 的招牌写法

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 值匹配,默认不穿透

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 true 表达式式 switch

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 干净。

switch 类型断言式

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 已经是具体类型。从接口边界进入具体类型逻辑时离不开它。

for Go 唯一的循环

// 三段式 (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。

for range slice/map/string/chan

// 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)。

break / continue / 带标签

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 用法。

defer 函数返回时执行

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。

defer LIFO 顺序,参数立刻求值

func main() {
    for i := 0; i < 3; i++ {
        defer fmt.Println(i)    // 打印 2, 1, 0
    }
}

// 参数在 defer 那一刻就求值
i := 10
defer fmt.Println(i)            // 输出 10
i = 20

defer 按 LIFO 顺序执行。关键:参数在 defer 那一行就立刻求值,不是真正调用的时候,所以上面循环打印的是 2、1、0,不是循环结束后的值打三遍。

panic 立刻中断

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 链,然后向上抛栈。仅在真的没法继续(不变量被违反、程序员错误)时用,普通错误处理不要用。

recover 捕获 panic

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)。常用于服务端边界,防止一个坏请求把整个进程拖死。

goto 有但几乎不用

// 几乎只在生成代码里用
for i := 0; i < 10; i++ {
    if shouldSkip(i) {
        goto next
    }
    process(i)
next:
}

goto 存在主要是为了生成的代码偶尔需要。手写 Go 不要用它,用 break/continue/提前 return 重构。

switch 带初始化语句

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 内。适合"算一个值再分发"又不想让变量泄漏出去。

defer 修改命名返回值

func double(n int) (result int) {
    defer func() {
        result *= 2            // 改最终返回值
    }()
    result = n + 1
    return result             // 实际返回 (n+1)*2
}

double(3)                     // 8

defer 的闭包能在 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 金字塔,扁平才好读。

for range 整数 (1.22+)

// 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 的值可以是任意类型

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。

带标签的 continue

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 少见,但同样是多层控制流的干净写法。

函数 (13)

func 基础声明

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
}

参数类型写在名字后面。连续相同类型的参数可以共用一次类型标注。返回值类型写在参数列表后面。

多返回值 值 + error

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 时的必备写法。

变参 ...args

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 是一等公民

// 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 是一等公民,可以作参数、存变量、做返回值。给签名起一个类型别名让代码更可读。

init() 包初始化

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 栈会动态增长,实际可用深度很高。递归适合树和分治;线性迭代用循环。

defer 给函数计时

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 闭包捕获循环变量

// 想让 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 里循环变量捕获坑的唯一解法。

方法 / 接口 (13)

method 接收者函数

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 习惯:只要有一个方法需要指针,其他全部用指针。

interface 定义行为

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 关键字(结构化类型)。接口定义在"使用方",不是在"类型定义方"。

interface 组合

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 才用。

type assertion i.(T)

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)。

embedding 组合优于继承

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,缺方法当场报错。常写在声明接口实现的文件末尾。

fmt.Stringer 定制 %v

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,会无限递归。

接口里的值 vs 指针接收者

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{}。

struct 嵌入接口

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 嵌入接口会提升它的方法,并且可以通过这个字段替换实现。常见装饰器套路:嵌入一个类型只覆盖其中一个方法,其余自动转发。

类型集接口 (1.18+)

// 接口现在能列具体类型集(用于约束)
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)而不是方法,用作泛型约束。~ 前缀包含底层类型匹配的任意类型。这种接口只能当约束,不能作变量类型。

接口装着 nil 指针

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 接口坑的根因。

goroutine (12)

go 启动 goroutine

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。

sync.WaitGroup 等 N 个 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()。

sync.Mutex 互斥锁

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。

sync.RWMutex 多读单写

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 更快。

sync.Once 只初始化一次

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)的首选。

sync/atomic 无锁计数

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 函数。

errgroup 并发且收集错误

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,让其他兄弟提前退出。并发跑一组可能失败任务的标准做法。

semaphore 限制并发数

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 配额)时必备。

goroutine 配合 context 取消

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 的无限循环就是个等着发生的泄漏。

sync.Pool 复用对象

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 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/测试里跑,不上生产。它只报告本次运行真实发生的竞争。

sync.WaitGroup.Go (1.25+)

// 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 起每轮新建的循环变量,函数体也不用手动捕获。

channel (16)

make chan 无缓冲

ch := make(chan int)

go func() {
    ch <- 42                        // 阻塞到有接收者
}()

v := <-ch                           // 阻塞到有数据
fmt.Println(v)                      // 42

无缓冲 channel 让收发双方同步,任一边没准备好都阻塞。发送 happens-before 接收返回,等于免费送一个同步原语。

make chan 带缓冲

ch := make(chan int, 3)

ch <- 1                             // 不阻塞
ch <- 2
ch <- 3
// ch <- 4                          // 阻塞,缓冲满了

fmt.Println(<-ch, <-ch, <-ch)       // 1 2 3

带缓冲 channel 不需要接收者就能存最多 N 个值。发送只有满了才阻塞,接收只有空了才阻塞。用于有界任务队列、削峰填谷。

close 与 comma-ok 接收

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。只有发送方应该关,接收方千万别关,更不能重复关。

for range 收到 close 才停

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 多路复用

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 default 非阻塞

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。

方向 channel chan<- / <-chan

// 只发送
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 是只接收通道。写到函数参数里能表达意图,编译器也会替你强制检查。

worker pool 模式

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 / fan-in 模式

// 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。

pipeline 模式

// 阶段链:每个阶段一个 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。入口抽空后必须关出口,这样关闭信号才能沿管道传下去。

信号 channel struct{}

done := make(chan struct{})

go func() {
    // ... 做事 ...
    close(done)                     // 通知完成
}()

<-done                              // 等通知
fmt.Println("worker finished")

chan struct{} 不带任何数据,纯信号。每次发送零内存,空 struct 也表达了"我只关心时机"。close 可以一次广播给所有等待者。

nil channel 永久阻塞

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 给接收加超时

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 堆积。

close 广播取消信号

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 停下。

用 ticker 限流

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。

带缓冲 channel 当令牌池

// 预填满,每次借一个用完归还
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 连接这种固定数量的昂贵资源。

context (10)

context.Background / TODO

ctx := context.Background()         // 根 context,永不取消

// 不确定该用什么时占位
ctx := context.TODO()

Background 是永不取消的空根 context,main、init、测试里用它。TODO 表示"还没想好",方便静态检查工具标出位置。所有 context 都必须能追溯到这两者之一。

context.WithCancel 手动取消

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() 告诉你原因。

context.WithTimeout 自动取消

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,已经触发也无害。

context.WithDeadline 截止时间

deadline := time.Now().Add(5 * time.Minute)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

WithDeadline 跟 WithTimeout 类似但传绝对时间。看哪个更能表达意图:"30 秒后"用 WithTimeout,"14:00 之前"用 WithDeadline。

context.WithValue 请求作用域数据

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。

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.Err 判断结束原因

<-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() 能及时退出,不用等下一个阻塞操作。

context.AfterFunc (1.21+)

// 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 存进 struct

// ❌ 反模式: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 都不建议。

context.WithoutCancel (1.21+)

// 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。

错误 (12)

errors.New 简单错误

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。

fmt.Errorf 带格式 error

n, err := strconv.Atoi(s)
if err != nil {
    return 0, fmt.Errorf("parse %q: %v", s, err)
}

fmt.Errorf 用 printf 风格拼错误消息。%v 是普通插值,%w 用来包装(下条)。

fmt.Errorf %w 包装 error

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 会断掉错误链。

errors.Is 比较哨兵错误

_, 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 包过,直接比较就匹配不上了。

errors.As 取出类型化 error

_, 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 错误码)时用它。

自定义 error 类型

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 才能写回去。字段按调用方可能需要的结构化信息加。

哨兵 vs 类型 error 怎么选

// 哨兵:调用方只需要"是 / 不是"
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。两者可以混用。

errors.Join 多错误合并

// 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 都会下钻到合并的错误。比自己写多错误类型干净,校验和批量操作都好用。

在 defer 里统一包装错误

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 时覆盖,避免真正的处理错误被关闭失败盖掉。

panic 还是返回 error

// ✅ 可预期的失败 → 返回 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 就是这个原则。

自定义 Is() 配合 errors.Is

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。

errors.Unwrap 手动拆链

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,少手动 Unwrap

errors.Unwrap 返回错误链里的下一层(被 %w 包的那个),到底层返回 nil。一般不直接调它,errors.Is 和 errors.As 会替你遍历链,但它是这两者的底层原语。

泛型 (10)

类型参数 [T any]

// 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

// 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")          // 1

comparable 是内置约束,表示支持 == 和 != 的类型。泛型代码里要比较值就用它。注意 slice、map、func 都不是 comparable。

自定义约束 interface

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 这种命名类型也算。

泛型 struct

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 关键字后不再写方括号。

泛型 map / filter

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 就优先用官方的。

slices 标准库 (1.21+)

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)                // true

slices 包(Go 1.21+,标准库)提供泛型的 Sort、Contains、Index、Max/Min、BinarySearch、Reverse、Clone、Equal 等,再也不用手写。SortFunc 传比较函数做自定义排序。

maps 标准库 (1.21+)

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。这些取代了过去人人复制粘贴的样板循环。

cmp.Ordered 与 cmp 包 (1.21+)

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 返回第一个非零参数。

泛型零值 var zero T

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]()。能推断时就省略,显式写反而是噪音。

标准库 (23)

fmt printf 一家

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 文件 / 参数 / 环境变量

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 一行搞定。

io Reader / Writer / Closer

// 把 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 常用 helper

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。

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 最常见的性能坑之一。

strconv 字符串 ↔ 数字

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 也行但更慢。

encoding/json 序列化

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 去掉零值。

time 时刻、时长、格式化

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。

net/http 服务端

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 等第三方。

net/http 客户端

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 / slog 日志

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。

flag 命令行参数

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 -debug

flag 是内置命令行解析。包级变量定义 flag,main 里 flag.Parse。返回指针的写法有点别扭但是惯例。要子命令、别名等更丰富体验用 cobra 或 urfave/cli。

bufio.Scanner 按行读

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 调大。

regexp 匹配与捕获

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,线性时间、无灾难性回溯,但不支持反向引用。

sort.Slice 自定义排序

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 替代。

bytes.Buffer 拼字节

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() 暴露底层切片,要在下次写入后还留着就拷一份。

path/filepath 跨平台路径

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+)高效遍历目录树。

math/rand/v2 (1.22+)

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/rand

math/rand/v2(Go 1.22+)自动随机种子,不用再写 rand.Seed 样板,命名改为 IntN/Int64N。它不是加密安全的,token、盐、密钥要用 crypto/rand。

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。

encoding/json RawMessage 与流式

// 延迟解析某个字段
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 的标准做法。只想原样转发一段时也避免了重新编码。

time Timer / Ticker / Since

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。

encoding/base64 与 hex

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 变体。

crypto/sha256 哈希

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 同样的写法。

测试 (10)

go test 基本

// 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 立刻中断。

table-driven 表驱动测试

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。

go test 性能测试

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/op

BenchmarkXxx(b *testing.B) 跑 b.N 次循环,框架按目标耗时(约 1s)算 N。加 -benchmem 还报内存分配。防止结果被死代码消除:赋给一个包级 sink 变量。

go test 模糊测试

// 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=FuzzReverse

FuzzXxx(f *testing.F) 用随机或种子语料找边界用例。用 f.Add 加种子。像 "Reverse(Reverse(x)) == x" 这种性质很容易 fuzz。语料存在 testdata/fuzz/ 下。

t.Helper / t.Cleanup

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 但跟测试生命周期挂钩,子测试结束后才跑。

t.Parallel 并行子测试

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。

httptest 测 HTTP handler

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 起一个真实的本地服务器。

TestMain 测试集前后置

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 它的返回码,否则测试不跑或退出码不对。

golden 文件测试

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、代码生成)很合适。

b.ResetTimer 与 b.ReportAllocs

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 但按基准单独开)。两者一起给出准确且关注分配的数据。

常见坑 (19)

nil interface 不等于 nil

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(裸的)。

for range 循环变量捕获 (1.22 之前)

// 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。

map 并发写 panic

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 重叠少的场景)。不要侥幸"应该没事"。

channel 死锁 无缓冲

// ❌ 主 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。

context 不 cancel goroutine 泄漏

// ❌ 忘记 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()。

slice 别名 append 惊吓

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 就会另起新数组。

循环里 defer 关闭太晚

// ❌ 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 在每轮内立刻触发。

http.Response.Body 必须 Close

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 错误使用

// ❌ 这个 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 + b

Go 整数溢出静默回绕,不 panic 不报错。不放心就主动用更宽类型,或者输入不可信时先做边界检查。math/bits 提供 64 位带检查算术。

json.Unmarshal 必须传指针

var u User
err := json.Unmarshal(data, u)          // ❌ 编译过,运行什么都没填
err = json.Unmarshal(data, &u)          // ✅ 传指针

// 大小写敏感:字段必须导出(首字母大写)
type User struct {
    name string                         // ❌ 小写,json 看不见
    Name string                         // ✅ 大写
}

json.Unmarshal 要传指针,传值能编过但啥都没写。json 包只能看见导出字段(首字母大写)。两个"差一点"的坑都很烧时间,一起记住。

copy 只复制较短长度

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 判断。

依赖 map 遍历顺序

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(int) 不是 Itoa

// ❌ 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。

循环里 time.After 泄漏 Timer

// ❌ 每轮新建一个 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,即便没人读发送方也能发完退出。

append 可能共享也可能不共享底层

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 处理数据。

工具细节

输入
文本 + 数值
页面会根据工具类型展示文本框、数值控件、文件选择或结构化输入。
输出
即时结果 + 复制
结果区优先给出可操作结果,支持项会显示复制、下载或可视化预览。
隐私
可能使用网络查询
组件源码里检测到网络调用,页面会按工具逻辑处理;敏感内容建议先脱敏。
保存 / 分享
免账号使用
打开页面即可使用;刷新后是否保留结果取决于具体工具。
性能预算
首屏 JS ≤ 30 KB
没有声明 WASM 依赖,适合快速打开和移动端使用。
适用场景
开发运维 · 程序员
分类和职业标签用于推荐相关工具、组织内链,并帮助用户快速判断是否适合当前任务。

怎么用

  1. 1. 输入

    把内容粘贴或拖入工具面板。

  2. 2. 处理

    点击按钮,在浏览器内本地处理,文件不上传。

  3. 3. 复制 / 下载

    一键复制结果或下载到本地。

Go (Golang) 速查表 适合怎么用

适合穿插在写代码、查问题、做 Review、上线前的小任务里。

适合开发场景

  • 格式化、校验、压缩或检查和代码相关的文本。
  • 把片段整理好再放进文档、工单、提交或交接材料。
  • 不切换工具,快速检查一个小 payload。

开发检查项

  • 压缩、混淆这类不可逆处理,先对副本操作。
  • 除非确认工具本地处理,不要粘贴密钥和敏感片段。
  • 转换后的代码上线前,仍要跑自己的测试或 lint。

下一步可以接着做

这些入口会把当前任务接到更完整的工具链里。

  1. 1 JSON 格式化与校验 浏览器内即时格式化、校验、压缩 JSON,数据不离开本地。 打开
  2. 2 Python 速查表 Python 速查表,100+ 段地道 Python 代码片段,涵盖字符串/列表/字典/文件/异步,带真实例子。 打开
  3. 3 TypeScript 速查表 TypeScript 速查表,100+ 段代码涵盖类型/泛型/工具类型/类型收窄/异步模式。 打开

真实使用场景

  • 把 Python 或 Node 服务改写成 Go,撞上满屏 err 判断

    你把一个 4 千行的 Node API 迁到 Go,突然每个调用点都要写 if err != nil。筛到「错误」和「控制流」,把 fmt.Errorf %w 包装和 errors.Is 判断直接抄走,一遍过给 db 层和 http 层套上。那条 nil 接口不等于 nil 的坑,帮你省掉一下午,不然你会卡在一个读起来非 nil 的类型化 nil 返回上。

  • 搭 worker pool 清掉 5 万条积压任务,又不想泄漏 goroutine

    手上有 5 万张图要缩放,单循环跑太慢。打开「channel」分类,把带 close jobs 和 WaitGroup 的 worker pool 抄下来,worker 数设成 8 对齐核数。fan-out fan-in 那条加上「忘 cancel」那条坑,帮你避开两种最经典的泄漏:channel 一直不 close,以及把 context 的 cancel 丢了。

  • 给一个高负载下永远挂死的 HTTP 客户端加 context 超时

    一次故障里,某个没设超时的下游调用钉住了 200 个 goroutine。筛到「context」和「标准库」,拿 context.WithTimeout 配 defer cancel,再拿带显式 Timeout 字段的 http.Client。一小时内你就给所有出站调用统一加上 3 秒超时,而「永远别扔掉 cancel 函数」那条,刚好拦住你差点又埋回去的泄漏。

  • 1.18 升级后给团队站会讲泛型

    组里一半人没写过类型参数。筛到「泛型」,当场过一遍 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 都装不了的气隙网络里,它都能照常用。

常见问题

类似工具组合

做你这行的人, 还会一起用这些。

Made by Toolora · 100% client-side · Updated 2026-06-13