Go 语言中数据编码与解码
基本概念
Go 语言 encoding
包提供对多种网络流行的数据格式编码和解码功能,主要包括对 JSON、XML、CSV 等数据格式转换和 Base64 编码的操作。
对数据的转换和还原也称为序列化(Serialization)和反序列化(Deserialization):
- 序列化:序列化是指将数据转换成特定格式,同时保存好数据结构关键信息。
- 反序列化:反序列化指从序列化的格式中恢复数据结构或对象状态。
数据序列化操作的目的是方便数据储存和传输,例如通过 YAML 来保存服务配置信息,通过 JSON 返回服务器响应内容。为了直观表达下面用「生成」和「解析」指代。
Base64
Base64 编码在网络编程中很常见,用于将二进制数据转换为纯文本格式(ASCII 字符)。这样一来可以通过纯文本来传输二进制数据,二来可以规避传输原始数据中的非法字符。在 Go 语言中内置标准库 encoding/base64
支持 Base64 编码和解码。
编码原理
Base64 编码将每 3 个字节的二进制数据转换成 4 个字节的文本数据。下面以字符串 Mon
为例说明过程:
- 字符串
Mon
中 3 个字符 ASCII 码分别是[77 111 110]
。 - 将字符串转为字节切片表示,切片中每个字节占 8 位,以二进制形式表示为:
[01001101 01101111 01101110]
,共 24 位。 - Base64 每次处理 3 个字节,重新按照 6 位划分,变成 4 个字节:
[010011 010110 111101 101110]
,对应十进制值为:[19 22 61 46]
。由于 6 位只能表示数字0
到63
共 64 种(2^6
)可能值,故得名 Base64。 - 根据 Base64 索引表,这 4 个数字对应的字符是
TW9u
,也就是字符串Mon
对应的 Base64 编码。Base64 索引表包括大小写字母、数字、加号(+
)和斜杠(/
)。 - 如果最后一次处理不足 3 个字节,会用全 0 填充到 24 位再处理。结果末 2 位字节为
00000000
时,用等号(=
)填充。 - 由于编码后数据以字符形式存储,相当于每转 3 个字节,就多出来 1 个字节,编码后体积比原先大 1/3。同理,Base32 编码后数据大小增加 3/5。
下面用简单直白的代码模拟这一过程,特意在原字符串前后加入控制字符,以观察填充处理:
package main
import (
"encoding/base64"
"fmt"
"strconv"
)
func main() {
// 定义数据,转为字节切片
a := "\u0003Mon\u0003"
b := []byte(a)
fmt.Println(b) // 输出:[3 77 111 110 3],对应字符 ASCII 编码
// 以 3 字节为倍数检查,补全位数
if len(b)%3 != 0 {
c := make([]byte, 3-len(b)%3)
b = append(b, c...)
}
fmt.Println(b) // 输出:[3 77 111 110 3 0]
// 转为二进制字符串并连接
d := ""
for _, b := range b {
d += fmt.Sprintf("%08b", b)
}
fmt.Println(d) // 输出:000000110100110101101111011011100000001100000000
// 分割为 6 位一组
e := make([]string, 0)
for i := 0; i < len(d); i += 6 {
e = append(e, d[i:i+6])
}
fmt.Println(e) // 输出:[000000 110100 110101 101111 011011 100000 001100 000000]
// 转为整型切片
f := make([]int, len(e))
for i, v := range e {
n, _ := strconv.ParseInt(v, 2, 64)
f[i] = int(n)
}
fmt.Println(f) // 输出:[0 52 53 47 27 32 12 0]
// 生成 BASE64 编码映射
g := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
h := make(map[int]string)
for i, v := range g {
h[i] = string(v)
}
fmt.Println(h)
// 对应到 BASE64 编码,输出最终结果
j := ""
for i, v := range f {
vv := h[v]
// 特殊规则,用 = 代替空位
if i > len(f)-3 && len(f)>3 && vv == "A" {
vv = "="
}
j += vv
}
fmt.Println(j) // 输出:A01vbgM=
}
解码原理
解码则是编码逆过程,将 4 个字符转为 3 个字节,还原出原始字符串。转换时遇到填充字符 =
则忽略:
package main
import (
"encoding/base64"
"fmt"
"strconv"
)
func main() {
// 定义编码数据和索引表
a := "A01vbgM="
b := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
c := make(map[string]int)
for i, v := range b {
c[string(v)] = i
}
fmt.Println(c)
// 转为整型切片
d := make([]int, 0)
for _, v := range a {
d = append(d, c[string(v)])
}
fmt.Println(d)
// 转为二进制字符串
e := ""
for _, v := range d {
e += fmt.Sprintf("%06b", v)
}
fmt.Println(e)
// 分割为 8 位一组
f := make([]string, 0)
for i := 0; i < len(e); i += 8 {
// 特殊规则,最后两位空位时忽略
if i > len(e)-24 && len(e)>24 && e[i:i+8] == "00000000" {
continue
}
f = append(f, e[i:i+8])
}
fmt.Println(f)
// 转为字节切片,打印结果
g := make([]byte, len(f))
for i, v := range f {
b, _ := strconv.ParseUint(v, 2, 8)
g[i] = byte(b)
}
fmt.Println(g) // 输出:[3 77 111 110 3]
fmt.Println(string(g))
}
标准编码
标准编码指使用标准 Base64 字符集进行编解码。在 Go 语言中,标准编码功能已经封装到 base64.StdEncoding
包中:
package main
import (
"encoding/base64"
"fmt"
)
func main() {
// 标准编码
fmt.Println(base64.StdEncoding.EncodeToString([]byte("a"))) // YQ==
// 标准解码
fmt.Println(base64.StdEncoding.DecodeString("YQ==")) // 输出:[97] <nil>
// 实际应用中需要处理解码错误
d, err := base64.StdEncoding.DecodeString("YQ=")
if err != nil {
fmt.Println("解码出错:", err) // 报错:illegal base64 data at input byte 3
return
}
fmt.Println(string(d))
}
安全编码
标准 Base64 字符集包含符号 +
和 /
,在网络传输时不能保证安全。可使用 URL 安全编码方式 base64.URLEncoding
,编码后 +
和 /
字符替换为 -
和 _
字符:
package main
import (
"encoding/base64"
"fmt"
)
func main() {
// 在编码结果中产生 + 和 / 字符
data := []byte{0xfb, 0xff, 0xbf}
// 标准 Base64 编码,原样输出
fmt.Println(base64.StdEncoding.EncodeToString(data)) // 输出:+/+/
// 安全编码
fmt.Println(base64.URLEncoding.EncodeToString(data)) // 输出:-_-_
// 解码时也要使用对应方法
fmt.Println(base64.URLEncoding.DecodeString("-_-_")) // 输出:[251 255 191] <nil>
}
JSON
JSON(JavaScript Object Notation)全称 JavaScript 对象表示法,是互联网上最流行的轻量级数据交换格式。在 Go 语言中,内置 encoding/json
库提供对 JSON 格式支持。
基本结构
JSON 使用文本格式保存(文件后缀名 .json
),可以支持键值对集合和数组结构:
- 对象(Object): 由花括号
{}
包围的一组无序键值对。键为字符串,值为任意类型。键和值之间用冒号分隔,键值对之间用逗号分隔。 - 数组(Array): 由方括号
[]
包围的有序值列表。值可以为任意类型。多个值之间用逗号分隔。 - 值(Value): 可以是字符串、数字、布尔值、数组、对象或者空值。
JSON 文档只能有一个顶级值,也就是说一个文档等于一个对象、数组或值,不能有多个并列顶级值。例如用对象保存用户信息:
{
"name": "张三", // 字符串,用双引号包围
"age": 28, // 数值,整数或浮点数
"isStudent": false, // 布尔值,true 或 false
"skills": ["Java", "Python", 100], // 数组,可以包含任意类型
"address": { // 对象,包含任意数量键值对
"street": null, // 空值
"city": "北京",
"money": 300,
"country": "中国" // 注意最后一个键值对后不能有逗号
}
}
在 Go 语言中,通过结构体标签来保存 JSON 字段名等信息。JSON 与 Go 语言类型默认对应关系如下:
类型 | JSON | Go |
---|---|---|
字符串 | String |
string |
数字 | Number |
float64 |
布尔值 | Boolean |
bool |
数组 | Array |
[]interface{} |
对象 | Object |
map[string]interface{} |
空值 | Null |
nil |
当然也可以自定义类型对应关系,例如把字符串格式的时间映射到 time.Time
类型,只要类型能匹配上:
package main
import (
"encoding/json"
"fmt"
"time"
)
// Event 结构体包含 time.Time 类型时间戳字段
type Event struct {
Name string `json:"name"`
Timestamp time.Time `json:"timestamp"`
}
func main() {
// JSON 字符串包含标准格式的日期时间字符串
j := `{
"name": "Webinar",
"timestamp": "2020-05-01T14:53:00Z"
}`
// 输出解析结果
var event Event
json.Unmarshal([]byte(j), &event)
fmt.Println(event.Name, event.Timestamp) // 输出:Webinar 2020-05-01 14:53:00 +0000 UTC
}
如果使用 GoLand
编辑器,向编辑区粘贴 JSON 格式内容,会提示生成对应结构体定义。
生成
序列化函数有 Marshal
和 MarshalIndent
,后者能生成美化后的格式:
package main
import (
"encoding/json"
"fmt"
"os"
)
// 先定义结构体类型,包含 JSON 标签
type User struct {
Name string `json:"name"`
}
func main() {
// 转换结构体
data, err := json.Marshal(User{"张三"})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(data)) // 输出:{"name":"张三"}
// 转换映射,键必须为字符串
data, _ = json.Marshal(map[string]string{"name": "李四"})
fmt.Println(string(data)) // 输出:{"name":"李四"}
// 转换结构体数组,使用 MarshalIndent 让最终结果好看一点
data, _ = json.MarshalIndent([]User{{"张三"}, {"李四"}}, "", " ")
fmt.Println(string(data)) // 输出:[{"name":"张三"},{"name":"李四"}]
// 结果为字节切片,可以直接写入到文件
file, _ := os.Create("data.json")
file.Write(data)
file.Close()
}
虽然可以将基础类型数据转为 JSON 格式,但是没有实用价值。此外,转换 JSON 格式不支持的数据类型(例如接口和通道)会报错。
解析
反序列化使用对应函数 Unmarshal
。一般先定义结构体,通过标签将字段名对应到 JSON 键名,然后传入结构体实例指针到函数,接收转换结果:
package main
import (
"encoding/json"
"fmt"
"os"
)
// 定义一个结构体,Port 可以是 int 也可以是 string
type Server struct {
Host string `json:"host"`
Port any `json:"port"`
}
func main() {
// JSON 字符串
jsonData := `
[
{
"name": "backend",
"host": "127.0.0.1",
"port": 1080
},
{
"name": "frontend",
"host": "127.0.0.2",
"port": "80"
}
]`
// 解析 JSON 到结构体切片,其中 name 字段不需要,直接忽略
var servers []Server
err := json.Unmarshal([]byte(jsonData), &servers) // 输出目标必须为实例指针
if err != nil {
fmt.Println("Error parsing JSON:", err)
}
// 打印结果,展示 port 字段类型多样性
for _, server := range servers {
fmt.Printf("Host: %s, Port: %v (%T)\n", server.Host, server.Port, server.Port)
}
// 从文件读取 JSON 内容不需要转为字节切片,直接使用
bytes, _ := os.ReadFile("data.json")
json.Unmarshal(bytes, &servers) // 存到同一个目标对象,会覆盖原数据
fmt.Printf("Verified: %+#v\n", servers)
}
当 JSON 数据是从一个流(如文件或网络)中获取时,可以直接使用 NewDecoder
配合 Decoder
函数来解码:
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
)
// User 结构体只取两个字段
type User struct {
Login string `json:"login"`
Id int `json:"id"`
}
func main() {
// 从网络获取数据
var user User
r, _ := http.Get("https://api.github.com/users/hxz393")
defer r.Body.Close()
json.NewDecoder(r.Body).Decode(&user) // 直接解码响应体后赋值给 user
fmt.Printf("%+#v\n", user) // 输出:main.User{Login:"hxz393", Id:5063578}
// 从文件获取数据
var users []User
file, _ := os.Open("data.json")
defer file.Close()
json.NewDecoder(file).Decode(&users) // 同样用法
fmt.Printf("%+#v\n", users) // 输出:[]main.User{main.User{Login:"张三", Id:0}, main.User{Login:"李四", Id:0}}
}
标签选项
在序列化和反序列化时,可以通过标签选项来控制字段转换行为:
-
:当标签值为-
时,无论是序列化还是反序列化,该字段都会被忽略。omitempty
:在序列化时,如果字段值是类型零值,则不生成到 JSON 数据中。string
:在序列化时,将字段值转为 JSON 中的字符串。
此外要注意大小写规则:
- 使用
json.Unmarshal
反序列化时,标签值匹配不区分字段大小写,例如json:"login"
可以匹配到 JSON 数据中Login
、login
或LOGIN
等字段。 - 非导出字段数据序列化时,不会生成到 JSON 数据中,也就是小写字母开头的字段,转换时会被忽略。而非导出字段也不能储存反序列化结果,会导致报错。
下面是演示代码:
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"Name"` // 必须处理字段,故意使用首字母大写
Age int `json:"age,omitempty,string"` // 如果为 0,则在序列化时忽略
City string `json:"-"` // 强制忽略字段
id int `json:"id"` // 不能转换字段
}
func main() {
// 反序列化。标签中的 Name 匹配到了 JSON 中 name 字段。city 字段值被忽略
jsonStr := `{"name":"Alice", "age":30, "city":"New York"}`
var p Person
json.Unmarshal([]byte(jsonStr), &p)
fmt.Printf("%+v\n", p) // 输出:{Name:Alice Age:0 City: id:0}
// 序列化。Age 和 City 字段被忽略,不会出现在结果 JSON 中
p = Person{Name: "", Age: 0, City: "Los Angeles", id: 011}
jsonData, _ := json.Marshal(p)
fmt.Println(string(jsonData)) // 输出:{"Name":""}
}
第三方库
内置库使用反射来实现解析,可以试试不用反射的第三方包 github.com/valyala/fastjson
,性能更好:
package main
import (
"encoding/json"
"testing"
"github.com/valyala/fastjson"
)
// 测试结构体
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Country string `json:"country"`
}
// 测试数据
var jsonData = `{"name":"John Doe","age":30,"country":"USA"}`
// 使用 encoding/json 解析
func BenchmarkEncodingJSON(b *testing.B) {
var p Person
for i := 0; i < b.N; i++ {
if err := json.Unmarshal([]byte(jsonData), &p); err != nil {
b.Fatal(err)
}
}
}
// 使用 fastjson 解析
func BenchmarkFastJSON(b *testing.B) {
var p fastjson.Parser
for i := 0; i < b.N; i++ {
v, err := p.Parse(jsonData)
if err != nil {
b.Fatal(err)
}
_ = v.GetStringBytes("name")
_ = v.GetInt("age")
_ = v.GetStringBytes("country")
}
}
基准测试结果如下:
goos: windows
goarch: amd64
pkg: new
cpu: AMD Ryzen Threadripper 2990WX 32-Core Processor
BenchmarkEncodingJSON
BenchmarkEncodingJSON-64 639985 1677 ns/op
BenchmarkFastJSON
BenchmarkFastJSON-64 5361942 226.0 ns/op
PASS
XML
XML(Extensible Markup Language)叫可扩展标记语言,是一种基于文本的结构化标记语言,过去广泛用于 Windows 平台应用配置。Go 语言内置 encoding/xml
库提供支持。
基本结构
XML 和 HTML 语言很相似,但 XML 允许用户自定义元素标签和文档结构:
- 声明(Prolog):在文档最开头声明 XML 版本和文件编码。例如:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
。 - 标签(Tags):XML 数据用标签包围,所有元素都必须有闭合标签或自闭合标签。例如:
<Tracker>Peer Exchange</Tracker>
。 - 属性(Attributes):标签内可以包含属性,用来提供更多关于 XML 元素的信息。例如:
<BitFieldStatus TotalLength="75312677588" PieceLength="16777216">
。 - 嵌套:标签内可以嵌套其他标签数据。例如:
<TrackerList><Tracker>Peer Exchange</Tracker></TrackerList>
。
和 JSON 不同,XML 中数据均以字符串形式保存。
生成
序列化时通过标签选项 attr
来设置标签属性:
package main
import (
"encoding/xml"
"os"
)
type Person struct {
Name string `xml:"name"`
Age int `xml:"age,attr"` // 序列化为 Person 标签属性
City string `xml:"city"`
}
func main() {
p := Person{Name: "Aku", Age: 30, City: "New York"}
// 带缩进格式序列化,指定前缀为空,省略错误处理
output, _ := xml.MarshalIndent(p, "", "\t")
os.Stdout.Write(output)
}
解析
反序列化是将 XML 数据转回 Go 数据结构,XML 文档中第一行声明会被自动忽略,不用特殊处理:
package main
import (
"encoding/xml"
"fmt"
"os"
)
type Person struct {
XMLName xml.Name `xml:"person"`
Name string `xml:"name"`
Age int `xml:"age"`
}
func main() {
// 支持从文件直接读取内容
data, _ := os.ReadFile("data.xml")
data = []byte(`<person><name>Aku</name><age>30</age></person>`)
// 输出结果:main.Person{XMLName:xml.Name{Space:"", Local:"person"}, Name:"Aku", Age:30}
var p Person
xml.Unmarshal(data, &p)
fmt.Printf("%+#v", p)
}
CSV
CSV(Comma-Separated Values)全名叫逗号分隔值,是一种用于存储表格数据的文件格式,常见于数据库和表格数据导入、导出和处理。Go 语言由内置库 encoding/csv
提供支持。
基本格式
CSV 文件数据结构是表格式的,有表头、行和列概念:
- 行:每行对应一条数据记录。第一行可以是标题行(表头),记录每个字段名。
- 分隔符:每条记录中,字段值之间用分隔符隔开。默认是逗号,也可以自定义分隔符,常用制表符和分号。
- 字段:字段使用双引号包围,支持转义字符。每条记录字段值数量必须一致,否则会解析出错。
CSV 采用纯文本格式储存数据,所有数据都是字符串类型。
解析
由于 CSV 是扁平数据结构,没有层级或嵌套关系,因此处理 CSV 文件和处理文本文件一样,可以直接读取得到二维切片:
package main
import (
"encoding/csv"
"fmt"
"io"
"os"
)
func main() {
// 打开 CSV 文件
file, _ := os.Open("data.csv")
defer file.Close()
// 创建 CSV 读取器
reader := csv.NewReader(file)
reader.Comma = ',' // 如果不是逗号作为分隔符,需要单独配置
// 读取所有数据到二维切片
data, _ := reader.ReadAll()
fmt.Printf("%+#v", data)
// 逐行读取方式
for {
record, err := reader.Read()
if err != nil {
if err == io.EOF { // 文件读取完成
break
}
}
fmt.Println(record)
}
// 带表头行,可以用映射切片来保存
headers := data[0]
var records []map[string]string
for _, row := range data[1:] { // 跳过表头行
record := make(map[string]string)
for i, value := range row {
record[headers[i]] = value
}
records = append(records, record)
}
fmt.Printf("%+#v", records)
for _, record := range records {
fmt.Println(record)
}
}
大型 CSV 文件应使用 Read
循环逐行读取,以避免内存溢出。
生成
生成 CSV 文件用到 csv.NewWriter
函数,并通过 writer.Comma
来设置分隔符:
package main
import (
"encoding/csv"
"os"
)
func main() {
// 原数据格式需要是二维切片
records := [][]string{
{"Name", "City", "Age"},
{"Alice", "New York", "30"},
{"Bob", "Los Angeles", "25"},
}
file, _ := os.Create("data.csv")
defer file.Close()
writer := csv.NewWriter(file)
writer.Comma = ',' // 可自定义分隔符为别的符号
defer writer.Flush()
// 循环写入到文件
for _, record := range records {
if err := writer.Write(record); err != nil {
panic(err)
}
}
}
YAML
YAML(YAML Ain’t Markup Language)格式储存数据方式类似 JSON,只是在书写格式上采用 Python 式缩进。Go 语言中需要用第三方库来处理 YAML,常用的是 gopkg.in/yaml/v3
。
基本语法
YAML 中用标量指代基本数据类型,如字符串、整数和浮点数:
- 标量:单个不可分割的值。可以是单行或多行文本。
- 列表:一系列有序排列值。列表元素前使用短横线
-
标记。 - 映射:键值对集合。使用冒号
:
分隔键和值。 - 注释:支持单行注释。注释行以井号
#
开始。 - 多文档:一个文件可以包含多个文档。使用
---
分隔符分隔多个文档。
下面是一个标准 YAML 文件内容:
name: Alice
skills:
- Python
- Golang
---
spring:
application:
name: order
servlet:
multipart:
max-file-size: 50MB
max-request-size: 100MB
mvc:
async:
request-timeout: 300000
sleuth:
enabled: true
server:
tomcat:
relaxed-query-chars: "[,]"
注意每个层次之间有两个空格缩进,空格数不正确会导致解析失败。
解析
YAML 反序列化时,甚至不需要依赖结构体标签,第三方库做了非常多适配处理:
package main
import (
"fmt"
"os"
"gopkg.in/yaml.v3" // 前面加空行来分组
)
// Config 结构体只取 spring 段
type Config struct {
Spring struct {
Application struct {
Name string `yaml:"name"`
} `yaml:"application"` // 结构体名相同时(不分大小写),标签可省
Servlet struct {
Multipart struct {
MaxSizeBytes string `yaml:"max-file-size"`
MaxRequestSize string `yaml:"max-request-size"`
}
}
}
}
// YAML 格式原生字符串
var data = `
spring:
application:
name: order
servlet:
multipart:
max-file-size: 50MB
max-request-size: 100MB
server:
tomcat:
relaxed-query-chars: "[,]"
`
func main() {
// 解析 YAML 到结构体
var config Config
yaml.Unmarshal([]byte(data), &config)
fmt.Printf("%+v\n", config)
// 从文件读取,直接使用流式处理
var configGo Config
file, _ := os.Open("data.yaml")
defer file.Close()
yaml.NewDecoder(file).Decode(&configGo)
fmt.Printf("%+v\n", configGo)
}
生成
序列化时,如果结构体没有标签,YAML 键名沿用小写结构体字段名。默认情况下生成的 YAML 就是标准格式,没有也不需要 MarshalIndent
函数,但可自定义层级缩进量:
package main
import (
"fmt"
"gopkg.in/yaml.v3"
"os"
)
type Config struct {
Spring struct {
Application struct {
Name string `yaml:"name"`
}
Servlet struct {
Multipart struct {
MaxSizeBytes string // 没有标签,自动生成键 maxsizebytes
MaxRequestSize []int `yaml:"max-request-size"`
}
}
}
}
func main() {
// 正常结构体示例,保存配置信息
var config Config
config.Spring.Application.Name = "pay"
config.Spring.Servlet.Multipart.MaxRequestSize = []int{50, 100}
// 从结构体生成 YAML 格式,默认缩进 4 个空格
data, _ := yaml.Marshal(&config)
fmt.Printf("%s\n", data)
// 使用流式处理写入到文件
file, _ := os.Create("config.yaml")
defer file.Close()
encoder := yaml.NewEncoder(file)
encoder.SetIndent(2) // 调整设置缩进为 2 个空格
encoder.Encode(config)
}
TOML
TOML(Tom’s Obvious, Minimal Language)是一种新近的格式,在 Rust 语言中作为配置用得比较多。TOML 在语法上类似传统的 INI 配置文件,但是支持更多数据类型。在 Go 语言中处理 TOML 文件常用 github.com/BurntSushi/toml
库和 github.com/pelletier/go-toml/v2
库。
基本语法
这里不详细说明 TOML 格式语句,仅简单介绍:
- 键值对:内容最基本组成部分,用等号
=
分隔键和值。 - 值类型:基本类型有字符串(支持多行和原生字符串)、数值、布尔值和时间(ISO 8601 格式)。此外还支持数组,数组用方括号
[]
括起相同基本类型元素,元素间用逗号,
分隔。 - 表:使用方括号
[]
括起表名,表示表的开始,支持嵌套。还有一种用双方括号[[]]
括起来的数组表。 - 注释:支持用井号
#
开头的注释。 - 点分键:用于在一个表中定义层嵌套表结构。
- 内联表:使用花括号
{}
。例如下面来自官网的示例:
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00Z类型
[database]
server = "192.168.1.1"
ports = [8001, 8001, 8002]
connection_max = 5000
enabled = true
解析
TOML 格式反序列化没有特别之处,库 github.com/BurntSushi/toml
提供从文件直接解析功能:
package main
import (
"fmt"
"os"
"time"
tomlB "github.com/BurntSushi/toml"
"github.com/pelletier/go-toml/v2"
)
// 内嵌结构体最好单独定义,不要使用匿名嵌套
type Config struct {
Title string
Owner OwnerInfo
Database DatabaseInfo
}
type OwnerInfo struct {
Name string
Dob time.Time
}
type DatabaseInfo struct {
Server string
Ports []int
ConnectionMax int `toml:"connection_max"`
Enabled bool
}
func main() {
var config, configB Config
// 先读取文件内容再解析
data, _ := os.ReadFile("config.toml")
toml.Unmarshal(data, &config)
// 直接从文件解析
tomlB.DecodeFile("config.toml", &configB)
// 输出结果一样
fmt.Printf("%+v\n", config)
fmt.Printf("%+v\n", configB)
}
生成
这里用一个最简配置来演示序列化:
package main
import (
"os"
tomlB "github.com/BurntSushi/toml"
"github.com/pelletier/go-toml/v2"
)
func main() {
config := struct{ Title string }{"TOML Example"}
// 序列化后写入
data, _ := toml.Marshal(config)
os.WriteFile("data.toml", data, 0644)
// 调用 NewEncoder 方法写入,实际上两个库用法一样
file, _ := os.Create("data.toml")
tomlB.NewEncoder(file).Encode(config)
}