Go 自定义 json 解析规则
在使用 go json 序列化数据时,有些类型/结构序列化没办法满足业务需求,比如,日期格式是“2024/05/20 14:30:00”,Go默认的time.Time根本解析不了,解析出来的结果没办法满足业务需求,今天分享如何使用自定义 JSON 解析规则完美解决,希望对大家有所帮助。
为什么需要自定义 JSON 解析?
Go 的 encoding/json 包提供了默认的序列化和反序列化能力,但实际开发中,默认规则往往满足不了复杂场景。
比如这几种常见情况:
- 第三方接口返回的字段名是下划线命名(如user_name),但我们代码用驼峰命名(UserName);
- 日期格式不标准(如“2024-05-20”“2024/05/20”),默认 time.Time 解析会报错;
- 枚举值需要转换(如数字 1 转成“待支付”字符串);
- 嵌套结构体需要扁平化处理,或某些字段需要特殊计算后再序列化。
这些场景下,就必须通过自定义解析规则来突破默认限制。
核心思路有两种:
- 一是通过结构体 tag 做简单配置,
- 二是实现 json.Marshaler 和 json.Unmarshaler 接口做复杂定制。
基础:结构体 Tag 配置
如果只是字段名映射、忽略空值、指定默认值等简单需求,用结构体 tag 是最高效的方式,不用写一行额外代码。
常用的 tag 关键字有4个:json、omitempty、string、default。
1. 核心 Tag:字段名映射与基础配置
最常用的场景是字段名映射,比如把 Go 的驼峰字段对应到 JSON 的下划线字段,同时忽略空值字段。
直接看代码示例:
package main
import (
"encoding/json"
"fmt"
)
// 订单结构体,通过tag配置JSON解析规则
type Order struct {
// json:"order_id":指定JSON字段名为order_id
OrderID string `json:"order_id"`
// omitempty:字段为空时不序列化到JSON中
UserName string `json:"user_name,omitempty"`
// 不指定json tag:默认用字段名作为JSON键(UserName -> UserName)
Amount float64
// -:序列化时忽略该字段,无论值是什么
Secret string `json:"-"`
}
func main() {
// 构造测试数据
order := Order{
OrderID: "ORD20240520001",
UserName: "", // 空值,会被omitempty忽略
Amount: 99.9,
Secret: "abc123", // 会被忽略
}
// 序列化:将结构体转JSON
data, err := json.MarshalIndent(order, "", " ")
if err != nil {
fmt.Printf("序列化失败:%v\n", err)
return
}
fmt.Println("序列化结果:")
fmt.Println(string(data))
}输出结果分析:UserName 因空值被忽略,Secret 被强制忽略,最终 JSON 只保留 order_id 和 Amount 两个字段,字段名也按 tag 指定的映射正确转换,完全符合对接第三方接口的常见需求。
2. 实用 Tag:string 与 default
string tag 能强制将数值类型序列化为字符串(避免前端解析数字精度丢失),default tag 可指定反序列化时的默认值(需结合第三方库,如 github.com/go-playground/validator)。
示例:
import "github.com/go-playground/validator"
// 产品结构体
type Product struct {
// string:序列化时将int64转为字符串
ID int64 `json:"id,string"`
// default:反序列化时若字段为空,默认值为"未分类"
Category string `json:"category" validate:"required" default:"未分类"`
Name string `json:"name"`
}
func main() {
// 1. 测试string tag序列化
product := Product{ID: 1234567890123456789, Name: "无线耳机", Category: "数码"}
data, _ := json.MarshalIndent(product, "", " ")
fmt.Println("带string tag的序列化结果:")
fmt.Println(string(data)) // 输出中id为字符串类型:"id":"1234567890123456789"
// 2. 测试default tag反序列化
jsonStr := `{"id":"9876543210987654321", "name":"键盘"}` // 缺少category字段
var p Product
json.Unmarshal([]byte(jsonStr), &p)
// 用第三方库触发默认值赋值
validate := validator.New()
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
return fld.Tag.Get("json")
})
validate.Struct(&p)
fmt.Println("反序列化后默认值:", p.Category) // 输出"未分类"
}注意事项:default tag 不是 encoding/json 包的原生功能,需要结合 validator 等第三方库实现,适合反序列化时补全默认字段的场景
进阶:实现 Marshaler/Unmarshaler 接口
当tag满足不了需求时,比如自定义日期格式、枚举值转换、嵌套结构处理,就需要给结构体实现json.Marshaler(序列化)和json.Unmarshaler(反序列化)接口。
这是自定义JSON解析的核心能力,也是实际开发中解决复杂问题的关键。
1. 实战场景1:自定义日期格式解析
这是最常见的复杂场景——第三方接口返回的日期是“2024/05/20 14:30:00”或“2024-05-20”,而Go默认只支持RFC3339格式(如“2024-05-20T14:30:00Z”)。
通过实现接口可完美解决:
package main
import (
"encoding/json"
"fmt"
"time"
)
// 自定义日期类型,基于time.Time
type CustomTime time.Time
// 定义常用日期格式
const (
TimeFormat1 = "2006/01/02 15:04:05"
TimeFormat2 = "2006-01-02"
)
// 实现json.Marshaler接口:自定义序列化逻辑
func (ct CustomTime) MarshalJSON() ([]byte, error) {
// 将CustomTime转为time.Time,再格式化为指定字符串
t := time.Time(ct)
return json.Marshal(t.Format(TimeFormat1))
}
// 实现json.Unmarshaler接口:自定义反序列化逻辑
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
// 先去除JSON字符串的引号
var timeStr string
if err := json.Unmarshal(data, &timeStr); err != nil {
return fmt.Errorf("日期字符串解析失败:%w", err)
}
// 尝试多种格式解析,适配不同场景
t, err := time.Parse(TimeFormat1, timeStr)
if err != nil {
t, err = time.Parse(TimeFormat2, timeStr)
if err != nil {
return fmt.Errorf("不支持的日期格式:%s,仅支持%s和%s", timeStr, TimeFormat1, TimeFormat2)
}
}
// 将解析后的time.Time赋值给CustomTime
*ct = CustomTime(t)
return nil
}
// 订单结构体,使用自定义日期类型
type Order struct {
OrderID string `json:"order_id"`
CreateAt CustomTime `json:"create_at"` // 自定义日期字段
PayTime *CustomTime `json:"pay_time,omitempty"` // 可选日期字段
}
func main() {
// 1. 测试序列化:将CustomTime转为指定格式字符串
now := CustomTime(time.Now())
order1 := Order{OrderID: "ORD001", CreateAt: now}
data1, _ := json.MarshalIndent(order1, "", " ")
fmt.Println("日期序列化结果:")
fmt.Println(string(data1))
// 2. 测试反序列化:解析不同格式的日期字符串
jsonStr := `{"order_id":"ORD002", "create_at":"2024/05/20 10:00:00", "pay_time":"2024-05-20"}`
var order2 Order
if err := json.Unmarshal([]byte(jsonStr), &order2); err != nil {
fmt.Printf("反序列化失败:%v\n", err)
return
}
fmt.Println("反序列化后创建时间:", time.Time(order2.CreateAt).Format(TimeFormat1))
fmt.Println("反序列化后支付时间:", time.Time(*order2.PayTime).Format(TimeFormat1))
}核心逻辑:通过自定义日期类型包装 time.Time,实现接口后接管序列化和反序列化过程,既支持多种格式解析,又能统一输出格式,完全适配第三方接口的不规则日期。
2. 实战场景2:枚举值转换
另一个高频场景是枚举值转换,比如将代码中的字符串枚举(“pending”“paid”)与接口返回的数字(1、2)互转。
示例:
package main
import (
"encoding/json"
"fmt"
)
// 订单状态枚举类型
type OrderStatus string
// 定义枚举值
const (
OrderStatusPending OrderStatus = "pending" // 待支付
OrderStatusPaid OrderStatus = "paid" // 已支付
OrderStatusCancel OrderStatus = "cancel" // 已取消
)
// 枚举值与数字的映射表
var statusToInt = map[OrderStatus]int{
OrderStatusPending: 1,
OrderStatusPaid: 2,
OrderStatusCancel: 3,
}
var intToStatus = map[int]OrderStatus{
1: OrderStatusPending,
2: OrderStatusPaid,
3: OrderStatusCancel,
}
// 实现序列化接口:将枚举字符串转为数字
func (s OrderStatus) MarshalJSON() ([]byte, error) {
// 查找映射的数字,若不存在返回错误
if code, ok := statusToInt[s]; ok {
return json.Marshal(code)
}
return nil, fmt.Errorf("不支持的订单状态:%s", s)
}
// 实现反序列化接口:将数字转为枚举字符串
func (s *OrderStatus) UnmarshalJSON(data []byte) error {
var code int
if err := json.Unmarshal(data, &code); err != nil {
return fmt.Errorf("状态码解析失败:%w", err)
}
// 查找映射的枚举值
if status, ok := intToStatus[code]; ok {
*s = status
return nil
}
return fmt.Errorf("不支持的状态码:%d", code)
}
type Order struct {
OrderID string `json:"order_id"`
Status OrderStatus `json:"status"` // 枚举字段
}
func main() {
// 1. 序列化:将"paid"转为2
order1 := Order{OrderID: "ORD003", Status: OrderStatusPaid}
data1, _ := json.MarshalIndent(order1, "", " ")
fmt.Println("枚举序列化结果:")
fmt.Println(string(data1)) // 输出{"order_id":"ORD003","status":2}
// 2. 反序列化:将1转为"pending"
jsonStr := `{"order_id":"ORD004", "status":1}`
var order2 Order
if err := json.Unmarshal([]byte(jsonStr), &order2); err != nil {
fmt.Printf("反序列化失败:%v\n", err)
return
}
fmt.Println("反序列化后状态:", order2.Status) // 输出pending
}高级:使用第三方库简化开发
如果项目中有大量复杂的 JSON 解析需求,重复实现接口会很繁琐,这时可以用第三方库提高效率。
推荐的有两个:easyjson 和 go-json,前者通过代码生成优化性能,后者支持更多灵活配置。
1. easyjson:性能优先,代码生成
easyjson 通过预生成代码替代反射,序列化和反序列化性能比原生 encoding/json 快 3-5 倍,同时支持自定义解析规则。
核心步骤:
// 1. 安装easyjson工具
go get -u github.com/mailru/easyjson/...
// 2. 在结构体所在文件添加注释,指定生成规则
//easyjson:json
type Order struct {
OrderID string `json:"order_id"`
CreateAt time.Time `json:"create_at"`
}
// 3. 生成解析代码
easyjson -all order.go生成后会得到 order_easyjson.go 文件,里面包含了自动生成的 MarshalJSON 和 UnmarshalJSON 方法,直接调用即可。
如果需要自定义规则,可在生成的代码基础上修改。
2. go-json:灵活配置,原生兼容
go-json 是 encoding/json 的兼容替代库,支持更多 tag 配置和自定义解析器,无需代码生成。
示例:
import (
"fmt"
"time"
"github.com/goccy/go-json"
)
type Order struct {
OrderID string `json:"order_id"`
CreateAt time.Time `json:"create_at" json:"format:'2006/01/02 15:04:05'"`
}
func main() {
jsonStr := `{"order_id":"ORD005", "create_at":"2024/05/20 16:30:00"}`
var order Order
// 直接使用go-json解析,自动适配自定义日期格式
if err := json.Unmarshal([]byte(jsonStr), &order); err != nil {
fmt.Printf("解析失败:%v\n", err)
return
}
fmt.Println("go-json解析结果:", order.CreateAt)
}常见问题
Q1. 反序列化时日期格式不匹配导致报错
现象:JSON 中的日期是“2024-05-20”,用默认 time.Time 解析时提示“parsing time “2024-05-20” as “2006-01-02T15:04:05Z07:00”: cannot parse "" as “T””。
原因:Go 原生 time.Time 只支持 RFC3339 标准格式,非标准格式需要自定义解析。
解决方案:按照前面“自定义日期格式解析”的方法,包装 time.Time 为自定义类型并实现接口,支持多种格式解析。
Q2. 枚举值转换时出现“不支持的状态码”错误
现象:第三方接口返回状态码4,但我们的映射表中没有对应枚举值,反序列化报错。
原因:映射表未覆盖所有可能的状态码,属于边界场景考虑不足。
解决方案:在反序列化时增加默认值处理,未知状态码映射为“未知状态”,避免程序崩溃
func (s *OrderStatus) UnmarshalJSON(data []byte) error {
var code int
if err := json.Unmarshal(data, &code); err != nil {
return err
}
if status, ok := intToStatus[code]; ok {
*s = status
} else {
*s = "unknown" // 未知状态默认值
}
return nil
}Q3. 嵌套结构体解析后部分字段为空
现象:JSON 中有嵌套结构“user":{“name”:“张三”,“age”:28},对应的 Go 结构体中 User 字段解析后为空。
原因:要么是结构体字段名与JSON键不匹配(未用 tag 配置),要么是嵌套结构体的字段未导出(首字母小写)。
解决方案:① 给嵌套结构体字段加正确的 json tag;② 确保嵌套结构体的所有字段首字母大写(导出),
示例:
// 正确的嵌套结构体定义
type Order struct {
OrderID string `json:"order_id"`
// 字段名首字母大写,tag对应JSON键
User User `json:"user"`
}
// 嵌套结构体字段首字母大写
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}Q4. 序列化时 float64 类型出现精度丢失
现象:Go 中的 float64(0.1) 序列化后变为 0.10000000000000001。
原因:float64 类型的固有精度问题,JSON 序列化时会暴露该问题。
解决方案:使用 decimal 库(如github.com/shopspring/decimal)处理小数,自定义序列化和反序列化:
import "github.com/shopspring/decimal"
type Decimal decimal.Decimal
func (d Decimal) MarshalJSON() ([]byte, error) {
return json.Marshal(decimal.Decimal(d).String())
}
func (d *Decimal) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
dec, err := decimal.NewFromString(s)
if err != nil {
return err
}
*d = Decimal(dec)
return nil
}总结
自定义 JSON 解析没有“万能方案”,关键是根据场景选对方法:
- 简单字段映射/忽略空值:直接用结构体 tag(json、omitempty),最快最省事;
- 日期/枚举/小数等特殊类型:包装基础类型并实现 Marshaler/Unmarshaler 接口,灵活可控;
- 高并发/高性能场景:用 easyjson 生成代码,替代原生反射提升性能;
- 复杂配置+原生兼容:用 go-json 库,支持更多 tag 和自定义规则,无需改代码结构。
其实自定义 JSON 解析的核心就是“接管序列化和反序列化的过程”,无论是用 tag 还是实现接口,本质都是根据业务需求调整数据的转换规则。
如果大家有更复杂的场景(比如 JSON 与 protobuf 互转),欢迎在评论区交流~
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!