Skip to main content

Go (Golang) Cheatsheet — 100+ Idiomatic Snippets covering Goroutines, Channels, Generics, and Pitfalls

Go (Golang) cheat sheet — 100+ idiomatic snippets covering syntax, goroutines, channels, generics, errors, stdlib.

  • Runs locally
  • Category Developer & DevOps
  • Best for Formatting, validating, shrinking, or inspecting code-adjacent text.
Section:
183 snippets
Basics (26)

package main — entry point

package main

import "fmt"

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

Every executable Go program starts in package main with a func main(). Files in the same package can share unexported (lowercase) identifiers.

import — single + grouped

import "fmt"

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

Group imports inside parens. Prefix with `_` for side-effect-only imports (driver registration). Add a left-hand identifier for an alias.

var — declare a variable

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 declares a variable with an optional initializer. Without an initializer, the zero value applies: 0 for numbers, "" for strings, false for bool, nil for slice/map/pointer/interface/chan/func.

:= short variable declaration

name := "Lei"
count := 42
ok, err := tryParse(input)   // 多返回值

// ❌ 包级别不能用
// := 只能在函数内部用

:= declares and assigns in one step, inferring the type. Only works inside functions. At least one variable on the left must be new.

const — compile-time constants

const Pi = 3.14159
const Greeting = "hello"

const (
    StatusOK    = 200
    StatusFound = 302
)

const MaxBuf = 1 << 20   // 1 MiB

const values are evaluated at compile time. They can be untyped (Pi above) or typed. Untyped constants convert implicitly to any compatible numeric type.

iota — auto-incrementing constant

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

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

iota resets to 0 at the start of each const block and increments by 1 on each line. Combine with bit shifts for flag enums.

basic types — 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 is platform-sized (32-bit on 32-bit OS, 64-bit on 64-bit OS). Use intN when size matters. byte is an alias for uint8, rune is an alias for int32 (a Unicode code point).

string — immutable bytes, 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

Strings are immutable UTF-8 bytes. len(s) returns byte count, not character count. Range over a string gives (byte index, rune) pairs. Convert to []rune for character-level work.

array — fixed-length sequence

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] 不变

Arrays have fixed length baked into the type — [3]int and [4]int are different types. They are value types: assignment copies the whole array. You rarely use arrays directly; slices are the usual choice.

slice — dynamic view over an array

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

A slice is a (pointer, len, cap) header over a backing array. append may grow the underlying array; always assign the return value. Three-index slicing s[low:high:max] limits cap so later appends do not stomp on the original data.

make — slice / map / chan with size

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 initializes slice / map / chan. The 3-arg slice form (make([]T, len, cap)) preallocates capacity so append does not realloc. For map, the size hint avoids rehashing.

copy — copy slice elements

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) copies min(len(dst), len(src)) elements and returns the count. It handles overlapping memory correctly, like C memmove.

map — key/value lookup

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 lookup returns the zero value if the key is missing — use the comma-ok form (v, ok := m[k]) to distinguish absence from a real zero value. delete(m, k) removes a key.

struct — named fields

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}

A struct is a fixed set of named fields. Initialize by field name (recommended — adding a field later won't break you). Anonymous structs are useful for local options or one-off shapes.

struct tag — JSON / DB / validate

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 tags are key:"value" metadata strings used by encoders, ORMs, and validators. omitempty drops zero-value fields from JSON. Unexported (lowercase) fields are skipped by encoding/json.

pointer — &x and *p

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 pointers are simple: & takes an address, * dereferences. No pointer arithmetic (which kills a whole class of bugs). new(T) allocates a zero T and returns *T — rarely used; struct literals + & are more idiomatic.

type conversion — explicit only

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 has no implicit numeric conversion — int + float64 won't compile. T(v) converts explicitly. string ↔ []byte and string ↔ []rune are common.

zero value — every type has one

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:""}

Every type has a useful zero value. var declarations without initializers use it. This eliminates the "uninitialized variable" bug class — but a nil map / chan / func will panic on use.

multiple assignment & swap

a, b := 1, 2
a, b = b, a                 // 交换,无需临时变量

x, y, z := 1, "two", 3.0    // 一次声明多个不同类型

// 函数返回值直接解构
q, r := divmod(17, 5)

Go evaluates the entire right-hand side before assigning, so a, b = b, a swaps without a temp. One line can declare or assign several variables of different types.

named type vs type alias

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

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

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

type T U defines a brand-new named type — it gets its own method set and needs explicit conversion to/from U. type T = U is an alias: T and U are interchangeable, used mainly for gradual refactors (byte = uint8).

untyped constant precision

const Big = 1 << 62            // 无类型,编译期任意精度
const Small = Big >> 60       // 4

var x int64 = Big             // 赋给变量时才定型
// var y int8 = Big           // ❌ 溢出,编译报错

const Third = 1.0 / 3.0       // 高精度,不是 float64 截断

Untyped constants are kept at arbitrary precision until assigned to a typed variable, where they must fit. 1<<62 is fine as a const but overflows int8. Untyped float constants like 1.0/3.0 carry more precision than float64.

slice of slices (2D)

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 has no built-in 2D slice; build it as a slice of slices. Each inner slice is allocated separately, so rows can have different lengths (jagged). Allocate row by row in a loop, or use a nested literal.

append — spread & prepend

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...) spreads slice b onto a. Prepending means appending onto a fresh slice. To delete index i without preserving order, overwrite it with the last element and shrink — O(1) instead of O(n).

map of slices / struct values

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

Appending to a missing map key works because the zero value is a nil slice and append handles nil. But map values are not addressable: m["a"].X = 2 won't compile. Read the struct out, modify, write back — or store *T values.

rune vs byte iteration

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
}

Indexing s[i] gives a byte and will split multi-byte UTF-8 characters. range over a string decodes runes (whole characters). Use indexed loops only for pure-ASCII work; otherwise range or []rune.

const block with iota expression

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 can drive an expression repeated across the block: each line re-evaluates 1 << (10*iota). Use a leading _ = iota to skip the zero value. This is the canonical way to define KB/MB/GB size constants.

Control flow (19)

if — with init statement

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 can declare a variable scoped to the if/else chain — common pattern for "compute then check". Conditions never need parens; braces are mandatory.

