Golang Panic & Throw & Map/Channel 并发笔记
1. Panic 与 Recover
Golang中除了error外,还有直接终止程序的panic,有些panic可以被recover捕获,有些不能,这是怎么回事呢?先介绍一下:
1.1 普通 Panic
可以被捕获,而且也可以开发者手动触发,代码有时候不注意会写出系统报的panic,空指针啥的。
- 通过
panic("message")
触发。 - 会沿调用栈向上传播,执行 defer 链。
- 可以用
recover()
捕获并恢复程序。 - 示例:
go
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
panic("boom!")
1.2 Throw(runtime fatal error)
这个就是不能被捕获的"panic",其实这个和上面的panic不是一个东西,但是都是会导致程序的终止,这个是开发者无法调用的。体现在源码里是throw()
方法,很常见,例如runtime/chan.go
中:
go
// compiler checks this but be safe.
if elem.Size_ >= 1<<16 {
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.Align_ > maxAlign {
throw("makechan: bad alignment")
}
mem, overflow := math.MulUintptr(elem.Size_, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
- 特性 :
- 不是 panic,不创建
_panic
对象。 - 不走 defer/recover 链。
- 直接 fatal error,终止进程。
- 不是 panic,不创建
- 示例:并发读写已初始化 map
go
m := make(map[int]int)
go func() { m[1] = 1 }()
_ = m[2] // concurrent map read and map write -> throw
2. Map 并发规则
map并发读写是会报不能recover的panic,但有个特例,就是这个map没初始化的时候,可以被recover。
情况 | map 是否 nil | 并发访问 | runtime 行为 | recover 可否 |
---|---|---|---|---|
读取 nil map | ✅ | 无 | 安全,返回零值 | n/a |
写入 nil map | ✅ | 无 | panic: assignment to entry in nil map | ✅ 可 recover |
并发读写未初始化 map | ✅ | ✅ | panic: assignment to entry in nil map | ✅ 可 recov |
可以用下面的例子体会一下:
go
fmt.Println("=== 1. 读取 nil map ===")
var nilMap map[int]int
fmt.Println(nilMap[1]) // 安全,返回零值
fmt.Println("\n=== 2. 写入 nil map(panic,可 recover) ===")
func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
nilMap[1] = 100 // panic: assignment to entry in nil map
fmt.Println("这行不会执行")
}()
fmt.Println("\n=== 3. 并发读写未初始化 map(panic, 可 recover) ===")
//m := make(map[int]int)
var m map[int]int
var wg sync.WaitGroup
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r) // 这里不会被触发
}
}()
wg.Add(2)
// 写 goroutine
go func() {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r) // 这里会被触发
}
}()
for i := 0; i < 1000; i++ {
m[i] = i
time.Sleep(time.Millisecond)
}
fmt.Println("到达不了这里")
}()
// 读 goroutine
go func() {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r) // 这里不会被触发,因为读是不会报错的,会读默认数
}
}()
for i := 0; i < 1000; i++ {
_ = m[i]
time.Sleep(time.Millisecond)
}
}()
wg.Wait()
关键点
- nil map 没有哈希桶结构,不会触发 runtime.throw。
- 写 nil map是普通 panic,可 recover。
- 初始化 map 并发读写会触发 runtime.throw,不可 recover。
另外,如果是加了把锁,那么map也是可以"并发"的:
go
func Test2(t *testing.T) {
m := make(map[int]int)
m[1] = 1
var wg sync.WaitGroup
ch := make(chan struct{}, 1)
wg.Add(2)
// 写 goroutine
go func() {
defer wg.Done()
for i := 0; i < 1000; i++ {
ch <- struct{}{}
m[i] = i
<-ch
time.Sleep(time.Millisecond)
}
//fmt.Println("到达不了这里")
}()
// 读 goroutine
go func() {
defer wg.Done()
for i := 0; i < 1000; i++ {
ch <- struct{}{}
_ = m[i]
<-ch
time.Sleep(time.Millisecond)
}
}()
wg.Wait()
}
3. Channel 类型规则
3.1 channel 元素类型
- 任意合法 Go 类型,包括:
- 基本类型(int, string...)
- struct、interface
- slice、map、chan、func(引用类型)
- 唯一禁止:
chan nil
或未实例化的泛型类型。
3.2 runtime makechan 检查
panic
:- 触发条件:channel 容量非法(size < 0 或过大)
- 可 recover
throw
:- 触发条件:元素类型过大(Size_ >= 1<<16)、对齐非法(Align_ > maxAlign)
- 不可 recover
- 用户一般触发不到 throw:
- 编译器会在类型大小或对齐不合法时直接报错
- 所以 throw 主要是 runtime 防御性检查
3.3 channel 元素类型可以包含引用类型
- slice、map、chan、func、interface 都可以作为 channel 元素
- 注意:
- 传入 channel 的值是按值拷贝的
- 引用类型底层数据共享,可能存在并发安全问题
- 风格上建议传值对象,避免 goroutine 间共享底层数据
4. 总结 panic / throw 区别
特性 | panic | throw |
---|---|---|
触发方式 | panic("msg") 或 runtime 检测逻辑 |
throw("fatal error") |
recover | ✅ 可以 | ❌ 不可 |
典型场景 | 数组越界、写 nil map、负容量 channel | map 并发读写、makechan 元素非法、bad alignment |
语义 | 可恢复错误 | runtime fatal error,程序不可继续运行 |
5. nil map vs 初始化 map
- nil map :
- 读安全
- 写触发普通 panic,可 recover
- 并发读写不会触发 throw
- 已初始化 map :
- 并发读写触发 runtime.throw,fatal error,不可 recover
6. 示例代码总结
go
// 1. 读取 nil map 安全
var m1 map[int]int
fmt.Println(m1[1]) // 0
// 2. 写 nil map panic,可 recover
defer func() {
if r := recover(); r != nil { fmt.Println("Recovered:", r) }
}()
m1[1] = 10 // panic: assignment to entry in nil map
// 3. 并发读写已初始化 map,fatal error,无法 recover
m2 := make(map[int]int)
var wg sync.WaitGroup
wg.Add(2)
go func() { for i:=0;i<5;i++{ m2[i]=i } wg.Done() }()
go func() { for i:=0;i<5;i++{ _=m2[i] } wg.Done() }()
wg.Wait() // fatal error: concurrent map read and map write
✅ 结论
- Go 里
panic
和throw
是两种不同机制 - recover 只能捕获 panic,不能捕获 runtime.throw
- nil map 安全读、写 panic 可 recover
- 初始化 map 并发读写 → throw,程序直接 crash
- channel 元素类型允许引用类型,但容量、大小和对齐有 runtime 检查