Go 1.27 编码 typed nil 时 panic,检查自定义 Marshaler 接口
自定义接口嵌入 json.Marshaler,并持有有类型的 nil 指针时,Go 1.27 的新 JSON 实现可能反射 panic。缩窄序列化 DTO 的字段接口,或在业务边界明确归一化 nil。
升级到 Go 1.27.3 后,json.Marshal 对某个对象直接 panic,错误是 reflect: call of reflect.Value.Type on zero Value。同一底层指针放在 json.Marshaler 字段中却正常,放在业务自定义接口里就失败,应检查这个接口是否嵌入了 json.Marshaler。
具体条件是接口里存有一个有类型的 nil 指针。接口值自身可能不等于 nil,但其动态值是 nil。Go 1.27 新 JSON 实现的兼容路径对这种自定义接口组合存在缺口,不能简单归为结构体没有导出字段,也不能假定每个 nil 都会触发它。
查看序列化字段的静态类型和动态值
先找到实际进入 Marshal 的 DTO 或字段声明,核对接口包含的方法以及动态指针类型。若普通 json.Marshaler 字段正常、自定义接口字段失败,这个差别比仅打印 value == nil 更有区分力。
不要通过调用 nil 接收者的方法来探测对象;它可能执行业务代码或另外 panic。已知具体实现类型时,可以在输入边界明确处理该指针的 nil 状态。不能把所有指针为 nil 的接口统一反射清空而不核对业务契约,有些自定义类型可能本来就赋予 nil 接收者特殊含义。
给输出建立只承担序列化的 DTO
若输出只需要 MarshalJSON,不需要业务接口上的其他方法,可让响应 DTO 使用更窄的标准接口。下面是已有业务对象 value 的投影片段,value.M 原本属于嵌入 Marshaler 的自定义接口:
type responseDTO struct {
M json.Marshaler `json:"m"`
}
data, err := json.Marshal(responseDTO{M: value.M})
需要导入 encoding/json,并按原应用检查 err。业务对象保留原类型;这里只明确 JSON 边界的字段接口。核对原标签、omitempty 行为和其他字段,不能用一个不完整 DTO 意外删掉响应内容。这里预期的是所述兼容场景下输出 null,不承诺所有自定义 MarshalJSON 的行为完全相同。
若项目无法局部调整接口,Go 1.27 提供构建期 GOEXPERIMENT=nojsonv2 作为临时兼容入口。它会影响整个程序的 JSON 实现,需要按完整应用重新构建、测试和回退,不能当作只修一个字段的运行时设置,更不能永久依赖未来可能移除的退出路径。
回归覆盖 nil 接口、已知类型的 nil 指针、正常对象以及直接使用 json.Marshaler 的对照,比较完整 JSON 键、null 和自定义方法效果。不要只在外层 recover 后返回空对象,这会掩盖响应生成失败。