if err != nil — Go's defining pattern

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

The most-typed Go idiom: check err immediately after every call that returns one. Wrap with %w to preserve the chain. Don't skip — silently dropping errors is the #1 source of mystery bugs.

switch — value match, no fallthrough

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")
}

switch in Go does NOT fall through by default — write fallthrough explicitly when you want it. case can list multiple values separated by commas. No break needed.

switch — true (expression-less)

switch {
case score >= 90:
    grade = "A"
case score >= 80:
    grade = "B"
case score >= 60:
    grade = "C"
default:
    grade = "F"
}

switch with no expression behaves like if-else-if — each case is a boolean expression evaluated top to bottom. Cleaner than long if-else ladders.

switch — type 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) inspects the dynamic type of an interface value. Inside each case, v has the concrete type. Essential when crossing an interface boundary into typed logic.

for — Go's only loop

// 三段式 (C 风格)
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

// while 风格
i := 0
for i < 10 {
    i++
}

// 死循环
for {
    if done() { break }
}

Go has exactly one loop keyword: for. It covers C-style, while-style, and infinite loops. No 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 works on slices, arrays, strings, maps, and channels. Map iteration order is intentionally random — don't depend on it. String range yields (byte-index, rune), not (i, byte).

break / continue / label

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 and continue affect the innermost loop by default. Use a labeled statement to jump out of an outer loop — the only acceptable use of labels in Go.

defer — run at function return

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 schedules a call to run when the enclosing function returns — by panic, by return, by normal flow. The canonical use is paired with resource open: defer close right after open.

defer — LIFO order, args evaluated now

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

Deferred calls run in LIFO order. Crucially, arguments are evaluated when defer is encountered, not when the call runs — so the loop above prints 2,1,0 not three times the post-loop value.

panic — abort with a message

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 stops normal flow, runs deferred functions, and unwinds the stack. Reserve it for truly unrecoverable situations (impossible invariants, programmer errors). Don't use it for ordinary error handling.

recover — catch a panic

func safeCall(f func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    f()
    return nil
}

recover only works inside a deferred function. It returns the value passed to panic (or nil if no panic is in flight). Use it at server boundaries to keep one bad request from killing the process.

goto — exists, almost never used

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

goto exists for the very rare case it makes generated code cleaner. Don't use it in hand-written Go — refactor with break/continue/early return instead.

switch with init statement

switch os := runtime.GOOS; os {
case "darwin":
    fmt.Println("macOS")
case "linux":
    fmt.Println("Linux")
default:
    fmt.Printf("%s\n", os)
}
// os 只在 switch 内可见

Like if, switch can run an init statement before the condition, scoped to the switch. Handy for "compute a value then dispatch on it" without leaking the variable.

defer modifies named return

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

double(3)                     // 8

A deferred closure can read and modify named return values after the return statement sets them but before the caller sees them. This is exactly how recover() injects an error into a named (err error) return.

early return — flatten nesting

// ✅ 守卫式:错误先退,主逻辑不缩进
func handle(r *Req) error {
    if r == nil {
        return errors.New("nil request")
    }
    if !r.Valid() {
        return errors.New("invalid")
    }
    // 主逻辑在最外层,读起来顺
    return process(r)
}

Idiomatic Go returns early on error/edge cases (guard clauses) so the happy path stays at the lowest indentation. Avoid deep if-else pyramids — flat is readable.

for range over integer (1.22+)

// Go 1.22+
for i := range 5 {
    fmt.Println(i)            // 0 1 2 3 4
}

// 不需要下标时
for range 3 {
    doOnce()
}

Go 1.22 added range over an integer n, iterating 0..n-1. Cleaner than for i := 0; i < n; i++ when you just need a count. Drop the variable entirely with for range n to repeat N times.

panic value can be any type

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 accepts any value, not just strings. Passing an error is most useful because recover() can type-switch and re-handle it. recover() returns nil when no panic is unwinding.

labeled 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 with a label jumps to the next iteration of the labeled outer loop, skipping the rest of the inner loop. Less common than labeled break but equally valid for clean multi-level control flow.

Functions (13)

func — basic declaration

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
}

Parameter type comes after the name. Consecutive parameters of the same type can share one type annotation. Return type comes after the parameter list.

multi-return — value + 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 functions can return multiple values. The (T, error) pattern is the convention for anything that can fail. Always return nil for the value and a real error, or a real value and nil error — never both non-nil.

named return values

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
}

Named returns declare result variables (already zero-initialized) and let you use a bare return. Best for short helpers, and required when a deferred recover() needs to set the error.

variadic — ...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

A trailing ...T parameter collects extra args into a []T inside the function. Callers can pass any count, including zero, or expand a slice with the spread suffix. fmt.Printf uses exactly this.

closure — captures outer variables

func counter() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}

c := counter()
c() // 1
c() // 2
c() // 3

A closure is a function value that references variables from its enclosing scope. The captured variable lives as long as the closure does — handy for counters, memo caches, and small state machines.

func value — first-class

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

Functions are first-class values: pass them as parameters, store them in variables, return them. Declare a named func type for readable signatures.

init() — package initialization

package config

var Settings = make(map[string]string)

func init() {
    Settings["host"] = "localhost"
    Settings["port"] = "8080"
    // 加载默认配置 / 注册驱动 ...
}

Each file can have one or more init() functions. They run automatically when the package is imported, after all package-level vars are initialized, before main(). Common for registering drivers and seeding defaults.

blank identifier _ — ignore return

_, err := fmt.Println("hi")     // 忽略写入字节数

v, _ := strconv.Atoi("42")      // 忽略 error (危险,少用)

// 仅副作用导入也用 _
import _ "github.com/lib/pq"

_ throws away a value you don't need. Useful for ignoring a return, an unused range index (for _, v := range xs), or for side-effect-only imports. Ignoring errors with _ should be deliberate, not lazy.

method value & method expression

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

A method value (t.Add) binds the receiver and yields a func(x int) int. A method expression (T.Add) does not bind — it yields func(T, int) int with the receiver as the first parameter. Useful for higher-order code.

functional options pattern

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

The functional options pattern keeps constructors backward-compatible: each option is a func(*T) that mutates the config. Callers pass only what they want; new options never break existing call sites. Used by gRPC, zap, and many libraries.

