Golang 备忘录
{Back to Index}  

Table of Contents

1. GOROOT

GOROOT 是 Go 语言的安装目录,包含:

  • Go 编译器、工具链(go、gofmt 等)
  • 标准库源码(如 fmt、net/http、os 等)
  • 运行时文件
# 查看 GOROOT
go env GOROOT
# 通常是 /usr/local/go 或 /opt/homebrew/opt/go/libexec (macOS)

2. GOPATH

从 Go 1.11+ 引入 Go Modules 后,GOPATH 的重要性大大降低。

现在 GOPATH 主要用于:

  • $GOPATH/bin - 存放 go install 安装的工具
  • $GOPATH/pkg/mod - 缓存下载的依赖模块

3. go.mod 常用命令

对于 go.mod 文件,你通常不需要手动编辑,而是通过命令行工具来管理。以下是常用的命令:

3.1. 添加新依赖

go get github.com/some/package@latest

3.2. 更新所有依赖到最新版本

go get -u ./...

3.3. 更新特定依赖

go get -u github.com/julienschmidt/httprouter

3.4. 整理依赖(移除未使用的,添加缺失的)

go mod tidy

3.5. 下载依赖到本地缓存

go mod download

下载的依赖会存放在: $GOPATH/pkg/mod/

对于日常开发,通常不需要手动运行 go mod download ,因为 go buildgo run 等命令会自动下载缺失的依赖。
这个命令主要用于CI/CD 优化或离线开发准备。

4. 数据结构

slice {指向底层数组的指针, len, cap} - 24 字节
map 指向 hmap 结构的指针 - 8 字节
channel 指向 hchan 结构的指针 - 8 字节
interface {类型指针, 数据指针} - 16 字节
string {指向字节数组的指针, len} - 16 字节

4.1. Array

// Declare an integer array of five elements.
// Initialize each element with a specific value.
array := [5]int{10, 20, 30, 40, 50}

// Declare an integer array.
// Initialize each element with a specific value.
// Capacity is determined based on the number of values initialized.
array := [...]int{10, 20, 30, 40, 50}

// Declare an integer pointer array of five elements.
// Initialize index 0 and 1 of the array with integer pointers.
array := [5]*int{0: new(int), 1: new(int)}
// Assign values to index 0 and 1.
*array[0] = 10
*array[1] = 20

4.2. Channel

4.2.1. 声明

var ch chan in
// 这种方式声明的管道,值为 nil
// 读写 nil 管道均会永久阻塞。关闭的管道仍然可以读取数据,向关闭的管道写数据会触发 panic

// nil channel 在某些时候有些妙用,例如在 select 的某个 case 分支 A 将其它某 case 分支 B 所操作的 channel 设置为 nil ,这将会禁用 case 分支 B

4.2.2. make()

ch1 := make(chan string)        // without buffer
ch1 := make(chan string, 5)     // with buffer

只有一个缓冲区的管道,写入数据类似于加锁,读出数据类似于释放锁,比如:

var counter int = 0
var ch = make(chan int, 1)
func Worker() {
    ch <- 1
    counter++
    <-ch
}

4.2.3. 数据读写

管道双向可读写,管道在函数间传递时也可以使用操作符限制管道的读写:

func ChanParamRW(ch chan int) {
    // 管道可读写
}

func ChanParamR(ch <-chan int) {
    // 只能从管道读取数据
}

func ChanParamW(ch chan<- int) {
    // 只能向管道写入数据
}

协程读取管道时,阻塞的条件有:

  • 管道无缓冲区
  • 管道的缓冲区中无数据
  • 管道的值为 nil

协程写入管道时,阻塞的条件有:

  • 管道无缓冲区
  • 管道的缓冲区已满
  • 管道的值为 nil

4.2.4. close

表示关闭 channel :

  • 关闭 channel 后,send 操作将导致 painc
  • 关闭 channel 后,recv 操作将返回对应类型的 0 值以及一个状态码 false
  • close 并非强制需要使用 close(ch) 来关闭 channel ,在某些时候可以自动被关闭
  • 如果使用 close() ,建议条件允许的情况下加上 defer
  • 只在 sender 端上显式使用 close() 关闭 channel ,因为关闭通道意味着没有数据再需要发送

4.3. Slice

slice_internal.png

Figure 1: Slice internals with underlying array

var s []int

s 本质是一个结构体(slice header),包含三个字段:

// 等价于 reflect.SliceHeader
type slice struct {
    Data uintptr   // 指向底层数组的指针
    Len  int       // 长度
    Cap  int       // 容量
}

验证:

slice_var := make([]int, 3)

// 这三个是一样的
fmt.Printf("%p\n", slice_var)        // 底层数组地址
fmt.Printf("%p\n", &slice_var[0])    // 第一个元素的地址

// 这个不一样
fmt.Printf("%p\n", &slice_var)       // slice header 变量的地址

4.3.1. 创建

// Create a slice of strings.
// Contains a length and capacity of 5 elements.
slice := make([]string, 5)

// Create a slice of integers.
// Contains a length and capacity of 3 elements.
slice := []int{10, 20, 30}

// Create a slice of strings. (slice with a length and capacity of 100 elements)
// Initialize the 100th element with an empty string.
slice := []string{99: ""}

// Create a nil slice of integers.
var slice []int

// Create Empty Slice
// Use make to create an empty slice of integers.
slice := make([]int, 0)
// Use a slice literal to create an empty slice of integers.
slice := []int{}

nil_slice.png

Figure 2: The representation of a nil slice

empty_slice.png

