导航菜单

数据序列化

encoding/json

JSON 序列化与反序列化

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。Go 的 encoding/json 包提供了 Marshal(序列化)和 Unmarshal(反序列化)函数,能将 Go 数据结构与 JSON 格式相互转换。

基础用法

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    // Marshal — Go 结构体 → JSON 字节切片
    p := Person{Name: "Alice", Age: 25}
    data, err := json.Marshal(p)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(data))  // {"name":"Alice","age":25}

    // MarshalIndent — 格式化输出(带缩进)
    pretty, _ := json.MarshalIndent(p, "", "  ")
    fmt.Println(string(pretty))
    // {
    //   "name": "Alice",
    //   "age": 25
    // }

    // Unmarshal — JSON 字节切片 → Go 结构体
    jsonStr := `{"name":"Bob","age":30}`
    var p2 Person
    err = json.Unmarshal([]byte(jsonStr), &p2)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", p2)  // {Name:Bob Age:30}
}

结构体标签详解

JSON 结构体标签

结构体标签(struct tag)是写在字段后面的反引号字符串,用于控制序列化/反序列化的行为。json 标签的格式为 `json:"字段名,选项"`

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    // 基本映射
    Name     string `json:"name"`             // JSON 键为 "name"
    Age      int    `json:"age"`              // JSON 键为 "age"

    // omitempty — 零值时忽略该字段
    Nickname string `json:"nickname,omitempty"` // 空字符串时不输出
    Bio      string `json:"bio,omitempty"`     // 空字符串时不输出

    // - — 完全忽略该字段(不序列化也不反序列化)
    Password string `json:"-"`

    // string — 强制以字符串形式输出
    Score int `json:",string"`  // {"Score":"100"}

    // 自定义键名
    HomeAddress string `json:"home_address"`   // JSON 键为 "home_address"

    // 忽略时指定其他键名(用于忽略输入但输出时使用)
    InternalID int `json:"-"`  // 完全忽略
    // 如果需要忽略输入但保留输出,可以用一个非导出字段暂存
}

func main() {
    u := User{
        Name:      "Alice",
        Age:       25,
        Nickname:  "",       // 零值,omitempty 会忽略
        Bio:       "Go 开发者", // 非零值,会输出
        Password:  "secret123",
        Score:     100,
    }

    data, _ := json.MarshalIndent(u, "", "  ")
    fmt.Println(string(data))
    // {
    //   "name": "Alice",
    //   "age": 25,
    //   "bio": "Go 开发者",
    //   "Score": "100",
    //   "home_address": ""
    // }
    // 注意:nickname 和 Password 都没有出现
}

嵌套结构体、切片与 map

package main

import (
    "encoding/json"
    "fmt"
)

type Address struct {
    City    string `json:"city"`
    Country string `json:"country"`
}

type Company struct {
    Name    string   `json:"name"`
    Users   []User   `json:"users"`
    Tags    []string `json:"tags,omitempty"`
    Config  map[string]any `json:"config"`
}

type User struct {
    Name    string  `json:"name"`
    Address Address `json:"address"`
}

func main() {
    // 嵌套结构体
    u := User{
        Name: "Alice",
        Address: Address{
            City:    "Beijing",
            Country: "China",
        },
    }
    data, _ := json.MarshalIndent(u, "", "  ")
    fmt.Println(string(data))
    // {
    //   "name": "Alice",
    //   "address": {
    //     "city": "Beijing",
    //     "country": "China"
    //   }
    // }

    // 切片
    users := []User{
        {Name: "Alice", Address: Address{City: "Beijing", Country: "China"}},
        {Name: "Bob", Address: Address{City: "Shanghai", Country: "China"}},
    }
    data2, _ := json.MarshalIndent(users, "", "  ")
    fmt.Println(string(data2))

    // map
    config := map[string]any{
        "debug": true,
        "port":  8080,
        "hosts": []string{"localhost", "127.0.0.1"},
    }
    data3, _ := json.MarshalIndent(config, "", "  ")
    fmt.Println(string(data3))

    // 反序列化到 map(不确定结构时)
    var m map[string]any
    json.Unmarshal([]byte(`{"key":"value","num":42}`), &m)
    fmt.Printf("%+v\n", m)
}