recursion — factorial / tree walk

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 has no tail-call optimization, so deep recursion can overflow the stack — but goroutine stacks grow dynamically, so practical depth is high. Recursion shines for trees and divide-and-conquer; use loops for linear iteration.

defer to time a function

func slow() {
    defer func(start time.Time) {
        log.Printf("slow took %v", time.Since(start))
    }(time.Now())            // ⚠ 参数立即求值,记下起点

    time.Sleep(100 * time.Millisecond)
}

Pass time.Now() as a defer argument so it's captured at function entry; the deferred closure logs time.Since at exit. Because defer args evaluate immediately, the start time is correct even with multiple return paths.

defer with closure over loop var

// 想让 defer 看到每轮的值,用参数传进去
for i, name := range files {
    defer func(idx int, n string) {
        log.Printf("closed %d:%s", idx, n)
    }(i, name)               // 立即求值,每个 defer 记下当轮值
}

Pass loop variables as defer arguments so each deferred call captures that iteration's values (args evaluate immediately). Pre-1.22 this was the only fix for the loop-variable-capture trap inside defer.

Methods & interfaces (13)

method — receiver function

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}

A method is a function with a receiver. Use a value receiver when the method doesn't mutate and the value is small; use a pointer receiver when the method modifies the receiver or copying is expensive.

receiver consistency — pick one

// ✅ 全部用指针接收者
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 }

Pick value or pointer receivers consistently per type. Mixing them confuses readers and breaks interface satisfaction in subtle ways. Standard library convention: if any method needs a pointer, all do.

interface — define behavior

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

An interface is a set of method signatures. Any type with matching methods satisfies it — no `implements` keyword needed (structural typing). Define interfaces where they are USED, not where the types live.

interface composition

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(...) ...
// }

Interfaces compose by embedding other interfaces — the resulting set is the union of methods. io.ReadWriter, io.ReadCloser, io.ReadWriteCloser are all examples in the standard library.

empty interface — any value

// 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{} (or its alias `any` in Go 1.18+) accepts any value. Use it sparingly — you lose static type checking. fmt.Println / json.Marshal accept it for genuine variadic typed APIs.

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) asserts the dynamic type of an interface value. The one-value form panics on mismatch; always prefer the comma-ok form unless you provably can't fail.

embedding — composition over inheritance

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 has no inheritance; it has embedding. An anonymous field exposes the inner type's fields and methods as if they were the outer type's. This is composition — clearer and more flexible than inheritance hierarchies.

satisfy interface at compile time

// 编译期断言: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"))
}

The idiomatic var _ Interface = (*T)(nil) trick forces the compiler to verify T satisfies Interface. Catches missing methods before the call site. Common at the bottom of files declaring interface implementations.

fmt.Stringer — custom %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

Implement String() string (the fmt.Stringer interface) and fmt verbs %v / %s use it automatically. Great for enums, IDs, and value types you print often. Avoid calling fmt inside String() on a *T receiver to prevent infinite recursion.

value vs pointer in interface

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{}      // ✅ 也行

If a method has a pointer receiver, only *T satisfies the interface — a value T does not. Value-receiver methods are in both T's and *T's method sets. This is why you often assign &Type{} to interface variables.

embed interface in 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) {}   // 屏蔽日志

Embedding an interface in a struct promotes its methods and lets you swap the implementation via the field. A common decorator trick: embed a type and override one method, leaving the rest forwarded.

type set constraint (1.18+)

// 接口现在能列具体类型集(用于约束)
type Ordered interface {
    ~int | ~int64 | ~float64 | ~string
}

