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 build 、 go 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.TypeOf 和 reflect.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)