症状
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
}