Figure 3: The representation of an empty slice

4.3.2. 切片

slicing.png

Figure 4: Two slices sharing the same underlying array

4.3.3. append

4.3.3.1. w/ available capacity
// Create a slice of integers.
// Contains a length and capacity of 5 elements.
slice := []int{10, 20, 30, 40, 50}
// Create a new slice.
// Contains a length of 2 and capacity of 4 elements.
newSlice := slice[1:3]
// Allocate a new element from capacity.
// Assign the value of 60 to the new element.
newSlice = append(newSlice, 60)

append_slice.png

Figure 5: The underlying array after the append operation

4.3.3.2. w/o available capacity
// Create a slice of integers.
// Contains a length and capacity of 4 elements.
slice := []int{10, 20, 30, 40}
// Append a new value to the slice.
// Assign the value of 50 to the new element.
newSlice := append(slice, 50)

append_slice2.png

Figure 6: The new underlying array after the append operation

4.4. Map

var m[int]string

m 变量本质上是一个指向 hmap (runtime/map_noswiss.go) 结构体的指针

// map 的头部结构
type hmap struct {
    count     int    // 元素个数,len(map) 返回这个值
    flags     uint8  // 状态标志(是否在写入等)
    B         uint8  // bucket 数量 = 2^B
    noverflow uint16 // 溢出桶的大致数量
    hash0     uint32 // 哈希种子(随机化,防止哈希碰撞攻击)

    buckets    unsafe.Pointer // 指向 bucket 数组(2^B 个)
    oldbuckets unsafe.Pointer // 扩容时指向旧的 bucket 数组
    nevacuate  uintptr        // 扩容进度计数器

    extra *mapextra // 可选字段
}
hmap
┌─────────────────────┐
│ count: 5            │
│ B: 2 (4 buckets)    │
│ hash0: 0x12345678   │
│ buckets ────────────┼──────► bucket 数组 (2^B = 4 个桶)
└─────────────────────┘        ┌─────────────────────────────┐
                               │ bucket 0                    │
                               │ ┌─────────────────────────┐ │
                               │ │ tophash: [8]uint8       │ │
                               │ │ keys:    [8]keytype     │ │
                               │ │ values:  [8]valuetype   │ │
                               │ │ overflow ───────────────┼─┼──► 溢出桶
                               │ └─────────────────────────┘ │
                               ├─────────────────────────────┤
                               │ bucket 1                    │
                               │ ...                         │
                               ├─────────────────────────────┤
                               │ bucket 2                    │
                               ├─────────────────────────────┤
                               │ bucket 3                    │
                               └─────────────────────────────┘

map 操作不是原子的,这意味着多个协程同时操作 map 时有可能产生读写冲突,读写冲突会触发 panic 从而导致程序退出。如果需要并发读写,则可以使用额外的锁(互斥锁、读写锁),也可以考虑使用标准库 sync 包中的 sync.Map

4.4.1. Create

// Create a map with a key of type string and a value of type int.
dict := make(map[string]int)
// Create a map with a key and value of type string.
// Initialize the map with 2 key/value pairs.
dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}

4.5. Func

4.5.1. 普通函数

普通函数在编译时确定,本质就是一个代码入口地址:

func add(a, b int) int {
    return a + b
}

f := add  // f 是一个函数值
f (函数变量)
┌───────────────┐
│ *funcval ptr ─┼──────► funcval
└───────────────┘       ┌───────────────┐
                        │ fn: 0x10a0b0 ─┼──► add 函数的机器码
                        └───────────────┘
// runtime/runtime2.go
type funcval struct {
    fn uintptr  // 函数代码的入口地址
    // 闭包捕获的变量紧跟其后...
}

函数变量实际是 *funcval(指向 funcval 的指针)。

4.5.2. 闭包

闭包会捕获外部变量, funcval 后面紧跟捕获的变量:

func makeCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}
f := makeCounter()

f (函数变量)
┌───────────────┐
│ *funcval ptr ─┼──────► funcval (堆上分配)
└───────────────┘        ┌──────────────────┐
                         │ fn: 0x10b0c0     │ ← 匿名函数代码地址
                         │ &count: 0xc00001 │ ← 捕获的变量(指针)
                         └──────────────────┘
                                │
                                ▼
                          ┌──────────┐
                          │ count: 0 │ (逃逸到堆上)
                          └──────────┘

4.5.3. 方法

方法只是第一个参数为 receiver 的函数:

type Dog struct{ Name string }

func (d Dog) Bark() string {
    return d.Name + ": woof!"
}

// 编译器实际生成的等价函数:
// func Dog_Bark(d Dog) string { ... }

// 以下两种调用等价
dog := Dog{"Buddy"}
dog.Bark()                    // 方法调用
Dog.Bark(dog)                 // 函数调用(方法表达式)

4.5.4. 接口中的函数

接口的方法通过 itab 分发:

var s Speaker = Dog{"Buddy"}

┌─────────────────┐
│ iface           │
│ ├─ itab ────────┼──► ┌───────────────────────┐
│ └─ data ───┐    │    │ itab                  │
└────────────┼────┘    │ ├─ inter (interface)  │
             │         │ ├─ _type (real type)  │
             ▼         │ └─ fun[0] ────────────┼──► Dog.Bark 代码
        ┌────────┐     └───────────────────────┘
        │ "Buddy"│
        └────────┘

4.6. 反射

Go 的反射建立在 interface 之上 。每个 interface 变量的底层结构是:

┌────────────────────────────┐
│  iface / eface             │
├───────────────┬────────────┤
│  type 指针    │  data 指针 │
│  (类型元数据) │  (实际值)  │
└───────────────┴────────────┘

具体来说,空接口 interface{} (即 any) 对应的运行时结构是:

// runtime/runtime2.go
type eface struct {
    _type *_type  // 指向类型描述符
    data  unsafe.Pointer  // 指向实际数据
}

反射就是通过拆解这两个指针来工作的。

4.6.1. reflect.TypeOfreflect.ValueOf 做了什么

var x float64 = 3.14

t := reflect.TypeOf(x)   // 提取 eface 中的 _type 指针 → 得到类型元数据
v := reflect.ValueOf(x)  // 提取 eface 中的 data 指针  → 得到值的副本

调用 reflect.TypeOf(x) 时:

  • x 被隐式装箱为 interface{} (即构造一个 eface)
  • TypeOf 读取 eface._type ,返回 reflect.Type

调用 reflect.ValueOf(x) 时:

  • 同样装箱为 interface{}
  • ValueOf 同时读取 _typedata ,返回 reflect.Value
源代码变量          interface{} 装箱            反射拆解

var x MyStruct  →  eface{                  →  reflect.TypeOf:  读 _type
                     _type: *MyStruct元数据       → Name(), Field(), Method()...
                     data:  *x的副本           reflect.ValueOf: 读 _type + data
                   }                             → Int(), String(), Set()...

4.6.2. 反射利用的三个性质

变量性质 反射如何利用
类型信息编译时嵌入二进制 Go 编译器为每个类型生成 runtime._type 结构体,包含大小、对齐、方法集、字段信息等,链接进可执行文件。反射在运行时读取这些元数据。
interface 保存了 (type, value) 对 反射的入口就是 interface{},它天然携带类型指针和数据指针,反射只是把这对指针"拆开看"。
内存布局是类型确定的 知道了类型(字段偏移、大小),就能用 unsafe.Pointer 算术直接读写结构体的任意字段。

4.6.3. 总结

  • 反射的入口必须是 interface{} ,因为只有 interface 在运行时同时携带 type 和 value。
  • 反射比直接访问慢–需要通过指针间接查表,无法被编译器内联优化。
  • reflect.Value.Set() 要求可寻址( addressable )–因为它需要真正修改原始内存,而不是 interface 中的值副本。

5. 流程控制

5.1. select

select 的行为模式主要是 对 channel 是否可读进行轮询 ,但也可以用来向 channel 发送数据。它的行为如下:

  • 如果所有的 case 语句块评估时都被阻塞,则阻塞直到某个语句块可以被处理
  • 如果多个 case 同时满足条件,则 随机 选择一个进行处理,对于这一次的选择,其它的 case 都不会被阻塞,而是处理完被选中的 case 后进入下一轮select ( 如果 select 在循环中 ) 或者结束 select ( 如果 select 不在循环中或循环次数结束 )
  • 如果存在 default 且其它 case 都不满足条件,则执行 default 。 所以 default 必须要可执行且不能阻塞

5.1.1. 永久阻塞

func main() {
    select {}
}

5.1.2. 限时等待

// 该函数返回一个管道,可用于在函数之间传递,但该管道会在指定时间后自动关闭
func waitForStopOrTimeout(stopCh <-chan struct{}, timeout time.Duration) <-chan struct{} {
    stopChWithTimeout := make(chan struct{})
    go func() {
        select {
        case <-stopCh: // 自然结束
        case <-time.After(timeout):    // 最长等待时间长
        }
        close(stopChWithTimeout) }()
    return stopChWithTimeout

}

5.2. type Switch 结构

package main

import (
    "fmt"
)

// Shaper 接口类型
type Shaper interface {
    Area() float64
}

// Circle struct类型
type Circle struct {
    radius float64
}

// Circle类型实现Shaper中的方法Area()
func (c *Circle) Area() float64 {
    return 3.14 * c.radius * c.radius
}

// Square struct类型
type Square struct {
    length float64
}

// Square类型实现Shaper中的方法Area()
func (s Square) Area() float64 {
    return s.length * s.length
}

func main() {
    s1 := &Square{3.3}
    whichType(s1)

    s2 := Square{3.4}
    whichType(s2)

    c1 := new(Circle)
    c1.radius = 2.3
    whichType(c1)
}

func whichType(n Shaper) {
    switch v := n.(type) {
    case *Square:
        fmt.Printf("Type Square %T\n", v)
    case Square:
        fmt.Printf("Type Square %T\n", v)
    case *Circle:
        fmt.Printf("Type Circle %T\n", v)
    case nil:
        fmt.Println("nil value: nothing to check?")
    default:
        fmt.Printf("Unexpected type %T", v)
    }
}

6. 并发

goroutine.png

Figure 7: How the Go scheduler manages goroutines

6.1. Goroutine Stack

以下内容以标准 Go 编译器 gc 和 Go 1.26 runtime 为准。运行时常量和分配细节可能随版本、操作系统和架构变化。

核心结论:每个 goroutine 都有自己独立的、连续但可移动的栈。栈从较小容量开始,空间不足时分配更大的连续内存并搬迁;使用量长期较小时,可在 GC 扫描期间缩容。

goroutine 的栈属于 G=,不属于执行它的 OS 线程 =M=。因此 goroutine 可以迁移到不同线程继续执行,而它的调用栈仍随 =G 一起保存。

6.1.1. 栈布局与使用

每个 goroutine 的 g 结构记录栈的地址范围 =[stack.lo, stack.hi)=。Go 支持的常见架构上,栈从高地址向低地址增长:

高地址  stack.hi
┌────────────────────────┐
│ 最早的调用帧            │
│ ...                    │
│ 当前函数调用帧          │  ← SP
├────────────────────────┤
│ 尚未使用的空间          │
├────────────────────────┤  ← stackguard0
│ NOSPLIT/扩栈路径预留区   │
│ OS 特殊处理预留区        │  ← stackSystem(部分平台)
└────────────────────────┘
低地址  stack.lo

栈帧通常包含:

  • 未逃逸的局部变量;
  • 返回地址、保存的寄存器和寄存器溢出区;
  • 部分参数和返回值,具体取决于寄存器 ABI 和编译优化;
  • defer、panic、栈对象等运行时需要的信息。

函数栈帧的大小基本在编译期确定。函数调用和返回主要通过移动 SP 完成,不需要像堆对象一样逐个申请、释放。

局部变量是否放在栈上由逃逸分析决定。编译器不能证明其生命周期受当前调用约束时,变量通常逃逸到堆:

func value() *int {
    x := 10
    return &x // x 通常逃逸到堆
}

使用以下命令查看逃逸分析结果:

go build -gcflags="-m=2" ./...

GC 会使用编译器生成的 stack map 精确识别活动栈帧中的指针槽。栈本身由 runtime 专门管理,但栈里的指针是 GC 根集合的一部分。

6.1.2. 创建与初始栈

go f() 会被编译器转换为调用 =runtime.newproc=。其主要流程是:

go f()
  ↓
runtime.newproc / runtime.newproc1
  ↓
从当前 P 的 gFree 获取可复用的 g
  ├─ 有:复用 g,以及尺寸合适的栈
  └─ 无:创建新的 g,并分配最小栈
  ↓
初始化 SP、PC 和 goexit 返回路径
  ↓
放入 P 的 runnable 队列

Go 1.26 中 =stackMin = 2048=,表示 Go 代码所需的最小栈为 2 KiB。实际最小分配值是 =fixedStack=,还要加入部分平台的 =stackSystem=,然后向上取到 2 的幂。

“每个 goroutine 永远从 2 KiB 开始”并不完全准确。runtime 还维护 =startingStackSize=:

上一轮 GC 扫描到的栈使用量平均值
  + stackGuard
  ↓
限制在 fixedStack 与 maxstacksize 之间
  ↓
向上取到 2 的幂
  ↓
作为后续 goroutine 的初始栈目标

这是避免 goroutine 在生命周期早期频繁扩栈的启发式策略。全新创建 g 且没有可复用项时,源码路径仍由 malg(stackMin) 分配最小栈;复用的 g 如果没有合适栈,则按当前 startingStackSize 重新分配。

6.1.3. 栈检查与扩容

编译器会在大多数函数入口插入栈检查。概念上类似:

if SP-frameSize < stackguard0 {
    runtime.morestack()
}

stackguard0 是逻辑检查边界,不等同于操作系统的不可访问 guard page。带 //go:nosplit 的函数不执行普通栈检查,因此 runtime 会为一串 NOSPLIT 调用以及扩栈路径预留空间。

发现空间不足后:

  1. morestack 保存当前 goroutine 的执行现场;
  2. 切换到当前 M 的 g0 系统栈,避免在已经不足的用户栈上分配内存;
  3. newstack 通常把容量扩大为原来的 2 倍;
  4. 如果即将进入的栈帧很大,则继续翻倍直到能够容纳;
  5. copystack 分配新的连续栈,只复制当前实际使用的部分;
  6. 修正栈帧、defer、panic、channel 等结构中指向旧栈的指针;
  7. 切换到新栈,释放旧栈并恢复执行。
2 KiB → 4 KiB → 8 KiB → 16 KiB → 32 KiB → ...

单次扩容需要复制当前活动栈,成本约为 =O(已使用栈大小)=;由于容量按倍数增长,总体具有较低的摊销成本。当前默认最大栈限制在 64 位平台约为 1 GB、32 位平台约为 250 MB,超过限制会触发不可恢复的 stack overflow。

现代 gc 工具链使用的是连续、可搬迁栈。Go 1.3 起已经不再使用早期的 segmented stack 模型。

6.1.4. 栈缩容与回收

函数返回只会恢复 SP,不会立即缩小底层栈。GC 扫描 goroutine 时,如果能够安全搬迁,并且满足:

已使用空间 + stackNosplit < 当前栈容量 / 4

runtime 会尝试把栈缩小为当前容量的一半,但不会低于 =fixedStack=。一次 GC 最多缩小一级;后续 GC 仍满足条件时可以继续缩小。

处于 syscall、异步安全点或某些 channel parking 临界状态时,栈不能立即搬迁,runtime 会把缩容延迟到后续同步安全点。

goroutine 结束后:

  • g 进入当前 P 或全局的空闲列表;
  • 尺寸等于当前 startingStackSize 的栈可以随 g 保留,供后续 goroutine 复用;
  • 其他尺寸的栈归还给栈缓存、全局池或 runtime 页分配器。

6.1.5. 栈的底层分配

stackalloc 必须运行在 g0 系统栈上,要求分配大小为 2 的幂。goroutine 栈不是普通 GC 堆对象,但通常复用 runtime 的页分配器和 manual =mspan=:

  • 小栈优先从当前 P 的 mcache.stackcache 获取;
  • 本地缓存为空时,从全局 stackpool 批量补充;
  • 较大的栈使用专用 manual span,并可通过大栈缓存复用;
  • 完全空闲的 stack span 会重新归还给通用 heap 页分配器。