// 既能当约束,也限定底层类型
func Max[T Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

Since 1.18 an interface can list a type set (T1 | T2 | ~T3) instead of methods, used as a generic constraint. The ~ prefix includes any type whose underlying type matches. Such interfaces can only be used as constraints, not as variable types.

interface holding nil pointer

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

Assigning a typed nil pointer to an interface gives a non-nil interface (it carries the *Dog type). Comparing a == nil is false, but calling a method that dereferences the receiver still panics. This is the root cause of the famous nil-interface gotcha.

Goroutines (12)

go — start a goroutine

go fmt.Println("hi from goroutine")

go func() {
    time.Sleep(100 * time.Millisecond)
    fmt.Println("delayed")
}()

// main 返回会杀掉所有 goroutine
time.Sleep(200 * time.Millisecond)

The go keyword starts a new goroutine — a lightweight thread managed by the Go runtime. Goroutines are cheap (~2KB initial stack) so spawning thousands is fine. When main returns, all goroutines die.

sync.WaitGroup — wait for N goroutines

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 is a counter. Add(n) bumps it, Done() (= Add(-1)) drops it, Wait() blocks until zero. Always call Add BEFORE go, and defer wg.Done() at the top of the goroutine.

sync.Mutex — exclusive lock

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 protects shared mutable state from concurrent access. Pattern: Lock, defer Unlock, mutate, return. Don't copy a Mutex — embed by value once and always pass *Mutex around.

sync.RWMutex — many readers, one writer

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 allows many concurrent RLock holders, but a Lock waits for all readers and blocks all of them. Use when reads vastly outnumber writes — otherwise plain Mutex is faster.

sync.Once — initialize exactly once

var (
    once   sync.Once
    config *Config
)

func GetConfig() *Config {
    once.Do(func() {
        config = loadConfig()       // 只跑一次
    })
    return config
}

sync.Once.Do(f) guarantees f runs at most one time, even if Do is called from many goroutines concurrently. Perfect for lazy singletons (config, DB pool, compiled regex).

sync/atomic — lock-free counters

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 provides lock-free operations on integers and pointers. Faster than Mutex for single-word updates. Go 1.19+ adds typed wrappers (atomic.Int64, atomic.Pointer[T]) — prefer these to the raw int64 funcs.

errgroup — concurrent with error

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 is a WaitGroup that collects the first non-nil error. With WithContext, the first failing goroutine cancels the shared ctx so siblings can bail out early. The standard way to run a bounded set of failable tasks concurrently.

semaphore — limit concurrency

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

A buffered channel sized N is a counting semaphore: send to acquire, receive to release. This caps concurrency without a third-party library — essential when each goroutine holds a scarce resource (DB conn, file handle, API quota).

goroutine + context cancellation

func worker(ctx context.Context, in <-chan int) {
    for {
        select {
        case <-ctx.Done():
            return               // 收到取消信号,干净退出
        case v, ok := <-in:
            if !ok {
                return
            }
            process(v)
        }
    }
}

Long-running goroutines should select on ctx.Done() so a cancelled context lets them exit cleanly instead of leaking. Always give a goroutine a way out — an unbounded loop with no exit case is a leak waiting to happen.

sync.Pool — reuse allocations

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 caches temporary objects to cut GC pressure in hot paths. Get may return a new (via New) or a recycled object — always Reset before use. The GC may drop pooled items at any time, so never rely on persistence.

race detector — 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

The -race flag instruments memory access to catch data races at runtime — concurrent unsynchronized access where at least one is a write. It slows execution ~10x, so run it in CI/tests, not production. It only flags races that actually happen during the run.

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 added WaitGroup.Go(f), which calls Add(1), runs f in a new goroutine, and Done() automatically — removing the easy-to-forget Add/Done boilerplate. Combined with 1.22 per-iteration loop variables, the body needs no manual capture.

Channels (16)

make chan — unbuffered

ch := make(chan int)

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

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

An unbuffered channel synchronizes sender and receiver — both block until the other is ready. The send happens-before the receive returns, giving you a synchronization primitive for free.

make chan — buffered

ch := make(chan int, 3)

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

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

A buffered channel holds up to N values without a receiver. Send blocks only when the buffer is full; receive blocks only when empty. Use for bounded work queues and burst smoothing.

close + comma-ok receive

ch := make(chan int, 3)
ch <- 1; ch <- 2
close(ch)

for {
    v, ok := <-ch
    if !ok {                        // 关了且抽空了
        break
    }
    fmt.Println(v)
}

close(ch) signals "no more values". Receiving from a closed channel returns the zero value with ok=false once the buffer is drained. Only the sender should close — never the receiver, and never close twice.

for range — drain until 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 loops until the channel is closed AND drained. Forget to close and the loop blocks forever — the most common channel deadlock.

select — multiplex channels

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 waits on multiple channel operations; the first one ready runs. If several are ready it picks one at random. time.After gives you a timeout case for free.

select with default — non-blocking

select {
case v := <-ch:
    fmt.Println("got:", v)
default:
    fmt.Println("no value ready")
}

// 非阻塞发送
select {
case ch <- 42:
    // 发出去了
default:
    // 缓冲满了,丢弃
}

A default case makes select non-blocking — if nothing else is ready, default runs immediately. Use sparingly; busy-loops on select+default eat CPU.

directional 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 is a send-only channel; <-chan T is a receive-only one. Use them in function parameters to make intent explicit and let the compiler enforce it.

worker pool pattern

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 goroutines read from a shared jobs channel and write to a results channel. Closing jobs signals workers to drain and exit; the orchestrator counts results. Bounded concurrency without manual semaphores.

fan-out / fan-in pattern

// 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 splits work across multiple goroutines; fan-in merges multiple channels into one. The merge helper above is the canonical fan-in: each input gets a goroutine, a WaitGroup closes the output when all are done.

pipeline pattern

// 阶段链:每个阶段一个 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
}

A pipeline chains stages, each a goroutine reading from an inbound channel and writing to an outbound. Always close the outbound when the inbound is drained — that propagates the shutdown signal downstream.

signal-only channel — struct{}

done := make(chan struct{})

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

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

chan struct{} carries no data — only the signal. Zero memory per send, and the empty struct documents intent: "I just want to know when". Close to broadcast to many waiters.

nil channel blocks forever

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

Send/receive on a nil channel blocks forever (it never panics). This is a feature: setting a channel variable to nil inside a select disables that case, a clean way to "turn off" an input once it's drained.

timeout a receive with 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 with a time.After case bounds how long you wait for a channel. Note time.After leaks a Timer until it fires — fine for one-shot, but in a hot loop use time.NewTimer + Stop to avoid piling up timers.

close to broadcast cancellation

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)                   // 一次广播给全部等待者

Closing a channel wakes every goroutine blocked on a receive from it — a one-to-many broadcast. The classic shutdown signal: a done chan struct{} that you close once to tell all workers to stop.

rate limiting with 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)

A time.Ticker fires on a fixed interval; receiving from its C channel paces a loop to a steady rate (here 5/s). For burst allowance, combine with a buffered token channel. golang.org/x/time/rate offers a richer token-bucket limiter.

buffered channel as a token pool

// 预填满,每次借一个用完归还
pool := make(chan *bytes.Buffer, 4)
for i := 0; i < 4; i++ {
    pool <- new(bytes.Buffer)
}

buf := <-pool                // 借
buf.Reset()
// ... 用 buf ...
pool <- buf                  // 还

A buffered channel pre-filled with N objects works as a fixed-size resource pool: receive to borrow, send to return. Unlike sync.Pool, the count is bounded and items never vanish — good for a fixed set of expensive resources like DB connections.

Context (10)

context.Background / TODO

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

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

Background is the empty root context — use it in main, init, and tests. TODO marks "I haven't decided yet" and lets static checkers flag the spot. Every context must trace back to one of these.

context.WithCancel — manual cancel

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 returns a cancellable child context. Always defer the cancel func — leaking it leaks the goroutine that propagates the signal. ctx.Done() is a channel closed on cancel; ctx.Err() tells you why.

context.WithTimeout — auto cancel

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 cancels automatically after the duration. Pair with http.NewRequestWithContext to enforce wall-clock deadlines on outbound HTTP. Still defer cancel — it's a no-op if already fired.

context.WithDeadline — until time

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

WithDeadline is like WithTimeout but takes an absolute time.Time. Pick whichever expresses intent better — "in 30s" → WithTimeout; "by 14:00" → WithDeadline.

context.WithValue — request-scoped data

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 carries request-scoped data (trace IDs, auth tokens). Use a private key TYPE (not a raw string) so different packages can't collide. Don't use it for function parameters proper.

context propagation — first arg

// 约定: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)
    // ...
}

Convention: ctx is the first parameter, always named ctx. Pass it down — never store on a struct, never start a fresh Background mid-call. Lets cancellation and deadlines flow through the whole call tree.

ctx.Err — why it ended

