JSON 处理:Go 与数据交换的桥梁

全面解析Go语言JSON处理技术,涵盖序列化反序列化、结构体标签、自定义编码器、流式处理和性能优化,从标准库到高性能第三方方案完整覆盖

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 处理:

  1. 序列化:json.Marshal、json.MarshalIndent
  2. 反序列化:json.Unmarshal
  3. 结构体标签:json:"name"、omitempty、-
  4. 嵌套结构:结构体嵌套和数组
  5. 动态 JSON:map[string]interface{}、json.RawMessage
  6. 自定义:MarshalJSON、UnmarshalJSON
  7. 流式处理:Encoder、Decoder
  8. 常见陷阱:未导出字段、数字类型、时间格式

JSON 是 Web 开发的基石。掌握了 Go 的 JSON 处理,你就掌握了构建 API 和服务的关键技能。

练习时间

  1. API 客户端:写一个调用 GitHub API 的客户端,解析返回的 JSON
  2. JSON 验证器:读取 JSON 文件,验证它是否符合指定的结构
  3. JSON 转换器:把一个格式的 JSON 转换成另一个格式(比如驼峰 → 蛇形)
  4. 流式处理:用 json.Decoder 处理一个很大的 JSON 文件
  5. 自定义类型:实现一个 Duration 类型,可以序列化为 “1h30m” 这种格式

进阶概念:深入 JSON 编码器实现

JSON 编码器内部细节

encoding/json 包使用反射(reflect)来遍历结构体字段,并通过类型断言快速处理基础类型。序列化时大致流程是:

  1. 获取值的 reflect.Type 和 reflect.Value
  2. 根据 kind(struct、map、slice 等)走不同的编码路径
  3. 对 struct,遍历所有字段并读取 tag 信息
  4. 将结果写入 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/jsoneasyjsonjson-iterator
实现方式反射(reflect)代码生成反射优化
序列化速度1x(基准)3-5x2-3x
反序列化速度1x(基准)3-5x2-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()

延伸阅读

我们下篇见!


参考资料:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 《Go 语言编程实战》目录
  2. 《Go 语言编程实战》18.3 上线、观测与迭代
  3. 《Go 语言编程实战》18.2 故障演练