一、基础语法
1.1 结构体与接口
Go 中结构体和接口通过**隐式实现(Duck Typing)**关联,不需要 implements 关键字。
go
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + ": Woof!"
}
// Dog 自动满足 Speaker 接口,无需声明
func MakeItSpeak(s Speaker) {
fmt.Println(s.Speak())
}
方法集规则:
T值只能调用值接收者方法*T指针可调用值+指针接收者方法
编译期验证:
go
var _ Speaker = Dog{} // 验证值接收者
var _ Speaker = (*Dog)(nil) // 验证指针接收者
1.2 不同接口同名方法
| 场景 | 结果 |
|---|---|
| 同名同签名 | 一次实现,同时满足两个接口 |
| 同名同签名,语义不同 | 编译通过但逻辑错误,需拆分类型 |
| 同名不同签名 | 无法同时满足,编译错误 |
Go 只看方法签名,不看接口名字或语义。
1.3 切片(Slice)
底层结构
go
type slice struct {
ptr *array // 指向底层数组的指针
len int // 当前长度
cap int // 容量
}
len vs cap
-
len :当前可读写的元素个数(
s[i]中0 <= i < len合法) -
cap:当前不重新分配底层数组,最多能装多少个元素
底层数组: [1] [2] [3] [4] [5] [6] [7] [8]
↑ ↑ ↑
ptr len cap -
[0, len)范围:可读可写 -
[len, cap)范围:空间已分配,但无法直接访问,只能通过append逐个扩展 -
超过 cap:自动扩容,不是硬限制
扩容规则(Go 1.18+)
| 条件 | 最终容量 |
|---|---|
| 期望容量 > 旧容量 * 2 | 直接用期望容量 |
| 旧容量 < 256 | 新容量 = 旧容量 * 2 |
| 旧容量 >= 256 | 新容量 = 旧容量 + 旧容量/4 + 192(平滑增长,趋近 ~1.25x) |
计算后还会进行内存对齐,实际 cap 可能更大。
扩容本质:分配新底层数组 + memmove 批量拷贝旧数据。
常见陷阱
go
// 1. 共享底层数组
a := []int{1, 2, 3, 4, 5}
b := a[1:3]
b = append(b, 99) // cap够,不扩容,覆盖 a[3]!
// 2. 三索引切割限制 cap
b := a[1:3:3] // len=2, cap=2,限制 cap
b = append(b, 99) // cap不够,扩容,不影响 a
排序
sort.Slice(推荐):
go
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age // 升序
})
slices.SortFunc(Go 1.21+):
go
slices.SortFunc(people, func(a, b Person) int {
return a.Age - b.Age // 升序:a-b,降序:b-a
})
cmp(a, b) 返回负数 -> a 排前面,正数 -> b 排前面,0 -> 相等。
1.4 数组 vs 切片
| 数组 | 切片 | |
|---|---|---|
| 长度 | 固定,编译期确定 | 动态,运行时可变 |
| 类型 | [3]int 和 [5]int 是不同类型 |
[]int 统一类型 |
| 传参 | 值拷贝 | 传切片头(廉价) |
[...]int{1,2,3} 是数组初始化的语法糖,编译器自动推断长度,结果等同于 [3]int{1,2,3},仍是固定长度数组。不能用于函数参数或类型声明。
1.5 可变参数
go
func sum(nums ...int) int { ... } // nums 内部是 []int
sum(1, 2, 3) // 直接传
sum(nums...) // 展开切片传入
1.6 基本数据类型
| 分类 | 类型 |
|---|---|
| 布尔 | bool |
| 有符号整数 | int8 int16 int32 int64 int |
| 无符号整数 | uint8 uint16 uint32 uint64 uint uintptr |
| 浮点 | float32 float64 |
| 复数 | complex64 complex128 |
| 字符串 | string |
| 别名 | byte(uint8) rune(int32) |
1.7 rune 与 string
- rune :Unicode 码点,
int32,一个整数编号 - string:UTF-8 编码后的字节序列
go
var r rune = '中' // 码点: 20013 (0x4E2D)
var s string = "中" // UTF-8: E4 B8 AD(3字节)
s := string(r) // 码点 → UTF-8 编码
r, _ := utf8.DecodeRuneInString(s) // UTF-8 → 码点
字符串索引 s[i] 返回 byte(uint8),range 遍历返回 rune(int32)。
二、make vs new
| new(T) | make(T) | |
|---|---|---|
| 返回值 | *T 指针 |
T 值 |
| 初始化 | 零值(不初始化内部结构) | 完整初始化(可直接使用) |
| 适用范围 | 任何类型 | 仅 slice、map、channel |
| 类型 | make | 字面量 | var 声明 | var 后能否直接用 |
|---|---|---|---|---|
| 切片 | 可 | 可 | 可 nil切片 | append 可,读写可 |
| Map | 可 | 可 | 可 nil map | 读取可,写入 panic |
| Channel | 可 | 不可 | 可 nil channel | 发送/接收永久阻塞,close panic |
三、并发编程
3.1 Channel
Channel 是 goroutine 之间传递数据和同步的管道。
Go 哲学:不要通过共享内存来通信,而要通过通信来共享内存。
无缓冲 vs 有缓冲
| 无缓冲 | 有缓冲 | |
|---|---|---|
| 创建 | make(chan int) |
make(chan int, N) |
| 发送阻塞 | 没人接收就阻塞 | 缓冲区满才阻塞 |
| 接收阻塞 | 没人发送就阻塞 | 缓冲区空才阻塞 |
| 本质 | 同步,一对一握手 | 异步,生产者-消费者 |
关闭 Channel
| 规则 | 说明 |
|---|---|
| 谁发送谁关闭 | 只有发送方关闭 |
| 不能关闭已关闭的通道 | panic |
| 不能向已关闭的通道发送 | panic |
不是必须关闭 ,唯一必须关闭的场景是接收方用 range 遍历时。不关闭 range 会死锁。
方向限定
go
func send(ch chan<- int) {} // 只写
func recv(ch <-chan int) {} // 只读
3.2 获取协程执行结果
goroutine 的返回值会被丢弃,只能通过 channel 获取结果。
go
type Result struct {
ID int
Value int
Err error
}
ch := make(chan Result, 3)
for i := 0; i < 3; i++ {
go func(id int) {
val, err := doWork(id)
ch <- Result{ID: id, Value: val, Err: err}
}(i)
}
for i := 0; i < 3; i++ {
r := <-ch
fmt.Printf("任务%d: 结果=%d\n", r.ID, r.Value)
}
3.3 GMP 调度模型
G - Goroutine 协程(用户态轻量线程)
M - Machine 操作系统线程(内核态)
P - Processor 逻辑处理器(调度上下文)
P 是 GMP 的灵魂:M 必须持有 P 才能执行 G。
M 从哪拿 G(优先级)
1. P 的本地队列(LRQ) ← 优先,无锁
2. 全局队列(GRQ) ← 每 61 次取 1 个,有锁但极少
3. 其他 P 偷(work stealing) ← 本地空了才偷
4. 网络轮询器(netpoll) ← 等 IO 的 G
5. 阻塞等待 ← 实在没有就休眠
两种阻塞的区别
| 系统调用阻塞 | Channel/IO 阻塞 | |
|---|---|---|
| 谁阻塞了 | M 阻塞(线程卡在内核) | G 阻塞(协程挂起) |
| P 要不要解绑 | 必须,M 卡住了 | 不需要,M 还活着 |
| M 的去向 | 等系统调用返回 | 继续执行其他 G |
| P 的去向 | 找新 M 绑定(hand off) | 不动,继续用当前 M |
全局队列的锁竞争
全局队列确实有锁,但访问频率极低:
- P 优先从本地无锁队列取 G(99%)
- 每 61 次调度才从全局队列取 1 个
- 取时批量获取,减少访问次数
- 本地队列满溢出时才放全局队列
抢占式调度
- G 执行超过 10ms 被标记为抢占
- Go 1.14+ 基于信号的抢占,即使死循环也能被踢走
四、垃圾回收(GC)
4.1 核心特点
| 特点 | 说明 |
|---|---|
| 算法 | 三色标记清除 |
| 触发方式 | 并发,不 Stop The World(大部分时间) |
| 不支持 | 无分代、无压缩 |
4.2 三色标记法
| 颜色 | 含义 |
|---|---|
| 白色 | 未被访问,GC 结束后回收 |
| 灰色 | 已访问,但引用的对象还没全访问 |
| 黑色 | 已访问,且引用的对象都已访问 |
过程:从根对象出发,标灰 → 标黑 + 引用标灰 → 直到没有灰色 → 剩余白色 = 垃圾。
4.3 写屏障
并发标记时,用户代码可能修改引用关系导致漏标。写屏障在每次修改引用时,强制将被引用的对象标灰,防止漏标。
写屏障只对堆生效,栈上没有写屏障(栈操作太频繁,加屏障性能损失大)。
4.4 GC 完整流程
1. 标记准备(STW,极短) → 开启写屏障,根对象标灰
2. 并发标记(和用户代码并行)→ 扫描灰色对象,写屏障保护
3. 标记终止(STW,极短) → 重扫描栈,处理剩余灰色,关闭写屏障
4. 并发清除(和用户代码并行)→ 回收白色对象
STW(Stop The World):暂停所有用户代码,只让 GC 干活。Go 只在开头和结尾各 STW 极短时间(微秒级)。
4.5 GC 触发条件
| 条件 | 说明 |
|---|---|
| 堆内存增长到阈值 | 默认 GOGC=100,堆翻倍时触发 |
| 2分钟未触发 | 强制触发一次 |
| 手动调用 | runtime.GC() |
4.6 减少GC压力
- 减少堆分配,用值类型
sync.Pool复用对象- 预分配切片
make([]int, 0, N) strings.Builder代替字符串拼接
五、init 函数
- 在 main 函数之前,包被导入时自动执行
- 不能被调用,不能有参数和返回值
- 一个文件可有多个 init,按定义顺序执行
- 跨包:依赖的包先初始化
- 只执行一次,即使包被多次导入
执行顺序:全局变量初始化 → init → main
六、defer
- 延迟执行,函数返回前调用,先进后出(栈)
- 参数在 defer 时就确定
- 最常用场景:资源释放(Close/Unlock)
- defer 可以修改命名返回值
- 循环中 defer 会堆积,改用匿名函数
go
func work() {
mu.Lock()
defer mu.Unlock()
// 不怕中间 return 忘记解锁
}
七、recover
只有在 defer 中调用 recover 才能捕获 panic,其他地方调用返回 nil。
go
defer func() {
if r := recover(); r != nil {
fmt.Println("捕获:", r)
}
}()
八、装饰器
Go 没有装饰器语法,用函数包装实现:
go
func timer(fn func()) func() {
return func() {
start := time.Now()
fn()
fmt.Printf("耗时: %v\n", time.Since(start))
}
}
work := timer(work) // 包装
work()
HTTP 中间件是最典型的装饰器模式。
九、JSON 处理
go
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"`
Pass string `json:"-"`
}
// 解析
var user User
json.Unmarshal([]byte(jsonStr), &user) // 必须传指针
// 序列化
data, _ := json.Marshal(user)
| tag | 作用 |
|---|---|
json:"name" |
JSON 字段名为 name |
json:"-" |
忽略该字段 |
json:"name,omitempty" |
为零值时省略 |
注意:数字默认解析为 float64。
十、反射(reflect)
10.1 TypeOf vs ValueOf
go
reflect.TypeOf(x) → reflect.Type → 类型信息(名字、字段、tag、方法签名)
reflect.ValueOf(x) → reflect.Value → 值信息(读值、改值、调方法)
10.2 操作结构体
go
t := reflect.TypeOf(u) // Type 上
v := reflect.ValueOf(u) // Value 上
// 字段
t.NumField() // 字段数量
t.Field(i).Name // 字段名
t.Field(i).Tag.Get("json") // tag
v.Field(i) // 字段值
v.FieldByName("Name") // 按名字取值
// 方法
t.NumMethod() // 方法数量
t.Method(i).Name // 方法名
v.MethodByName("X").Call(args) // 调用方法
10.3 Map 相关
go
// Type 上
t.Key() // map 的 key 类型
t.Elem() // map 的 value 类型 / slice 元素类型 / chan 元素类型
// Value 上
v.MapKeys() // 所有 key
v.MapIndex(key) // 根据 key 取 value
10.4 修改值(必须传指针)
go
v := reflect.ValueOf(&x)
v = v.Elem() // 解引用
v.SetInt(100) // 修改
10.5 Kind 种类
go
reflect.Int / reflect.String / reflect.Bool / reflect.Struct /
reflect.Map / reflect.Slice / reflect.Ptr / reflect.Chan / reflect.Func
十一、版本管理工具
| 工具 | 特点 |
|---|---|
| goenv | Go 版本切换,最常用 |
| gvm | 功能全但不推荐,维护不活跃 |
| mise | 多语言版本管理,asdf 的 Rust 重写,新项目推荐 |
| asdf | 多语言版本管理元老,慢,已被 mise 取代 |
mise 安装:
powershell
# Windows
winget install jdx.mise --source winget --location "D:\software"
# macOS
brew install mise
mise 替代的是版本管理(nvm/pyenv/goenv),不是包管理。数据科学仍需 conda。
十二、Python match-case(对比参考)
Python 3.10+ 的 match-case 不是简单 switch,而是模式匹配,支持解构、类型判断、条件守卫:
python
match point:
case (0, 0): print("Origin")
case (x, 0): print(f"X axis: {x}")
case (x, y): print(f"Point: ({x}, {y})")
| 特性 | Go switch | Python match-case |
|---|---|---|
| 基本值匹配 | 支持 | 支持 |
| 解构匹配 | 不支持 | 支持 |
| 类型匹配 | 不支持 | 支持 |
| 条件守卫 | 不支持 | case x if cond: |