自定义 MarshalJSON / UnmarshalJSON

自定义序列化方法

通过实现 json.Marshaler 接口(MarshalJSON() ([]byte, error))和 json.Unmarshaler 接口(UnmarshalJSON([]byte) error),可以自定义类型的序列化和反序列化行为。

package main

import (
    "encoding/json"
    "fmt"
    "strings"
    "time"
)

// 自定义时间格式
type CustomTime struct {
    time.Time
}

const customLayout = "2006-01-02"

func (ct CustomTime) MarshalJSON() ([]byte, error) {
    return []byte(`"` + ct.Format(customLayout) + `"`), nil
}

func (ct *CustomTime) UnmarshalJSON(data []byte) error {
    s := strings.Trim(string(data), `"`)
    t, err := time.Parse(customLayout, s)
    if err != nil {
        return err
    }
    ct.Time = t
    return nil
}

// 自定义字符串切片(用分号分隔)
type StringList []string

func (sl StringList) MarshalJSON() ([]byte, error) {
    return json.Marshal(strings.Join(sl, ";"))
}

func (sl *StringList) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }
    if s != "" {
        *sl = strings.Split(s, ";")
    }
    return nil
}

type Event struct {
    Name      string     `json:"name"`
    Date      CustomTime `json:"date"`
    Attendees StringList `json:"attendees"`
}

func main() {
    e := Event{
        Name:      "Go 大会",
        Date:      CustomTime{time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)},
        Attendees: StringList{"Alice", "Bob", "Charlie"},
    }

    data, _ := json.MarshalIndent(e, "", "  ")
    fmt.Println(string(data))
    // {
    //   "name": "Go 大会",
    //   "date": "2024-03-15",
    //   "attendees": "Alice;Bob;Charlie"
    // }

    var e2 Event
    json.Unmarshal(data, &e2)
    fmt.Printf("%+v\n", e2)
    // {Name:Go 大会 Date:{Time:2024-03-15 00:00:00 +0000 UTC} Attendees:[Alice Bob Charlie]}
}

json.RawMessage 延迟解析

json.RawMessage

json.RawMessage[]byte 的别名,它告诉 json.Marshaljson.Unmarshal 跳过该字段的处理,保留原始 JSON 文本。适合处理结构不确定的 JSON 字段,实现延迟解析。

package main

import (
    "encoding/json"
    "fmt"
)

type Message struct {
    Type    string          `json:"type"`
    Payload json.RawMessage `json:"payload"`
}

type TextPayload struct {
    Content string `json:"content"`
}

type ImagePayload struct {
    URL  string `json:"url"`
    Size int    `json:"size"`
}

func main() {
    // 解析不同的 payload 类型
    raw := `{"type":"text","payload":{"content":"你好世界"}}`

    var msg Message
    json.Unmarshal([]byte(raw), &msg)
    fmt.Printf("类型: %s\n", msg.Type)  // 类型: text

    // 根据类型延迟解析 payload
    switch msg.Type {
    case "text":
        var text TextPayload
        json.Unmarshal(msg.Payload, &text)
        fmt.Printf("内容: %s\n", text.Content)  // 内容: 你好世界
    case "image":
        var img ImagePayload
        json.Unmarshal(msg.Payload, &img)
        fmt.Printf("URL: %s, 大小: %d\n", img.URL, img.Size)
    }
}

JSON Stream 处理

json.Decoder 与 json.Encoder

json.Decoderio.Reader 流式读取 JSON,json.Encoderio.Writer 流式写入 JSON。适合处理大文件或网络流中的 JSON 数据,避免一次性加载整个数据到内存。

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "strings"
)

