在 Go 语言中操作 YAML 格式,最常用也是最推荐的方式是使用 gopkg.in/yaml.v2 (或者 gopkg.in/yaml.v3) 这个第三方库。它提供了非常方便的 序列化 (Encoding) 和 反序列化 (Decoding) 功能,可以将 Go 语言的结构体、切片、Map 等类型与 YAML 格式之间进行转换。
下面我将详细介绍如何使用 gopkg.in/yaml.v2 (v3 的用法也大同小异,但 v3 提供了更多功能和改进,如果你是新项目,更推荐使用 v3) 来操作 YAML。
yaml 库
首先,你需要安装这个库:bash
go get gopkg.in/yaml.v2
或者如果你想使用 v3:bash
go get gopkg.in/yaml.v3
YAML 是一种人类可读的数据序列化格式,其结构通常是键值对、列表(数组)和嵌套结构。在 Go 中,我们将使用 struct 来表示 YAML 的键值对和嵌套结构,使用 [] 来表示列表。
关键点:
* 字段名与 YAML 键的对应: yaml 库会尝试将 Go 结构体的字段名映射到 YAML 的键。为了更好地控制映射关系,我们可以使用 结构体标签 (Struct Tags)。
* 大小写敏感性: YAML 的键是大小写敏感的。
* 空值处理: yaml 库可以很好地处理 Go 中的零值(如 0、""、nil)。
序列化就是将 Go 的数据结构(如 struct、map、slice)编码成 YAML 格式的字符串或字节流。
示例:
假设我们有一个 Go 结构体:go
package main
import (
"fmt"
"log"
"gopkg.in/yaml.v2" // 或者 "gopkg.in/yaml.v3"
)
type Person struct {
Name string `yaml:"name"` // 使用结构体标签指定 YAML 键名
Age int `yaml:"age"`
City string `yaml:"city,omitempty"` // omitempty: 如果字段是零值,则不输出
Hobbies []string `yaml:"hobbies,flow"` // flow: 以紧凑的方式输出列表
Address struct {
Street string `yaml:"street"`
Zip string `yaml:"zip"`
} `yaml:"address"`
}
func main() {
// 创建一个 Person 实例
p := Person{
Name: "Alice",
Age: 30,
City: "New York",
Hobbies: []string{
"reading",
"hiking",
},
Address: struct {
Street string `yaml:"street"`
Zip string `yaml:"zip"`
}{
Street: "123 Main St",
Zip: "10001",
},
}
// 将 Go 结构体序列化为 YAML 字节切片
yamlBytes, err := yaml.Marshal(p)
if err != nil {
log.Fatalf("error: %v", err)
}
// 将字节切片转换为字符串并打印
fmt.Println(string(yamlBytes))
// 演示 omitempty 的效果
pEmptyCity := Person{
Name: "Bob",
Age: 25,
Hobbies: []string{
"coding",
},
Address: struct {
Street string `yaml:"street"`
Zip string `yaml:"zip"`
}{
Street: "456 Oak Ave",
Zip: "20002",
},
}
yamlBytesEmpty, err := yaml.Marshal(pEmptyCity)
if err != nil {
log.Fatalf("error: %v", err)
}
fmt.Println("\n-- With omitempty --")
fmt.Println(string(yamlBytesEmpty))
}
输出示例:yaml
name: Alice
age: 30
city: New York
hobbies: [reading, hiking]
address:
street: 123 Main St
zip: "10001"
-- With omitempty --
name: Bob
age: 25
hobbies: [coding]
address:
street: 456 Oak Ave
zip: "20002"
解释:
* yaml.Marshal(data): 这是核心函数,将 data(一个 Go 值)编码成 YAML 格式的 []byte。
* 结构体标签 yaml:"key": yaml:"name" 告诉库将 Name 字段映射到 YAML 中的 name 键。
* omitempty: yaml:"city,omitempty" 确保如果 City 字段是零值(空字符串 ""),则在生成的 YAML 中不包含 city 键。
* flow: yaml:"hobbies,flow" 告诉库将 hobbies 列表以紧凑的行内格式([item1, item2])输出,而不是默认的多行块格式。
* 嵌套结构体: Address 字段作为一个嵌套的结构体,被正确地映射到 YAML 中的嵌套结构。
反序列化就是将 YAML 格式的字符串或字节流解析成 Go 的数据结构。
示例:go
package main
import (
"fmt"
"log"
"gopkg.in/yaml.v2" // 或者 "gopkg.in/yaml.v3"
)
// 使用与序列化相同的结构体
type Person struct {
Name string `yaml:"name"`
Age int `yaml:"age"`
City string `yaml:"city,omitempty"`
Hobbies []string `yaml:"hobbies,flow"`
Address struct {
Street string `yaml:"street"`
Zip string `yaml:"zip"`
} `yaml:"address"`
}
func main() {
// 示例 YAML 字符串
yamlData := `
name: Charlie
age: 28
city: London
hobbies: [swimming, photography]
address:
street: 789 Pine Ln
zip: "30003"
`
// 创建一个 Person 变量来接收反序列化的数据
var p Person
// 将 YAML 字节切片反序列化到 Person 变量
err := yaml.Unmarshal([]byte(yamlData), &p) // 注意:需要传入变量的指针
if err != nil {
log.Fatalf("error: %v", err)
}
// 打印反序列化后的 Go 结构体
fmt.Printf("Name: %s\n", p.Name)
fmt.Printf("Age: %d\n", p.Age)
fmt.Printf("City: %s\n", p.City)
fmt.Printf("Hobbies: %v\n", p.Hobbies)
fmt.Printf("Address Street: %s\n", p.Address.Street)
fmt.Printf("Address Zip: %s\n", p.Address.Zip)
// 演示 YAML 中字段缺失的情况 (City 字段缺失)
yamlDataMissingField := `
name: David
age: 35
hobbies: [gaming]
address:
street: 101 Maple Dr
zip: "40004"
`
var pMissing Person
err = yaml.Unmarshal([]byte(yamlDataMissingField), &pMissing)
if err != nil {
log.Fatalf("error: %v", err)
}
fmt.Println("\n-- With Missing Field (City) --")
fmt.Printf("Name: %s\n", pMissing.Name)
fmt.Printf("Age: %d\n", pMissing.Age)
fmt.Printf("City: %s (will be empty string if not present)\n", pMissing.City) // City is ""
fmt.Printf("Hobbies: %v\n", pMissing.Hobbies)
}
输出示例:
Name: Charlie
Age: 28
City: London
Hobbies: [swimming photography]
Address Street: 789 Pine Ln
Address Zip: 30003
-- With Missing Field (City) --
Name: David
Age: 35
City: (will be empty string if not present)
Hobbies: [gaming]
解释:
* yaml.Unmarshal(data, v): 这是核心函数,将 data(一个 YAML 格式的 []byte)解析并填充到 v(一个 Go 变量的指针)中。
* 指针传递: Unmarshal 需要一个 指针 (&p),因为它需要修改传入的变量。
* 零值填充: 如果 YAML 中某个键不存在,对应的 Go 结构体字段会被赋以其 零值(例如,string 为 "",int 为 0,slice 为 nil)。
除了 struct,yaml 库也能很好地处理 Go 的 map 类型。go
package main
import (
"fmt"
"log"
"gopkg.in/yaml.v2"
)
func main() {
// Map to YAML
dataMap := map[string]interface{}{
"name": "Eve",
"age": 22,
"config": map[string]string{
"database": "postgres",
"port": "5432",
},
"items": []int{1, 2, 3},
}
yamlBytes, err := yaml.Marshal(dataMap)
if err != nil {
log.Fatalf("error: %v", err)
}
fmt.Println("--- Map to YAML ---")
fmt.Println(string(yamlBytes))
// YAML to Map
yamlData := `
user:
name: Frank
roles:
- admin
- editor
settings:
theme: dark
language: en
`
var resultMap map[string]interface{}
err = yaml.Unmarshal([]byte(yamlData), &resultMap)
if err != nil {
log.Fatalf("error: %v", err)
}
fmt.Println("\n--- YAML to Map ---")
fmt.Printf("User Name: %v\n", resultMap["user"].(map[string]interface{})["name"])
fmt.Printf("User Roles: %v\n", resultMap["user"].(map[string]interface{})["roles"])
fmt.Printf("Settings Theme: %v\n", resultMap["settings"].(map[string]interface{})["theme"])
}
输出示例:yaml
--- Map to YAML ---
name: Eve
age: 22
config:
database: postgres
port: "5432"
items: [1, 2, 3]
--- YAML to Map ---
User Name: Frank
User Roles: [admin editor]
Settings Theme: dark
注意:
* 当使用 map[string]interface{} 进行反序列化时,你需要进行 类型断言 (Type Assertion) 来访问嵌套的值,例如 resultMap["user"].(map[string]interface{})["name"]。这可能会比较繁琐,所以对于结构固定的数据,使用 struct 通常是更好的选择。
* YAML 的键通常是字符串,所以 map[string]... 是最常见的。
gopkg.in/yaml.v2 (v3 也是) 支持更多有用的结构体标签选项:
* yaml:"key,omitempty": 如果字段值为零值,则不输出。
* yaml:"key,flow": 以紧凑的行内格式输出列表或 map。
* yaml:"key,inline": 类似 flow,但更侧重于结构体内的嵌套。
* yaml:"key,disc": 仅用于 Map,表示 Map 的键是不可变的(通常不常用)。
* yaml:"key,hyphen": (v3 独有) 适用于 Map,将 Map 的键中的驼峰命名转为 kebab-case(例如 MyKey -> my-key)。
如果你选择使用 gopkg.in/yaml.v3,你会发现它有以下一些优势:
* 更好的兼容性: 遵循 YAML 1.2 标准。
* 更丰富的类型支持: 对 Go 的各种类型有更好的支持。
* 更灵活的配置: 提供了更多的选项来控制序列化和反序列化的行为。
* 性能优化: 通常比 v2 性能更好。
* yaml:"key,omitempty,flow" 组合使用: 可以直接组合使用多个标签。
使用 v3 的代码示例基本与 v2 相同,只需要修改 import 语句即可。
使用 Go 操作 YAML 格式非常简单,主要依赖于 gopkg.in/yaml.v2 或 gopkg.in/yaml.v3 库。
* 序列化: 使用 yaml.Marshal() 将 Go 数据结构转换为 []byte。
* 反序列化: 使用 yaml.Unmarshal() 将 []byte 解析到 Go 数据结构中。
* 结构体标签: yaml:"key,options" 是控制 YAML 键名和行为的关键。
* struct vs map: 对于结构固定的数据,使用 struct 更安全、类型更安全。对于需要灵活映射或未知结构的 YAML,map[string]interface{} 是一个选择,但需要类型断言。
在实际开发中,你会经常在配置文件、API 请求/响应、数据存储等场景中使用 YAML。