在典型 64 位 Unix 平台的 Go 1.26 中,2、4、8、16 KiB 栈走小栈缓存,32 KiB 及以上通常使用专用 span。该阈值属于 runtime 实现细节。

6.1.6. stackSystem

stackSystem 是 runtime 在每个 goroutine 栈底,为部分操作系统的信号、异常等特殊处理路径额外预留的空间。它不是另一块栈,也不是普通 Go 函数可以自由使用的额外容量。

多数 Unix 系统可以通过 sigaltstack 使用独立的信号栈。Windows、Plan 9 和 iOS 的相关路径不使用独立栈,因此即使 goroutine 已接近栈底,也必须保证信号或异常处理代码有空间执行,不能依赖此时再进行普通扩栈。

Go 1.26 中的定义为:

stackSystem = goos.IsWindows*4096 +
    goos.IsPlan9*512 +
    goos.IsIos*goarch.IsArm64*1024
平台 stackSystem
Windows 4096 字节
Plan 9 512 字节
iOS/arm64 1024 字节
Linux/macOS 等 0 字节

它同时参与最小栈和 guard 边界的计算:

fixedStack = roundUpToPowerOfTwo(stackMin + stackSystem)
stackGuard = stackNosplit + stackSystem + abi.StackSmall

例如:

平台 stackMin + stackSystem fixedStack
Linux/macOS 2048 + 0 2048
Windows 2048 + 4096 8192
Plan 9 2048 + 512 4096
iOS/arm64 2048 + 1024 4096

因此 stackSystem 会增加最小栈分配,并把 stackguard0 向高地址移动,使普通函数更早触发扩栈,从而保留栈底空间给 OS 特殊处理路径。

6.1.7. runtime.MemStats.StackInuseStackSys

两者当前的关系可以概括为:

StackInuse = runtime 保留作栈用途的 stack span 容量

StackSys   = StackInuse
             + runtime 能统计到的、直接由 OS 分配的线程栈
指标 含义
StackInuse 当前划为栈用途的 mspan 总容量,包括 goroutine/runtime 栈、缓存、span 内空闲槽位和每个栈尚未使用的容量
StackSys StackInuse 加上 runtime 能统计到的、由 OS 直接分配的线程栈

StackInuse 不是所有 goroutine 当前活动栈帧字节数之和。例如 goroutine 已分配 16 KiB 栈、当前只使用 3 KiB,统计口径仍按分配容量;小栈 span 内的碎片和缓存也可能计入。

纯 Go、非 cgo 程序中,当前通常有:

StackSys == StackInuse

某些 cgo、=c-shared= 或 c-archive 场景中,直接由 OS 提供的线程栈会让 StackSys > StackInuse=。但目前 cgo 栈统计并不完整,C 代码自行创建的线程栈通常不会全部计入,所以 =StackSys 不能作为进程全部线程栈的真实总量。

例如:

StackInuse = 64 MiB  // runtime 管理并保留作栈用途的内存
StackSys   = 72 MiB  // 再加 8 MiB 可统计到的 OS 线程栈

这不表示当前调用帧实际占用了 64 MiB,也不表示进程有 72 MiB 常驻物理内存。

没有 StackIdle 字段。stack span 完全不再用于栈之后,会归还给通用 heap 页分配器,内存分类可能从 StackInuse 转移到 =HeapIdle=:

GC/栈收缩前:StackInuse 较高
GC/栈释放后:StackInuse 下降,HeapIdle 可能上升

这种分类变化不代表内存已经归还操作系统,因此进程 RSS 不一定同步下降。

对应的 runtime/metrics 指标为:

runtime/metrics 含义
/memory/classes/heap/stacks:bytes 对应 StackInuse
/memory/classes/os-stacks:bytes 当前约等于 StackSys - StackInuse
/gc/scan/stack:bytes 上一轮 GC 扫描的栈字节数,更接近活动栈规模,但不是当前时刻精确值
/gc/stack/starting-size:bytes 新 goroutine 的当前初始栈目标值

不要使用 StackInuse / runtime.NumGoroutine() 推导单个 goroutine 的精确栈用量,因为其中包含 runtime goroutine、不同栈容量、栈缓存和 span 粒度带来的开销。

6.1.8. 实际影响

  • goroutine 很轻量,但不是零成本;goroutine 泄漏会保留栈以及栈引用的堆对象;
  • 深递归会持续扩栈,达到上限后导致不可恢复的 stack overflow;
  • 栈会搬迁,不要把由栈地址转换得到的 uintptr 长期保存;隐藏在 uintptr 中的地址不会随栈搬迁自动修正;
  • StackInuse=、=StackSys 都是 runtime 内存分类指标,不等同于实际活动栈大小或进程 RSS。

6.1.9. 参考资料

7. 并发模式

7.1. Join

01-join.svg

Figure 8: 单任务 Join

02-join-with-exit-status.svg

Figure 9: 带结果的 Join

03-join-multiple.svg

Figure 10: 多任务 Join

04-join-with-timeout.svg

Figure 11: 带超时的 Join

7.2. Notify and Wait

01-notify-and-wait-for-one.svg

Figure 12: 通知并等待单个 Worker

02-notify-and-wait-for-multiple.svg

Figure 13: 通知并等待多个 Worker

7.3. Pipe

pipe.png

Figure 14: Pipe 的扇出模式和扇入模式

7.4. Work Pool

一个固定大小的 goroutine 工作池。调用方只负责提交任务,Pool 负责限制并发数量、分发任务以及统一关闭 Worker。