func main() {
    // Encoder — 流式写入
    file, _ := os.Create("data.jsonl")
    encoder := json.NewEncoder(file)
    // 使用 encoder 可以一行行写入(JSON Lines 格式)

    users := []map[string]any{
        {"name": "Alice", "age": 25},
        {"name": "Bob", "age": 30},
        {"name": "Charlie", "age": 35},
    }

    for _, u := range users {
        encoder.Encode(u)  // 每次写入一行 JSON
    }
    file.Close()

    // Decoder — 流式读取
    file2, _ := os.Open("data.jsonl")
    decoder := json.NewDecoder(file2)

    for decoder.More() {  // 检查是否还有更多数据
        var user map[string]any
        if err := decoder.Decode(&user); err != nil {
            break
        }
        fmt.Println(user)
    }
    file2.Close()
    os.Remove("data.jsonl")

    // 从字符串流读取
    reader := strings.NewReader(`{"name":"A"}{"name":"B"}{"name":"C"}`)
    dec := json.NewDecoder(reader)
    for dec.More() {
        var item map[string]string
        dec.Decode(&item)
        fmt.Println(item)
    }
}

encoding/xml

XML 序列化

encoding/xml 包提供了 XML 的编解码功能,用法与 encoding/json 类似。通过结构体标签 xml:"..." 控制 XML 元素和属性的映射。

package main

import (
    "encoding/xml"
    "fmt"
)

type Person struct {
    XMLName   xml.Name `xml:"person"`
    ID        int      `xml:"id,attr"`      // XML 属性: <person id="1">
    Name      string   `xml:"name"`
    Age       int      `xml:"age"`
    Address   string   `xml:"address,omitempty"`
    Emails    []string `xml:"emails>email"` // 嵌套元素
}

func main() {
    p := Person{
        XMLName: xml.Name{Local: "person"},
        ID:      1,
        Name:    "Alice",
        Age:     25,
        Emails:  []string{"alice@example.com", "work@example.com"},
    }

    // Marshal
    data, _ := xml.MarshalIndent(p, "", "  ")
    fmt.Println(string(data))
    // <person id="1">
    //   <name>Alice</name>
    //   <age>25</age>
    //   <emails>
    //     <email>alice@example.com</email>
    //     <email>work@example.com</email>
    //   </emails>
    // </person>

    // Unmarshal
    xmlStr := `<person id="2"><name>Bob</name><age>30</age></person>`
    var p2 Person
    xml.Unmarshal([]byte(xmlStr), &p2)
    fmt.Printf("%+v\n", p2)
}

encoding/gob

gob 编码

encoding/gob 是 Go 原生的二进制序列化格式,专为 Go 语言设计。它比 JSON 更紧凑、编码更快,但只能用于 Go 程序之间的通信(不支持跨语言)。

package main

import (
    "bytes"
    "encoding/gob"
    "fmt"
)

type Record struct {
    Name  string
    Score int
    Tags  []string
}

func main() {
    // 编码
    r := Record{
        Name:  "Alice",
        Score: 95,
        Tags:  []string{"优秀", "推荐"},
    }

    var buf bytes.Buffer
    encoder := gob.NewEncoder(&buf)

    err := encoder.Encode(r)
    if err != nil {
        panic(err)
    }

    fmt.Printf("gob 编码大小: %d 字节\n", buf.Len())
    // gob 编码大小: 约 60 字节(比 JSON 更紧凑)

    // 解码
    var r2 Record
    decoder := gob.NewDecoder(&buf)
    decoder.Decode(&r2)
    fmt.Printf("%+v\n", r2)
    // {Name:Alice Score:95 Tags:[优秀 推荐]}
}

encoding/csv

CSV 读写

encoding/csv 包提供了 CSV(Comma-Separated Values)格式的读写功能,支持自定义分隔符、引号处理等选项。

package main

import (
    "encoding/csv"
    "fmt"
    "os"
    "strings"
)

