Go Examples
{Back to Index}
Table of Contents
- 1. 基础语法与类型
- 1.1. Hello World
- 1.2. Values
- 1.3. Variables
- 1.4. Constants
- 1.5. For
- 1.6. If/Else
- 1.7. Switch
- 1.8. Arrays
- 1.9. Slices
- 1.10. Maps
- 1.11. Functions
- 1.12. Multiple Return Values
- 1.13. Variadic Functions
- 1.14. Closures
- 1.15. Recursion
- 1.16. Range over Built-in Types
- 1.17. Pointers
- 1.18. Strings and Runes
- 1.19. Structs
- 1.20. Methods
- 1.21. Interfaces
- 1.22. Enums
- 1.23. Struct Embedding
- 1.24. Generics
- 1.25. Range over Iterators
- 1.26. Errors
- 1.27. Custom Errors
- 2. 并发与同步
- 2.1. Goroutines
- 2.2. Channels
- 2.3. Channel Buffering
- 2.4. Channel Synchronization
- 2.5. Channel Directions
- 2.6. Select
- 2.7. Timeouts
- 2.8. Non-Blocking Channel Operations
- 2.9. Closing Channels
- 2.10. Range over Channels
- 2.11. Timers
- 2.12. Tickers
- 2.13. Worker Pools
- 2.14. WaitGroups
- 2.15. Rate Limiting
- 2.16. Atomic Counters
- 2.17. Mutexes
- 2.18. Stateful Goroutines
- 3. 控制流与文本处理
- 4. 数据、时间与编码
- 5. 文件、构建与测试
- 6. 命令行、网络与系统
1. 基础语法与类型
1.1. Hello World
这是 Go 的最小程序:使用 package main 声明可执行程序,导入 fmt=,并在 =main 中调用 fmt.Println 输出文本;可以用 go run 直接运行,也可以用 go build 生成独立二进制文件,体现 Go 从源码到可执行程序的基本流程。
package main import "fmt" func main() { fmt.Println("hello world") }
1.2. Values
Go 直接支持字符串、整数、浮点数和布尔值等基本类型;字符串可以用 + 拼接,数值可以参与算术运算,布尔值可以使用 &&=、=|| 和 ! 等逻辑运算。这个示例重点是熟悉字面量和基本值,而不是展示复杂的类型转换。
package main import "fmt" func main() { fmt.Println("go" + "lang") fmt.Println("1+1 =", 1+1) fmt.Println("7.0/3.0 =", 7.0/3.0) fmt.Println(true && false) fmt.Println(true || false) fmt.Println(!true) }
1.3. Variables
变量可以用 var 显式声明,也可以在函数内部使用 := 同时声明和初始化;Go 编译器会检查变量类型,未显式初始化的变量得到对应类型的零值。需要注意,短变量声明只能在函数体内使用,并且至少要声明一个新变量。
package main import "fmt" func main() { var a = "initial" fmt.Println(a) var b, c int = 1, 2 fmt.Println(b, c) var d = true fmt.Println(d) var e int fmt.Println(e) f := "apple" fmt.Println(f) }
1.4. Constants
常量使用 const 声明,可以是字符、字符串、布尔值或数值;常量表达式支持高精度计算,而且无类型数值常量会在赋值或传参时根据上下文获得类型。常量适合表示编译期不会变化的配置和枚举值。
package main import ( "fmt" "math" ) const s string = "constant" func main() { fmt.Println(s) const n = 500000000 const d = 3e20 / n fmt.Println(d) fmt.Println(int64(d)) fmt.Println(math.Sin(n)) }
1.5. For
Go 只有 for 这一种循环关键字,但它可以写成传统的初始化、条件、递增形式,也可以省略部分内容实现类似 while 的循环,甚至省略全部内容形成无限循环;=break= 和 continue 用于控制循环流程。遍历集合时通常会使用后面的 range 形式。
package main import "fmt" func main() { i := 1 for i <= 3 { fmt.Println(i) i = i + 1 } for j := 0; j < 3; j++ { fmt.Println(j) } for i := range 3 { fmt.Println("range", i) } for { fmt.Println("loop") break } for n := range 6 { if n%2 == 0 { continue } fmt.Println(n) } }
1.6. If/Else
条件分支使用 if 和 =else=,条件表达式不需要括号;Go 还允许在条件前放一个只在该分支作用域内有效的初始化语句。多个条件可以使用 =else if=,但条件必须是布尔表达式,不能把整数等值直接当作真值使用。
package main import "fmt" func main() { if 7%2 == 0 { fmt.Println("7 is even") } else { fmt.Println("7 is odd") } if 8%4 == 0 { fmt.Println("8 is divisible by 4") } if 8%2 == 0 || 7%2 == 0 { fmt.Println("either 8 or 7 are even") } if num := 9; num < 0 { fmt.Println(num, "is negative") } else if num < 10 { fmt.Println(num, "has 1 digit") } else { fmt.Println(num, "has multiple digits") } }
1.7. Switch
switch 用于表达多分支条件,普通形式按值匹配,也可以在一个 case 中写多个表达式;省略表达式时,它可以代替一串 =if/else=。类型 switch 则用于判断接口值的动态类型,适合处理一个接口可能承载多种具体类型的情况。
package main import ( "fmt" "time" ) func main() { i := 2 fmt.Print("Write ", i, " as ") switch i { case 1: fmt.Println("one") case 2: fmt.Println("two") case 3: fmt.Println("three") } switch time.Now().Weekday() { case time.Saturday, time.Sunday: fmt.Println("It's the weekend") default: fmt.Println("It's a weekday") } t := time.Now() switch { case t.Hour() < 12: fmt.Println("It's before noon") default: fmt.Println("It's after noon") } whatAmI := func(i any) { switch t := i.(type) { case bool: fmt.Println("I'm a bool") case int: fmt.Println("I'm an int") default: fmt.Printf("Don't know type %T\n", t) } } whatAmI(true) whatAmI(1) whatAmI("hey") }
1.8. Arrays
数组是长度固定且长度属于类型的一组同类型元素,未初始化元素使用零值;可以按下标访问,也可以用字面量、=…= 自动推导长度和带下标的初始化方式创建。数组可以组合成多维数组,但日常 Go 代码中更常使用长度可变的 slice。
package main import "fmt" func main() { var a [5]int fmt.Println("emp:", a) a[4] = 100 fmt.Println("set:", a) fmt.Println("get:", a[4]) fmt.Println("len:", len(a)) b := [5]int{1, 2, 3, 4, 5} fmt.Println("dcl:", b) b = [...]int{1, 2, 3, 4, 5} fmt.Println("dcl:", b) b = [...]int{100, 3: 400, 500} fmt.Println("idx:", b) var twoD [2][3]int for i := range 2 { for j := range 3 { twoD[i][j] = i + j } } fmt.Println("2d: ", twoD) twoD = [2][3]int{ {1, 2, 3}, {1, 2, 3}, } fmt.Println("2d: ", twoD) }
1.9. Slices
slice 是建立在底层数组之上的动态视图,包含指针、长度和容量;可以用字面量或 make 创建,用切片表达式截取,并用 append 增长。slice 共享底层数组,扩容时可能更换底层存储,因此传递和修改 slice 时要区分“视图变化”和“底层数组变化”。
package main import ( "fmt" "slices" ) func main() { var s []string fmt.Println("uninit:", s, s == nil, len(s) == 0) s = make([]string, 3) fmt.Println("emp:", s, "len:", len(s), "cap:", cap(s)) s[0] = "a" s[1] = "b" s[2] = "c" fmt.Println("set:", s) fmt.Println("get:", s[2]) fmt.Println("len:", len(s)) s = append(s, "d") s = append(s, "e", "f") fmt.Println("apd:", s) c := make([]string, len(s)) copy(c, s) fmt.Println("cpy:", c) l := s[2:5] fmt.Println("sl1:", l) l = s[:5] fmt.Println("sl2:", l) l = s[2:] fmt.Println("sl3:", l) t := []string{"g", "h", "i"} fmt.Println("dcl:", t) t2 := []string{"g", "h", "i"} if slices.Equal(t, t2) { fmt.Println("t == t2") } twoD := make([][]int, 3) for i := range 3 { innerLen := i + 1 twoD[i] = make([]int, innerLen) for j := range innerLen { twoD[i][j] = i + j } } fmt.Println("2d: ", twoD) }
1.10. Maps
map 是键到值的无序映射,使用 make 或字面量创建,通过 m[key]=value 写入、下标读取和 delete 删除;读取时可以用双返回值形式判断键是否存在。遍历 map 的顺序不固定,因此不能依赖 range 的输出顺序。
package main import ( "fmt" "maps" ) func main() { m := make(map[string]int) m["k1"] = 7 m["k2"] = 13 fmt.Println("map:", m) v1 := m["k1"] fmt.Println("v1:", v1) v3 := m["k3"] fmt.Println("v3:", v3) fmt.Println("len:", len(m)) delete(m, "k2") fmt.Println("map:", m) clear(m) fmt.Println("map:", m) _, prs := m["k2"] fmt.Println("prs:", prs) n := map[string]int{"foo": 1, "bar": 2} fmt.Println("map:", n) n2 := map[string]int{"foo": 1, "bar": 2} if maps.Equal(n, n2) { fmt.Println("n == n2") } }
1.11. Functions
函数使用 func 声明,可以接收参数并返回结果;Go 函数是一等值,能够赋给变量、作为参数传递或作为返回值。函数参数和返回值的类型必须明确,多个参数可以共享类型声明,良好的函数拆分有助于控制程序复杂度。
package main import "fmt" func plus(a int, b int) int { return a + b } func plusPlus(a, b, c int) int { return a + b + c } func main() { res := plus(1, 2) fmt.Println("1+2 =", res) res = plusPlus(1, 2, 3) fmt.Println("1+2+3 =", res) }
1.12. Multiple Return Values
Go 函数可以返回多个值,常用于同时返回计算结果和错误;调用者可以把多个结果分别赋给变量,也可以使用空白标识符 _ 忽略其中一项。这种设计让错误处理显式出现在调用点,是 Go API 中非常常见的习惯。
package main import "fmt" func vals() (int, int) { return 3, 7 } func main() { a, b := vals() fmt.Println(a) fmt.Println(b) _, c := vals() fmt.Println(c) }
1.13. Variadic Functions
可变参数函数在参数类型前使用 ...=,调用时可以传入任意数量的尾部参数;函数内部它表现为一个 slice,也可以用 =slice... 把已有 slice 展开传入。=fmt.Println= 等标准库函数就是这种设计的典型例子。
package main import "fmt" func sum(nums ...int) { fmt.Print(nums, " ") total := 0 for _, num := range nums { total += num } fmt.Println(total) } func main() { sum(1, 2) sum(1, 2, 3) nums := []int{1, 2, 3, 4} sum(nums...) }
1.14. Closures
Go 支持匿名函数,匿名函数如果引用外部变量,就会形成闭包并保存这部分词法环境;因此函数可以携带自己的状态。多个调用同一个闭包会共享它捕获的变量,而多次创建的闭包通常拥有彼此独立的状态。
package main import "fmt" func intSeq() func() int { i := 0 return func() int { i++ return i } } func main() { nextInt := intSeq() fmt.Println(nextInt()) fmt.Println(nextInt()) fmt.Println(nextInt()) newInts := intSeq() fmt.Println(newInts()) }
1.15. Recursion
递归函数会在函数体内调用自身,适合表达树遍历、阶乘或分治等问题;递归必须有明确的终止条件,否则会不断消耗栈空间。Go 的函数可以直接递归调用,也可以通过闭包或函数变量组织更复杂的递归逻辑。
package main import "fmt" func fact(n int) int { if n == 0 { return 1 } return n * fact(n-1) } func main() { fmt.Println(fact(7)) var fib func(n int) int fib = func(n int) int { if n < 2 { return n } return fib(n-1) + fib(n-2) } fmt.Println(fib(7)) }
1.16. Range over Built-in Types
range 可以遍历数组、slice、map、字符串和 channel,并根据被遍历对象返回索引、键、值或 rune;如果只需要一个结果,可以使用 _ 忽略另一个结果。遍历 map 的顺序不保证,遍历字符串时得到的是 UTF-8 解码后的 rune 及其字节偏移。
package main import "fmt" func main() { nums := []int{2, 3, 4} sum := 0 for _, num := range nums { sum += num } fmt.Println("sum:", sum) for i, num := range nums { if num == 3 { fmt.Println("index:", i) } } kvs := map[string]string{"a": "apple", "b": "banana"} for k, v := range kvs { fmt.Printf("%s -> %s\n", k, v) } for k := range kvs { fmt.Println("key:", k) } for i, c := range "go" { fmt.Println(i, c) } }
1.17. Pointers
指针保存变量的地址,使用 & 取地址、=*= 解引用;函数接收指针后可以修改调用方的变量,也可以用指针表达“没有值”的状态。Go 允许返回局部变量的指针,因为垃圾回收器会保证仍被引用的对象继续存活,但应避免不必要的指针共享。
package main import "fmt" func zeroval(ival int) { ival = 0 } func zeroptr(iptr *int) { *iptr = 0 } func main() { i := 1 fmt.Println("initial:", i) zeroval(i) fmt.Println("zeroval:", i) zeroptr(&i) fmt.Println("zeroptr:", i) fmt.Println("pointer:", &i) p := new(42) fmt.Println("value at *p:", *p) zeroptr(p) fmt.Println("value at *p:", *p) }
1.18. Strings and Runes
Go 的 string 是只读的字节序列,通常保存 UTF-8 文本;一个用户感知的字符可能占多个字节,因此字符串下标得到的是单个字节,而 range 遍历得到的是 rune。需要按 Unicode 字符计数或处理非法 UTF-8 时,应使用 unicode/utf8 等相关 API。
package main import ( "fmt" "unicode/utf8" ) func main() { const s = "สวัสดี" fmt.Println("Len:", len(s)) for i := 0; i < len(s); i++ { fmt.Printf("%x ", s[i]) } fmt.Println() fmt.Println("Rune count:", utf8.RuneCountInString(s)) for idx, runeValue := range s { fmt.Printf("%#U starts at %d\n", runeValue, idx) } fmt.Println("\nUsing DecodeRuneInString") for i, w := 0, 0; i < len(s); i += w { runeValue, width := utf8.DecodeRuneInString(s[i:]) fmt.Printf("%#U starts at %d\n", runeValue, i) w = width examineRune(runeValue) } } func examineRune(r rune) { if r == 't' { fmt.Println("found tee") } else if r == 'ส' { fmt.Println("found so sua") } }
1.19. Structs
struct 是由多个有名字字段组成的类型,适合表达记录和领域对象;可以使用位置或字段名初始化,也可以通过指针访问字段,编译器会自动完成常见的指针解引用。构造函数通常只是命名约定,例如 =newPerson=,并不是语言内置的特殊语法。
package main import "fmt" type person struct { name string age int } func newPerson(name string) *person { p := person{name: name} p.age = 42 return &p } func main() { fmt.Println(person{"Bob", 20}) fmt.Println(person{name: "Alice", age: 30}) fmt.Println(person{name: "Fred"}) fmt.Println(&person{name: "Ann", age: 40}) fmt.Println(newPerson("Jon")) s := person{name: "Sean", age: 50} fmt.Println(s.name) sp := &s fmt.Println(sp.age) sp.age = 51 fmt.Println(sp.age) dog := struct { name string isGood bool }{ "Rex", true, } fmt.Println(dog) }
1.20. Methods
方法是带有 receiver 的函数,可以绑定到自定义类型上;值 receiver 会复制接收者,指针 receiver 则可以修改原对象并避免复制较大的结构。方法集合还会影响一个类型是否满足接口,因此选择值 receiver 还是指针 receiver 时要考虑接口和可变性。
package main import "fmt" type rect struct { width, height int } func (r *rect) area() int { return r.width * r.height } func (r rect) perim() int { return 2*r.width + 2*r.height } func main() { r := rect{width: 10, height: 5} fmt.Println("area: ", r.area()) fmt.Println("perim:", r.perim()) rp := &r fmt.Println("area: ", rp.area()) fmt.Println("perim:", rp.perim()) }
1.21. Interfaces
接口只描述方法集合,类型不需要显式声明实现某个接口,只要方法集合匹配就自动满足;这使调用方可以依赖行为而不是具体类型。接口值包含动态类型和值,空接口可以承载任意值,但使用时应注意类型断言失败和 nil 接口的区别。
package main import ( "fmt" "math" ) type geometry interface { area() float64 perim() float64 } type rect struct { width, height float64 } type circle struct { radius float64 } func (r rect) area() float64 { return r.width * r.height } func (r rect) perim() float64 { return 2*r.width + 2*r.height } func (c circle) area() float64 { return math.Pi * c.radius * c.radius } func (c circle) perim() float64 { return 2 * math.Pi * c.radius } func measure(g geometry) { fmt.Println(g) fmt.Println(g.area()) fmt.Println(g.perim()) } func detectCircle(g geometry) { if c, ok := g.(circle); ok { fmt.Println("circle with radius", c.radius) } } func main() { r := rect{width: 3, height: 4} c := circle{radius: 5} measure(r) measure(c) detectCircle(r) detectCircle(c) }
1.22. Enums
Go 没有独立的 enum 关键字,通常使用自定义整数类型、常量和 iota 组合实现有限状态集合;实现 fmt.Stringer 可以让状态打印成可读文本。自定义类型还能阻止把普通整数直接传入状态转换函数,从而获得一定的编译期类型安全。
package main import "fmt" type ServerState int const ( StateIdle ServerState = iota StateConnected StateError StateRetrying ) var stateName = map[ServerState]string{ StateIdle: "idle", StateConnected: "connected", StateError: "error", StateRetrying: "retrying", } func (ss ServerState) String() string { return stateName[ss] } func main() { ns := transition(StateIdle) fmt.Println(ns) ns2 := transition(ns) fmt.Println(ns2) } func transition(s ServerState) ServerState { switch s { case StateIdle: return StateConnected case StateConnected, StateRetrying: return StateIdle case StateError: return StateError default: panic(fmt.Errorf("unknown state: %s", s)) } }
1.23. Struct Embedding
struct embedding 通过不带字段名的嵌入字段组合类型,嵌入类型的字段和方法可以被外层类型直接访问,方法集也可能因此满足某个接口。它是组合而不是传统继承;不要把它和 //go:embed 文件嵌入指令混淆。
package main import "fmt" type base struct { num int } func (b base) describe() string { return fmt.Sprintf("base with num=%v", b.num) } type container struct { base str string } func main() { co := container{ base: base{ num: 1, }, str: "some name", } fmt.Printf("co={num: %v, str: %v}\n", co.num, co.str) fmt.Println("also num:", co.base.num) fmt.Println("describe:", co.describe()) type describer interface { describe() string } var d describer = co fmt.Println("describer:", d.describe()) }
1.24. Generics
泛型允许函数和类型声明类型参数,从而在保持静态类型检查的同时复用适用于多种类型的算法;类型约束可以限制类型参数支持的操作。泛型适合抽象真正具有相同逻辑的代码,不应为了消除少量重复而牺牲可读性。
package main import "fmt" func SlicesIndex[S ~[]E, E comparable](s S, v E) int { for i := range s { if v == s[i] { return i } } return -1 } type List[T any] struct { head, tail *element[T] } type element[T any] struct { next *element[T] val T } func (lst *List[T]) Push(v T) { if lst.tail == nil { lst.head = &element[T]{val: v} lst.tail = lst.head } else { lst.tail.next = &element[T]{val: v} lst.tail = lst.tail.next } } func (lst *List[T]) AllElements() []T { var elems []T for e := lst.head; e != nil; e = e.next { elems = append(elems, e.val) } return elems } func main() { var s = []string{"foo", "bar", "zoo"} fmt.Println("index of zoo:", SlicesIndex(s, "zoo")) _ = SlicesIndex[[]string, string](s, "zoo") lst := List[int]{} lst.Push(10) lst.Push(13) lst.Push(23) fmt.Println("list:", lst.AllElements()) }
1.25. Range over Iterators
Go 支持让 range 遍历迭代器函数,迭代器可以通过回调逐步产生一个值或键值对;调用者不必显式管理迭代状态。这个模式适合懒加载、树遍历和流式数据处理,但迭代器的终止、错误传播和提前退出需要在设计时明确。
package main import ( "fmt" "iter" "slices" "strings" ) type List[T any] struct { head, tail *element[T] } type element[T any] struct { next *element[T] val T } func (lst *List[T]) Push(v T) { if lst.tail == nil { lst.head = &element[T]{val: v} lst.tail = lst.head } else { lst.tail.next = &element[T]{val: v} lst.tail = lst.tail.next } } func (lst *List[T]) All() iter.Seq[T] { return func(yield func(T) bool) { for e := lst.head; e != nil; e = e.next { if !yield(e.val) { return } } } } func genFib() iter.Seq[int] { return func(yield func(int) bool) { a, b := 0, 1 for { if !yield(a) { return } a, b = b, a+b } } } func main() { lst := List[int]{} lst.Push(10) lst.Push(13) lst.Push(23) for e := range lst.All() { fmt.Println(e) } all := slices.Collect(lst.All()) fmt.Println("all:", all) for part := range strings.SplitSeq("go-by-example", "-") { fmt.Printf("part: %s\n", part) } for n := range genFib() { if n >= 10 { break } fmt.Println(n) } }
1.26. Errors
Go 通常把错误作为函数的最后一个返回值,成功时返回 nil=;可以用 =errors.New 创建错误,用 fmt.Errorf("%w") 包装错误,并通过 errors.Is 判断错误链中是否包含特定错误。错误处理应在靠近产生和调用的位置完成,而不是依赖异常机制把控制流跳到远处。
package main import ( "errors" "fmt" ) func f(arg int) (int, error) { if arg == 42 { return -1, errors.New("can't work with 42") } return arg + 3, nil } var ErrOutOfTea = errors.New("no more tea available") var ErrPower = errors.New("can't boil water") func makeTea(arg int) error { if arg == 2 { return ErrOutOfTea } else if arg == 4 { return fmt.Errorf("making tea: %w", ErrPower) } return nil } func main() { for _, i := range []int{7, 42} { if r, e := f(i); e != nil { fmt.Println("f failed:", e) } else { fmt.Println("f worked:", r) } } for i := range 5 { if err := makeTea(i); err != nil { if errors.Is(err, ErrOutOfTea) { fmt.Println("We should buy new tea!") } else if errors.Is(err, ErrPower) { fmt.Println("Now it is dark.") } else { fmt.Printf("unknown error: %s\n", err) } continue } fmt.Println("Tea is ready!") } }
1.27. Custom Errors
自定义错误类型只要实现 Error() string 就满足 error 接口,可以携带参数、状态码等结构化信息;通过 errors.AsType 或相关错误链 API 可以把错误恢复为具体类型。自定义错误应表达调用者确实需要处理的语义,普通描述信息则使用包装错误即可。
package main import ( "errors" "fmt" ) type argError struct { arg int message string } func (e *argError) Error() string { return fmt.Sprintf("%d - %s", e.arg, e.message) } func f(arg int) (int, error) { if arg == 42 { return -1, &argError{arg, "can't work with it"} } return arg + 3, nil } func main() { _, err := f(42) if ae, ok := errors.AsType[*argError](err); ok { fmt.Println(ae.arg) fmt.Println(ae.message) } else { fmt.Println("err doesn't match argError") } }
2. 并发与同步
2.1. Goroutines
goroutine 是由 Go 运行时调度的轻量并发执行单元,只需在函数调用前加 go 就能异步运行;主 goroutine 结束时进程也会结束,因此示例使用延迟或同步手段等待工作完成。goroutine 很便宜但并非没有成本,必须设计好退出和资源释放路径。
package main import ( "fmt" "time" ) func f(from string) { for i := range 3 { fmt.Println(from, ":", i) } } func main() { f("direct") go f("goroutine") go func(msg string) { fmt.Println(msg) }("going") time.Sleep(time.Second) fmt.Println("done") }
2.2. Channels
channel 是 goroutine 之间传递类型化数据的管道,使用 ch <- value 发送、=value := <-ch= 接收;无缓冲 channel 的发送和接收会相互等待,因此天然提供同步点。channel 适合传递所有权和结果,但不应把所有共享状态都机械地改写成 channel。
package main import "fmt" func main() { messages := make(chan string) go func() { messages <- "ping" }() msg := <-messages fmt.Println(msg) }
2.3. Channel Buffering
无缓冲 channel 只有在发送者和接收者同时准备好时才能完成发送,而带缓冲 channel 可以在容量范围内暂存数据;=make(chan T, n)= 创建容量为 n 的缓冲 channel。缓冲区只能吸收有限的突发流量,容量过大可能掩盖消费者变慢或 goroutine 泄漏问题。
package main import "fmt" func main() { messages := make(chan string, 2) messages <- "buffered" messages <- "channel" fmt.Println(<-messages) fmt.Println(<-messages) }
2.4. Channel Synchronization
可以使用一个 channel 作为完成通知:工作 goroutine 完成后发送值,等待方通过接收操作阻塞直到收到通知。这个方式适合等待一个明确的异步事件;需要等待多个 goroutine 或传播错误时,通常应考虑 sync.WaitGroup 或 =errgroup=。
package main import ( "fmt" "time" ) func worker(done chan bool) { fmt.Print("working...") time.Sleep(time.Second) fmt.Println("done") done <- true } func main() { done := make(chan bool, 1) go worker(done) <-done }
2.5. Channel Directions
channel 类型可以限制为只发送的 chan<- T 或只接收的 =<-chan T=,作为函数参数时能够表达调用约束并获得编译期检查。把双向 channel 传给只读或只写参数是允许的,但反向操作会在编译期被拒绝。
package main import "fmt" func ping(pings chan<- string, msg string) { pings <- msg } func pong(pings <-chan string, pongs chan<- string) { msg := <-pings pongs <- msg } func main() { pings := make(chan string, 1) pongs := make(chan string, 1) ping(pings, "passed message") pong(pings, pongs) fmt.Println(<-pongs) }
2.6. Select
select 同时等待多个 channel 操作,哪个 case 先准备好就执行哪个;它还可以配合 default 实现非阻塞尝试。多个 case 同时就绪时选择是伪随机的,因此不要把 case 顺序当作优先级;空的 select 会永久阻塞。
package main import ( "fmt" "time" ) func main() { c1 := make(chan string) c2 := make(chan string) go func() { time.Sleep(1 * time.Second) c1 <- "one" }() go func() { time.Sleep(2 * time.Second) c2 <- "two" }() for range 2 { select { case msg1 := <-c1: fmt.Println("received", msg1) case msg2 := <-c2: fmt.Println("received", msg2) } } }
2.7. Timeouts
使用 select 同时等待工作结果和 time.After 产生的超时事件,可以给外部调用设置明确的最长等待时间;示例对比了操作在 2 秒完成而超时时间分别为 1 秒和 3 秒的情况。超时后还要考虑后台 goroutine 是否能退出,避免结果无人接收导致泄漏。
package main import ( "fmt" "time" ) func main() { c1 := make(chan string, 1) go func() { time.Sleep(2 * time.Second) c1 <- "result 1" }() select { case res := <-c1: fmt.Println(res) case <-time.After(1 * time.Second): fmt.Println("timeout 1") } c2 := make(chan string, 1) go func() { time.Sleep(2 * time.Second) c2 <- "result 2" }() select { case res := <-c2: fmt.Println(res) case <-time.After(3 * time.Second): fmt.Println("timeout 2") } }
2.8. Non-Blocking Channel Operations
在 channel 的 select 中加入 =default=,就能在发送或接收当前无法立即完成时继续执行其他逻辑,从而实现非阻塞操作。它适合轮询、尝试发送和可选事件处理,但高频空转会消耗 CPU,必要时应加入定时器或退避。
package main import "fmt" func main() { messages := make(chan string) signals := make(chan bool) select { case msg := <-messages: fmt.Println("received message", msg) default: fmt.Println("no message received") } msg := "hi" select { case messages <- msg: fmt.Println("sent message", msg) default: fmt.Println("no message sent") } select { case msg := <-messages: fmt.Println("received message", msg) case sig := <-signals: fmt.Println("received signal", sig) default: fmt.Println("no activity") } }
2.9. Closing Channels
关闭 channel 表示发送方不会再发送数据,接收方可以通过双返回值接收判断 channel 是否已经关闭;关闭后的 channel 仍可读取剩余数据,空且关闭的 channel 返回元素类型的零值。通常由发送方负责关闭,而且同一个 channel 不能重复关闭,否则会触发 panic。
package main import "fmt" func main() { jobs := make(chan int, 5) done := make(chan bool) go func() { for { j, more := <-jobs if more { fmt.Println("received job", j) } else { fmt.Println("received all jobs") done <- true return } } }() for j := 1; j <= 3; j++ { jobs <- j fmt.Println("sent job", j) } close(jobs) fmt.Println("sent all jobs") <-done _, ok := <-jobs fmt.Println("received more jobs:", ok) }
2.10. Range over Channels
对 channel 使用 range 会持续接收数据,直到 channel 被关闭;这让生产者和消费者之间的循环更自然,也避免手动反复检查关闭状态。生产者必须最终关闭 channel,否则消费者会一直等待;关闭 channel 不会自动终止仍在运行的发送 goroutine。
package main import "fmt" func main() { queue := make(chan string, 2) queue <- "one" queue <- "two" close(queue) for elem := range queue { fmt.Println(elem) } }
2.11. Timers
timer 表示未来只发生一次的事件,通过它的 C channel 等待触发;与 time.Sleep 不同,timer 可以在触发前使用 Stop 取消。需要复用 timer 时还要正确处理已触发事件和 channel 清空,否则可能读到旧事件或产生竞态。
package main import ( "fmt" "time" ) func main() { timer1 := time.NewTimer(2 * time.Second) <-timer1.C fmt.Println("Timer 1 fired") timer2 := time.NewTimer(time.Second) go func() { <-timer2.C fmt.Println("Timer 2 fired") }() stop2 := timer2.Stop() if stop2 { fmt.Println("Timer 2 stopped") } time.Sleep(2 * time.Second) }
2.12. Tickers
ticker 按固定间隔重复向 channel 发送时间事件,适合周期性任务;调用 Stop 后不会再产生新的 tick。ticker 持有运行时资源,任务结束时应显式停止,且不应把每次 tick 都当作一定会被逐个处理的可靠消息。
package main import ( "fmt" "time" ) func main() { ticker := time.NewTicker(500 * time.Millisecond) done := make(chan bool) go func() { for { select { case <-done: return case t := <-ticker.C: fmt.Println("Tick at", t) } } }() time.Sleep(1600 * time.Millisecond) ticker.Stop() done <- true fmt.Println("Ticker stopped") }
2.13. Worker Pools
worker pool 用多个 goroutine 从 jobs channel 取任务,再把结果写入 results channel;任务数量可以多于 worker 数量,而并发度由 worker 数量控制。关闭 jobs channel 表示没有新任务,收集所有结果也同时保证 worker 能够结束,适合批量任务和受控并发。
package main import ( "fmt" "time" ) func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Println("worker", id, "started job", j) time.Sleep(time.Second) fmt.Println("worker", id, "finished job", j) results <- j * 2 } } func main() { const numJobs = 5 jobs := make(chan int, numJobs) results := make(chan int, numJobs) for w := 1; w <= 3; w++ { go worker(w, jobs, results) } for j := 1; j <= numJobs; j++ { jobs <- j } close(jobs) for a := 1; a <= numJobs; a++ { <-results } }
2.14. WaitGroups
sync.WaitGroup 用于等待一组 goroutine 全部完成,启动任务前增加计数,任务结束时调用 Done=,主流程用 =Wait 阻塞等待。计数器必须在 goroutine 启动前正确增加,并且 WaitGroup 通常通过指针传递;它只负责等待,不负责传递错误或结果。
package main import ( "fmt" "sync" "time" ) func worker(id int) { fmt.Printf("Worker %d starting\n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done\n", id) } func main() { var wg sync.WaitGroup for i := 1; i <= 5; i++ { wg.Go(func() { worker(i) }) } wg.Wait() }
2.15. Rate Limiting
可以用 ticker 作为定时令牌源,对每次操作设置固定间隔,从而限制请求速率;也可以使用带缓冲 channel 模拟令牌桶,允许有限的突发请求。限速策略应区分单个 goroutine、整个进程和分布式系统三个范围,示例只展示进程内的基础机制。
package main import ( "fmt" "time" ) func main() { requests := make(chan int, 5) for i := 1; i <= 5; i++ { requests <- i } close(requests) limiter := time.Tick(200 * time.Millisecond) for req := range requests { <-limiter fmt.Println("request", req, time.Now()) } burstyLimiter := make(chan time.Time, 3) for range 3 { burstyLimiter <- time.Now() } go func() { for t := range time.Tick(200 * time.Millisecond) { burstyLimiter <- t } }() burstyRequests := make(chan int, 5) for i := 1; i <= 5; i++ { burstyRequests <- i } close(burstyRequests) for req := range burstyRequests { <-burstyLimiter fmt.Println("request", req, time.Now()) } }
2.16. Atomic Counters
sync/atomic 提供无需互斥锁的原子读写和加法操作,适合多个 goroutine 共享简单计数器;示例启动多个 goroutine 并发增加操作次数,最后用原子读取获得稳定结果。原子操作只保证特定变量访问的原子性,复杂状态仍需要 mutex 或更高层的同步设计。
package main import ( "fmt" "sync" "sync/atomic" ) func main() { var ops atomic.Uint64 var wg sync.WaitGroup for range 50 { wg.Go(func() { for range 1000 { ops.Add(1) } }) } wg.Wait() fmt.Println("ops:", ops.Load()) }
2.17. Mutexes
互斥锁用来保护多个 goroutine 共享的临界区,=sync.Mutex= 的 Lock 和 Unlock 应成对出现,常见写法是在成功加锁后立即 =defer Unlock=。锁的保护范围要覆盖所有相关读写,避免把受保护数据的一部分暴露给无锁访问。
package main import ( "fmt" "sync" ) type Container struct { mu sync.Mutex counters map[string]int } func (c *Container) inc(name string) { c.mu.Lock() defer c.mu.Unlock() c.counters[name]++ } func main() { c := Container{ counters: map[string]int{"a": 0, "b": 0}, } var wg sync.WaitGroup doIncrement := func(name string, n int) { for range n { c.inc(name) } } wg.Go(func() { doIncrement("a", 10000) }) wg.Go(func() { doIncrement("a", 10000) }) wg.Go(func() { doIncrement("b", 10000) }) wg.Wait() fmt.Println(c.counters) }
2.18. Stateful Goroutines
除了锁和原子操作,还可以让一个专门的 goroutine 独占状态,其他 goroutine 通过 channel 发送查询或更新请求;这样状态访问顺序由该 goroutine 串行决定。这个模型能减少共享内存竞态,但需要设计请求协议、退出机制和高负载下的队列行为。
package main import ( "fmt" "math/rand" "sync/atomic" "time" ) type readOp struct { key int resp chan int } type writeOp struct { key int val int resp chan bool } func main() { var readOps uint64 var writeOps uint64 reads := make(chan readOp) writes := make(chan writeOp) go func() { var state = make(map[int]int) for { select { case read := <-reads: read.resp <- state[read.key] case write := <-writes: state[write.key] = write.val write.resp <- true } } }() for range 100 { go func() { for { read := readOp{ key: rand.Intn(5), resp: make(chan int)} reads <- read <-read.resp atomic.AddUint64(&readOps, 1) time.Sleep(time.Millisecond) } }() } for range 10 { go func() { for { write := writeOp{ key: rand.Intn(5), val: rand.Intn(100), resp: make(chan bool)} writes <- write <-write.resp atomic.AddUint64(&writeOps, 1) time.Sleep(time.Millisecond) } }() } time.Sleep(time.Second) readOpsFinal := atomic.LoadUint64(&readOps) fmt.Println("readOps:", readOpsFinal) writeOpsFinal := atomic.LoadUint64(&writeOps) fmt.Println("writeOps:", writeOpsFinal) }
3. 控制流与文本处理
3.1. Sorting
Go 的 slices 和 sort 相关 API 可以对基本类型集合进行排序,也可以检查集合是否已经有序;排序通常会直接修改传入的 slice。排序前应确认是否允许改变原数据,并注意字符串排序按字节/Unicode 码点规则处理,不等同于自然语言排序。
package main import ( "fmt" "slices" ) func main() { strs := []string{"c", "a", "b"} slices.Sort(strs) fmt.Println("Strings:", strs) ints := []int{7, 2, 4} slices.Sort(ints) fmt.Println("Ints: ", ints) s := slices.IsSorted(ints) fmt.Println("Sorted: ", s) }
3.2. Sorting by Functions
自定义排序可以提供比较函数,或者使用支持比较器的 slice 排序 API,按结构体字段、字符串长度等业务规则排序。比较函数必须建立稳定的一致顺序,否则排序结果可能不确定;需要保留原顺序或避免修改输入时,应先复制 slice。
package main import ( "cmp" "fmt" "slices" ) func main() { fruits := []string{"peach", "banana", "kiwi"} lenCmp := func(a, b string) int { return cmp.Compare(len(a), len(b)) } slices.SortFunc(fruits, lenCmp) fmt.Println(fruits) type Person struct { name string age int } people := []Person{ Person{name: "Jax", age: 37}, Person{name: "TJ", age: 25}, Person{name: "Alex", age: 72}, } slices.SortFunc(people, func(a, b Person) int { return cmp.Compare(a.age, b.age) }) fmt.Println(people) }
3.3. Panic
panic 表示程序遇到无法正常恢复的异常状态,可以传入任意值并立即停止当前函数的普通执行流程;未被恢复时会沿调用栈展开并终止程序。普通输入错误、网络错误和文件错误不应滥用 panic,而应通过显式 error 返回。
package main import ( "os" "path/filepath" ) func main() { panic("a problem") path := filepath.Join(os.TempDir(), "file") _, err := os.Create(path) if err != nil { panic(err) } }
3.4. Defer
defer 把函数调用安排到当前函数返回前执行,常用于关闭文件、解锁和释放资源;多个 defer 按后进先出的顺序运行。资源成功获取后应尽快注册清理动作,同时不要忽略 deferred Close 等操作可能产生的错误。
package main import ( "fmt" "os" "path/filepath" ) func main() { path := filepath.Join(os.TempDir(), "defer.txt") f := createFile(path) defer closeFile(f) writeFile(f) } func createFile(p string) *os.File { fmt.Println("creating") f, err := os.Create(p) if err != nil { panic(err) } return f } func writeFile(f *os.File) { fmt.Println("writing") fmt.Fprintln(f, "data") } func closeFile(f *os.File) { fmt.Println("closing") err := f.Close() if err != nil { panic(err) } }
3.5. Recover
recover 只能在 deferred 函数中捕获当前 goroutine 的 panic,并把控制权恢复到正常返回路径;HTTP 服务或任务边界有时会用它防止单个请求拖垮整个进程。recover 不会跨 goroutine 生效,也不应掩盖所有 panic 后继续运行在未知状态的程序。
package main import "fmt" func mayPanic() { panic("a problem") } func main() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered. Error:\n", r) } }() mayPanic() fmt.Println("After mayPanic()") }
3.6. String Functions
strings 包提供常用字符串操作,例如查找子串、计数、前后缀判断、替换、大小写转换、拆分、连接和重复;这些函数通常返回新字符串,不会修改原 string。处理 Unicode 文本时要区分字节长度、rune 数量和用户感知字符数量。
package main import ( "fmt" s "strings" ) var p = fmt.Println func main() { p("Contains: ", s.Contains("test", "es")) p("Count: ", s.Count("test", "t")) p("HasPrefix: ", s.HasPrefix("test", "te")) p("HasSuffix: ", s.HasSuffix("test", "st")) p("Index: ", s.Index("test", "e")) p("Join: ", s.Join([]string{"a", "b"}, "-")) p("Repeat: ", s.Repeat("a", 5)) p("Replace: ", s.Replace("foo", "o", "0", -1)) p("Replace: ", s.Replace("foo", "o", "0", 1)) p("Split: ", s.Split("a-b-c-d-e", "-")) p("ToLower: ", s.ToLower("TEST")) p("ToUpper: ", s.ToUpper("test")) }
3.7. String Formatting
fmt 包使用格式动词把值格式化为字符串或输出到 Writer,支持占位符、宽度、精度和不同进制等效果;=Sprintf= 返回字符串,=Printf= 直接输出,=Fprintf= 可以写入任意 io.Writer。格式动词与参数类型不匹配时会产生带提示的格式化结果,因此应认真检查格式串。
package main import ( "fmt" "os" ) type point struct { x, y int } func main() { p := point{1, 2} fmt.Printf("struct1: %v\n", p) fmt.Printf("struct2: %+v\n", p) fmt.Printf("struct3: %#v\n", p) fmt.Printf("type: %T\n", p) fmt.Printf("bool: %t\n", true) fmt.Printf("int: %d\n", 123) fmt.Printf("bin: %b\n", 14) fmt.Printf("char: %c\n", 33) fmt.Printf("hex: %x\n", 456) fmt.Printf("float1: %f\n", 78.9) fmt.Printf("float2: %e\n", 123400000.0) fmt.Printf("float3: %E\n", 123400000.0) fmt.Printf("str1: %s\n", "\"string\"") fmt.Printf("str2: %q\n", "\"string\"") fmt.Printf("str3: %x\n", "hex this") fmt.Printf("pointer: %p\n", &p) fmt.Printf("width1: |%6d|%6d|\n", 12, 345) fmt.Printf("width2: |%6.2f|%6.2f|\n", 1.2, 3.45) fmt.Printf("width3: |%-6.2f|%-6.2f|\n", 1.2, 3.45) fmt.Printf("width4: |%6s|%6s|\n", "foo", "b") fmt.Printf("width5: |%-6s|%-6s|\n", "foo", "b") s := fmt.Sprintf("sprintf: a %s", "string") fmt.Println(s) fmt.Fprintf(os.Stderr, "io: an %s\n", "error") }
3.8. Text Templates
text/template 用模板动作和数据生成动态文本,支持字段访问、map、条件和 range 循环;模板可以从字符串解析,也可以通过 template.Must 在初始化阶段把解析错误转为 panic。生成 HTML 时应使用拥有自动转义能力的 =html/template=,不要直接把不可信数据拼接进页面。
package main import ( "os" "text/template" ) func main() { t1 := template.New("t1") t1, err := t1.Parse("Value is {{.}}\n") if err != nil { panic(err) } t1 = template.Must(t1.Parse("Value: {{.}}\n")) t1.Execute(os.Stdout, "some text") t1.Execute(os.Stdout, 5) t1.Execute(os.Stdout, []string{ "Go", "Rust", "C++", "C#", }) Create := func(name, t string) *template.Template { return template.Must(template.New(name).Parse(t)) } t2 := Create("t2", "Name: {{.Name}}\n") t2.Execute(os.Stdout, struct { Name string }{"Jane Doe"}) t2.Execute(os.Stdout, map[string]string{ "Name": "Mickey Mouse", }) t3 := Create("t3", "{{if . -}} yes {{else -}} no {{end}}\n") t3.Execute(os.Stdout, "not empty") t3.Execute(os.Stdout, "") t4 := Create("t4", "Range: {{range .}}{{.}} {{end}}\n") t4.Execute(os.Stdout, []string{ "Go", "Rust", "C++", "C#", }) }
3.9. Regular Expressions
regexp 包使用正则表达式进行匹配、查找、替换和捕获分组;=MustCompile= 适合固定且已知正确的模式,运行时模式则应显式处理编译错误。正则表达式适合局部文本模式,不适合替代完整的 HTML、JSON 或复杂编程语言解析器。
package main import ( "bytes" "fmt" "regexp" ) func main() { match, _ := regexp.MatchString("p([a-z]+)ch", "peach") fmt.Println(match) r, _ := regexp.Compile("p([a-z]+)ch") fmt.Println(r.MatchString("peach")) fmt.Println(r.FindString("peach punch")) fmt.Println("idx:", r.FindStringIndex("peach punch")) fmt.Println(r.FindStringSubmatch("peach punch")) fmt.Println(r.FindStringSubmatchIndex("peach punch")) fmt.Println(r.FindAllString("peach punch pinch", -1)) fmt.Println("all:", r.FindAllStringSubmatchIndex( "peach punch pinch", -1)) fmt.Println(r.FindAllString("peach punch pinch", 2)) fmt.Println(r.Match([]byte("peach"))) r = regexp.MustCompile("p([a-z]+)ch") fmt.Println("regexp:", r) fmt.Println(r.ReplaceAllString("a peach", "<fruit>")) in := []byte("a peach") out := r.ReplaceAllFunc(in, bytes.ToUpper) fmt.Println(string(out)) }
4. 数据、时间与编码
4.1. JSON
encoding/json 可以把 struct、map 和 slice 编码成 JSON,也能把 JSON 解码到结构体或通用值;struct tag 如 json:"field" 控制字段名和忽略规则。编码和解码都可能失败,生产代码应检查错误,并注意数字精度、未知字段以及 nil 与空集合的区别。
package main import ( "bytes" "encoding/json/v2" "fmt" "strings" ) type response1 struct { Page int Fruits []string } type response2 struct { Page int `json:"page"` Fruits []string `json:"fruits"` } func main() { bolB, _ := json.Marshal(true) fmt.Println(string(bolB)) intB, _ := json.Marshal(1) fmt.Println(string(intB)) fltB, _ := json.Marshal(2.34) fmt.Println(string(fltB)) strB, _ := json.Marshal("gopher") fmt.Println(string(strB)) slcD := []string{"apple", "peach", "pear"} slcB, _ := json.Marshal(slcD) fmt.Println(string(slcB)) mapD := map[string]int{"apple": 5, "lettuce": 7} mapB, _ := json.Marshal(mapD) fmt.Println(string(mapB)) res1D := &response1{ Page: 1, Fruits: []string{"apple", "peach", "pear"}} res1B, _ := json.Marshal(res1D) fmt.Println(string(res1B)) res2D := &response2{ Page: 1, Fruits: []string{"apple", "peach", "pear"}} res2B, _ := json.Marshal(res2D) fmt.Println(string(res2B)) byt := []byte(`{"num":6.13,"strs":["a","b"]}`) var dat map[string]any if err := json.Unmarshal(byt, &dat); err != nil { panic(err) } fmt.Println(dat) num := dat["num"].(float64) fmt.Println(num) strs := dat["strs"].([]any) str1 := strs[0].(string) fmt.Println(str1) str := `{"page": 1, "fruits": ["apple", "peach"]}` res := response2{} _ = json.Unmarshal([]byte(str), &res) fmt.Println(res) fmt.Println(res.Fruits[0]) d := map[string]int{"apple": 5, "lettuce": 7} var buf bytes.Buffer _ = json.MarshalWrite(&buf, d) fmt.Println(buf.String()) res1 := response2{} _ = json.UnmarshalRead(strings.NewReader(str), &res1) fmt.Println(res1) }
4.2. XML
encoding/xml 提供 XML 的编码和解码,struct tag 可以控制元素、属性和嵌套路径,=XMLName= 可以指定根元素名称。XML 输入可能不符合预期结构,因此解码后的错误和字段缺失都需要处理;XML 与 JSON 一样,结构体标签是数据模型的重要一部分。
package main import ( "encoding/xml" "fmt" ) type Plant struct { XMLName xml.Name `xml:"plant"` Id int `xml:"id,attr"` Name string `xml:"name"` Origin []string `xml:"origin"` } func (p Plant) String() string { return fmt.Sprintf("Plant id=%v, name=%v, origin=%v", p.Id, p.Name, p.Origin) } func main() { coffee := &Plant{Id: 27, Name: "Coffee"} coffee.Origin = []string{"Ethiopia", "Brazil"} out, _ := xml.MarshalIndent(coffee, " ", " ") fmt.Println(string(out)) fmt.Println(xml.Header + string(out)) var p Plant if err := xml.Unmarshal(out, &p); err != nil { panic(err) } fmt.Println(p) tomato := &Plant{Id: 81, Name: "Tomato"} tomato.Origin = []string{"Mexico", "California"} type Nesting struct { XMLName xml.Name `xml:"nesting"` Plants []*Plant `xml:"parent>child>plant"` } nesting := &Nesting{} nesting.Plants = []*Plant{coffee, tomato} out, _ = xml.MarshalIndent(nesting, " ", " ") fmt.Println(string(out)) }
4.3. Time
time.Time 表示带时区位置的时间,支持构造、提取年月日时分秒、比较、计算差值和加减 duration;=time.Now= 获取当前时间,=Sub= 得到时间间隔。时间点、时区、持续时间和日期格式是不同概念,业务代码不应把它们混为一个整数处理。
package main import ( "fmt" "time" ) func main() { p := fmt.Println now := time.Now() p(now) then := time.Date( 2009, 11, 17, 20, 34, 58, 651387237, time.UTC) p(then) p(then.Year()) p(then.Month()) p(then.Day()) p(then.Hour()) p(then.Minute()) p(then.Second()) p(then.Nanosecond()) p(then.Location()) p(then.Weekday()) p(then.Before(now)) p(then.After(now)) p(then.Equal(now)) diff := now.Sub(then) p(diff) p(diff.Hours()) p(diff.Minutes()) p(diff.Seconds()) p(diff.Nanoseconds()) p(then.Add(diff)) p(then.Add(-diff)) }
4.4. Epoch
Unix epoch 是从 1970-01-01 UTC 起计算的时间基准,Go 可以通过 Unix=、=UnixMilli 和 UnixNano 获取秒、毫秒或纳秒时间戳,也能把时间戳转换回 =time.Time=。跨系统传递时间戳时必须明确单位和时区,避免把秒误当成毫秒。
package main import ( "fmt" "time" ) func main() { now := time.Now() fmt.Println(now) fmt.Println(now.Unix()) fmt.Println(now.UnixMilli()) fmt.Println(now.UnixNano()) fmt.Println(time.Unix(now.Unix(), 0)) fmt.Println(time.Unix(0, now.UnixNano())) }
4.5. Time Formatting / Parsing
Go 使用参考时间 Mon Jan 2 15:04:05 MST 2006 作为布局模板,通过 Format 格式化时间、=Parse= 解析字符串;布局中的示例时间不是随意占位符,必须使用规定的数字和文本。解析外部输入时应处理错误,并明确输入是否带时区和偏移量。
package main import ( "fmt" "time" ) func main() { p := fmt.Println t := time.Now() p(t.Format(time.RFC3339)) t1, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41+00:00") p(t1) p(t.Format("3:04PM")) p(t.Format("Mon Jan _2 15:04:05 2006")) p(t.Format("2006-01-02T15:04:05.999999-07:00")) form := "3 04 PM" t2, _ := time.Parse(form, "8 41 PM") p(t2) fmt.Printf("%d-%02d-%02dT%02d:%02d:%02d-00:00\n", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) _, err := time.Parse("Mon Jan _2 15:04:05 2006", "8:41PM") p(err) }
4.6. Random Numbers
随机数 API 可以生成整数、浮点数和指定范围内的值,也可以创建带独立随机源的生成器;需要不可预测性时应使用 =crypto/rand=,而不是普通的伪随机数。随机种子、并发使用方式和版本变化会影响可重复性,测试代码应显式控制随机源。
package main import ( "fmt" "math/rand/v2" ) func main() { fmt.Print(rand.IntN(100), ",") fmt.Print(rand.IntN(100)) fmt.Println() fmt.Println(rand.Float64()) fmt.Print((rand.Float64()*5)+5, ",") fmt.Print((rand.Float64() * 5) + 5) fmt.Println() s2 := rand.NewPCG(42, 1024) r2 := rand.New(s2) fmt.Print(r2.IntN(100), ",") fmt.Print(r2.IntN(100)) fmt.Println() s3 := rand.NewPCG(42, 1024) r3 := rand.New(s3) fmt.Print(r3.IntN(100), ",") fmt.Print(r3.IntN(100)) fmt.Println() }
4.7. Number Parsing
strconv 可以把字符串解析成整数、无符号整数或浮点数,并通过位数参数约束结果范围;解析函数同时返回错误,用于区分格式错误、溢出和非法进制。展示给用户的数字通常还需要先校验空白、符号和业务范围。
package main import ( "fmt" "strconv" ) func main() { f, _ := strconv.ParseFloat("1.234", 64) fmt.Println(f) i, _ := strconv.ParseInt("123", 0, 64) fmt.Println(i) d, _ := strconv.ParseInt("0x1c8", 0, 64) fmt.Println(d) u, _ := strconv.ParseUint("789", 0, 64) fmt.Println(u) k, _ := strconv.Atoi("135") fmt.Println(k) _, e := strconv.Atoi("wat") fmt.Println(e) }
4.8. URL Parsing
net/url 能拆解 URL 的 scheme、认证信息、host、port、path、query 和 fragment,并把查询参数解析为 map;用户信息应通过专门方法读取,主机和端口则使用 SplitHostPort 等 API。解析 URL 不等于验证 URL 是否允许访问,服务端仍需进行协议、域名和 SSRF 等安全校验。
package main import ( "fmt" "net" "net/url" ) func main() { s := "postgres://user:pass@host.com:5432/path?k=v#f" u, err := url.Parse(s) if err != nil { panic(err) } fmt.Println(u.Scheme) fmt.Println(u.User) fmt.Println(u.User.Username()) p, _ := u.User.Password() fmt.Println(p) fmt.Println(u.Host) host, port, _ := net.SplitHostPort(u.Host) fmt.Println(host) fmt.Println(port) fmt.Println(u.Path) fmt.Println(u.Fragment) fmt.Println(u.RawQuery) m, _ := url.ParseQuery(u.RawQuery) fmt.Println(m) fmt.Println(m["k"][0]) }
4.9. SHA256 Hashes
crypto/sha256 可以对字节序列计算 SHA-256 摘要,结果是固定长度的字节数组,常用十六进制格式展示。摘要适合完整性校验和内容指纹,不适合直接保存密码;密码存储应使用专门的慢哈希和随机 salt。
package main import ( "crypto/sha256" "fmt" ) func main() { s := "sha256 this string" h := sha256.New() h.Write([]byte(s)) bs := h.Sum(nil) fmt.Println(s) fmt.Printf("%x\n", bs) }
4.10. Base64 Encoding
encoding/base64 支持标准 Base64 和 URL 兼容 Base64 的编码与解码,编码输入通常是 =[]byte=,解码会返回数据和错误。Base64 只是二进制到文本的表示方式,不提供加密或保密性,放在 URL、JSON 或邮件中时仍要考虑长度和填充规则。
package main import ( b64 "encoding/base64" "fmt" ) func main() { data := "abc123!?$*&()'-=@~" sEnc := b64.StdEncoding.EncodeToString([]byte(data)) fmt.Println(sEnc) sDec, _ := b64.StdEncoding.DecodeString(sEnc) fmt.Println(string(sDec)) fmt.Println() uEnc := b64.URLEncoding.EncodeToString([]byte(data)) fmt.Println(uEnc) uDec, _ := b64.URLEncoding.DecodeString(uEnc) fmt.Println(string(uDec)) }
5. 文件、构建与测试
5.1. Reading Files
读取文件可以使用 os.ReadFile 一次性获得全部内容,也可以用 os.Open 配合 reader 分块读取;读取文件、打开文件和关闭文件都可能失败。小文件适合一次性读取,大文件或流式输入则应控制缓冲和内存占用,并及时关闭句柄。
package main import ( "bufio" "fmt" "io" "os" "path/filepath" ) func check(e error) { if e != nil { panic(e) } } func main() { path := filepath.Join(os.TempDir(), "dat") dat, err := os.ReadFile(path) check(err) fmt.Print(string(dat)) f, err := os.Open(path) check(err) b1 := make([]byte, 5) n1, err := f.Read(b1) check(err) fmt.Printf("%d bytes: %s\n", n1, string(b1[:n1])) o2, err := f.Seek(6, io.SeekStart) check(err) b2 := make([]byte, 2) n2, err := f.Read(b2) check(err) fmt.Printf("%d bytes @ %d: ", n2, o2) fmt.Printf("%v\n", string(b2[:n2])) _, err = f.Seek(2, io.SeekCurrent) check(err) _, err = f.Seek(-4, io.SeekEnd) check(err) o3, err := f.Seek(6, io.SeekStart) check(err) b3 := make([]byte, 2) n3, err := io.ReadAtLeast(f, b3, 2) check(err) fmt.Printf("%d bytes @ %d: %s\n", n3, o3, string(b3)) _, err = f.Seek(0, io.SeekStart) check(err) r4 := bufio.NewReader(f) b4, err := r4.Peek(5) check(err) fmt.Printf("5 bytes: %s\n", string(b4)) f.Close() }
5.2. Writing Files
写文件可以直接使用 os.WriteFile=,也可以打开文件后调用 =Write=、=WriteString 或通过 bufio.Writer 缓冲写入;需要持久化语义时可调用 =Sync=。打开文件后应及时 defer Close,并明确文件权限、覆盖行为和部分写入错误。
package main import ( "bufio" "fmt" "os" "path/filepath" ) func check(e error) { if e != nil { panic(e) } } func main() { d1 := []byte("hello\ngo\n") path1 := filepath.Join(os.TempDir(), "dat1") err := os.WriteFile(path1, d1, 0644) check(err) path2 := filepath.Join(os.TempDir(), "dat2") f, err := os.Create(path2) check(err) defer f.Close() d2 := []byte{115, 111, 109, 101, 10} n2, err := f.Write(d2) check(err) fmt.Printf("wrote %d bytes\n", n2) n3, err := f.WriteString("writes\n") check(err) fmt.Printf("wrote %d bytes\n", n3) f.Sync() w := bufio.NewWriter(f) n4, err := w.WriteString("buffered\n") check(err) fmt.Printf("wrote %d bytes\n", n4) w.Flush() }
5.3. Line Filters
行过滤器从标准输入逐行读取文本、根据条件处理并把结果写到标准输出,示例使用 =bufio.Scanner=;这种 Unix 风格程序可以通过管道组合成更大的处理流程。Scanner 有默认 token 大小限制,处理超长行时应调整 buffer 或改用更适合的 reader。
package main import ( "bufio" "fmt" "os" "strings" ) func main() { scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { ucl := strings.ToUpper(scanner.Text()) fmt.Println(ucl) } if err := scanner.Err(); err != nil { fmt.Fprintln(os.Stderr, "error:", err) os.Exit(1) } }
5.4. File Paths
path/filepath 提供跨平台的路径拼接、清理、拆分、绝对路径和目录遍历操作,使用 Join 比手动拼接分隔符更可靠。路径字符串本身不等于安全路径,接收用户输入时仍需防范路径穿越、符号链接和访问范围越界。
package main import ( "fmt" "path/filepath" "strings" ) func main() { p := filepath.Join("dir1", "dir2", "filename") fmt.Println("p:", p) fmt.Println(filepath.Join("dir1//", "filename")) fmt.Println(filepath.Join("dir1/../dir1", "filename")) fmt.Println("Dir(p):", filepath.Dir(p)) fmt.Println("Base(p):", filepath.Base(p)) fmt.Println(filepath.IsAbs("dir/file")) fmt.Println(filepath.IsAbs("/dir/file")) filename := "config.json" ext := filepath.Ext(filename) fmt.Println(ext) fmt.Println(strings.TrimSuffix(filename, ext)) rel, err := filepath.Rel("a/b", "a/b/t/file") if err != nil { panic(err) } fmt.Println(rel) rel, err = filepath.Rel("a/b", "a/c/t/file") if err != nil { panic(err) } fmt.Println(rel) }
5.5. Directories
目录操作包括 Mkdir=、=MkdirAll=、=ReadDir=、=Chdir 和 =WalkDir=,可以创建层级目录、列出条目或递归访问文件树;临时目录使用后应删除。递归遍历时要处理权限错误、符号链接和目录内容在遍历过程中变化等情况。
package main import ( "fmt" "io/fs" "os" "path/filepath" ) func check(e error) { if e != nil { panic(e) } } func main() { err := os.Mkdir("subdir", 0755) check(err) defer os.RemoveAll("subdir") createEmptyFile := func(name string) { d := []byte("") check(os.WriteFile(name, d, 0644)) } createEmptyFile("subdir/file1") err = os.MkdirAll("subdir/parent/child", 0755) check(err) createEmptyFile("subdir/parent/file2") createEmptyFile("subdir/parent/file3") createEmptyFile("subdir/parent/child/file4") c, err := os.ReadDir("subdir/parent") check(err) fmt.Println("Listing subdir/parent") for _, entry := range c { fmt.Println(" ", entry.Name(), entry.IsDir()) } err = os.Chdir("subdir/parent/child") check(err) c, err = os.ReadDir(".") check(err) fmt.Println("Listing subdir/parent/child") for _, entry := range c { fmt.Println(" ", entry.Name(), entry.IsDir()) } err = os.Chdir("../../..") check(err) fmt.Println("Visiting subdir") err = filepath.WalkDir("subdir", visit) check(err) } func visit(path string, d fs.DirEntry, err error) error { if err != nil { return err } fmt.Println(" ", path, d.IsDir()) return nil }
5.6. Temporary Files and Directories
os.CreateTemp 和 os.MkdirTemp 会在系统临时目录中创建不易冲突的临时文件或目录,适合中间结果和测试数据;使用完成后应显式删除,通常用 defer 注册清理。临时路径不应被拼接成可预测的共享文件名,也不应把敏感数据长期留在其中。
package main import ( "fmt" "os" "path/filepath" ) func check(e error) { if e != nil { panic(e) } } func main() { f, err := os.CreateTemp("", "sample") check(err) fmt.Println("Temp file name:", f.Name()) defer os.Remove(f.Name()) _, err = f.Write([]byte{1, 2, 3, 4}) check(err) dname, err := os.MkdirTemp("", "sampledir") check(err) fmt.Println("Temp dir name:", dname) defer os.RemoveAll(dname) fname := filepath.Join(dname, "file1") err = os.WriteFile(fname, []byte{1, 2}, 0666) check(err) }
5.7. Embed Directive
//go:embed 在编译时把文件或目录嵌入 Go 二进制,可以得到字符串、字节切片或 =embed.FS=;路径相对于包含 Go 源文件的目录。嵌入资源便于发布单文件程序,但会增加二进制体积,且 Playground 等受限环境可能无法运行该示例。
package main import ( "embed" ) //go:embed folder/single_file.txt var fileString string //go:embed folder/single_file.txt var fileByte []byte //go:embed folder/single_file.txt //go:embed folder/*.hash var folder embed.FS func main() { print(fileString) print(string(fileByte)) content1, _ := folder.ReadFile("folder/file1.hash") print(string(content1)) content2, _ := folder.ReadFile("folder/file2.hash") print(string(content2)) }
5.8. Testing and Benchmarking
测试文件使用 _test.go 命名,测试函数以 Test 开头,断言失败可用 Error 或立即停止的 Fatal=;表驱动测试和 =t.Run 适合覆盖多组输入。Benchmark 函数以 Benchmark 开头,基准循环放在 b.N 控制的循环中,执行前应把不计入测量的准备工作放在循环外。
package main import ( "fmt" "testing" ) func IntMin(a, b int) int { if a < b { return a } return b } func TestIntMinBasic(t *testing.T) { ans := IntMin(2, -2) if ans != -2 { t.Errorf("IntMin(2, -2) = %d; want -2", ans) } } func TestIntMinTableDriven(t *testing.T) { var tests = []struct { a, b int want int }{ {0, 1, 0}, {1, 0, 0}, {2, -2, -2}, {0, -1, -1}, {-1, 0, -1}, } for _, tt := range tests { testname := fmt.Sprintf("%d,%d", tt.a, tt.b) t.Run(testname, func(t *testing.T) { ans := IntMin(tt.a, tt.b) if ans != tt.want { t.Errorf("got %d, want %d", ans, tt.want) } }) } } func BenchmarkIntMin(b *testing.B) { for b.Loop() { IntMin(1, 2) } }
6. 命令行、网络与系统
6.1. Command-Line Arguments
os.Args 提供原始命令行参数,其中第一个元素通常是程序路径,后续元素才是用户传入的参数;参数可以用普通 slice 下标读取。它适合简单命令,复杂选项应使用 flag 或专门的 CLI 框架,并注意参数缺失和非法输入。
package main import ( "fmt" "os" ) func main() { argsWithProg := os.Args argsWithoutProg := os.Args[1:] arg := os.Args[3] fmt.Println(argsWithProg) fmt.Println(argsWithoutProg) fmt.Println(arg) }
6.2. Command-Line Flags
flag 包支持字符串、整数、布尔值等命令行选项,声明 flag 后调用 flag.Parse 解析,返回的指针再解引用取得值;位置参数可通过 flag.Args 获取。标准 flag 要求选项出现在位置参数之前,=–help= 等帮助信息由包自动生成。
package main import ( "flag" "fmt" ) func main() { wordPtr := flag.String("word", "foo", "a string") numbPtr := flag.Int("numb", 42, "an int") forkPtr := flag.Bool("fork", false, "a bool") var svar string flag.StringVar(&svar, "svar", "bar", "a string var") flag.Parse() fmt.Println("word:", *wordPtr) fmt.Println("numb:", *numbPtr) fmt.Println("fork:", *forkPtr) fmt.Println("svar:", svar) fmt.Println("tail:", flag.Args()) }
6.3. Command-Line Subcommands
子命令可以为每个命令创建独立的 flag.FlagSet=,例如为 =foo 和 bar 分别定义自己的选项,再根据第一个参数选择解析器。这样能保持不同子命令的参数互不污染;更大型的工具还需要统一帮助、错误码和命令发现策略。
package main import ( "flag" "fmt" "os" ) func main() { fooCmd := flag.NewFlagSet("foo", flag.ExitOnError) fooEnable := fooCmd.Bool("enable", false, "enable") fooName := fooCmd.String("name", "", "name") barCmd := flag.NewFlagSet("bar", flag.ExitOnError) barLevel := barCmd.Int("level", 0, "level") if len(os.Args) < 2 { fmt.Println("expected 'foo' or 'bar' subcommands") os.Exit(1) } switch os.Args[1] { case "foo": fooCmd.Parse(os.Args[2:]) fmt.Println("subcommand 'foo'") fmt.Println(" enable:", *fooEnable) fmt.Println(" name:", *fooName) fmt.Println(" tail:", fooCmd.Args()) case "bar": barCmd.Parse(os.Args[2:]) fmt.Println("subcommand 'bar'") fmt.Println(" level:", *barLevel) fmt.Println(" tail:", barCmd.Args()) default: fmt.Println("expected 'foo' or 'bar' subcommands") os.Exit(1) } }
6.4. Environment Variables
环境变量是进程配置的通用方式,=os.Setenv= 设置、=os.Getenv= 读取、=os.Environ= 列出全部键值;读取不存在的变量时 Getenv 返回空字符串,因此需要区分“未设置”和“设置为空”时可使用更明确的 API。环境变量适合部署注入配置,但敏感值仍需控制日志和进程环境暴露。
package main import ( "fmt" "os" "strings" ) func main() { os.Setenv("FOO", "1") fmt.Println("FOO:", os.Getenv("FOO")) fmt.Println("BAR:", os.Getenv("BAR")) fmt.Println() for _, e := range os.Environ() { pair := strings.SplitN(e, "=", 2) fmt.Println(pair[0]) } }
6.5. Logging
标准 log 包提供带时间信息的日志输出、前缀和等级之外的基础能力,并能写入指定的 =io.Writer=;日志调用通常会自动附加换行。生产服务应统一日志格式、输出目标、级别和敏感字段脱敏,不能把日志当成结构化审计系统的完整替代品。
package main import ( "bytes" "fmt" "log" "os" "log/slog" ) func main() { log.Println("standard logger") log.SetFlags(log.LstdFlags | log.Lmicroseconds) log.Println("with micro") log.SetFlags(log.LstdFlags | log.Lshortfile) log.Println("with file/line") mylog := log.New(os.Stdout, "my:", log.LstdFlags) mylog.Println("from mylog") mylog.SetPrefix("ohmy:") mylog.Println("from mylog") var buf bytes.Buffer buflog := log.New(&buf, "buf:", log.LstdFlags) buflog.Println("hello") fmt.Print("from buflog:", buf.String()) jsonHandler := slog.NewJSONHandler(os.Stderr, nil) myslog := slog.New(jsonHandler) myslog.Info("hi there") myslog.Info("hello again", "key", "val", "age", 25) }
6.6. HTTP Client
net/http 的 Client 可以发送请求、设置方法和请求头、读取响应状态与响应体;响应体必须关闭,否则可能无法复用连接并造成资源泄漏。访问外部服务时应配置超时、检查 HTTP 状态码、限制响应大小,并避免把默认无限等待的客户端直接用于生产网络调用。
package main import ( "bufio" "fmt" "net/http" ) func main() { resp, err := http.Get("https://gobyexample.com") if err != nil { panic(err) } defer resp.Body.Close() fmt.Println("Response status:", resp.Status) scanner := bufio.NewScanner(resp.Body) for i := 0; scanner.Scan() && i < 5; i++ { fmt.Println(scanner.Text()) } if err := scanner.Err(); err != nil { panic(err) } }
6.7. HTTP Server
HTTP 服务可以用 http.HandleFunc 注册路由,再通过 http.ListenAndServe 监听端口;Handler 从请求读取路径、查询参数和请求体,并向 ResponseWriter 写响应。真实服务通常应使用带超时的 =http.Server=,并验证请求方法、请求体大小和并发退出流程。
package main import ( "fmt" "net/http" ) func hello(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "hello\n") } func headers(w http.ResponseWriter, req *http.Request) { for name, headers := range req.Header { for _, h := range headers { fmt.Fprintf(w, "%v: %v\n", name, h) } } } func main() { http.HandleFunc("/hello", hello) http.HandleFunc("/headers", headers) http.ListenAndServe(":8090", nil) }
6.8. TCP Server
net.Listen 创建 TCP listener,服务端循环调用 Accept 获取客户端连接,并为每个连接启动 goroutine;示例使用 bufio.Reader 读取一行,再通过连接写回响应。TCP 只有字节流语义,不保证一次 Write 对应一次 Read,因此实际协议必须自行定义边界、超时和半关闭行为。
package main import ( "bufio" "fmt" "log" "net" "strings" ) func main() { listener, err := net.Listen("tcp", ":8090") if err != nil { log.Fatal("Error listening:", err) } defer listener.Close() for { conn, err := listener.Accept() if err != nil { log.Println("Error accepting conn:", err) continue } go handleConnection(conn) } } func handleConnection(conn net.Conn) { defer conn.Close() reader := bufio.NewReader(conn) message, err := reader.ReadString('\n') if err != nil { log.Printf("Read error: %v", err) return } ackMsg := strings.ToUpper(strings.TrimSpace(message)) response := fmt.Sprintf("ACK: %s\n", ackMsg) _, err = conn.Write([]byte(response)) if err != nil { log.Printf("Server write error: %v", err) } }
6.9. Context
context.Context 用于在 API 边界和 goroutine 之间传递取消信号、截止时间和请求范围的值;HTTP 请求通常自带 context,处理函数可以监听 Done 并在客户端取消时提前结束。context 不应作为普通配置容器,也不应把 nil context 传给需要它的函数。
package main import ( "fmt" "net/http" "time" ) func hello(w http.ResponseWriter, req *http.Request) { ctx := req.Context() fmt.Println("server: hello handler started") defer fmt.Println("server: hello handler ended") select { case <-time.After(10 * time.Second): fmt.Fprintf(w, "hello\n") case <-ctx.Done(): err := ctx.Err() fmt.Println("server:", err) internalError := http.StatusInternalServerError http.Error(w, err.Error(), internalError) } } func main() { http.HandleFunc("/hello", hello) http.ListenAndServe(":8090", nil) }
6.10. Spawning Processes
os/exec 可以启动外部程序,使用 Command 创建命令,通过 Output 获取合并输出,或使用 Stdout=、=Stderr 连接到指定流。启动外部程序时应检查错误、设置超时和退出回收策略,涉及用户输入时还要避免把未经处理的字符串拼成 Shell 命令。
package main import ( "errors" "fmt" "io" "os/exec" ) func main() { dateCmd := exec.Command("date") dateOut, err := dateCmd.Output() if err != nil { panic(err) } fmt.Println("> date") fmt.Println(string(dateOut)) _, err = exec.Command("date", "-x").Output() if err != nil { if e, ok := errors.AsType[*exec.Error](err); ok { fmt.Println("failed executing:", e) } else if e, ok := errors.AsType[*exec.ExitError](err); ok { exitCode := e.ExitCode() fmt.Println("command exit rc =", exitCode) } else { panic(err) } } grepCmd := exec.Command("grep", "hello") grepIn, _ := grepCmd.StdinPipe() grepOut, _ := grepCmd.StdoutPipe() grepCmd.Start() grepIn.Write([]byte("hello grep\ngoodbye grep")) grepIn.Close() grepBytes, _ := io.ReadAll(grepOut) grepCmd.Wait() fmt.Println("> grep hello") fmt.Println(string(grepBytes)) lsCmd := exec.Command("bash", "-c", "ls -a -l -h") lsOut, err := lsCmd.Output() if err != nil { panic(err) } fmt.Println("> ls -a -l -h") fmt.Println(string(lsOut)) }
6.11. Exec'ing Processes
Unix 的 syscall.Exec 会用另一个程序替换当前进程映像,而不是创建一个仍由父进程管理的子进程;成功后原程序不会继续执行。它适合实现启动器、容器入口等场景,但平台兼容性、文件描述符继承和环境变量传递都需要明确处理。
package main import ( "os" "os/exec" "syscall" ) func main() { binary, lookErr := exec.LookPath("ls") if lookErr != nil { panic(lookErr) } args := []string{"ls", "-a", "-l", "-h"} env := os.Environ() execErr := syscall.Exec(binary, args, env) if execErr != nil { panic(execErr) } }
6.12. Signals
os/signal 可以通过 channel 接收操作系统信号,例如用 signal.Notify 监听中断信号并执行清理;服务通常要在收到终止信号后停止接收新请求,再等待已有工作结束。signal channel 必须有合理容量和退出逻辑,不能把所有信号都简单地忽略。
package main import ( "context" "fmt" "os/signal" "syscall" ) func main() { ctx, stop := signal.NotifyContext( context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() fmt.Println("awaiting signal") <-ctx.Done() fmt.Println() fmt.Println(context.Cause(ctx)) fmt.Println("exiting") }
6.13. Exit
os.Exit 会立即以指定状态码结束进程,不会执行 defer,因此只适合在顶层处理最终退出状态;普通函数更适合返回 error,让 main 统一记录错误并退出。退出码应保持稳定而有意义,方便 Shell、CI 和进程管理器判断成功或失败。
package main import ( "fmt" "os" ) func main() { defer fmt.Println("!") os.Exit(3) }