Goroutine执行nil函数引发段错误问题排查求助
假期开发了一个简易文件系统“数据库”测试程序,用goroutine实现“表锁”,每张表对应一个worker并通过worker ID标识。但程序稳定触发段错误,执行路径难以追踪。
循环开头设有处理删除请求(req.f == nil)的分支,本应跳过后续逻辑,但循环底部代码仍会在req.f == nil时执行。此外,仅清理函数会创建f==nil的请求,不清楚这类请求为何出现。
疑惑点:
- 为何goroutine会在
f==nil时执行? - 为何会立即出现清理请求?
type workId uint64 type workFunc func() type workReq struct { wId workId // set to nil to delete from list f *workFunc } const workCacheTimeout = time.Second * 5 func workRunner(wId workId, fc chan workFunc) { for f := range fc { f() } } for req := range c { log.Println("req1", req) if req.f == nil { n, ok := workers[req.wId] if ok { close(n.work) delete(workers, req.wId) } log.Println("deleted", req) continue } log.Println("req2", req) n, ok := workers[req.wId] if ! ok { n.work = make(chan workFunc) n.cleanupTimer = time.AfterFunc(workCacheTimeout, func () { c <- workReq{req.wId, nil} }) workers[req.wId] = n go workRunner(req.wId, n.work) } log.Println("req3", req) go func() { log.Println("req4", req) n.cleanupTimer.Reset(workCacheTimeout) n.work <- *req.f }() }
2022/12/25 11:51:14 req1 {650304 0xc0000a71e0} 2022/12/25 11:51:14 req2 {650304 0xc0000a71e0} 2022/12/25 11:51:14 req3 {650304 0xc0000a71e0} 2022/12/25 11:51:14 req1 {650304 <nil>} 2022/12/25 11:51:14 deleted {650304 <nil>} 2022/12/25 11:51:14 n {0xc0004a9e60 0xc000452c30} 2022/12/25 11:51:14 req4 {650304 <nil>} panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x64e405] goroutine 4715 [running]: main.handleWorkChan.func2() /redacted/dir_sync.go:62 +0x125
1. 为何goroutine会在f==nil时执行?
核心问题是闭包的延迟绑定特性。你创建匿名goroutine时直接引用了外层循环的req变量:
go func() { log.Println("req4", req) n.cleanupTimer.Reset(workCacheTimeout) n.work <- *req.f }()
外层循环会持续从通道c读取新请求,而匿名goroutine直到执行时才会获取req的当前值。当第一个正常请求的goroutine还未执行时,清理请求已经被处理,此时req已被更新为f==nil的请求,goroutine执行时就会读到空的f,触发空指针解引用错误。
修复方法是创建goroutine时,将当前的req和n作为参数传入,避免延迟绑定:
go func(r workReq, worker workerInfo) { log.Println("req4", r) worker.cleanupTimer.Reset(workCacheTimeout) if r.f != nil { worker.work <- *r.f } }(req, n)
2. 为何会立即出现清理请求?
这是因为**workers变量没有并发保护**。在创建worker的逻辑中,检查workers[req.wId]是否存在、创建worker并写入workers的过程不是原子操作。如果多个相同wId的请求同时进入,可能会重复创建worker,其中某个worker的清理定时器触发后发送删除请求,此时另一个请求的goroutine还在处理,就会出现“立即”收到清理请求的现象。
另外,关闭n.work后,后续尝试向该通道发送数据会触发panic,所以删除worker后要确保没有goroutine还在尝试发送数据,或在发送前做必要检查。
其他优化点
- 给
workers加sync.Mutex互斥锁,防止并发读写的竞态问题。 - 不需要使用
*workFunc,直接用workFunc类型即可,减少空指针风险。 - 清理定时器的闭包同样存在延迟绑定问题,当前引用的
req.wId可能被后续请求覆盖,应提前赋值给局部变量再传入闭包:
wId := req.wId n.cleanupTimer = time.AfterFunc(workCacheTimeout, func () { c <- workReq{wId, nil} })
内容的提问来源于stack exchange,提问作者Banyoghurt