func main() {
    // 写入 CSV
    file, _ := os.Create("data.csv")
    writer := csv.NewWriter(file)

    // 写入表头
    writer.Write([]string{"姓名", "年龄", "城市"})

    // 写入数据行
    records := [][]string{
        {"Alice", "25", "Beijing"},
        {"Bob", "30", "Shanghai"},
        {"Charlie", "28", "Guangzhou"},
    }
    writer.WriteAll(records)
    writer.Flush()  // 确保所有数据写入文件
    file.Close()

    // 读取 CSV
    file2, _ := os.Open("data.csv")
    reader := csv.NewReader(file2)

    // 读取所有记录
    allRecords, err := reader.ReadAll()
    if err != nil {
        panic(err)
    }
    for i, record := range allRecords {
        fmt.Printf("第 %d 行: %v\n", i+1, record)
    }
    file2.Close()

    // 从字符串读取
    csvStr := `name,age
Alice,25
Bob,30`
    reader2 := csv.NewReader(strings.NewReader(csvStr))
    for {
        record, err := reader2.Read()
        if err != nil {
            break
        }
        fmt.Println(record)
    }

    os.Remove("data.csv")
}

encoding/yaml(第三方库)

YAML 支持

Go 标准库不包含 YAML 支持。最常用的 YAML 库是 gopkg.in/yaml.v3,它的 API 设计与 encoding/json 几乎一致,使用非常自然。

安装与使用

go get gopkg.in/yaml.v3
package main

import (
    "fmt"
    "gopkg.in/yaml.v3"
)

type Config struct {
    Server struct {
        Host string `yaml:"host"`
        Port int    `yaml:"port"`
    } `yaml:"server"`

    Database struct {
        Driver string `yaml:"driver"`
        DSN    string `yaml:"dsn"`
    } `yaml:"database"`

    Debug   bool     `yaml:"debug"`
    LogFile string   `yaml:"log_file,omitempty"`
    Tags    []string `yaml:"tags,omitempty"`
}

func main() {
    // 解析 YAML
    yamlStr := `
server:
  host: localhost
  port: 8080
database:
  driver: mysql
  dsn: "root:password@tcp(localhost:3306)/mydb"
debug: true
tags:
  - web
  - api
`

    var cfg Config
    err := yaml.Unmarshal([]byte(yamlStr), &cfg)
    if err != nil {
        panic(err)
    }
    fmt.Printf("服务器: %s:%d\n", cfg.Server.Host, cfg.Server.Port)
    fmt.Printf("数据库: %s\n", cfg.Database.Driver)
    fmt.Printf("调试模式: %v\n", cfg.Debug)
    fmt.Printf("标签: %v\n", cfg.Tags)

    // 生成 YAML
    out, err := yaml.Marshal(&cfg)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(out))
}

练习题

练习 1:JSON 配置解析器

编写一个程序,读取以下 JSON 配置文件并验证必填字段。缺少必填字段时返回清晰的错误信息。

{
  "app_name": "my-app",
  "version": "1.0.0",
  "database": {
    "host": "localhost",
    "port": 3306,
    "name": "mydb"
  }
}

必填字段:app_namedatabase.hostdatabase.namedatabase.port

参考答案

解题思路:使用结构体标签解析 JSON,然后遍历检查必填字段是否为零值。

代码

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type DatabaseConfig struct {
    Host string `json:"host"`
    Port int    `json:"port"`
    Name string `json:"name"`
    User string `json:"user,omitempty"`
    Pass string `json:"password,omitempty"`
}

type AppConfig struct {
    AppName  string         `json:"app_name"`
    Version  string         `json:"version,omitempty"`
    Database DatabaseConfig `json:"database"`
}

func (c *AppConfig) Validate() []string {
    var errors []string

    if c.AppName == "" {
        errors = append(errors, "app_name 为必填字段")
    }
    if c.Database.Host == "" {
        errors = append(errors, "database.host 为必填字段")
    }
    if c.Database.Name == "" {
        errors = append(errors, "database.name 为必填字段")
    }
    if c.Database.Port == 0 {
        errors = append(errors, "database.port 为必填字段")
    }

    return errors
}

