Go 与 JSON 的类型映射
encoding/json 是 Go 标准库提供的 JSON 处理包。它基于反射实现,无需外部依赖即可完成绝大多数 JSON 任务。理解 Go 类型和 JSON 类型之间的映射规则是正确使用该包的前提。
| Go 类型 | JSON 类型 | 行为说明 |
|---|---|---|
bool | true/false | 直接映射 |
string | string | 自动处理 Unicode 转义 |
int/float 系列 | number | int64 超大值用 string tag 保精度 |
[]T | array | nil 切片输出 null |
map[string]T | object | 键必须为 string 类型 |
*T(指针) | null/值 | nil 指针输出 null |
struct | object | 仅导出字段参与序列化 |
有几个重要规则值得特别注意:只有首字母大写的导出字段才会被序列化,未导出字段会被静默忽略;nil 指针和 nil 切片序列化为 null,但零值结构体字段(空字符串、0、false)默认都会被输出,除非使用 omitempty 标签抑制。
json.Marshal 与 json.Unmarshal 基础
json.Marshal 接收任意 Go 值并返回 JSON 字节切片,json.Unmarshal 执行反向转换。这是 Go 中最基础的 JSON 操作。
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"`
}
func main() {
u := User{Name: "张三", Age: 28}
// 序列化:返回 []byte 和 error
data, err := json.Marshal(u)
if err != nil {
panic(err)
}
fmt.Println(string(data))
// {"name":"张三","age":28}
// 反序列化:第二个参数必须是指针类型
var u2 User
err = json.Unmarshal(data, &u2)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", u2) // {Name:张三 Age:28 Email:}
}
json.Unmarshal 的第二个参数必须传入指针。如果传入值类型,反序列化的数据会被写入一个不可见的副本,调用者完全无法感知,这是 Go 新手最高频的错误之一。编译器不会对此报错,因为 interface{} 接受任何类型。好在大部分 IDE 和 linter 插件的静态分析可以捕捉到这种错误。
Struct Tag 完全解析
struct tag 是 Go JSON 序列化的核心配置机制,也是 Go 语言 “标签驱动开发” 哲学的典型体现。所有行为在 struct 定义时一次性声明完毕,无需额外的 XML 或 JSON 配置文件。
type Product struct {
// 基础重命名:JSON 键名 "product_id"
ID int64 `json:"product_id"`
// omitempty:零值时省略该字段
// 零值定义:数值 = 0, 字符串 = "", 指针/map/切片 = nil, bool = false
Description string `json:"description,omitempty"`
// string:强制序列化为 JSON 字符串(避免大整数精度丢失)
SKU int64 `json:"sku,string"`
// "-"(连字符):完全忽略,不序列化也不反序列化
InternalCode string `json:"-"`
// "-,"(连字符加逗号):字段名就是 "-"
Meta string `json:"-,"`
// 嵌入结构体(匿名字段):字段提升到父级
AuditInfo
}
type AuditInfo struct {
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
omitempty 的判断基于 Go 的零值语义,而不是自定义的空值定义。例如,一个被赋值为 "" 的 string 字段在 omitempty 下不会输出,但一个赋值为 0 的 int 字段同样不会输出——即使 0 本身是业务上的合法值。对于这种场景,改用指针类型 *int 可以让 nil 和 0 明确区分:nil 被省略,0 正常输出。
自定义序列化:MarshalJSON 与 UnmarshalJSON
实现 json.Marshaler 和 json.Unmarshaler 接口可以完全接管某个类型的 JSON 表示。这是 Go 中处理自定义类型最灵活的方式。
import (
"encoding/json"
"time"
)
type Duration struct {
time.Duration
}
// 序列化为人类可读格式:"5s"、"2m30s" 等
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Duration.String())
}
// 从字符串还原 Duration
func (d *Duration) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
dur, err := time.ParseDuration(s)
if err != nil {
return fmt.Errorf("invalid duration %q: %w", s, err)
}
d.Duration = dur
return nil
}
// 使用示例
type Config struct {
Timeout Duration `json:"timeout"`
}
cfg := Config{Timeout: Duration{5 * time.Second}}
data, _ := json.Marshal(cfg)
fmt.Println(string(data)) // {"timeout":"5s"}
注意 UnmarshalJSON 方法必须定义在指针接收者上(*Duration),否则反序列化的数据无法被写入原对象。自定义序列化的常见场景包括:时间格式处理(如 yyyy-MM-dd 而非 ISO 8601)、枚举常量转换、数值单位修饰、敏感字段脱敏、以及带版本的序列化逻辑。
流式处理:json.Encoder 与 json.Decoder
处理 HTTP 请求体或大型日志文件时,应使用流式 API 避免在内存中中转整个 JSON,从而降低内存峰值并提升吞吐。
import (
"encoding/json"
"net/http"
"os"
)
// 从 HTTP 请求体中安全地反序列化并做校验
func HandleCreateUser(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
decoder := json.NewDecoder(r.Body)
// 拒绝 JSON 中存在但目标结构体不认识的字段
decoder.DisallowUnknownFields()
var user User
if err := decoder.Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 业务处理...
}
// 将大量数据流式写入文件
func WriteUsersToFile(users []User, filename string) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
return enc.Encode(users)
}
json.Decoder 还支持 UseNumber() 方法,将 JSON 中的数字解析为 json.Number(本质是字符串)而非 float64,从而避免大整数精度丢失和浮点误差。DisallowUnknownFields() 对 API 严格校验非常有用,能尽早暴露客户端发送了多余字段的问题。
json.RawMessage 延迟解析
当 JSON 中某个字段的类型取决于同一结构中另一个字段的值时(多态消息),json.RawMessage 允许暂缓解析,先提取类型判别字段,再根据类型决定后续的解析目标。
type Message struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"` // 原始 JSON 字节,暂不解析
}
type TextPayload struct {
Content string `json:"content"`
}
type ImagePayload struct {
URL string `json:"url"`
Width int `json:"width"`
Height int `json:"height"`
}
func ParseMessage(data []byte) (any, error) {
var msg Message
if err := json.Unmarshal(data, &msg); err != nil {
return nil, err
}
switch msg.Type {
case "text":
var p TextPayload
if err := json.Unmarshal(msg.Payload, &p); err != nil {
return nil, err
}
return p, nil
case "image":
var p ImagePayload
if err := json.Unmarshal(msg.Payload, &p); err != nil {
return nil, err
}
return p, nil
default:
return nil, fmt.Errorf("unknown message type: %q", msg.Type)
}
}
这种模式在 WebSocket 消息分发、消息队列消费、配置文件的多态 section 解析中非常常见。它避免了两次全量解析的性能开销:外层只解析到 Payload 为止(拿到原始字节),内层按需选择性解析。
常见陷阱与应对
Go 的 JSON 处理有一类颇具 Go 特色的陷阱,理解其根本原因便不难规避。
// 陷阱1: nil slice vs empty slice —— 序列化结果不同
type Collection struct {
Items []string `json:"items"`
}
c1 := Collection{} // Items 为 nil
c2 := Collection{Items: []string{}} // Items 为空切片(长度为 0 的非 nil 切片)
j1, _ := json.Marshal(c1) // {"items":null}
j2, _ := json.Marshal(c2) // {"items":[]}
// 最佳实践:统一使用 make([]T, 0) 初始化切片
// 或使用 omitempty 标签统一行为(都省略)
// 陷阱2: time.Time 格式固定为 RFC 3339
type Event struct {
Time time.Time `json:"time"`
}
// 输出:2026-07-15T10:30:00Z(无法通过 tag 改变格式)
// 自定义格式必须实现 MarshalJSON/MarshalText 接口或使用自定义类型
// 陷阱3: map 的键顺序不可靠
m := map[string]int{"c": 3, "a": 1, "b": 2}
data, _ := json.Marshal(m)
// 每次运行的输出顺序可能不同!
// 根本原因:Go 的 map 遍历顺序是故意随机化的
map 键顺序问题源于 Go 运行时的 map 迭代顺序本身就是随机的(这是安全策略,防止代码依赖 map 迭代顺序)。标准化解决方案是按 keys 排序后逐个写入,或直接使用有序结构体代替 map。encoding/json 包本身不提供键排序参数,这点和 Python 的 sort_keys 不同。
高性能替代方案
对于 API 网关、日志管道等高吞吐低延迟场景,标准库的反射开销可能成为瓶颈。Go 社区提供了多种加速方案。
| 库 | 方案 | 速度(相对标准库) | 兼容性 |
|---|---|---|---|
| jsoniter | 运行时优化 | 约 2-3 倍 | 完全兼容标准库 drop-in |
| sonic | JIT + SIMD 汇编 | 约 3-5 倍 | 需 amd64 + Go 1.15+,API 兼容 |
| easyjson | 代码生成(零反射) | 约 4-6 倍 | 需 go generate,struct 固定 |
选择建议:普通项目直接用标准库;中型 API 服务可考虑 jsoniter(零迁移成本,修改 import 路径即可);对延迟敏感且运行在 amd64 环境用 sonic;编译时结构和数据变化频率低的场景用 easyjson 通过提前生成代码获得最佳性能。
// jsoniter 的 drop-in 替换:只需改 import 别名
import jsoniter "github.com/json-iterator/go"
var json = jsoniter.ConfigCompatibleWithStandardLibrary
// 后续所有 json.Marshal / json.Unmarshal 调用自动使用 jsoniter
data, _ := json.Marshal(myStruct)
jsoniter 的 ConfigCompatibleWithStandardLibrary 模式提供与标准库几乎完全一致的行为,可以安全地在现有代码库中替换而不需要修改任何业务逻辑代码。sonic 则提供了不兼容标准库的优化路径,需要开发者主动适配其 API。