Golang Panic & Throw & Map/Channel 并发笔记

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,终止进程。
  • 示例:并发读写已初始化 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 里 panicthrow 是两种不同机制
  • recover 只能捕获 panic,不能捕获 runtime.throw
  • nil map 安全读、写 panic 可 recover
  • 初始化 map 并发读写 → throw,程序直接 crash
  • channel 元素类型允许引用类型,但容量、大小和对齐有 runtime 检查
相关推荐
朗迹 - 张伟2 小时前
Golang安装笔记
开发语言·笔记·golang
yzx9910132 小时前
生活在数字世界:一份人人都能看懂的网络安全生存指南
运维·开发语言·网络·人工智能·自动化
小周同学@2 小时前
谈谈对this的理解
开发语言·前端·javascript
橙*^O^*安3 小时前
Go 语言基础:变量与常量
运维·开发语言·后端·golang·kubernetes
NiKo_W3 小时前
Linux 文件系统与基础指令
linux·开发语言·指令
工程师小星星3 小时前
Golang语言的文件组织方式
开发语言·后端·golang
乂爻yiyao3 小时前
java 代理模式实现
java·开发语言·代理模式
张子夜 iiii4 小时前
实战项目-----Python+OpenCV 实现对视频的椒盐噪声注入与实时平滑还原”
开发语言·python·opencv·计算机视觉
2301_770373734 小时前
Java集合
java·开发语言