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. Channel

4.1.1. 声明

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

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

4.1.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.1.3. 数据读写

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

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

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

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

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

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

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

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

4.1.4. close

表示关闭 channel :

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

4.2. Slice

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. 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. Func

4.4.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.4.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.4.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.4.4. 接口中的函数

接口的方法通过 itab 分发:

var s Speaker = Dog{"Buddy"}

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

4.5. 反射

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

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

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

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

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

4.5.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 同时读取 _type 和 =data=,返回 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.5.2. 反射利用的三个性质

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

4.5.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. 并发控制

6.1. WaitGroup

比如某个 goroutine 需要等待其他几个 goroutine 全部完成,那么使用 WaitGroup 可以轻松实现。

package main

import (
    "fmt"
    "sync"
    "time"
)

func process(i int, wg *sync.WaitGroup) {
    fmt.Println("started Goroutine ", i)
    time.Sleep(2 * time.Second)
    fmt.Printf("Goroutine %d ended\n", i)
    wg.Done()
}

func main() {
    no := 3
    var wg sync.WaitGroup
    for i := 0; i < no; i++ {
        wg.Add(1)
        go process(i, &wg)      // 这里要使用指针类型作为参数
    }
    wg.Wait()
    fmt.Println("All go routines finished executing")
}

7. OOP

7.1. 结构体嵌入(Embedding)

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

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

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

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

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

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

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

7.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"(仍可访问原始方法)

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

7.2. interface 本质

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

7.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
└─────────────────────┘

7.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 实例
└─────────────────────┘
7.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] ───► ...       │
└───────────────────────┘
7.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 ❌ (经典坑!)

7.2.3. 两种赋值方式

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

var s Speaker = Dog{"Buddy"}   // ✅ 值类型
var s Speaker = &Dog{"Buddy"}  // ✅ 指针类型
7.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
7.2.3.3. 区别
特性 Dog{"Buddy"} (值) &Dog{"Buddy"} (指针)
data 指向 Dog 的副本 Dog 的指针
修改原变量 不影响接口中的值 影响接口中的值
拷贝成本 拷贝整个 Dog 只拷贝一个指针(8字节)
itab <Speaker, Dog> <Speaker, *Dog>
7.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"(拷贝了一份)
7.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-02-12 Thu 12:01

Emacs 30.1 (Org mode 9.7.11)