JSON 处理:Go 与数据交换的桥梁
如果你在做 Web 开发,那你几乎每天都会和 JSON 打交道。JSON(JavaScript Object Notation)已经成为当今互联网最流行的数据交换格式——API 返回 JSON,配置文件用 JSON,消息队列里传输的也是 JSON。
Go 语言的 encoding/json 包提供了强大而优雅的 JSON 处理能力。它最大的特色是利用结构体标签(struct tags)来控制序列化和反序列化的行为,让你用很少的代码就能完成复杂的数据转换。
今天我们就来全面学习 Go 的 JSON 处理,从基础到高级,让你成为 JSON 大师。
基础:序列化和反序列化
序列化:结构体 → JSON
把 Go 的数据结构转换成 JSON 字符串叫做序列化(marshaling)。
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string
Age int
Email string
}
func main() {
user := User{
Name: "张三",
Age: 25,
Email: "zhangsan@example.com",
}
// Marshal:结构体 → JSON 字节切片
data, err := json.Marshal(user)
if err != nil {
fmt.Println("序列化失败:", err)
return
}
fmt.Println(string(data))
// {"Name":"张三","Age":25,"Email":"zhangsan@example.com"}
// MarshalIndent:格式化的 JSON(带缩进)
pretty, _ := json.MarshalIndent(user, "", " ")
fmt.Println(string(pretty))
}
输出:
{"Name":"张三","Age":25,"Email":"zhangsan@example.com"}
{
"Name": "张三",
"Age": 25,
"Email": "zhangsan@example.com"
}
MarshalIndent 的第二个参数是前缀,第三个参数是缩进字符。通常用于生成可读的 JSON 文件。
反序列化:JSON → 结构体
把 JSON 字符串转换回 Go 的数据结构叫做反序列化(unmarshaling)。
jsonData := `{"Name":"李四","Age":30,"Email":"lisi@example.com"}`
var user User
err := json.Unmarshal([]byte(jsonData), &user)
if err != nil {
fmt.Println("反序列化失败:", err)
return
}
fmt.Printf("%+v\n", user)
// {Name:李四 Age:30 Email:lisi@example.com}
注意:Unmarshal 的第一个参数是 []byte,不是 string。如果你的 JSON 是字符串,需要先转换。
结构体标签(Struct Tags)
默认情况下,Go 用字段名作为 JSON 的键。但 JSON 通常使用小写或驼峰命名,而 Go 字段需要大写才能导出。这就是结构体标签的用武之地。
基本标签
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
CreatedAt string `json:"created_at"`
}
现在序列化时,JSON 键就是小写的了:
user := User{
Name: "张三",
Age: 25,
Email: "zhangsan@example.com",
}
data, _ := json.Marshal(user)
fmt.Println(string(data))
// {"name":"张三","age":25,"email":"zhangsan@example.com","created_at":""}
常用标签选项
omitempty:零值字段不序列化
type User struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
Email string `json:"email,omitempty"`
Address string `json:"address,omitempty"`
}
user := User{Name: "张三", Age: 25}
data, _ := json.Marshal(user)
fmt.Println(string(data))
// {"name":"张三","age":25}
// 注意:email 和 address 因为是零值,被省略了
-:完全忽略字段
type User struct {
Name string `json:"name"`
Password string `json:"-"` // 永远不序列化
Age int `json:"age"`
Internal string `json:"-,omitempty"` // 字段名就叫 "-"
}
注意:json:"-" 和 json:"-,omitempty" 是不同的。前者是完全忽略,后者是字段名为 -。
多个标签共存
一个字段可以有多个标签(json、xml、yaml 等),用空格分隔:
type User struct {
Name string `json:"name" xml:"Name" yaml:"name"`
Email string `json:"email,omitempty" xml:"Email,omitempty"`
}
处理嵌套结构
JSON 可以嵌套,Go 结构体也可以嵌套:
type Address struct {
City string `json:"city"`
Province string `json:"province"`
}
type User struct {
Name string `json:"name"`
Address Address `json:"address"`
}
func main() {
user := User{
Name: "张三",
Address: Address{
City: "北京",
Province: "北京市",
},
}
data, _ := json.MarshalIndent(user, "", " ")
fmt.Println(string(data))
}
输出:
{
"name": "张三",
"address": {
"city": "北京",
"province": "北京市"
}
}
嵌套数组
type User struct {
Name string `json:"name"`
Tags []string `json:"tags"`
Scores []int `json:"scores"`
}
user := User{
Name: "张三",
Tags: []string{"developer", "gopher"},
Scores: []int{95, 88, 92},
}
data, _ := json.MarshalIndent(user, "", " ")
fmt.Println(string(data))
处理未知结构的 JSON
有时候你不知道 JSON 的完整结构,或者只想提取其中一部分数据。这时候可以用 map[string]interface{}:
jsonData := `{
"name": "张三",
"age": 25,
"skills": ["Go", "Python", "JavaScript"],
"address": {
"city": "北京",
"province": "北京市"
}
}`
var data map[string]interface{}
json.Unmarshal([]byte(jsonData), &data)
fmt.Println("姓名:", data["name"])
fmt.Println("年龄:", data["age"])
skills := data["skills"].([]interface{})
for _, skill := range skills {
fmt.Println("技能:", skill)
}
address := data["address"].(map[string]interface{})
fmt.Println("城市:", address["city"])
注意:JSON 数字反序列化到 interface{} 时,会变成 float64 类型。如果你需要整数,要做类型转换:
age := int(data["age"].(float64))
json.RawMessage:延迟解析
如果你只想解析 JSON 的一部分,可以用 json.RawMessage 保存原始 JSON 数据:
type Response struct {
Status string `json:"status"`
Message string `json:"message"`
Data json.RawMessage `json:"data"` // 暂时不解析
}
func main() {
jsonStr := `{
"status": "ok",
"message": "success",
"data": {"user": "张三", "age": 25}
}`
var resp Response
json.Unmarshal([]byte(jsonStr), &resp)
fmt.Println("状态:", resp.Status)
// 稍后再根据情况解析 Data
var userData map[string]interface{}
json.Unmarshal(resp.Data, &userData)
fmt.Println("用户:", userData)
}
自定义序列化和反序列化
有时候默认的序列化行为不够灵活,你可以实现 Marshaler 和 Unmarshaler 接口来自定义。
自定义 MarshalJSON
package main
import (
"encoding/json"
"fmt"
"time"
)
type Event struct {
Name string `json:"name"`
Timestamp time.Time `json:"timestamp"`
}
// MarshalJSON 自定义序列化
func (e Event) MarshalJSON() ([]byte, error) {
type Alias Event
return json.Marshal(&struct {
Timestamp string `json:"timestamp"`
*Alias
}{
Timestamp: e.Timestamp.Format("2006-01-02 15:04:05"),
Alias: (*Alias)(&e),
})
}
func main() {
event := Event{
Name: "会议",
Timestamp: time.Now(),
}
data, _ := json.MarshalIndent(event, "", " ")
fmt.Println(string(data))
}
自定义 UnmarshalJSON
// UnmarshalJSON 自定义反序列化
func (e *Event) UnmarshalJSON(data []byte) error {
type Alias Event
aux := &struct {
Timestamp string `json:"timestamp"`
*Alias
}{
Alias: (*Alias)(e),
}
if err := json.Unmarshal(data, aux); err != nil {
return err
}
t, err := time.Parse("2006-01-02 15:04:05", aux.Timestamp)
if err != nil {
return err
}
e.Timestamp = t
return nil
}
小贴士:注意 type Alias Event 的用法——它创建一个和 Event 结构相同但没有自定义方法的别名,避免递归调用 MarshalJSON。
流式处理
对于大文件或者持续输出的 JSON 数据,用流式处理更合适。
解码器(Decoder)
package main
import (
"encoding/json"
"fmt"
"strings"
)
func main() {
jsonStream := `
{"name": "张三", "age": 25}
{"name": "李四", "age": 30}
{"name": "王五", "age": 28}
`
decoder := json.NewDecoder(strings.NewReader(jsonStream))
for decoder.More() {
var user struct {
Name string `json:"name"`
Age int `json:"age"`
}
if err := decoder.Decode(&user); err != nil {
fmt.Println("解码失败:", err)
break
}
fmt.Printf("用户: %s, 年龄: %d\n", user.Name, user.Age)
}
}
编码器(Encoder)
package main
import (
"encoding/json"
"os"
)
func main() {
file, _ := os.Create("output.json")
defer file.Close()
encoder := json.NewEncoder(file)
users := []struct {
Name string `json:"name"`
Age int `json:"age"`
}{
{"张三", 25},
{"李四", 30},
{"王五", 28},
}
for _, user := range users {
if err := encoder.Encode(user); err != nil {
fmt.Println("编码失败:", err)
}
}
}
处理 JSON 数组
// 解码数组
jsonStr := `[{"name":"张三","age":25},{"name":"李四","age":30}]`
var users []struct {
Name string `json:"name"`
Age int `json:"age"`
}
json.Unmarshal([]byte(jsonStr), &users)
for _, u := range users {
fmt.Printf("%s: %d\n", u.Name, u.Age)
}
// 编码数组
data, _ := json.Marshal(users)
fmt.Println(string(data))
实战:JSON 配置管理
让我们写一个配置管理工具:
package main
import (
"encoding/json"
"fmt"
"os"
)
type Config struct {
AppName string `json:"app_name"`
Version string `json:"version"`
Debug bool `json:"debug"`
Port int `json:"port"`
Database DatabaseConfig `json:"database"`
Features []string `json:"features"`
Options map[string]string `json:"options,omitempty"`
}
type DatabaseConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"-"` // 密码不序列化
Database string `json:"database"`
}
func (c *Config) Save(path string) error {
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}
func main() {
config := &Config{
AppName: "MyApp",
Version: "1.0.0",
Debug: true,
Port: 8080,
Database: DatabaseConfig{
Host: "localhost",
Port: 5432,
Username: "admin",
Password: "secret123",
Database: "mydb",
},
Features: []string{"logging", "metrics", "tracing"},
Options: map[string]string{
"log_level": "debug",
"timeout": "30s",
},
}
// 保存配置
err := config.Save("config.json")
if err != nil {
fmt.Println("保存失败:", err)
return
}
fmt.Println("配置已保存到 config.json")
// 加载配置
loaded, err := LoadConfig("config.json")
if err != nil {
fmt.Println("加载失败:", err)
return
}
fmt.Printf("配置已加载: %s v%s\n", loaded.AppName, loaded.Version)
fmt.Printf(" 数据库: %s:%d/%s\n",
loaded.Database.Host,
loaded.Database.Port,
loaded.Database.Database)
}
常见陷阱
1. 未导出字段不会被序列化
type User struct {
Name string // 会序列化
age int // 不会序列化(未导出)
Email string // 会序列化
}
2. 数字类型转换问题
JSON 数字反序列化到 interface{} 时默认是 float64:
var data map[string]interface{}
json.Unmarshal([]byte(`{"age": 25}`), &data)
// 直接转 int 会 panic
// age := data["age"].(int)
// 正确做法
age := int(data["age"].(float64))
3. 时间格式问题
默认的 time.Time 序列化使用 RFC3339 格式:
type Event struct {
Time time.Time `json:"time"`
}
// {"time":"2021-02-16T13:10:00+08:00"}
如果你需要自定义格式,要实现 MarshalJSON 和 UnmarshalJSON。
4. 循环引用
结构体有循环引用时,序列化会导致栈溢出:
type Node struct {
Value int
Next *Node
}
// 如果有循环,Marshal 会无限递归
第三方 JSON 库
虽然标准库够用,但如果你对性能有极致要求,可以考虑第三方库:
- json-iterator/go:比标准库快 2-3 倍
- goccy/go-json:号称最快的 JSON 库
- segmentio/encoding/json:性能更好,API 兼容标准库
import jsoniter "github.com/json-iterator/go"
var json = jsoniter.ConfigCompatibleWithStandardLibrary
// 后续用法和标准库一样
小结
今天我们全面学习了 Go 的 JSON 处理:
- 序列化:
json.Marshal、json.MarshalIndent - 反序列化:
json.Unmarshal - 结构体标签:
json:"name"、omitempty、- - 嵌套结构:结构体嵌套和数组
- 动态 JSON:
map[string]interface{}、json.RawMessage - 自定义:
MarshalJSON、UnmarshalJSON - 流式处理:
Encoder、Decoder - 常见陷阱:未导出字段、数字类型、时间格式
JSON 是 Web 开发的基石。掌握了 Go 的 JSON 处理,你就掌握了构建 API 和服务的关键技能。
练习时间
- API 客户端:写一个调用 GitHub API 的客户端,解析返回的 JSON
- JSON 验证器:读取 JSON 文件,验证它是否符合指定的结构
- JSON 转换器:把一个格式的 JSON 转换成另一个格式(比如驼峰 → 蛇形)
- 流式处理:用
json.Decoder处理一个很大的 JSON 文件 - 自定义类型:实现一个
Duration类型,可以序列化为 “1h30m” 这种格式
进阶概念:深入 JSON 编码器实现
JSON 编码器内部细节
encoding/json 包使用反射(reflect)来遍历结构体字段,并通过类型断言快速处理基础类型。序列化时大致流程是:
- 获取值的 reflect.Type 和 reflect.Value
- 根据 kind(struct、map、slice 等)走不同的编码路径
- 对 struct,遍历所有字段并读取 tag 信息
- 将结果写入 bytes.Buffer
// 理解 encodeState 结构
type encodeState struct {
bytes.Buffer // 输出缓冲区
ptrLevel uint // 指针递归深度(防循环引用)
}
标准库通过缓存常见类型的编码器(encoderCache)来减少反射开销。但对于巨型结构体和高吞吐量场景,反射的开销仍然显著。
流式编码/解码
Decoder 和 Encoder 的设计基于流式 I/O,适合处理持续输入输出:
package main
import (
"encoding/json"
"fmt"
"strings"
)
func streamDemo() {
// Decoder 可以从任意 io.Reader 读取
reader := strings.NewReader(`
{"id":1,"name":"A"}
{"id":2,"name":"B"}
{"id":3,"name":"C"}
`)
decoder := json.NewDecoder(reader)
// 可选:设置 UseNumber 避免 float64 问题
decoder.UseNumber()
for decoder.More() || true {
var item map[string]interface{}
if err := decoder.Decode(&item); err != nil {
break
}
fmt.Printf("decoded: %+v\n", item)
}
}
func encoderDemo() {
// Encoder 写入任意 io.Writer
type Log struct {
Time int64 `json:"ts"`
Level string `json:"level"`
Message string `json:"msg"`
}
var sb strings.Builder
encoder := json.NewEncoder(&sb)
encoder.Encode(Log{Time: 1690000000, Level: "info", Message: "started"})
encoder.Encode(Log{Time: 1690000001, Level: "warn", Message: "slow query"})
fmt.Println(sb.String())
}
UseNumber() 方法让 JSON 数字反序列化为 json.Number 而非 float64,在需要精确整数值的场景非常有用。
自定义 MarshalJSON/UnmarshalJSON
实现自定义编码/解码时,核心原则是利用 Alias 类型避免无限递归:
package main
import (
"encoding/json"
"fmt"
"strconv"
"time"
)
// Timestamp 自定义时间戳类型
type Timestamp time.Time
func (t Timestamp) MarshalJSON() ([]byte, error) {
unix := time.Time(t).Unix()
return []byte(strconv.FormatInt(unix, 10)), nil
}
func (t *Timestamp) UnmarshalJSON(data []byte) error {
var unix int64
if err := json.Unmarshal(data, &unix); err != nil {
return err
}
*t = Timestamp(time.Unix(unix, 0))
return nil
}
type Event struct {
Name string `json:"name"`
At Timestamp `json:"at"`
}
func customTypeDemo() {
e := Event{Name: "launch", At: Timestamp(time.Now())}
data, _ := json.Marshal(e)
fmt.Println(string(data))
// {"name":"launch","at":1690000000}
}
匿名 Struct Tag
有时你需要接收 API 返回的不同字段,按需组合。可以通过匿名结构体和 inline tag 实现:
// 组合不同来源的字段
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price string `json:"price,omitempty"`
}
// 内嵌匿名 struct 可以扁平化嵌套
func embeddedDemo() {
type Wrapper struct {
Data struct {
Product `json:",inline"`
} `json:"data"`
}
jsonStr := `{"data":{"id":1,"name":"Laptop","price":"9999"}}`
var w Wrapper
json.Unmarshal([]byte(jsonStr), &w)
fmt.Printf("%+v\n", w.Data)
// {ID:1 Name:Laptop Price:9999}
}
实战案例
动态 JSON(map[string]interface{} 与 json.RawMessage)
处理 REST API 中不确定结构的响应是常见需求:
package main
import (
"encoding/json"
"fmt"
)
// 动态响应处理
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"` // 延迟解析
}
// 不同 API 有不同的 Data 结构
func handleAPIResponse(body []byte) {
var resp APIResponse
if err := json.Unmarshal(body, &resp); err != nil {
fmt.Println("parse error:", err)
return
}
fmt.Printf("code=%d, msg=%s\n", resp.Code, resp.Message)
// 根据 code 或业务逻辑决定如何解析 data
switch resp.Code {
case 200:
var user struct {
ID int `json:"id"`
Name string `json:"name"`
}
json.Unmarshal(resp.Data, &user)
fmt.Printf("user: %+v\n", user)
case 201:
var order struct {
OrderID string `json:"order_id"`
Amount float64 `json:"amount"`
}
json.Unmarshal(resp.Data, &order)
fmt.Printf("order: %+v\n", order)
}
}
JSON Schema 验证
对于需要严格数据校验的场景,可以用第三方库:
// go get github.com/xeipuuv/gojsonschema
import "github.com/xeipuuv/gojsonschema"
func validateJSON(document []byte) bool {
schemaLoader := gojsonschema.NewStringLoader(`{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name", "age"]
}`)
documentLoader := gojsonschema.NewBytesLoader(document)
result, _ := gojsonschema.Validate(schemaLoader, documentLoader)
if !result.Valid() {
for _, err := range result.Errors() {
fmt.Println(err)
}
return false
}
return true
}
性能对比:encoding/json vs easyjson vs json-iterator
| 项目 | encoding/json | easyjson | json-iterator |
|---|---|---|---|
| 实现方式 | 反射(reflect) | 代码生成 | 反射优化 |
| 序列化速度 | 1x(基准) | 3-5x | 2-3x |
| 反序列化速度 | 1x(基准) | 3-5x | 2-3x |
| 内存分配 | 较多 | 少 | 中等 |
| 使用方式 | 零依赖 | go generate | 替换 import |
easyjson 使用方式:
//go:generate easyjson -all model.go
package model
//easyjson:json
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
go install github.com/mailru/easyjson/...@latest
go generate ./...
easyjson 会生成 model_easyjson.go,在相同包中提供 MarshalJSON/UnmarshalJSON 方法,完全绕过反射。
FAQ
Q1: json.Unmarshal 时出现 cannot unmarshal string into Go value of type… 怎么办?
A: 类型不匹配。JSON 字符串 "25" 不能反序列化到 Go 的 int 字段。检查 JSON 数据和结构体字段类型是否一致。可以用 json.Number 来临时接收数字字符串。
Q2: omitempty 对什么值生效?
A: omitempty 对零值生效:bool 的 false、int 的 0、string 的 “"、slice 的 nil、map 的 nil、指针的 nil、interface 的 nil。注意空 slice []string{} 不是 nil,所以不会 omit。如果想要空 slice 也不序列化,需要使用指针 *[]string。
Q3: 如何解析未知 JSON 中的深度嵌套字段?
A: 可以使用 json.RawMessage 逐层手动解析。或者用第三方库 gjson(tidwall/gjson)直接根据路径提取:
// gjson 直接按路径获取
value := gjson.Get(jsonStr, "data.users.0.name")
fmt.Println(value.String())
Q4: 标准库 JSON 序列化很慢怎么办?
A: 三个方案:1)升级到最新 Go 版本,每个版本都有 reflect 相关的优化;2)对高频结构体实现 MarshalJSON/UnmarshalJSON,绕过反射;3)使用 easyjson 或 json-iterator 第三方库。
Q5: JSON key 中有特殊字符(如空格、连字符)怎么办?
A: 在 struct tag 中用原始标识。如果 key 包含空格,可以用反引号包裹:
type Response struct {
FirstName string `json:"first name"` // key 含空格
AuthToken string `json:"x-auth-token"` // key 含连字符
}
Q6: map[string]interface{} 反序列化后数字变成 float64 是 bug 吗?
A: 不是 bug,这是 JSON 标准决定的——JSON 不区分 int 和 float,所以默认反序列化为 float64。解决方案:使用 Decoder.UseNumber() 让数字变为 json.Number 类型,之后用 .Int64() 转换。
Q7: 如何处理 JSON 中存在而 struct 中没有的字段?
A: 默认情况下多余字段会被忽略,不会报错。如果需要检测多余字段,可以使用 Decoder.DisallowUnknownFields():
dec := json.NewDecoder(reader)
dec.DisallowUnknownFields()
var v MyStruct
if err := dec.Decode(&v); err != nil {
// 报错:json: unknown field "extra_field"
}
Q8: 如何让 json.Marshal 不转义 HTML 特殊字符?
A: 标准库的 Encoder 默认会转义 <、>、& 为 Unicode 转义序列,防止 HTML 注入。如果你确定输出不需要 HTML 安全(如 API 返回),可以设置:encoder.SetEscapeHTML(false)。
最佳实践
不要对外暴露内部 struct
如果 struct 字段在内部使用但在 JSON 序列化时不应暴露,使用 json:"-" 或转为小写。对于 API 响应结构体,建议单独定义 DTO(Data Transfer Object),不要直接暴露内部模型。
使用指针避免零值问题
对于可选字段,使用指针可以明确区分"零值"和"未设置”:
type User struct {
Name string `json:"name"`
Age *int `json:"age,omitempty"` // nil 表示未设置
}
age := 25
user := User{Name: "张三", Age: &age}
data, _ := json.Marshal(user)
// {"name":"张三","age":25}
user2 := User{Name: "李四"}
data2, _ := json.Marshal(user2)
// {"name":"李四"} // age 字段完全不存在
使用 json.Number 精确控制数字
在需要精确处理大整数或不想用 float64 的场景,启用 UseNumber:
dec := json.NewDecoder(reader)
dec.UseNumber()
var v map[string]interface{}
dec.Decode(&v)
n := v["id"].(json.Number)
id, _ := n.Int64()
延伸阅读
我们下篇见!
参考资料:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。