AIGC标识 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.MarshalIndentMarshal 基础上增加了缩进控制:

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.DecoderDisallowUnknownFields()
  • 大小写不敏感:如果同时定义了 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.Encoderjson.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,适合大文件或网络流的场景。

十、踩坑提醒

  1. 未导出的字段不会被序列化:小写开头的字段对 JSON 包不可见。
  2. omitempty + bool = 陷阱:带 omitempty 的 bool 字段,false 值会被吞掉。
  3. 时间格式time.Time 默认序列化为 RFC 3339 格式(如 "2026-07-10T12:00:00Z"),需要其他格式请自定义 MarshalJSON
  4. map 的 key 必须是 string:JSON 对象的键只能是字符串,所以 map[int]string 无法直接序列化——要么换用 map[string]string,要么实现自定义序列化。
  5. []byte 会被 Base64 编码:JSON 不支持原始二进制,[]byte 类型会自动做 Base64 编码。
posted @ 2026-07-24 09:27  FfHUCisI  阅读(3)  评论(0)    收藏  举报