← 中文资源

简体中文版本

Go — panic:向 nil map 赋值

通过定位未初始化的 map、区分"读 nil map 安全"与"写 nil map 必崩",修复 panic: assignment to entry in nil map。

阅读英文原文

症状

  panic 信息本身就是关于“罪行”的完整句子:assignment to entry in nil map,栈顶帧指向第一次 m[key] = value 的写入。教学点在于不对称性:从 nil map 读取完全合法——var m map[string]int 后 _ = m[“k”] 返回零值,而 m[“k”] = 1 立即 panic。这解释了“只查询的测试全过、第一条写路径一跑就崩”的现象:写入通常在注册、缓存这类冷门分支里。

诊断思路

  先读 panic 栈定位确切写入行与所在函数;再 grep 所有 var cache map[…] 声明——没有 make 也没有复合字面量的就是零值 nil,每次写入都是潜在 panic;最后检查错误路径:函数在 err 分支 return nil, err,调用方跳过错误检查直接写入,就埋好了雷。JSON/YAML 反序列化到 nil map 指针且输入为空时同样保持 nil,是隐蔽变体。

根因

  Go map 的零值是 nil。读 nil map 有定义(返回零值),写 nil map 必 panic——因为没有底层哈希表可供插入。程序一直正常,直到第一次写入落在未初始化的 map 上。

修复与验证

  两种正确姿势:构造时初始化——结构体构造函数里用复合字面量 entries: make(map[string]Entry),保证字段即刻可用;或在写边界惰性初始化——Register 里先判 nil 再 make,这是缓存类代码的惯用模式。同时尊重构造函数错误:panic 常常是忽略 nil 返回值的延迟后果。验证时跑通此前 panic 的路径,并补一个测试遍历每个构造函数返回的 map 做一次写入。注意不要在结构体创建时盲目初始化所有 map——那会掩盖本应大声失败的路径;要么在写入处初始化,要么在承诺可用值的构造函数里初始化。

代码示例

panic: assignment to entry in nil map

goroutine 1 [running]:
main.(*Registry).Register(...)
        /app/src/registry.go:31
var m map[string]int
_ = m["k"]     // fine: zero value, false
m["k"] = 1     // panic: assignment to entry in nil map
func NewRegistry() *Registry {
    return &Registry{
        entries: make(map[string]Entry), // usable immediately
    }
}
func (r *Registry) Register(key string, e Entry) {
    if r.entries == nil {
        r.entries = make(map[string]Entry)
    }
    r.entries[key] = e
}
reg, err := OpenRegistry(path)
if err != nil {
    return err // otherwise reg is nil and the first write panics
}