01-work-pool-structure.png

Figure 15: 核心结构

02-work-pool-lifecycle.png

Figure 16: 运行流程

特点:

  • 通过固定数量的 Worker 限制并发度,避免为每个任务无限创建 goroutine。
  • 无缓冲 channel 提供自然背压,让任务生产速度受 Worker 处理能力约束。
  • 任务提交和任务完成是两个不同时间点。
  • Shutdown 前必须确保不再调用 Run;关闭后继续发送会触发 panic。
  • maxGoroutines 应大于 0,否则调用 Run 会一直阻塞。

7.5. Resource Pool

并发安全的资源复用池。资源必须实现 io.Closer,Pool 负责按需创建资源、缓存空闲资源,并在资源多余或 Pool 关闭时释放资源。

它与 Worker Pool 不同:Worker Pool 限制并发任务数;这里的 Resource Pool 主要限制空闲资源的缓存数量。

01-resource-pool-flow.png

Figure 17: 核心流程

7.6. Runner

统一管理 一组顺序任务 ,并在任务完成、执行超时或收到操作系统中断时返回对应结果。

01-runner-flow.png

Figure 18: 核心流程

02-runner-timeout-semantics.png

Figure 19: 超时语义

模式特点与限制:

  • 适合按固定顺序运行一组不可并发的任务。
  • 统一对外暴露正常完成、超时和中断三种结果。
  • 任务函数没有错误返回值,业务错误无法通过 Runner 汇总。
  • 任务 panic 没有恢复机制。
  • Add 与 Start 不能安全地并发调用。
  • Runner 的 timer 和 channel 都按单次运行设计,不适合重复调用 Start。
  • 若需要真正取消任务,应让任务接收 context.Context 或取消 channel,并在 timeout 或 interrupt 时主动传播取消信号。

8. OOP

8.1. 结构体嵌入(Embedding)

核心思想:Go 用组合(composition) 代替继承(inheritance),嵌入是实现组合的语法糖。

8.1.1. 组合代替继承(最常见)

Go 没有继承,用嵌入实现"is-a"或"has-a"关系:

type Animal struct {
    Name string
    Age  int
}

func (a Animal) Speak() string {
    return a.Name + " makes a sound"
}

type Dog struct {
    Animal        // 嵌入,Dog "继承"了 Animal 的字段和方法
    Breed string
}

d := Dog{Animal{"Buddy", 3}, "Labrador"}
d.Name              // ✅ 直接访问,不用 d.Animal.Name
d.Speak()           // ✅ 直接调用,不用 d.Animal.Speak()

8.1.2. 添加功能(装饰器模式)

给已有类型"附加"能力:

// 给任何结构体加锁能力
type SafeCounter struct {
    sync.Mutex             // 嵌入锁
    count int
}

func (c *SafeCounter) Inc() {
    c.Lock()               // 直接调用,不用 c.Mutex.Lock()
    defer c.Unlock()
    c.count++
}

8.1.3. 接口实现委托

嵌入一个已实现接口的类型,自动满足接口:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type MyReader struct {
    io.Reader              // 嵌入,自动实现 io.Reader 接口
    readCount int
}

// 可以直接传给需要 io.Reader 的地方
func process(r io.Reader) { ... }

f, _ := os.Open("file.txt")
mr := MyReader{Reader: f}
process(mr)                // ✅ MyReader 实现了 io.Reader

8.1.4. 覆盖/扩展方法

嵌入后可以覆盖方法:

type Base struct{}
func (b Base) String() string { return "base" }

type Extended struct {
    Base
}
func (e Extended) String() string { return "extended" }  // 覆盖

e := Extended{}
e.String()        // "extended"(调用 Extended 的方法)
e.Base.String()   // "base"(仍可访问原始方法)

8.1.5. 嵌入指针 vs 嵌入值

// 嵌入值 - 拷贝语义
type A struct {
    Inner       // Outer 包含 Inner 的完整拷贝
}

// 嵌入指针 - 共享语义(更常用于大结构体)
type B struct {
    *Inner      // Outer 只包含指针,多个实例可共享
}

例子:

type Config struct {
    Debug bool
    Port  int
}

type ServerB struct {
    *Config  // 嵌入指针
    Name string
}

func TestEmbedPointer(t *testing.T) {
    cfg := &Config{true, 8080}

    s1 := ServerB{cfg, "server1"}
    s2 := ServerB{cfg, "server2"}  // 共享同一个 Config

    s2.Debug = false  // 修改 s2

    fmt.Println(s1.Debug)  // false  ← s1 也变了!
    fmt.Println(s2.Debug)  // false
}

8.1.6. 方法提升、方法集与可寻址性

嵌入字段的方法可以提升到外层结构体,因此可以省略中间的字段选择器。方法提升并不等于继承,它仍然受 Go 方法集规则的约束。

type T1 struct{}

func (T1) T1M1()   {}
func (T1) T1M2()   {}
func (*T1) PT1M3() {}

type T2 struct{}

func (T2) T2M1()   {}
func (T2) T2M2()   {}
func (*T2) PT2M3() {}

type T struct {
    T1
    *T2
}

在这个例子中, T1 是值类型嵌入, *T2 是指针类型嵌入。提升后的方法集如下 :

外层类型 方法集
T PT2M3T1M1T1M2T2M1T2M2
*T PT1M3PT2M3T1M1T1M2T2M1T2M2

