You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 09:45:31