<-ctx.Done()

switch ctx.Err() {
case context.Canceled:
    fmt.Println("手动取消")
case context.DeadlineExceeded:
    fmt.Println("超时")
}

// 在循环里主动检查
if err := ctx.Err(); err != nil {
    return err                // 提前退出
}

After ctx.Done() fires, ctx.Err() returns context.Canceled (manual cancel) or context.DeadlineExceeded (timeout/deadline). Check ctx.Err() inside long loops to bail out promptly instead of waiting for the next blocking op.

context.AfterFunc (1.21+)

// Go 1.21+:ctx 结束时自动跑回调
stop := context.AfterFunc(ctx, func() {
    cleanup()                 // 取消或超时后触发
})
defer stop()                  // 返回 false 表示回调已跑过

// 比手动起 goroutine 监听 Done() 干净

context.AfterFunc (Go 1.21+) registers a callback that runs in its own goroutine once ctx is done. Cleaner than spawning a goroutine that just selects on ctx.Done(). The returned stop() unregisters it if not yet fired.

don't store context in a struct

// ❌ 反模式:context 存字段
type Service struct {
    ctx context.Context       // 别这样
}

// ✅ 每个方法显式接收
type Service struct{ /* 其他字段 */ }
func (s *Service) Do(ctx context.Context) error {
    return s.call(ctx)
}

Pass context per-call as the first argument, never store it on a struct. A stored context has the wrong lifetime — it can't reflect a specific request's deadline/cancellation and tends to outlive its scope. The standard library and go vet both discourage it.

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+) returns a context that keeps the parent's values but is NOT cancelled when the parent is. Use it for fire-and-forget background work that must outlive the originating request (audit logs, cleanup) while still carrying trace IDs.

Errors (12)

errors.New — simple error

import "errors"

var ErrNotFound = errors.New("not found")

func find(id int) (*User, error) {
    if id < 0 {
        return nil, ErrNotFound
    }
    // ...
}

errors.New creates an error with a fixed message. Declare sentinel errors as package-level vars so callers can compare with errors.Is. Convention: name them ErrXxx.

fmt.Errorf — formatted error

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

fmt.Errorf builds an error with a printf-style message. Use %v for plain interpolation, or %w to wrap (next entry).

fmt.Errorf %w — wrap 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 wraps an inner error so the chain can be inspected with errors.Is / errors.As. Use exactly one %w per Errorf (Go 1.20 allows multiple, rarely needed). Plain %v breaks the chain.

errors.Is — sentinel compare

_, 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 unwraps the chain looking for a sentinel match. Use it instead of == — direct comparison fails the moment something wraps the error with %w.

errors.As — extract typed 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 walks the chain and assigns the first matching typed error into target. Use it to dig out structured error info like file path, HTTP status, SQL error code.

custom error type

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

Any type with an Error() string method is an error. Make the receiver a pointer so errors.As can populate it. Add fields for whatever structured info callers might want.

sentinel vs typed — when each

// 哨兵:调用方只需要"是 / 不是"
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()
}

Sentinel errors when callers only need "is this it?". Typed errors when callers need to read fields off the error (retry-after, validation field, status code). Mix both freely.

errors.Join — multi-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+) combines multiple errors into one. errors.Is / errors.As both descend into joined errors. Cleaner than building your own multi-error type for validators and batch operations.

wrap error in 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
}

A deferred closure with a named (err error) return can both add context to the function's error and surface a Close() error — but only overwrite err when it's still nil, so a real processing error isn't masked by a close failure.

panic vs error — which to use

// ✅ 可预期的失败 → 返回 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
}

Return errors for anything a caller can reasonably expect and handle (bad input, missing file, network failure). Reserve panic for programmer mistakes and broken invariants — situations where continuing makes no sense. regexp.MustCompile follows this rule.

errors.Is with custom 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

A custom error can implement Is(target error) bool to define its own matching logic for errors.Is. Here any 4xx HTTPError matches the ErrClient sentinel. Likewise an As(any) bool method customizes errors.As.

errors.Unwrap — manual chain walk

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 returns the next error in the chain (the one wrapped with %w), or nil at the bottom. You rarely call it directly — errors.Is and errors.As walk the chain for you — but it's the primitive they're built on.

Generics (10)

type parameter — [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})

Type parameters in brackets after the function name. any (alias for interface{}) accepts any type. Most calls don't need explicit type args — the compiler infers from arguments.

constraint — 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 is a built-in constraint for types that support == and !=. Use it whenever your generic code compares values. Slices, maps, and funcs are NOT comparable.

custom constraint — 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})

Constraints are interfaces listing allowed types separated by |. The ~ prefix means "this type or any type whose underlying type is this" — covers named types like `type Cents int`.

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

A generic struct parameterizes its fields. Methods inherit the type parameters via the receiver: func (s *Stack[T]) ... — no extra brackets on the func keyword.

generic 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
}

Two-parameter generics power functional helpers like Map. Note Map[T, U] preserves input order and length. For real projects, prefer the official golang.org/x/exp/slices and maps helpers when possible.

slices package (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

The slices package (Go 1.21+, stdlib) provides generic Sort, Contains, Index, Max/Min, BinarySearch, Reverse, Clone, Equal, and more — no more hand-rolling these. SortFunc takes a comparison for custom orderings.

maps package (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 }

The maps package (Go 1.21+) offers generic Clone, Equal, Copy, and DeleteFunc. Go 1.23 adds Keys/Values returning iterators (iter.Seq). These replace the boilerplate loops everyone used to copy-paste.

cmp.Ordered & cmp package (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+) is the built-in constraint for all types supporting < <= >= > — use it instead of hand-writing a type set. cmp.Compare returns -1/0/+1 (handy for slices.SortFunc), and cmp.Or returns the first non-zero argument.

generic zero value — 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]
}

Inside generic code you can't write a literal zero for an unknown T, so declare var zero T — it yields T's zero value whatever T turns out to be. The standard idiom for "return nothing" branches in generic functions.

explicit type argument

func Make[T any]() []T {
    return make([]T, 0)
}

// 无参数可推断时必须显式写
xs := Make[int]()               // []int
ys := Make[string]()            // []string

// 通常能推断就让编译器推
First([]int{1, 2})              // 不用写 First[int]