*TT 多出一个 PT1M3 。一般规则是 :

  • 如果 S 嵌入值类型 U ,那么 S*S 的方法集都包含接收者为 U 的提升方法;只有 *S 的方法集还包含接收者为 *U 的提升方法。
  • 如果 S 嵌入指针类型 *U ,那么 S*S 的方法集都包含接收者为 U*U 的提升方法。
8.1.6.1. 背后的逻辑

方法集描述的是类型本身稳定拥有的能力,不能依赖某个具体表达式是否恰好可以取地址。

  • 值嵌入只保存一个 U 值。调用 *U 的方法需要取得内嵌值的地址,因此外层为 *S 时总能完成转发,而普通的 S 值不一定可寻址。
  • 指针嵌入直接保存一个 *U 指针。即使外层 S 不可寻址,也可以通过已经保存的指针调用 U*U 的方法。

局部变量通常可以取地址,所以 Go 会为方法调用自动取地址 :

var t T
t.PT1M3()             // 可以,等价于 (&t).PT1M3()

func newT() T {
    return T{}
}

newT().PT1M3()        // 编译错误:临时值不可寻址

values := map[string]T{"a": T{}}
values["a"].PT1M3()  // 编译错误:map 元素不可寻址

自动取地址只是调用语法上的便利,不会改变 T 的方法集。这个区别会直接影响接口实现 :

type I interface {
    PT1M3()
}

var _ I = T{}          // 编译错误:T 的方法集没有 PT1M3
var _ I = (*T)(nil)    // 可以:*T 的方法集包含 PT1M3

如果检查方法集的辅助函数内部使用 reflect.TypeOf(i).Elem() ,那么传入 &t 得到的是 T ,传入 &pt 得到的是 *T ,其中 pt 的类型为 *T 。额外的一层取地址只是为了配合 Elem() ,实际比较的仍然是 T*T 的方法集。

8.2. interface 本质

概念 本质
interface{} eface{_type, data}(两个指针,16字节)
非空 interface iface{tab, data}(两个指针,16字节)
itab 接口类型 + 具体类型 + 方法表
方法调用 通过 itab.fun[] 间接跳转(类似 C++ vtable)
赋值 拷贝数据到堆 + 创建/复用 itab
nil 判断 tab 和 data 都为 nil 才是 nil

method_set_spec.png

Figure 20: Method sets as described by the Go specification

method_set.png

Figure 21: Method sets from the perspective of the receiver type

8.2.1. 空接口 interface{} / any - eface

// runtime/runtime2.go
type eface struct {
    _type *_type          // 指向类型信息
    data  unsafe.Pointer  // 指向实际数据
}
var i interface{} = 42

i (eface, 16字节)
┌─────────────────────┐
│ _type ──────────────┼──► type info (int)
│ data  ──────────────┼──► 42
└─────────────────────┘

8.2.2. 非空接口 - iface

// runtime/runtime2.go
type iface struct {
    tab  *itab            // 类型信息 + 方法表
    data unsafe.Pointer   // 指向实际数据
}
var w io.Writer = os.Stdout

w (iface, 16字节)
┌─────────────────────┐
│ tab  ───────────────┼──► itab
│ data ───────────────┼──► os.Stdout 实例
└─────────────────────┘
8.2.2.1. itab 结构(核心)
type itab struct {
    inter *interfacetype  // 接口类型信息
    _type *_type          // 具体类型信息
    hash  uint32          // 类型哈希,用于快速类型断言
    _     [4]byte
    fun   [1]uintptr      // 方法表(实际大小按方法数量分配)
}


type _type struct {      // 类型元信息
    size       uintptr   // 类型大小
    ptrdata    uintptr   // 包含指针的前缀大小
    hash       uint32    // 类型哈希
    tflag      tflag
    align      uint8     // 对齐
    fieldAlign uint8
    kind       uint8     // 类型种类 (int, string, struct, ...)
    equal      func(unsafe.Pointer, unsafe.Pointer) bool
    gcdata     *byte
    str        nameOff
    ptrToThis  typeOff
}
itab
┌───────────────────────┐
│ inter ───► io.Writer  │  (接口类型)
│ _type ───► *os.File   │  (具体类型)
│ hash:  0x12345678     │
│ fun[0] ───► Write     │  (方法地址)
│ fun[1] ───► ...       │
└───────────────────────┘
8.2.2.2. 完整内存布局
type Speaker interface {
    Speak() string
}

type Dog struct {
    Name string
}

func (d Dog) Speak() string { return d.Name + ": woof!" }

var s Speaker = Dog{"Buddy"}
s (iface)
┌──────────┐      itab                    interfacetype
│ tab ─────┼────► ┌─────────────────┐     ┌──────────────┐
│ data ──┐ │      │ inter ──────────┼────►│ Speaker      │
└────────┼─┘      │ _type ──────────┼──┐  │ methods:     │
         │        │ hash            │  │  │  - Speak     │
         │        │ fun[0]: Speak ──┼──┼──────► Dog.Speak 代码
         │        └─────────────────┘  │
         │                             ▼
         │        _type               ┌──────────────┐
         │        ┌──────────────┐    │ Dog 类型信息 │
         │        │ size: 16     │    └──────────────┘
         │        │ kind: struct │
         │        └──────────────┘
         ▼
    ┌──────────────┐
    │ Dog{         │  (堆上分配)
    │  Name:"Buddy"│
    │ }            │
    └──────────────┘

赋值时发生了什么

var s Speaker = Dog{"Buddy"}

