总览
JSON是最流行的序列化格式之一。 它是人类可读的,相当简洁的,任何Web应用程序都可以使用JavaScript轻松地对其进行解析。 Go作为一种现代编程语言,在其标准库中对JSON序列化提供了一流的支持。
但是这里有些角落和缝隙。 在本教程中,您将学习如何有效地对JSON以及从JSON序列化和反序列化任意数据和结构化数据。 您还将学习如何处理高级场景,例如序列化枚举。
json包
Go在其标准库的编码包中支持多种序列化格式。 其中之一是流行的JSON格式。 您可以使用Marshal()函数将Golang值序列化为一个字节片。 您可以使用Unmarshal()函数将字节的片段反序列化为Golang值。 就这么简单。 在本文的上下文中,以下术语是等效的:
- 序列化/编码/编组
- 反序列化/解码/解组
我更喜欢序列化,因为它反映了以下事实:您将潜在的分层数据结构与字节流进行了相互转换。
元帅
Marshal()函数可以接受任何内容,这在Go中表示空接口,并返回一片字节和错误。 这是签名:
func Marshal(v interface{}) ([]byte, error)
如果Marshal()无法序列化输入值,它将返回非nil错误。 Marshal()有一些严格的限制(我们将在后面看到如何使用自定义编组器克服它们):
- 映射键必须是字符串。
- 映射值必须是json包可序列化的类型。
- 不支持以下类型:通道,复杂和功能。
- 不支持循环数据结构。
- 指针将被编码(然后解码)为指针所指向的值(如果指针为nil,则为“ null”)。
元帅
Unmarshal()函数采用一个字节片,该字节片有望表示有效的JSON和目标接口,该接口通常是指向结构或基本类型的指针。 它以通用方式将JSON反序列化到接口中。 如果序列化失败,它将返回错误。 这是签名:
func Unmarshal(data []byte, v interface{}) error
序列化简单类型
您可以轻松地序列化简单类型,例如使用json包。 结果将不是完整的JSON对象,而是一个简单的字符串。 在这里,int 5被序列化为字节数组[53],它对应于字符串“ 5”。
// Serialize int
var x = 5
bytes, err := json.Marshal(x)
if err != nil {
fmt.Println("Can't serislize", x)
}
fmt.Printf("%v => %v, '%v'\n", x, bytes, string(bytes))
// Deserialize int
var r int
err = json.Unmarshal(bytes, &r)
if err != nil {
fmt.Println("Can't deserislize", bytes)
}
fmt.Printf("%v => %v\n", bytes, r)
Output:
- 5 => [53], '5'
- [53] => 5
如果尝试序列化不支持的类型(如函数),则会收到错误消息:
// Trying to serialize a function
foo := func() {
fmt.Println("foo() here")
}
bytes, err = json.Marshal(foo)
if err != nil {
fmt.Println(err)
}
Output:
json: unsupported type: func()
使用地图序列化任意数据
JSON的强大之处在于它可以很好地表示任意分层数据。 JSON包支持它,并利用通用的空接口(interface {})表示任何JSON层次结构。 这是反序列化并随后序列化二叉树的示例,其中每个节点都有一个int值以及左右两个分支,其中可能包含另一个节点或为null。
JSON空值等于Go nil。 从输出中可以看到, json.Unmarshal()
函数成功地将JSON blob转换为Go数据结构,该数据结构由嵌套的接口映射组成,并将值类型保留为int。 json.Marshal()
函数成功地将所得嵌套对象序列化为相同的JSON表示形式。
// Arbitrary nested JSON
dd := `
{
"value": 3,
"left": {
"value": 1,
"left": null,
"right": {
"value": 2,
"left": null,
"right": null
}
},
"right": {
"value": 4,
"left": null,
"right": null
}
}`
var obj interface{}
err = json.Unmarshal([]byte(dd), &obj)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("--------\n", obj)
}
data, err = json.Marshal(obj)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("--------\n", string(data))
}
}
Output:
--------
map[right:map[value:4
left:<nil>
right:<nil>]
value:3
left:map[left:<nil>
right:map[value:2
left:<nil>
right:<nil>]
value:1]]
--------
{"left":{
"left":null,
"right":{"left":null,"right":null,"value":2},
"value":1},
"right":{"left":null,
"right":null,
"value":4},
"value":3}
要遍历接口的通用映射,您需要使用类型断言。 例如:
func dump(obj interface{}) error {
if obj == nil {
fmt.Println("nil")
return nil
}
switch obj.(type) {
case bool:
fmt.Println(obj.(bool))
case int:
fmt.Println(obj.(int))
case float64:
fmt.Println(obj.(float64))
case string:
fmt.Println(obj.(string))
case map[string]interface{}:
for k, v := range(obj.(map[string]interface{})) {
fmt.Printf("%s: ", k)
err := dump(v)
if err != nil {
return err
}
}
default:
return errors.New(
fmt.Sprintf("Unsupported type: %v", obj))
}
return nil
}
序列化结构化数据
使用结构化数据通常是更好的选择。 围棋提供了从JSON序列化到/极好的支持structs
通过其struct
的标签。 让我们创建一个与我们的JSON树相对应的struct
,以及一个更聪明的Dump()
函数来打印它:
type Tree struct {
value int
left *Tree
right *Tree
}
func (t *Tree) Dump(indent string) {
fmt.Println(indent "value:", t.value)
fmt.Print(indent "left: ")
if t.left == nil {
fmt.Println(nil)
} else {
fmt.Println()
t.left.Dump(indent " ")
}
fmt.Print(indent "right: ")
if t.right == nil {
fmt.Println(nil)
} else {
fmt.Println()
t.right.Dump(indent " ")
}
}
与任意JSON方法相比,这很棒而且更干净。 但这有效吗? 并不是的。 没有错误,但JSON并未填充我们的树对象。
jsonTree := `
{
"value": 3,
"left": {
"value": 1,
"left": null,
"right": {
"value": 2,
"left": null,
"right": null
}
},
"right": {
"value": 4,
"left": null,
"right": null
}
}`
var tree Tree
err = json.Unmarshal([]byte(dd), &tree)
if err != nil {
fmt.Printf("- Can't deserislize tree, error: %v\n", err)
} else {
tree.Dump("")
}
Output:
value: 0
left: <nil>
right: <nil>
问题在于“树”字段是私有的。 JSON序列化仅适用于公共字段。 因此,我们可以将struct
字段公开。 json包足够聪明,可以透明地将小写键“ value”,“ left”和“ right”转换为它们对应的大写字段名称。
type Tree struct {
Value int `json:"value"`
Left *Tree `json:"left"`
Right *Tree `json:"right"`
}
Output:
value: 3
left:
value: 1
left: <nil>
right:
value: 2
left: <nil>
right: <nil>
right:
value: 4
left: <nil>
right: <nil>
json包将静默忽略JSON中的未映射字段以及struct
私有字段。 但是有时您可能想要将JSON中的特定键映射到struct
具有不同名称的字段。 您可以为此使用struct
标记。 例如,假设我们在JSON中添加了另一个名为“标签”的字段,但是我们需要在结构中将其映射到名为“ Tag”的字段。
type Tree struct {
Value int
Tag string `json:"label"`
Left *Tree
Right *Tree
}
func (t *Tree) Dump(indent string) {
fmt.Println(indent "value:", t.Value)
if t.Tag != "" {
fmt.Println(indent "tag:", t.Tag)
}
fmt.Print(indent "left: ")
if t.Left == nil {
fmt.Println(nil)
} else {
fmt.Println()
t.Left.Dump(indent " ")
}
fmt.Print(indent "right: ")
if t.Right == nil {
fmt.Println(nil)
} else {
fmt.Println()
t.Right.Dump(indent " ")
}
}
这是新的JSON,树的根节点标记为“ root”,已正确序列化到Tag字段中并打印在输出中:
dd := `
{
"label": "root",
"value": 3,
"left": {
"value": 1,
"left": null,
"right": {
"value": 2,
"left": null,
"right": null
}
},
"right": {
"value": 4,
"left": null,
"right": null
}
}`
var tree Tree
err = json.Unmarshal([]byte(dd), &tree)
if err != nil {
fmt.Printf("- Can't deserislize tree, error: %v\n", err)
} else {
tree.Dump("")
}
Output:
value: 3
tag: root
left:
value: 1
left: <nil>
right:
value: 2
left: <nil>
right: <nil>
right:
value: 4
left: <nil>
right: <nil>
编写自定义编组器
您将经常需要序列化不符合Marshal()函数严格要求的对象。 例如,您可能想使用int键序列化映射。 在这些情况下,您可以通过实现Marshaler
和Unmarshaler
接口来编写自定义编组器/解Unmarshaler
器。
关于拼写的注意事项:在Go中,约定是通过在方法名称后附加“ er”后缀来用单个方法命名接口。 因此,即使更常见的拼写是“ Marshaller”(双L),接口名称也只是“ Marshaler”(单L)。
以下是Marshaler和Unmarshaler接口:
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
即使要序列化内置类型或内置类型的组合(例如map[int]string
,也必须在进行自定义序列化时创建类型。 在这里,我定义一个名为类型IntStringMap
并实现Marshaler
和Unmarshaler
这种类型的接口。
MarshalJSON()
方法创建一个map[string]string
,将其自身的int密钥转换为字符串,并使用标准的json.Marshal()
函数使用字符串密钥将地图序列化。
type IntStringMap map[int]string
func (m *IntStringMap) MarshalJSON() ([]byte, error) {
ss := map[string]string{}
for k, v := range *m {
i := strconv.Itoa(k)
ss[i] = v
}
return json.Marshal(ss)
}
UnmarshalJSON()方法的作用恰恰相反。 它将数据字节数组反序列化为map[string]string
,然后将每个字符串键转换为int并进行填充。
func (m *IntStringMap) UnmarshalJSON(data []byte ) error {
ss := map[string]string{}
err := json.Unmarshal(data, &ss)
if err != nil {
return err
}
for k, v := range ss {
i, err := strconv.Atoi(k)
if err != nil {
return err
}
(*m)[i] = v
}
return nil
}
这是在程序中使用它的方法:
m := IntStringMap{4: "four", 5: "five"}
data, err := m.MarshalJSON()
if err != nil {
fmt.Println(err)
}
fmt.Println("IntStringMap to JSON: ", string(data))
m = IntStringMap{}
jsonString := []byte("{\"1\": \"one\", \"2\": \"two\"}")
m.UnmarshalJSON(jsonString)
fmt.Printf("IntStringMap from JSON: %v\n", m)
fmt.Println("m[1]:", m[1], "m[2]:", m[2])
Output:
IntStringMap to JSON: {"4":"four","5":"five"}
IntStringMap from JSON: map[2:two 1:one]
m[1]: one m[2]: two
序列化枚举
Go枚举序列化可能非常麻烦。 撰写有关Go json序列化的文章的想法来自一个同事问我的有关如何序列化枚举的问题。 这是一个Go enum
。 常数零和一等于整数0和1。
type EnumType int
const (
Zero EnumType = iota
One
)
尽管您可能认为它是一个int,并且在很多方面都可以,但是您不能直接对其进行序列化。 您必须编写一个自定义封送拆收器/拆封拆收器。 在上一节之后,这不是问题。 下面的MarshalJSON()
和UnmarshalJSON()
会将常量ZERO和ONE序列化/反序列化到相应字符串“ Zero”和“ One” /中。
func (e *EnumType) UnmarshalJSON(data []byte) error {
var s string
err := json.Unmarshal(data, &s)
if err != nil {
return err
}
value, ok := map[string]EnumType{"Zero": Zero, "One": One}[s]
if !ok {
return errors.New("Invalid EnumType value")
}
*e = value
return nil
}
func (e *EnumType) MarshalJSON() ([]byte, error) {
value, ok := map[EnumType]string{Zero: "Zero", One:"One"}[*e]
if !ok {
return nil, errors.New("Invalid EnumType value")
}
return json.Marshal(value)
}
让我们尝试将此EnumType
嵌入struct
并对其进行序列化。 main函数创建一个EnumContainer
并将其初始化为名称“ Uno”和enum
常量ONE
的值,该值等于int 1。
type EnumContainer struct {
Name string
Value EnumType
}
func main() {
x := One
ec := EnumContainer{
"Uno",
x,
}
s, err := json.Marshal(ec)
if err != nil {
fmt.Printf("fail!")
}
var ec2 EnumContainer
err = json.Unmarshal(s, &ec2)
fmt.Println(ec2.Name, ":", ec2.Value)
}
Output:
Uno : 0
预期的输出是“ Uno:1”,但它是“ Uno:0”。 发生了什么? 编组/解组代码中没有错误。 事实证明,如果要序列化枚举,则不能按值嵌入枚举。 您必须嵌入一个指向枚举的指针。 这是可以按预期工作的修改版本:
type EnumContainer struct {
Name string
Value *EnumType
}
func main() {
x := One
ec := EnumContainer{
"Uno",
&x,
}
s, err := json.Marshal(ec)
if err != nil {
fmt.Printf("fail!")
}
var ec2 EnumContainer
err = json.Unmarshal(s, &ec2)
fmt.Println(ec2.Name, ":", *ec2.Value)
}
Output:
Uno : 1
结论
Go提供了许多用于序列化和反序列化JSON的选项。 了解编码/ json包的来龙去脉以利用其功能很重要。
本教程掌握了所有功能,包括如何序列化难以捉摸的Go枚举。
去序列化一些对象!
翻译自: https://code.tutsplus.com/tutorials/json-serialization-with-golang--cms-30209