When the compiler can't infer a type parameter from arguments (e.g. a function with no params of type T), supply it explicitly in brackets: Make[int](). When inference works, omit it — explicit args are noise.

Standard library (23)

fmt — printf family

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 is Go's printf. Println auto-adds spaces and newline; Printf takes a format string; Sprintf returns a string. Most-used verbs: %v %+v %d %s %q %t %T.

os — files, args, env

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 bundles process and filesystem basics. Note os.Exit doesn't run deferred functions — return from main instead. os.ReadFile/WriteFile handle small files in one call.

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 and io.Writer are the most important interfaces in stdlib — almost every stream type implements them. io.Copy streams without buffering everything; io.ReadAll loads it all (use carefully). io.EOF marks end-of-stream and is not a true error.

strings — common helpers

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]  按空白拆

The strings package has every helper you usually need: split, join, trim, replace, prefix/suffix checks, case conversion. strings.Builder for efficient concatenation in a loop.

strings.Builder — efficient concat

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 amortizes growth like a vector — total cost O(n) for n writes. Concatenating with += in a loop is O(n²) because every += allocates and copies. The single most common Go perf footgun.

strconv — string ↔ number

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 handles primitive ↔ string conversion. Atoi/Itoa are int shortcuts; ParseFloat/ParseBool/ParseInt take a bit-width and base. fmt.Sprintf works too but is slower.

encoding/json — marshal / unmarshal

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 marshals exported fields by struct tag. Marshal returns bytes; Unmarshal takes a pointer (must!). For streams, Encoder/Decoder avoid loading the whole document. omitempty drops zero values.

time — instants, durations, formatting

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 uses a unique reference time "Mon Jan 2 15:04:05 MST 2006" for parse/format strings — memorize 01/02 03:04:05 PM '06. Durations are typed: 2 * time.Hour, not raw seconds. Ticker.Stop() always defered.

net/http — server

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 gives you a production server in a dozen lines. Always wrap http.Server with explicit timeouts — the defaults are no timeout, which leaks goroutines on slow clients. Use ServeMux or a router like chi.

net/http — client

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)

Build your own http.Client with a Timeout — http.DefaultClient has none. NewRequestWithContext ties the request to a context for cancellation. Always defer resp.Body.Close() right after the err check.

log / slog — structured logging

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

Plain log is fine for scripts. For services, use log/slog (Go 1.21+) — structured key-value pairs that ship cleanly to JSON, Loki, Datadog. log.Fatal calls os.Exit(1) and skips defers.

flag — command-line flags

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 is the built-in CLI parser. Define flags as package vars, then call flag.Parse in main. Pointer-returning forms feel awkward but are idiomatic. For richer UX (subcommands, aliases), use cobra or urfave/cli.

bufio.Scanner — read lines

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 reads a stream line by line (or token by token) with constant memory — ideal for large files. Always check sc.Err() after the loop; Scan returning false hides both EOF and real errors. Default max line is 64KB; raise it with Buffer.

regexp — match & capture

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

Compile a pattern once with regexp.MustCompile (panics on bad pattern, fine for constants) and reuse it. FindStringSubmatch returns the full match plus capture groups. Go uses RE2 — linear time, no catastrophic backtracking, but no backreferences.

sort.Slice — custom sort

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 sorts in place with a less(i, j) callback comparing by index. Use sort.SliceStable when equal elements must keep their original order. For Go 1.21+, slices.SortFunc is the generic, type-safe replacement.

bytes.Buffer — build byte data

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 is a growable byte buffer that implements both io.Reader and io.Writer, so it plugs into anything streaming. Use it to assemble byte output efficiently. Buffer.Bytes() exposes the backing slice — copy it if you need to keep it past the next write.

path/filepath — portable paths

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 joins and splits paths using the OS separator, so code stays portable across Linux/Windows. Always use filepath.Join over string concat — it normalizes separators and cleans the path. WalkDir (1.16+) walks a tree efficiently.

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+) auto-seeds, so no more rand.Seed boilerplate, and uses IntN/Int64N naming. It is NOT cryptographically secure — for tokens, salts, and keys use crypto/rand instead.

crypto/rand — secure random

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 draws from the OS CSPRNG — use it for anything security-sensitive: session tokens, salts, keys, nonces. rand.Read fills a byte slice; rand.Int gives a uniform value in [0, max). Never use math/rand for secrets.

encoding/json — RawMessage & streaming

// 延迟解析某个字段
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 holds raw bytes unparsed, letting you defer decoding a field until you know its concrete shape — the standard way to handle polymorphic "tagged union" JSON. It also avoids re-encoding when you just want to pass a chunk through.

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) is the clean way to measure elapsed time. Timer fires once on its C channel; Ticker fires repeatedly. Always Stop a Timer/Ticker you no longer need. Resetting a Timer safely requires draining C first if it already fired.

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 handles standard base64; URLEncoding swaps +/ for -_ so the output is safe in URLs and filenames. hex.EncodeToString gives lowercase hex — handy for hashes and binary IDs. Both have streaming Encoder/Decoder variants.

crypto/sha256 — hashing

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 hashes a byte slice in one call and returns a [32]byte array. For large inputs, sha256.New() gives a hash.Hash you can io.Copy a stream into, then call Sum(nil). Same pattern works for md5, sha1, sha512.

Testing (10)

go test — basic

// 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 files end in _test.go and live alongside the code. Test functions are TestXxx(t *testing.T). Use t.Errorf for failed assertions (test keeps running), t.Fatalf to stop the test immediately.

table-driven test

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

The idiomatic Go way to test: a slice of cases driven by a single loop with t.Run(name, ...) for subtest names. Adding a case is a one-line change. Run a single subtest with `go test -run TestAdd/positive`.

go test — benchmark

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) runs the loop b.N times — the framework picks N so the run takes ~1s. Add -benchmem to also report allocations. Avoid dead-code elimination by assigning to a package-level sink var.

go test — fuzz

// 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) feeds random / corpus inputs to find edge cases. Seed with f.Add. Properties like "Reverse(Reverse(x)) == x" are easy to fuzz. Corpus is stored under 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() marks a function as a test helper so failure traces point at the caller, not inside the helper. t.Cleanup registers teardown — like defer but tied to the test lifecycle (runs after subtests).