func main() {
    data, err := os.ReadFile("config.json")
    if err != nil {
        fmt.Fprintf(os.Stderr, "读取配置文件失败: %v\n", err)
        os.Exit(1)
    }

    var cfg AppConfig
    if err := json.Unmarshal(data, &cfg); err != nil {
        fmt.Fprintf(os.Stderr, "解析 JSON 失败: %v\n", err)
        os.Exit(1)
    }

    if errs := cfg.Validate(); len(errs) > 0 {
        fmt.Println("配置验证失败:")
        for _, e := range errs {
            fmt.Printf("  ❌ %s\n", e)
        }
        os.Exit(1)
    }

    fmt.Printf("✅ 配置验证通过\n")
    fmt.Printf("应用名: %s\n", cfg.AppName)
    fmt.Printf("数据库: %s:%d/%s\n", cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
}

关键点:零值检查是验证必填字段的常用方式。对于更复杂的验证,可以使用第三方库如 go-playground/validator

练习 2:JSON Schema 过滤器

编写一个函数 FilterFields(data []byte, keep []string) ([]byte, error),从 JSON 中只保留指定的字段,过滤掉其他字段。

参考答案

解题思路:先将 JSON 反序列化为 map[string]any,遍历 map 只保留指定字段,再序列化回 JSON。

代码

package main

import (
    "encoding/json"
    "fmt"
)

func FilterFields(data []byte, keep []string) ([]byte, error) {
    var m map[string]any
    if err := json.Unmarshal(data, &m); err != nil {
        return nil, err
    }

    // 构建快速查找集合
    keepSet := make(map[string]bool)
    for _, k := range keep {
        keepSet[k] = true
    }

    // 过滤字段
    filtered := make(map[string]any)
    for key, value := range m {
        if keepSet[key] {
            filtered[key] = value
        }
    }

    return json.MarshalIndent(filtered, "", "  ")
}

func main() {
    data := []byte(`{
        "name": "Alice",
        "age": 25,
        "email": "alice@example.com",
        "password": "secret",
        "address": "Beijing"
    }`)

    // 只保留 name 和 email
    result, err := FilterFields(data, []string{"name", "email"})
    if err != nil {
        panic(err)
    }
    fmt.Println(string(result))
    // {
    //   "email": "alice@example.com",
    //   "name": "Alice"
    // }
}

关键点:使用 map[string]bool 作为集合来检查字段是否需要保留,查询时间为 O(1)。

练习 3:CSV 转换器

编写一个程序,读取 CSV 文件,并将每行转换为 JSON 对象,最终输出 JSON 数组。要求支持命令行参数指定输入文件名。

参考答案

解题思路:使用 encoding/csv 读取 CSV,第一行作为 JSON 键名,后续每行作为值。

代码

package main

import (
    "encoding/csv"
    "encoding/json"
    "fmt"
    "os"
)

func CSVToJSON(filePath string) ([]byte, error) {
    file, err := os.Open(filePath)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    reader := csv.NewReader(file)
    records, err := reader.ReadAll()
    if err != nil {
        return nil, err
    }

    if len(records) < 2 {
        return json.Marshal([]struct{}{})
    }

    // 第一行作为表头
    headers := records[0]
    var result []map[string]string

    for _, record := range records[1:] {
        row := make(map[string]string)
        for i, header := range headers {
            if i < len(record) {
                row[header] = record[i]
            }
        }
        result = append(result, row)
    }

    return json.MarshalIndent(result, "", "  ")
}

func main() {
    if len(os.Args) < 2 {
        fmt.Println("用法: go run main.go <csv文件>")
        os.Exit(1)
    }

    data, err := CSVToJSON(os.Args[1])
    if err != nil {
        fmt.Fprintf(os.Stderr, "转换失败: %v\n", err)
        os.Exit(1)
    }

    fmt.Println(string(data))
}

关键点records[0] 作为表头映射到 JSON 的键,后续每行的对应列映射到值。注意检查列数是否匹配。

搜索