Go JSON 编码:序列化、反序列化与结构体标签
Go JSON 编码:序列化、反序列化与结构体标签
一、为什么需要 JSON 处理
在现代软件开发中,JSON(JavaScript Object Notation)是应用最广泛的数据交换格式。无论是前后端通信、微服务之间的 RPC 调用、还是配置文件存储,JSON 几乎无处不在。Go 语言在标准库中提供了 encoding/json 包,让 JSON 的序列化和反序列化变得简单而高效。
Go 处理 JSON 的核心思想是:将 JSON 数据映射到 Go 的结构体上。这是因为 Go 是静态类型语言,我们需要在运行时之前就知道数据的形状(shape)。结构体恰好提供了这种"数据蓝图"。
二、序列化:Go 值 → JSON 字节
2.1 基础 Marshal
json.Marshal 函数接受任意 Go 值,返回 JSON 格式的字节切片。其函数签名如下:
func Marshal(v interface{}) ([]byte, error)
最基础的使用姿势是把一个结构体传进去:
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string
Age int
Email string
}
func main() {
u := User{
Name: "张三",
Age: 28,
Email: "zhangsan@example.com",
}
data, err := json.Marshal(u)
if err != nil {
fmt.Println("序列化失败:", err)
return
}
fmt.Println(string(data))
// 输出: {"Name":"张三","Age":28,"Email":"zhangsan@example.com"}
}
注意:默认情况下,JSON 的键名使用的是 Go 结构体的字段名本身(首字母大写),这是 Go 的导出规则决定的——只有导出的字段才能被 JSON 包访问。
2.2 MarshalIndent:可读的 JSON
调试时我们通常希望 JSON 是格式化的。json.MarshalIndent 在 Marshal 基础上增加了缩进控制:
data, err := json.MarshalIndent(u, "", " ")
// 第二个参数是每一行的前缀
// 第三个参数是每一层级的缩进字符串
输出效果:
{
"Name": "张三",
"Age": 28,
"Email": "zhangsan@example.com"
}
2.3 支持的类型映射
Go 值与 JSON 类型之间的映射关系:
| Go 类型 | JSON 类型 | 说明 |
|---|---|---|
bool |
boolean | 直接映射 |
float32/float64 |
number | 所有数字都映射为 float64 |
int/uint 系列 |
number | 整数也会被 JSON 当作数字 |
string |
string | UTF-8 字符串,默认会转义 HTML 字符 |
[]byte |
string | Base64 编码的字符串 |
nil/空接口 nil |
null | |
slice/array |
array | |
map |
object | key 必须是 string |
struct |
object | 仅导出字段会被包含 |
time.Time |
string | RFC 3339 格式 |
三、结构体标签(Struct Tags)
结构体标签是 Go 中一种强大的元编程机制。它是写在结构体字段后面的反引号字符串,为字段提供额外的元数据。JSON 包会根据标签来决定序列化和反序列化时的行为。
3.1 基本语法
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Category string `json:"category,omitempty"`
internal string // 小写开头,不会被序列化
}
标签的格式是 json:"键名,选项1,选项2"。主要的选项有:
- omitempty:当字段为零值时不输出该字段
- string:将数字或布尔值序列化成 JSON 字符串(带引号)
- -:完全忽略该字段(即使它是导出的)
3.2 omitempty 的陷阱
omitempty 判断"零值"的逻辑和直觉有时不一致:
type Config struct {
Port int `json:"port,omitempty"` // 0 时被忽略
Timeout float64 `json:"timeout,omitempty"` // 0.0 时被忽略
Enabled bool `json:"enabled,omitempty"` // false 时被忽略
Tags []string `json:"tags,omitempty"` // nil 或空切片时被忽略
// 注意:空结构体{}不会被 omitempty 忽略!
}
一个常见的坑:当你有一个 bool 类型的"是否启用"字段,false 是合法值,但 omitempty 会导致它被省略。解决方法是使用指针类型 *bool,nil 才表示"未设置"。
3.3 string 选项
string 选项会把数字或布尔值序列化为带引号的字符串。这在对接某些要求严格字符串类型的第三方 API 时很有用:
type Response struct {
Code int `json:"code,string"` // 输出 "code": "200" 而不是 "code": 200
Ok bool `json:"ok,string"` // 输出 "ok": "true" 而不是 "ok": true
}
四、反序列化:JSON 字节 → Go 值
4.1 基础 Unmarshal
json.Unmarshal 将 JSON 字节解析到 Go 值中:
func Unmarshal(data []byte, v interface{}) error
第二个参数必须是一个指针,因为 Unmarshal 需要修改传入对象的内容:
jsonStr := `{"name":"李四","age":25,"email":"lisi@example.com"}`
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}
var u User
err := json.Unmarshal([]byte(jsonStr), &u)
if err != nil {
fmt.Println("反序列化失败:", err)
return
}
fmt.Printf("%+v\n", u) // {Name:李四 Age:25 Email:lisi@example.com}
4.2 Unmarshal 的容错能力
Go 的 Unmarshal 比较宽容,它遵循以下原则:
- 忽略未知字段:JSON 中有但结构体中没有的字段会被静默跳过。这是一把双刃剑——方便但可能掩盖拼写错误。
- 类型不匹配时静默失败:如果 JSON 中某个字段类型和结构体不匹配,Unmarshal 不会报错,而是返回零值。想严格校验,需要用
json.Decoder的DisallowUnknownFields()。 - 大小写不敏感:如果同时定义了
json:"Name"标签和实际字段名Name,Go 会优先匹配标签名。
4.3 严格反序列化
如果你需要严格的 JSON 反序列化(拒绝未知字段),使用 json.Decoder:
func strictUnmarshal(data []byte, v interface{}) error {
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
return decoder.Decode(v)
}
五、嵌套结构与 JSON
现实世界的数据通常是嵌套的。Go 通过嵌套结构体来映射嵌套 JSON:
type Address struct {
City string `json:"city"`
ZipCode string `json:"zip_code"`
}
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address Address `json:"address"`
Hobbies []string `json:"hobbies,omitempty"`
}
func main() {
p := Person{
Name: "王五",
Age: 30,
Address: Address{
City: "北京",
ZipCode: "100000",
},
Hobbies: []string{"编程", "跑步"},
}
data, _ := json.MarshalIndent(p, "", " ")
fmt.Println(string(data))
}
输出:
{
"name": "王五",
"age": 30,
"address": {
"city": "北京",
"zip_code": "100000"
},
"hobbies": ["编程", "跑步"]
}
5.1 匿名嵌入(组合)
Go 的结构体组合特性在 JSON 中也很好用。当匿名嵌入一个结构体时,其字段会被"提升"到父级 JSON 对象中:
type BaseModel struct {
ID int `json:"id"`
CreatedAt string `json:"created_at"`
}
type Article struct {
BaseModel // 匿名嵌入,字段会被提升
Title string `json:"title"`
Content string `json:"content"`
}
// 序列化后得到: {"id":1,"created_at":"2026-07-10","title":"Go学习","content":"..."}
六、自定义序列化逻辑
6.1 实现 json.Marshaler 和 json.Unmarshaler 接口
当标准行为不满足需求时,可以实现这两个接口来自定义序列化逻辑:
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
一个实际例子:处理灵活的"可能是字符串也可能是数字"的字段:
type FlexibleInt int
func (fi *FlexibleInt) UnmarshalJSON(data []byte) error {
// 先尝试按数字解析
var num int
if err := json.Unmarshal(data, &num); err == nil {
*fi = FlexibleInt(num)
return nil
}
// 再尝试按字符串解析
var str string
if err := json.Unmarshal(data, &str); err == nil {
n, err := strconv.Atoi(str)
if err != nil {
return fmt.Errorf("无法解析为整数: %s", str)
}
*fi = FlexibleInt(n)
return nil
}
return fmt.Errorf("FlexibleInt: 无法解析 %s", string(data))
}
type Item struct {
ID FlexibleInt `json:"id"`
Name string `json:"name"`
}
6.2 json.RawMessage:延迟解析
json.RawMessage 本质上是一个 []byte 别名,它可以暂存一段原始 JSON 数据,等确定结构后再解析:
type Event struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"` // 先原样保存
}
// 后续根据 Type 的值决定如何解析 Payload
switch event.Type {
case "user_login":
var login LoginPayload
json.Unmarshal(event.Payload, &login)
case "order_create":
var order OrderPayload
json.Unmarshal(event.Payload, &order)
}
七、流式处理:Encoder 和 Decoder
当处理大量数据或网络流时,使用 json.Encoder 和 json.Decoder 比一次性 Marshal / Unmarshal 更高效——它们直接操作 io.Writer / io.Reader,避免了中间大块内存分配。
7.1 Encoder:边写边编码
type LogEntry struct {
Timestamp string `json:"timestamp"`
Level string `json:"level"`
Message string `json:"message"`
}
// 模拟写入日志文件
file, _ := os.Create("app.log")
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ") // 可选:格式化输出
encoder.Encode(LogEntry{"2026-07-10T12:00:00Z", "INFO", "服务启动"})
encoder.Encode(LogEntry{"2026-07-10T12:01:00Z", "ERROR", "数据库连接超时"})
7.2 Decoder:边读边解码
// 从 HTTP 请求体中流式读取
resp, _ := http.Get("https://api.example.com/users")
defer resp.Body.Close()
decoder := json.NewDecoder(resp.Body)
// 读取开头的 [
token, _ := decoder.Token()
// 确保是数组开始
if delim, ok := token.(json.Delim); !ok || delim != '[' {
return errors.New("期望 JSON 数组")
}
// 逐个解码数组中的元素
for decoder.More() {
var user User
if err := decoder.Decode(&user); err != nil {
break
}
fmt.Printf("用户: %+v\n", user)
}
八、实际场景练习
练习 1:配置文件解析器
编写一个从 JSON 配置文件读取数据库连接信息的工具:
package main
import (
"encoding/json"
"fmt"
"os"
)
type DBConfig struct {
Host string `json:"host"`
Port int `json:"port"`
User string `json:"user"`
Password string `json:"password"` // 实际项目中不应该明文存储
Database string `json:"database"`
Pool struct {
MaxOpenConns int `json:"max_open_conns"`
MaxIdleConns int `json:"max_idle_conns"`
} `json:"pool"`
}
func (d DBConfig) DSN() string {
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4",
d.User, d.Password, d.Host, d.Port, d.Database)
}
func LoadDBConfig(path string) (*DBConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
var cfg DBConfig
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields() // 不允许出现未知字段,防止拼写错误
if err := decoder.Decode(&cfg); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
// 校验必填字段
if cfg.Host == "" {
return nil, fmt.Errorf("配置项 host 不能为空")
}
if cfg.Port == 0 {
return nil, fmt.Errorf("配置项 port 不能为空")
}
return &cfg, nil
}
练习 2:API 响应包装器
设计一个通用的 API 响应结构,支持任意类型的数据载荷:
package main
import (
"encoding/json"
"net/http"
)
var (
ErrBadRequest = &AppError{Code: 400, Message: "请求参数错误"}
ErrNotFound = &AppError{Code: 404, Message: "资源不存在"}
ErrInternal = &AppError{Code: 500, Message: "服务器内部错误"}
)
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (e *AppError) Error() string {
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
type APIResponse struct {
Success bool `json:"success"`
Data json.RawMessage `json:"data,omitempty"`
Error *AppError `json:"error,omitempty"`
}
// WriteSuccess 向 http.ResponseWriter 写入成功响应
func WriteSuccess(w http.ResponseWriter, data interface{}) error {
dataBytes, err := json.Marshal(data)
if err != nil {
return err
}
resp := APIResponse{
Success: true,
Data: json.RawMessage(dataBytes),
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
return json.NewEncoder(w).Encode(resp)
}
// WriteError 向 http.ResponseWriter 写入错误响应
func WriteError(w http.ResponseWriter, err error) {
appErr, ok := err.(*AppError)
if !ok {
appErr = ErrInternal
}
resp := APIResponse{
Success: false,
Error: appErr,
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(appErr.Code)
json.NewEncoder(w).Encode(resp)
}
九、关键知识点总结
| 知识点 | 核心要点 |
|---|---|
json.Marshal |
将 Go 值序列化为 JSON 字节。只处理导出字段。 |
json.Unmarshal |
将 JSON 字节反序列化为 Go 值。参数必须是指针。默认忽略未知字段和类型不匹配。 |
| 结构体标签 | json:"name,omitempty,string" 控制键名、忽略零值、类型包装。 |
omitempty |
零值时跳过字段。注意 bool 的 false 也是零值,可能导致数据丢失。 |
DisallowUnknownFields |
使用 Decoder 设置,拒绝 JSON 中包含结构体未知的字段,适合严格校验的场景。 |
| 自定义序列化 | 实现 Marshaler / Unmarshaler 接口即可接管单个类型的序列化逻辑。 |
json.RawMessage |
字节别名,可以暂存原始 JSON 数据,延迟到后续确定结构后再解析。 |
| Encoder / Decoder | 流式处理接口,直接操作 io.Writer / io.Reader,适合大文件或网络流的场景。 |
十、踩坑提醒
- 未导出的字段不会被序列化:小写开头的字段对 JSON 包不可见。
- omitempty + bool = 陷阱:带
omitempty的 bool 字段,false值会被吞掉。 - 时间格式:
time.Time默认序列化为 RFC 3339 格式(如"2026-07-10T12:00:00Z"),需要其他格式请自定义MarshalJSON。 - map 的 key 必须是 string:JSON 对象的键只能是字符串,所以
map[int]string无法直接序列化——要么换用map[string]string,要么实现自定义序列化。 - []byte 会被 Base64 编码:JSON 不支持原始二进制,
[]byte类型会自动做 Base64 编码。

浙公网安备 33010602011771号