t.Parallel — run subtests concurrently

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() pauses the subtest until all serial code in the parent finishes, then runs marked subtests concurrently — speeds up I/O-bound suites. Pre-1.22, capture the loop variable (tc := tc) or all subtests see the last case.

httptest — test HTTP handlers

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 captures a handler's response without a real network. Call the handler directly with a fake request and inspect status/headers/body. httptest.NewServer spins up a real local server when you need to test a client.

TestMain — suite setup/teardown

func TestMain(m *testing.M) {
    // 全局 setup:连数据库、起容器
    db := setupDB()

    code := m.Run()             // 跑所有测试

    // 全局 teardown
    db.Close()
    os.Exit(code)               // ⚠ 必须用 m.Run 的返回码
}

TestMain(m *testing.M) wraps the whole package's tests, giving one place for expensive shared setup (DB, fixtures, containers) and teardown. You must call m.Run() and os.Exit with its result — otherwise tests don't run or the exit code is wrong.

golden file testing

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 file tests compare output against a checked-in reference file in testdata/. A -update flag regenerates the file when expected output legitimately changes, so you review the diff in code review. Ideal for big structured output (rendered HTML, code generation).

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() zeroes the timer after expensive setup so the benchmark measures only the hot loop. b.ReportAllocs() adds B/op and allocs/op to the output (same as -benchmem but per-benchmark). Together they give accurate, allocation-aware numbers.

Common pitfalls (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
}

An interface value is (type, value) — it's nil ONLY when BOTH are nil. Returning a typed nil pointer gives a non-nil interface with a nil value. The most-feared Go gotcha; always return bare `nil` for the no-error case.

for range loop var capture (pre-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) })
}

Before Go 1.22, the loop variable was reused across iterations, so closures captured the same variable and saw its final value. Go 1.22+ gives each iteration a fresh variable. If you can't require 1.22+, shadow with `v := v`.

concurrent map write 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")

Built-in maps panic with "concurrent map writes" if accessed concurrently for write. Wrap with sync.Mutex, or use sync.Map for read-mostly caches where keys rarely overlap. Don't pretend it's "probably fine".

channel deadlock — unbuffered

// ❌ 主 goroutine 自己等自己
ch := make(chan int)
ch <- 1                                 // 死锁:没人收
v := <-ch

// ✅ 起 goroutine 或用缓冲
ch := make(chan int, 1)
ch <- 1
v := <-ch                               // OK

Unbuffered send/receive must pair across goroutines. Sending from the same goroutine that's supposed to receive deadlocks instantly. Either spawn a goroutine or use a buffered channel sized at least 1.

context not cancelled → goroutine leak

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

Every Withxxx returns a cancel func; never throw it away. Even WithTimeout needs cancel for the case where work finishes before the deadline — otherwise the internal timer goroutine sticks around. Always `defer cancel()` on the next line.

slice aliasing — append surprise

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 不变

Slices share their backing array. Appending into a slice with extra capacity writes through and silently mutates other slices pointing at the same array. Three-index slicing s[low:high:max] caps the slice so append forces a new array.

defer in a loop — late close

// ❌ 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 runs at function return — not loop iteration. Deferring file closes inside a loop holds every handle until the outer function exits, blowing up FD limits. Wrap each iteration in an anonymous function so defer fires per iteration.

http.Response.Body — must 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()

Forgetting resp.Body.Close() leaks TCP connections and breaks keep-alive. Always defer Close right after the err check. To re-use the connection, drain the body (io.Copy(io.Discard, body)) before closing.

unbuffered chan in select default

// ❌ 这个 select 永远走 default,busy loop
for {
    select {
    case v := <-ch:
        process(v)
    default:
        // 啥都不干
    }
}                                       // CPU 100%

// ✅ 没事干就别加 default
for v := range ch {
    process(v)
}

A select with default and nothing else in the loop spins on the CPU when the channel is empty. Either drop default and block, add a time.After case, or sleep briefly. Pure busy-loops are almost always wrong.

shadowing with :=

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 永远是上一次的值

:= creates a new variable if any LHS name is new in the current scope. Inside an if/for block, this shadows the outer name — easy to read your code and miss it. `go vet -shadow` catches the bad cases.

integer overflow — silent wrap

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 integer overflow wraps silently — no panic, no error. Use larger int types proactively, or check bounds before adding when input is untrusted. math/bits has helpers for 64-bit checked arithmetic.

JSON unmarshal — pointer required

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

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

json.Unmarshal needs a pointer — passing the value compiles but writes nowhere. Only exported (uppercase) fields are visible to the json package. Two off-by-one mistakes that lose hours; remember both.

copy copies min length

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 copies min(len(dst), len(src)) elements — it never grows dst. A common bug: make a dst with cap but len 0, then copy gives you nothing. Size dst with len(src), or use append(nil, src...) to clone.

comparing floats with ==

// ❌ 浮点累积误差
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)             // 用这个判断

Floating-point arithmetic accumulates rounding error, so == on computed floats is almost always wrong (0.1+0.2 != 0.3). Compare with an epsilon tolerance. NaN is never equal to anything including itself — test it with math.IsNaN.

relying on map iteration order

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 iteration order is randomized on purpose to stop code from depending on it. Output that must be deterministic (tests, JSON, logs) needs sorted keys — collect keys into a slice, sort, then iterate. This bites people in tests that pass locally and fail in CI.

string(int) is not 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) converts the int as a Unicode code point, not its decimal text — string(65) is "A", not "65". A classic beginner trap; go vet now warns on it. Use strconv.Itoa or fmt.Sprint to get the decimal string.

time.After in a loop leaks timers

// ❌ 每轮新建一个 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 allocates a fresh Timer each call; in a hot loop the un-fired ones live until they expire, piling up memory. Reuse a single time.NewTimer with Stop/Reset, or move time.After out of the loop. A subtle leak in long-running servers.

unbuffered send blocks if receiver gone

// ❌ 接收方提前 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
}

A goroutine sending on an unbuffered channel blocks forever if the receiver gives up early (e.g. a context timeout returns before reading) — a silent goroutine leak. Buffering the channel by 1 lets the sender complete and exit even if no one reads.

append may or may not share backing

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 的内容

