登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go json.Marshal 怎么实现自定义枚举文本和值校验

来源:17golang原创

时间:2026-09-09 05:09:34 367浏览 收藏

Go 的自定义枚举通常底层是 int。直接调用 json.Marshal 时,客户端看到的往往是 12 这样的数字;而接口协议更希望看到 "paid""canceled"。可行的做法是给枚举实现 MarshalJSON() ([]byte, error):合法值先映射成文本,再调用 json.Marshal 编成 JSON 字符串;未定义值直接返回错误,不让脏数据悄悄出现在响应里。

要点速览
  • 命名整数类型默认按 JSON 数字输出,只有实现 json.Marshaler 才能改变表示方式。
  • 合法性判断应集中在 statusLabelMarshalJSON 只负责把判断结果交给 JSON 编码器。
  • 非法枚举返回自定义错误,外层可用 errors.Is 判断,json.Marshal 会将它包装成 json.MarshalerError

先把枚举的合法集合写成唯一出口

先定义状态和值域。这里故意保留零值 StatusUnknown,因为 Go 结构体的零值很容易进入业务对象;是否允许它出现在接口中,要由映射函数明确决定。映射表只表达“哪个值对应哪个文本”,不在多个方法里重复写 switch。

package order

import (
    "errors"
    "fmt"
)

type Status uint8

const (
    StatusUnknown Status = iota // 零值单独保留,避免误当成已支付
    StatusPending                // 等待支付
    StatusPaid                   // 已支付
    StatusCanceled               // 已取消
)

var ErrInvalidStatus = errors.New("invalid order status")

func statusLabel(s Status) (string, error) {
    switch s {
    case StatusPending:
        return "pending", nil
    case StatusPaid:
        return "paid", nil
    case StatusCanceled:
        return "canceled", nil
    default:
        // 未定义值不输出数字,直接阻止它进入接口响应。
        return "", fmt.Errorf("%w: %d", ErrInvalidStatus, s)
    }
}

StatusStatusPendingStatusPaidStatusCanceledstatusLabel 共同组成状态表。以后增加一个枚举时,只需要同时补常量和这个出口;遗漏就会在编码阶段暴露,而不是等客户端猜一个数字含义。

Go Status 枚举、statusLabel 状态表与合法文本值之间的静态关系
图1:查看 Status 与 statusLabel 的状态表关系,理解合法文本值和未知整数为何要分开。

MarshalJSON 里先校验,再编码文本

实现方法时使用值接收者,普通的 Status 值和结构体字段都能直接参与编码。不要手写带引号的字符串,因为枚举文本一旦包含引号、换行或其他特殊字符,手写结果就可能不是合法 JSON;让 json.Marshal 负责最后一步更稳妥。

func (s Status) MarshalJSON() ([]byte, error) {
    label, err := statusLabel(s)
    if err != nil {
        // 保留 ErrInvalidStatus,调用方可以用 errors.Is 识别原因。
        return nil, err
    }
    // 由 encoding/json 负责字符串转义和合法 JSON 格式。
    return json.Marshal(label)
}

上面的代码还需要在 import 中加入 encoding/json。关键顺序是“先判定、后编码”:Status(99) 不会被当成普通整数输出,合法的 StatusPaid 则得到 "paid"。官方 encoding/json Marshaler 文档规定了这个方法契约,返回错误时不应继续拼装部分 JSON。

json.Marshal、Status.MarshalJSON、statusLabel 与 ErrInvalidStatus 的静态调用关系
图2:图中展示 json.Marshal、Status.MarshalJSON、statusLabel 与 ErrInvalidStatus 的关系,定位文本输出和校验责任。

嵌入结构体后区分成功输出和失败边界

把枚举放进业务结构体,观察的是整个响应边界,而不只是单独调用方法。合法状态会自然嵌入字段;未知状态则让整次 json.Marshal 失败,这通常比返回一个无法解释的数字更容易监控和回滚。

type Order struct {
    ID     string `json:"id"`     // 对外稳定的订单编号
    Status Status `json:"status"` // 使用 Status 的自定义 JSON 表示
}

func encodeOrder(order Order) ([]byte, error) {
    data, err := json.Marshal(order)
    if err != nil {
        // 外层只把原始错误作为根因,避免吞掉非法枚举信息。
        if errors.Is(err, ErrInvalidStatus) {
            return nil, fmt.Errorf("encode order status: %w", err)
        }
        return nil, err
    }
    return data, nil
}

Order{ID: "A-100", Status: StatusPaid} 的结果是 {"id":"A-100","status":"paid"}。当状态为 Status(99) 时,底层错误会被 json.MarshalerError 包装,但 errors.Is(err, ErrInvalidStatus) 仍可识别根因。日志里应记录订单编号和状态数值,接口层则返回统一的内部编码失败响应。

JSON 表示处理方式
StatusPending"pending"正常输出
StatusPaid"paid"正常输出
StatusUnknown / Status(99)无输出返回 ErrInvalidStatus

需要接收文本时再补上 UnmarshalJSON

如果服务还要接收 "paid",可以为同一个 Status 增加 UnmarshalJSON,解析文本后复用相同的合法集合。不要只实现一半却在写入数据库前假设输入一定合法;反序列化是另一条边界,未知文本也应返回错误。

func (s *Status) UnmarshalJSON(data []byte) error {
    var label string
    if err := json.Unmarshal(data, &label); err != nil {
        // 非字符串输入直接失败,避免把 JSON 数字误当成文本状态。
        return err
    }
    labels := map[string]Status{
        "pending":  StatusPending,
        "paid":     StatusPaid,
        "canceled": StatusCanceled,
    }
    value, ok := labels[label]
    if !ok {
        return fmt.Errorf("%w: %q", ErrInvalidStatus, label)
    }
    // 只有完整匹配后才改变目标值,避免失败时留下半更新状态。
    *s = value
    return nil
}

若只需要对外输出文本,不必为了“成对”而增加反序列化代码。真正需要接收 JSON 文本时,再决定大小写、空字符串、旧别名和兼容期;这些都是协议决策,不能由枚举常量的数字值自动推导。

常见问题

为什么不直接给 Status 实现 String 方法?

String 只影响显式格式化,encoding/json 不会因为它存在就自动采用文本结果。要改变 JSON 表示,应实现 MarshalJSON

MarshalJSON 用指针接收者可以吗?

可以,但值是否可寻址会影响旧版 encoding/json 调用方法的机会。枚举通常不需要修改自身,使用值接收者更直接。

未知值应该输出空字符串吗?

如果空字符串也是合法协议值,可以单独定义它;否则返回错误更安全。静默输出空字符串会把“程序漏填”伪装成“正常状态”。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>