数据序列化
encoding/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}
}结构体标签详解
结构体标签(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 都没有出现
}omitempty 对零值的处理
omitempty 会让字段在值为零值时不输出。零值包括:0、""、false、nil 切片/指针/map。如果需要区分”未设置”和”设置为空”,请使用指针类型 *string。
嵌套结构体、切片与 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 是 []byte 的别名,它告诉 json.Marshal 和 json.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 从 io.Reader 流式读取 JSON,json.Encoder 向 io.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)
}
}Decoder 与 Unmarshal 的选择
json.Unmarshal:一次性将整个 JSON 解析到内存,适合小数据json.Decoder:流式读取,适合大文件、网络流、NDJSON 格式json.Encoder:流式写入,与 Decoder 配对使用
encoding/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
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:[优秀 推荐]}
}gob vs JSON
- gob:Go 原生二进制格式,更小更快,但仅限 Go 之间使用
- JSON:通用文本格式,可跨语言,可读性好
- 在微服务之间通信(全部 Go 服务)时,gob 是高效的选择;对外 API 则使用 JSON
encoding/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(第三方库)
Go 标准库不包含 YAML 支持。最常用的 YAML 库是 gopkg.in/yaml.v3,它的 API 设计与 encoding/json 几乎一致,使用非常自然。
安装与使用
go get gopkg.in/yaml.v3package 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))
}YAML 标签与 JSON 标签的区别
- YAML 使用
yaml:"..."标签 - JSON 使用
json:"..."标签 - 如果需要同时支持两种格式,可以同时声明两个标签:
type Config struct {
Host string `json:"host" yaml:"host"`
Port int `json:"port" yaml:"port"`
}练习题
练习 1:JSON 配置解析器
编写一个程序,读取以下 JSON 配置文件并验证必填字段。缺少必填字段时返回清晰的错误信息。
{
"app_name": "my-app",
"version": "1.0.0",
"database": {
"host": "localhost",
"port": 3306,
"name": "mydb"
}
}必填字段:app_name、database.host、database.name、database.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 的键,后续每行的对应列映射到值。注意检查列数是否匹配。