Whether append shares the original backing array depends on capacity: if there's spare cap it writes in place (aliasing the source), otherwise it allocates a new array. This makes append non-deterministic in aliasing — never assume, and don't mutate a slice after appending to derive another.

What this tool does

Searchable Go (Golang) cheat sheet, 100+ idiomatic snippets working Go devs actually type — not hello- world filler. Twelve sections: basics (package, var / const / iota, slice, map, struct + tag, pointer, zero value, make / copy, type conversion), control flow (if, switch / type switch, for / range, labeled break, defer LIFO + arg eval, panic, recover), functions (multi-return value+error, named return, variadic, closure, init), methods + interfaces (value vs pointer receiver, composition, any, type assertion, embedding, compile-time check), goroutines (go, WaitGroup, Mutex / RWMutex, Once, sync/atomic typed wrappers), channels (unbuffered vs buffered, close + comma-ok, range, select + default, directional chan, worker pool / fan-out fan-in / pipeline / signal patterns), context (Background, WithCancel / WithTimeout / WithDeadline / WithValue, ctx-first-arg propagation), errors (errors.New, fmt.Errorf %w, errors.Is / errors.As, custom error, errors.Join), generics 1.18+ (type parameter, any, comparable, custom constraints with ~ and |, generic struct, Map / Filter), standard library (fmt, os, io.Reader / Writer, strings.Builder, strconv, encoding/json, time with reference time, net/http server + client with timeouts, log/slog, flag), testing (testing.T, table-driven with t.Run, benchmark + -benchmem, fuzz, t.Helper / t.Cleanup), and 12 real pitfalls (nil interface != nil, pre-1.22 loop var capture, concurrent map panic, unbuffered chan self-deadlock, missing cancel = goroutine leak, slice aliasing through append, defer in loop holds FDs, forgetting resp.Body.Close, select default busy loop, := variable shadowing, silent integer overflow, json.Unmarshal needs a pointer). Every entry: bilingual title + code + bilingual description. Search across title / code / description; filter by section. Native EN/ZH copy, written separately, not machine-translated. Pair with Python / TypeScript / Redis / PostgreSQL cheat sheets.

Tool details

Input
Text + Numbers
The page exposes text boxes, numeric controls, file pickers, or structured inputs depending on the tool.
Output
Live result + Copy
The result area focuses on usable output, with copy, download, or preview actions when supported.
Privacy
May use a live lookup
A network call is detected in the component, so redact sensitive data when appropriate.
Save / share
No account required
Open the page and use it; whether results survive refresh depends on the tool.
Performance budget
Initial JS <= 30 KB
No WASM budget is declared, keeping the tool quick to open on mobile.
Best fit
Developer & DevOps · Developer
Category and role tags drive related tools, internal links, and quick fit checks.

How to use

  1. 1. Input

    Paste or drop your content into the tool panel.

  2. 2. Process

    Click the button. All processing is local in your browser.

  3. 3. Copy / Download

    Copy the result or download to disk in one click.

How Go (Golang) Cheatsheet fits into your work

Use it in the small gaps between coding, reviewing, debugging, and shipping.

Developer jobs

  • Formatting, validating, shrinking, or inspecting code-adjacent text.
  • Preparing snippets for documentation, tickets, commits, or handoff.
  • Checking a small payload quickly without switching tools.

Developer checks

  • Run irreversible transforms like minify or obfuscate on a copy.
  • Keep secrets out of pasted snippets unless the tool explicitly stays local.
  • Use your normal tests or linter before shipping transformed code.

Good next steps

These links move the current task into a more complete workflow.

  1. 1 JSON Formatter & Validator Format, validate, and minify JSON instantly — right in your browser. Open
  2. 2 Python Cheatsheet Python cheat sheet — 100+ idiomatic Python snippets for string, list, dict, file, async, with real examples. Open
  3. 3 TypeScript Cheatsheet TypeScript cheat sheet — 100+ snippets for types, generics, utility types, narrowing, async patterns. Open

Real-world use cases

  • Translating a Python or Node service to Go and hitting the err pattern wall

    You move a 4k-line Node API to Go and suddenly every call site needs if err != nil. Filter to Errors and Control flow, copy the fmt.Errorf %w wrap plus the errors.Is check, and wrap your db and http layers in one pass. The nil-interface pitfall entry saves the afternoon you would have spent on a typed-nil return that read as non-nil.

  • Building a worker pool to drain a 50k-job backlog without leaking goroutines

    You have 50,000 image-resize jobs and a single loop is too slow. Open the Channels section, copy the worker pool with a closed jobs channel and a WaitGroup, and set 8 workers to match your cores. The fan-out fan-in entry and the missing-cancel pitfall keep you from the two classic ways this leaks: never closing the channel and dropping the context cancel.

  • Adding context timeouts to an HTTP client that hangs forever under load

    A downstream call with no deadline pins 200 goroutines during an incident. Filter to Context and stdlib, grab context.WithTimeout with defer cancel and the http.Client with an explicit Timeout field. You ship a 3-second deadline across every outbound call in an hour, and the entry on never throwing away the cancel func stops the leak you almost re-introduced.

  • Teaching a team standup on generics after the 1.18 upgrade

    Half the team has never written a type parameter. Filter to Generics, walk through any, comparable, the ~ and | constraint syntax, and the generic Map and Filter helpers live on screen. Search channel or errors mid-talk to jump to a related idiom without leaving the page, so a 20-minute session stays concrete instead of slideware.

Common pitfalls

  • Returning a typed nil pointer where the signature says error. The interface holds a non-nil type so the caller sees err != nil. In the happy path always return the bare keyword nil, never var p *MyError.

  • Spawning goroutines with context.WithTimeout but forgetting defer cancel(). Even on the success path the timer goroutine lingers; always defer cancel() on the line after you create the context.

  • Writing select with a bare default in a hot loop, which spins the CPU to 100 percent. Use a time.After case or a blocking receive instead, and reserve default for genuine non-blocking polls.

Privacy

Everything runs in your browser. The cheat sheet is one static page and the search filters an in-memory array of snippets, so nothing you type is sent anywhere, logged, or written to the URL. There is no Go execution and no upload. It works behind corporate proxies and on air-gapped networks where you cannot even go install a CLI tool.

FAQ

Tool combos

Folks in your role tend to reach for these alongside this tool.

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