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

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)

Author: Hao Ruan (ruanhao1116@gmail.com)

Created: 2022-12-30 Fri 15:27

Updated: 2026-08-02 Sun 10:39

Emacs 30.1 (Org mode 9.7.11)