Go枚举JSON序列化默认输出数字而非字符串,因json包不识别String()方法,需实现MarshalJSON/UnmarshalJSON接口;底层宜用int节省内存,iota需配合类型别名使用,String()与JSON序列化逻辑应分离且严格校验非法输入。

Go 里枚举值默认用 json.Marshal 序列化出来是数字,不是字符串;只加 String() 方法对 JSON 没用——这是最常被忽略的分水岭。
为什么 String() 不影响 json.Marshal
String() 是 fmt 包调用的,用于打印和日志;json 包完全不看它。你写 fmt.Printf("%v", StatusPending) 能输出 "pending",但 json.Marshal(StatusPending) 依然吐出 0。
- 根本原因:JSON 序列化走的是
json.Marshaler接口,不是fmt.Stringer - 常见错误现象:
map[string]interface{}{"status": myStatus}被序列化后字段值仍是数字,前端解析失败或语义丢失 - 别试图在
MarshalJSON里直接return json.Marshal(s.String())——会无限递归
实现 MarshalJSON 和 UnmarshalJSON 的最小安全写法
必须成对实现,否则反序列化时遇到非法字符串(比如 "unknown")会静默转为零值,极难排查。
-
MarshalJSON返回合法 JSON 字符串(带双引号):用json.Marshal(s.String())是错的,应该用[]byte(`"` + s.String() + `"`)或更稳妥的json.Marshal(s.String())仅当确保s.String()不含特殊字符且你接受嵌套编码 -
UnmarshalJSON必须校验输入:先解码为string,再查map[string]Status,找不到就返回 error(不能 return nil) - 底层类型用
string还是int?选int更省内存、兼容数据库整型字段;选string则无需额外实现UnmarshalJSON,但占更多内存且无法做数值比较
用 iota 定义时最容易踩的 const 块陷阱
iota 不是“枚举语法”,只是 const 块里的行号计数器——它的行为完全取决于你写 const 的方式。
立即学习“go语言免费学习笔记(深入)”;
- 漏写类型别名:写
const ( Pending = iota ),Pending是 untyped int,后续赋给Status类型变量会编译报错 - 混用 iota 和手动赋值:同一 const 块里
Pending = iota后跟Running = 100,stringer工具会跳过整个块,不生成String() - 空行或注释行不影响 iota 计数,但人为插入
_ = iota占位容易让后续值偏移,且可读性差;真要跳过,用显式赋值更清晰,比如Unused Status = -1
什么时候该用 map 查表而不是 switch 实现 String()
switch 在枚举成员少于 10 个时性能和可读性都更好;但一旦超过这个量级,或需要运行时动态注册(如插件系统),就得用 map。
- map 初始化必须在
init()函数里做,不能用包级变量直接初始化(否则常量未定义时报错) - map 键必须是字符串字面量,且和 const 值严格一一对应;建议在
init()中遍历所有 const 值做存在性校验 - 别把
String()和MarshalJSON()都指向同一个 map——String()可以容忍未知值返回"unknown",但MarshalJSON()遇到非法值应 panic 或返回 error,二者语义不同
真正麻烦的从来不是怎么写枚举,而是怎么让它的边界行为(非法输入、越界访问、跨服务序列化)不悄悄吞掉错误。别省那几行校验代码。


