// 1. 查找或创建 itab(<Speaker, Dog> 这对组合)
// 2. Dog{"Buddy"} 拷贝到堆上
// 3. iface.tab = &itab
// 4. iface.data = 堆上数据的地址

方法调用过程

s.Speak()

// 实际执行:
// 1. 取 s.tab → itab
// 2. 取 itab.fun[0] → Dog.Speak 的地址
// 3. 调用 Speak(s.data)

用 unsafe 验证

func TestInterfaceInternal(t *testing.T) {
    type iface struct {
        tab  uintptr
        data uintptr
    }

    var s Speaker = Dog{"Buddy"}

    // 查看 iface 结构
    i := (*iface)(unsafe.Pointer(&s))
    t.Logf("tab:  0x%x", i.tab)   // itab 地址
    t.Logf("data: 0x%x", i.data)  // Dog 数据地址

    // 通过 data 指针读出 Dog
    d := (*Dog)(unsafe.Pointer(i.data))
    t.Logf("Name: %s", d.Name)    // "Buddy"
}

nil interface vs interface 包含 nil

// nil interface - tab 和 data 都是 nil
var s Speaker           // tab=nil, data=nil
s == nil                // ✅ true

// 非 nil interface,但 data 是 nil
var d *Dog              // nil 指针
var s Speaker = d       // tab=&itab(Speaker,*Dog), data=nil
s == nil                // ❌ false!因为 tab 不是 nil
nil interface:           包含 nil 值的 interface:
┌──────────────┐         ┌──────────────────────┐
│ tab:  nil    │         │ tab:  &itab (非nil!)  │
│ data: nil    │         │ data: nil             │
└──────────────┘         └──────────────────────┘
   == nil ✅                == nil ❌ (经典坑!)

8.2.3. 两种赋值方式

8.2.3.1. 值 receiver - 两种都行
func (d Dog) Speak() string { return d.Name }

var s Speaker = Dog{"Buddy"}   // ✅ 值类型
var s Speaker = &Dog{"Buddy"}  // ✅ 指针类型
8.2.3.2. 指针 receiver - 只能用指针 (常用)
func (d *Dog) Speak() string { return d.Name }

var s Speaker = Dog{"Buddy"}   // ❌ 编译错误:Dog 没实现 Speaker
var s Speaker = &Dog{"Buddy"}  // ✅ *Dog 实现了 Speaker
8.2.3.3. 区别
特性 Dog{"Buddy"} (值) &Dog{"Buddy"} (指针)
data 指向 Dog 的副本 Dog 的指针
修改原变量 不影响接口中的值 影响接口中的值
拷贝成本 拷贝整个 Dog 只拷贝一个指针(8字节)
itab <Speaker, Dog> <Speaker, *Dog>
8.2.3.3.1. 值赋值

var s Speaker = Dog{"Buddy"}

赋值时,Dog 值被 拷贝到堆上:

s (iface, 栈上, 16字节)
┌──────────────────────┐
│ tab:  &itab          │
│ data ────────────────┼──► ┌──────────────┐ (堆上,拷贝)
└──────────────────────┘    │ Name: "Buddy"│
                            └──────────────┘
                            (这是 Dog 的副本,和原始值无关)

赋值后修改原变量不影响接口:

d := Dog{"Buddy"}
var s Speaker = d
d.Name = "Max"
// s 里的还是 "Buddy"(拷贝了一份)
8.2.3.3.2. 指针赋值

var s Speaker = &Dog{"Buddy"}

data 存的是 指向 Dog 的指针:

s (iface, 栈上, 16字节)
┌──────────────────────┐
│ tab:  &itab          │
│ data ────────────────┼──► ┌───────────┐     ┌──────────────┐
└──────────────────────┘    │ *Dog ptr ─┼──►  │ Name: "Buddy"│
                            └───────────┘     └──────────────┘
                            (指针)            (Dog 实例,堆上)

共享同一个 Dog:

d := &Dog{"Buddy"}
var s Speaker = d
d.Name = "Max"
// s 里的也变成 "Max"(共享同一个 Dog)

9. 工具

9.1. go-wrk

go install github.com/adjust/go-wrk
go-wrk http://localhost:8080
#concurrency 400, thread 8, total calls 100000
go-wrk -c=400 -t=8 -n=100000 http://localhost

9.2. objdump

go tool objdump 和系统的 objdump 都能反汇编二进制中的机器指令,但关注点不同:前者面向 Go 程序,能识别 Go 包和函数符号、关联源码行,默认输出 Go/Plan 9 汇编语法;后者是通用目标文件工具,除反汇编外还可检查 ELF、Mach-O、PE 的文件头、section、符号表和重定位信息。

go build -o demo main.go

# 反汇编所有 Go text symbol
go tool objdump demo

# 只查看 main.main,并在汇编旁显示 Go 源码
go tool objdump -S -s 'main\.main' demo

# 在 Go 汇编旁附加 GNU 汇编语法(目标架构支持时)
go tool objdump -gnu -s 'main\.main' demo

# 使用系统 objdump 反汇编;具体选项因 GNU/LLVM 实现而异
objdump -d demo

分析某个 Go 函数生成了哪些指令时,优先使用 go tool objdump ;检查整个二进制的 section、符号、重定位或 cgo 代码时,使用系统 objdump 更合适。两者输出看起来不同通常只是汇编语法和符号展示方式不同,底层机器指令仍来自同一个二进制。

Author: Hao Ruan (ruanhao1116@gmail.com)

Created: 2022-12-30 Fri 15:27

Updated: 2026-08-07 Fri 12:32

Emacs 30.1 (Org mode 9.7.11)