如何在 Go 中使用 JSON

作者选择了 多样性在技术基金作为 写给捐款计划的一部分接受捐款。

介绍

在现代程序中,重要的是在一个程序和另一个程序之间进行通信,无论是 Go程序检查用户是否可以访问另一个程序,一个 JavaScript程序获得在网站上显示的过去订单列表,还是一个 [Rust](https://en.wikipedia.org/wiki/Rust_(programming_language])程序从文件中读取测试结果,程序需要一种方式来提供其他程序的数据。然而,许多编程语言有自己的内部存储方式,其他语言无法理解。

许多现代编程语言包括在其标准库中将数据转换为和从JSON的方法,Go也是如此。通过使用Go提供的 coding/json包,您的Go程序也将能够与任何其他可以使用JSON进行通信的系统进行交互。

在本教程中,您将开始创建一个使用编码/json包来编码数据从地图到JSON数据的程序,然后更新您的程序以使用结构类型来编码数据,然后,您将更新您的程序来解码JSON数据到地图,然后最终将JSON数据解码为结构类型。

前提条件

要遵循本教程,您将需要:

使用地图生成 JSON

Go 对 JSON 编码和解码的支持是由标准库的编码/json包提供的。您从该包中使用的第一个函数是 json.Marshal]函数。 Marshalling,有时也被称为 serialization,是将记忆中的程序数据转换为可以传输或存储到其他地方的格式的过程。 然后,json.Marshal``函数被用来将 Go 数据转换为 JSON 数据。 json.Marshal``函数接受一个界面类型作为将 marshal 值转移到 JSON,因此任何允许作为参数传输的值将作为结果返回 JSON 数据。 在

大多数 JSON 被表示为一个对象,具有字符串密钥和各种其他类型的值,因此,在 Go 中生成 JSON 数据的最灵活的方式是使用字符串密钥和接口值将数据放入地图

要开始使用一个程序中的coding/json包,你需要有一个程序目录. 在本教程中,你将使用一个名为项目的目录。

首先,创建项目目录并导航到它:

1mkdir projects
2cd projects

接下来,为您的项目创建目录. 在这种情况下,使用目录 jsondata:

1mkdir jsondata
2cd jsondata

jsondata目录中使用nano或您最喜欢的编辑器来打开main.go文件:

1nano main.go

main.go文件中,您将添加一个main函数来运行您的程序。接下来,您将添加一个map[string]interface{}值,包含各种密钥和数据类型。

将下列行添加到 main.go:

 1[label main.go]
 2package main
 3
 4import (
 5    "encoding/json"
 6    "fmt"
 7)
 8
 9func main() {
10    data := map[string]interface{}{
11    	"intValue":    1234,
12    	"boolValue":   true,
13    	"stringValue": "hello!",
14    	"objectValue": map[string]interface{}{
15    		"arrayValue": []int{1, 2, 3, 4},
16    	},
17    }
18
19    jsonData, err := json.Marshal(data)
20    if err != nil {
21    	fmt.Printf("could not marshal json: %s\n", err)
22    	return
23    }
24
25    fmt.Printf("json data: %s\n", jsonData)
26}

数据变量中,你会看到每个值都有一个字符串作为密钥,但这些密钥的值有所不同. 一个是int值,另一个是bool值,一个甚至是另一个map[string]interface{}值,里面有[]int值。

当您将数据变量传输到json.Marshal时,该函数将查看您提供的所有值,并确定它们的类型以及如何在 JSON 中表示它们。如果翻译中出现任何问题,则json.Marshal函数将返回描述问题的错误。如果翻译成功,则jsonData变量将包含 marshalled JSON 数据的[]byte。由于一个[]byte值可以使用myString := string(jsonData)或一个格式字符串中的%sverb [[LINK]转换为string值,您可以使用fmt.Printf打印到屏幕上的 JSON 数据。

保存并关闭文件。

要查看您的程序的输出,请使用go run命令并提供main.go文件:

1go run main.go

你的输出将看起来像这样:

1[secondary_label Output]
2json data: {"boolValue":true,"intValue":1234,"objectValue":{"arrayValue":[1,2,3,4]},"stringValue":"hello!"}

在输出中,你会看到上层的 JSON 值是由围绕它的弯曲的轴承({})表示的对象.你在 data 中包含的所有值都存在.你还会看到 objectValuemap[string] interface{} 被翻译成另一个 JSON 对象,周围是 }{,并且还包含了 arrayValue' 与 [1,2,3,4] 的数组值。

在 JSON 中编码时间

虽然编码/json包不仅支持类型如stringint值,它还可以编码更复杂的类型,它支持的更复杂的类型之一是从 time包的time.Time类型。

<$>[注] **注:**有关Go的时间包的更多信息,请参阅如何在Go中使用日期和时间(https://andsky.com/tech/tutorials/how-to-use-dates-and-times-in-go)。

若要看到此情况,请再次打开您的 main.go 文件,并使用time.Date函数将time.Time值添加到您的数据中:

 1[label main.go]
 2package main
 3
 4import (
 5    "encoding/json"
 6    "fmt"
 7    "time"
 8)
 9
10func main() {
11    data := map[string]interface{}{
12    	"intValue":    1234,
13    	"boolValue":   true,
14    	"stringValue": "hello!",
15    	"dateValue":   time.Date(2022, 3, 2, 9, 10, 0, 0, time.UTC),
16    	"objectValue": map[string]interface{}{
17    		"arrayValue": []int{1, 2, 3, 4},
18    	},
19    }
20
21    ...
22}

此更新将将UTC时区中的2022年3月2日日期和9:10:00 AM时间分配给dateValue键。

一旦您保存了更改,请使用与以前相同的运行命令再次运行您的程序:

1go run main.go

你的输出将看起来像这样:

1[secondary_label Output]
2json data: {"boolValue":true,"dateValue":"2022-03-02T09:10:00Z","intValue":1234,"objectValue":{"arrayValue":[1,2,3,4]},"stringValue":"hello!"}

此时,在输出中,您将在 JSON 数据中看到一个dateValue字段,使用使用 RFC 3339格式格式格式格式格式,该格式用于将日期和时间作为字符串值传输。

在 JSON 中编码null

根据你的程序与哪些系统互动,你可能需要在你的JSON数据中发送null值,Go的编码/json包也可以为你处理。

要在 JSON 输出中添加几个null值,请重新打开您的main.go文件并添加以下行:

 1[label main.go]
 2...
 3
 4func main() {
 5    data := map[string]interface{}{
 6    	"intValue":    1234,
 7    	"boolValue":   true,
 8    	"stringValue": "hello!",
 9                "dateValue":   time.Date(2022, 3, 2, 9, 10, 0, 0, time.UTC),
10    	"objectValue": map[string]interface{}{
11    		"arrayValue": []int{1, 2, 3, 4},
12    	},
13    	"nullStringValue": nil,
14    	"nullIntValue":    nil,
15    }
16
17    ...
18}

您添加到数据的值有关键字表示它是字符串值或int值,但实际上代码中没有任何一个值使其成为这些值。

一旦您将更改保存到main.go,请使用go run运行您的程序:

1go run main.go

你的输出将看起来像这样:

1[secondary_label Output]
2json data: {"boolValue":true,"dateValue":"2022-03-02T09:10:00Z","intValue":1234,"nullIntValue":null,"nullStringValue":null,"objectValue":{"arrayValue":[1,2,3,4]},"stringValue":"hello!"}

现在在输出中,你会看到nullIntValuenullStringValue字段包含一个JSONnull值,这样你仍然可以使用map[string]interface{}值将Go数据转换成预期字段的JSON数据。

在本节中,您创建了一个程序,可以将map[string]interface{}值转化为 JSON 数据,然后,您将一个time.Time字段添加到数据中,还包括了一对null值字段。

虽然使用map[string] interface{}来编辑 JSON 数据可以非常灵活,但如果您需要在多个地方发送相同的数据,也可以成为麻烦。如果您在代码中将这些数据复制到多个位置,则很容易意外误写一个字段名称,或者将错误的数据分配给一个字段。

使用结构生成 JSON

使用像 Go 这样的 静态键入语言的优点之一是,您可以使用这些类型来让编译器在您的程序中检查或执行一致性。 Go 的编码/json包允许您通过定义一个结构类型来代表 JSON 数据来利用此。

当你使用struct来定义 JSON 数据时,你所期望的字段名称(而不是struct类型名称本身)必须被导出,这意味着它们必须以大字母开始,例如IntValue,或者encoding/json包将无法访问字段来将它们翻译成 JSON。如果你不使用结构标签来控制这些字段的命名,则字段名称将被直接翻译,因为它们位于struct上。使用默认名称可能是你在 JSON 数据中想要的,取决于你希望你的数据如何被组成。如果是这样的情况,你不需要添加任何结构标签。然而,许多 JSON 消费者使用字段名称格式如intalueint_value,所以添加这些结构标签将允许

例如,假设您有一个名为IntValue结构,您将其转换为 JSON:

1type myInt struct {
2    IntValue int
3}
4
5data := &myInt{IntValue: 1234}

如果您使用json.Marshal函数将数据变量转换为 JSON,您将获得以下值:

1{"IntValue":1234}

但是,如果您的 JSON 用户预计该字段将被命名为intValue而不是IntValue,那么您将需要一个方法来说encoding/json。因为json.Marshal不知道您希望该字段在 JSON 数据中被命名为什么,您将通过将结构标签添加到该字段来告诉它。

1type myInt struct {
2    IntValue int `json:"intValue"`
3}
4
5data := &myInt{IntValue: 1234}

这一次,如果将数据变量转换为 JSON,则json.Marshal函数将看到json结构标签,并知道如何命名字段intValue,因此您将获得预期的结果:

1{"intValue":1234}

现在,你会更新你的程序以使用你的JSON数据的结构值。你会添加一个myJSON``结构类型来定义你的顶级JSON对象,以及一个myObject``结构来定义你的内部JSON对象为ObjectValue字段。

打开main.go文件,并进行以下更改:

 1[label main.go]
 2...
 3
 4type myJSON struct {
 5    IntValue int       `json:"intValue"`
 6    BoolValue bool      `json:"boolValue"`
 7    StringValue string    `json:"stringValue"`
 8    DateValue time.Time `json:"dateValue"`
 9    ObjectValue     *myObject `json:"objectValue"`
10    NullStringValue *string   `json:"nullStringValue"`
11    NullIntValue    *int      `json:"nullIntValue"`
12}
13
14type myObject struct {
15    ArrayValue []int `json:"arrayValue"`
16}
17
18func main() {
19    otherInt := 4321
20    data := &myJSON{
21    	IntValue:    1234,
22    	BoolValue:   true,
23    	StringValue: "hello!",
24    	DateValue:   time.Date(2022, 3, 2, 9, 10, 0, 0, time.UTC),
25    	ObjectValue: &myObject{
26    		ArrayValue: []int{1, 2, 3, 4},
27    	},
28    	NullStringValue: nil,
29    	NullIntValue:    &otherInt,
30    }
31
32    ...
33}

这些变化中的许多类似于以前的IntValue字段名称示例,但其中一些更改值得特别调用。其中一个是ObjectValue字段,它使用了*myObject的参考类型,告诉JSON马歇勒预期要么对myObject值进行引用,要么对null值进行引用。这就是你如何定义一个具有多层定制对象深度的JSON对象。如果你的JSON数据要求它,你也可以在myObject类型中引用另一个struct类型,等等。

在上面的代码中要查看的另一对字段是NullStringValueNullIntValue。 与StringValueIntValue不同,这些值类型是参考类型*string*int。 默认情况下,stringint类型不能有null值,因为它们的值是"0。 因此,如果您想要代表一个可以是单个类型或null的字段,则需要将其作为参考。

此代码还更新了NullIntValue字段,将值分配为4321,以显示您如何将值分配给一个参考类型,例如*int。在Go中,您只能使用变量创建对intstring等原始类型的引用,因此,为了将值分配给NullIntValue字段,您首先将该值分配给另一个变量,即otherInt,然后使用&otherInt(而不是直接做&4321)来引用该变量。

一旦您保存了更新,请使用去运行来运行您的程序:

1go run main.go

你的输出将看起来像这样:

1[secondary_label Output]
2json data: {"intValue":1234,"boolValue":true,"stringValue":"hello!","dateValue":"2022-03-02T09:10:00Z","objectValue":{"arrayValue":[1,2,3,4]},"nullStringValue":null,"nullIntValue":4321}

您将看到此输出与您使用map[string] interface{}值相同,但这次nullIntValue的值为4321,因为它是其他Int的值。

起初,设置结构值可能需要一些额外的时间,但一旦定义了它们,您可以在代码中反复使用它们,结果将是相同的,无论您在哪里使用它们。

Go’s JSON Marshaller 还允许您根据该值是否为空,来控制一个字段是否应包含在 JSON 输出中。有时,您可能有一个大型 JSON 对象或可选的字段,您不希望随时被包含,所以忽略这些字段可以是有用的。

现在,更新你的程序,使NullStringValue字段omitempty,并添加一个名为EmptyString的新字段,具有相同的选项:

 1[label main.go]
 2...
 3
 4type myJSON struct {
 5    ...
 6    
 7    NullStringValue *string   `json:"nullStringValue,omitempty"`
 8    NullIntValue    *int      `json:"nullIntValue"`
 9    EmptyString string    `json:"emptyString,omitempty"`
10}
11
12...

现在,当myJSON被调用时,如果它们的值是空的,则将从输出中排除EmptyStringNullStringValue字段。

保存更改后,使用去运行来运行您的程序:

1go run main.go

你的输出将看起来像这样:

1[secondary_label Output]
2json data: {"intValue":1234,"boolValue":true,"stringValue":"hello!","dateValue":"2022-03-02T09:10:00Z","objectValue":{"arrayValue":[1,2,3,4]},"nullIntValue":4321}

此时在输出中,你会看到nullStringValue字段不再显示。由于它被认为是空的,因为它具有null值,所以omitempty选项将其排除在输出中。

在本节中,您更新了您的程序以使用结构类型来生成 JSON 数据,使用json.Marshal而不是地图类型。

然而,为了让您的程序融入JSON生态系统,您需要做的不仅仅是生成JSON数据,您还需要能够读取被发送的JSON数据以响应您的请求,或其他系统向您发送请求。

JSON 使用地图

与本教程的第一个部分类似,您将使用map[string] interface{}作为生成 JSON 数据的灵活方式,您还可以使用它作为阅读 JSON 数据的灵活方式。 json.Unmarshal函数,本质上与json.Marshal函数相反,会将 JSON 数据转换回 Go 数据。您将提供json.Unmarshal与 JSON 数据以及 Go 变量,将未编辑的数据放入其中,如果无法做到这一点,它将返回一个错误值,或者如果成功了,将更新到nile错误值。

现在,更新您的程序以使用json.Unmarshal将 JSON 数据解析到map[string] interface{}。你将开始用包含 JSON 字符串的jsonData变量来替换原始的data变量,然后你将宣布新的data变量作为map[string] interface{}来接收 JSON 数据,最后,你将使用json.Unmarshal与这些变量来访问 JSON 数据。

打开main.go文件,并用以下字符代替main函数中的行:

 1[label main.go]
 2...
 3
 4func main() {
 5    jsonData := `
 6    	{
 7    		"intValue":1234,
 8    		"boolValue":true,
 9    		"stringValue":"hello!",
10    		"dateValue":"2022-03-02T09:10:00Z",
11    		"objectValue":{
12    			"arrayValue":[1,2,3,4]
13    		},
14    		"nullStringValue":null,
15    		"nullIntValue":null
16    	}
17    `
18
19    var data map[string]interface{}
20    err := json.Unmarshal([]byte(jsonData), &data)
21    if err != nil {
22    	fmt.Printf("could not unmarshal json: %s\n", err)
23    	return
24    }
25
26    fmt.Printf("json map: %v\n", data)
27}

在此更新中,将jsonData变量设置为使用 raw string literal以允许声明覆盖多个行以便更容易阅读。

jsonData变量被转换为json.Unmarshal作为一个[]byte,因为该函数需要一个[]byte类型,而jsonData最初被定义为一个字符串类型,这是因为Go中的字符串可以转换为[]byte,反之亦然。

最后,一旦 JSON 数据被解析成数据变量,您可以使用fmt.Printf将其打印到屏幕上。

要运行您的更新程序,保存您的更改,并使用运行来运行该程序:

1go run main.go

结果将看起来像这样:

1[secondary_label Output]
2json map: map[boolValue:true dateValue:2022-03-02T09:10:00Z intValue:1234 nullIntValue:<nil> nullStringValue:<nil> objectValue:map[arrayValue:[1 2 3 4]] stringValue:hello!]

这一次,你的输出显示了JSON翻译的Go侧。你有一个地图值,包括来自JSON数据的各种字段。

现在,因为您的 Go 数据位于map[string] interface{},需要使用数据做一些工作,您需要使用所需的string键值从map中获取值,然后您需要确保您收到的值是您期望的值,因为它作为interface{}值返回给您。

要做到这一点,打开main.go文件,并更新您的程序,以便使用以下代码读取dateValue字段:

 1[label main.go]
 2...
 3
 4func main() {
 5    ...
 6    
 7    fmt.Printf("json map: %v\n", data)
 8
 9    rawDateValue, ok := data["dateValue"]
10    if !ok {
11    	fmt.Printf("dateValue does not exist\n")
12    	return
13    }
14    dateValue, ok := rawDateValue.(string)
15    if !ok {
16    	fmt.Printf("dateValue is not a string\n")
17    	return
18    }
19    fmt.Printf("date value: %s\n", dateValue)
20}

在此更新中,您使用data[dateValue]rawDateValue作为界面{}类型,并使用OK变量确保dateValue字段位于地图中。

然后,您使用 类型声明来声称 rawDateValue' 类型实际上是一个 字符串' 值,然后将其分配给变量 `dateValue'。

最后,您可以使用「fmt.Printf」来打印「dateValue」。

若要再次运行更新后的程序,请保存您的更改并使用运行来运行它:

1go run main.go

你的输出将看起来像这样:

1[secondary_label Output]
2json map: map[boolValue:true dateValue:2022-03-02T09:10:00Z intValue:1234 nullIntValue:<nil> nullStringValue:<nil> objectValue:map[arrayValue:[1 2 3 4]] stringValue:hello!]
3date value: 2022-03-02T09:10:00Z

您可以看到日期值行,显示从地图中提取的dateValue字段,并转换为字符串值。

在本节中,您更新了您的程序,以使用json.Unmarshal函数与map[string] interface{}变量,以将 JSON 数据解析为 Go 数据。

然而,这项更新确实显示了使用map[string] interface{}在 Go 中解析 JSON 的缺点之一,因为Go 不知道每个字段的数据类型是什么(它知道的唯一事情是界面{}),它可以做的最好的事情是解析数据是做出最好的猜测。这意味着在dateValue字段中的time.Time等复杂值不能为您解析,并且只能作为string访问。如果您试图以这种方式访问map中的任何数字值,就会出现类似的问题。

虽然使用地图来解码 JSON 数据可以是灵活的,但在解读你所拥有的数据时,它也会为你留下更多的工作。类似于json.Marshal函数如何使用struct值来生成 JSON 数据,json.Unmarshal函数可以使用struct值来读取 JSON 数据。

JSON 使用结构

当您阅读 JSON 数据时,很可能您已经知道所接收的数据的结构;否则,这将是很难解释的。

在以前的部分中,您定义了myJSONmyObject结构值,并添加了json结构标签,以便让Go知道在生成JSON时如何命名字段。现在您可以使用相同的结构值来解码您所使用的JSON字符串,这可能有益于减少程序中的重复代码,如果您正在编辑和删除相同的JSON数据。 使用结构来解析JSON数据的另一个好处是您可以告诉Go每个字段的预期数据类型。

现在,打开您的main.go文件,更新data变量声明以使用myJSON``结构的引用,并添加几个fmt.Printf行以显示myJSON上的各种字段的数据:

 1[label main.go]
 2...
 3
 4func main() {
 5    ...
 6    
 7    var data *myJSON
 8    err := json.Unmarshal([]byte(jsonData), &data)
 9    if err != nil {
10    	fmt.Printf("could not unmarshal json: %s\n", err)
11    	return
12    }
13
14    fmt.Printf("json struct: %#v\n", data)
15    fmt.Printf("dateValue: %#v\n", data.DateValue)
16    fmt.Printf("objectValue: %#v\n", data.ObjectValue)
17}

由于您之前已经定义了结构类型,所以您只需要更新数据字段的类型,以支持结构的解散。

现在,保存您的更新并使用去运行来运行您的程序:

1go run main.go

你的输出将看起来像这样:

1[secondary_label Output]
2json struct: &main.myJSON{IntValue:1234, BoolValue:true, StringValue:"hello!", DateValue:time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC), ObjectValue:(*main.myObject)(0x1400011c180), NullStringValue:(*string)(nil), NullIntValue:(*int)(nil), EmptyString:""}
3dateValue: time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC)
4objectValue: &main.myObject{ArrayValue:[]int{1, 2, 3, 4}}

你会看到在json struct行和dateValue行,你的JSON数据中的日期值现在已经转换为time.Time值(time.Date格式是当%#v被用作格式词汇时所显示的)。

另一个要注意的是,在json struct线上出现了EmptyString,即使它没有被包含在原始的JSON数据中。如果一个字段被包含在用于JSON解散的struct上,并且没有被包含在JSON数据中,那一个字段就被设置为其类型的默认值,并且被忽略了。这样,你就可以安全地定义你的JSON数据可能有所有可能的字段,而不用担心如果一个字段不存在在过程的两侧。

类似于你的struct上的EmptyString字段被json.Unmarshal忽略了,当emptyString字段从JSON数据中缺少时,相反的情况也是如此。如果一个字段包含在JSON数据中,但在Gostruct上没有相应的字段,那么这个JSON字段被忽略,并继续对接下来的JSON字段进行解析。这样,如果你正在阅读的JSON数据非常大,而你的程序只关心这些字段的少数,你可以选择创建一个struct,其中只包含你关心的字段。在struct上没有定义的JSON数据中的任何字段都被忽略,而Go的JSON解析器将继续在下一个字段。

要在行动中看到这一点,最后一次打开你的main.go文件,并更新jsonData,以包含在myJSON中未包含的字段:

 1[label main.go]
 2...
 3
 4func main() {
 5    jsonData := `
 6    	{
 7    		"intValue":1234,
 8    		"boolValue":true,
 9    		"stringValue":"hello!",
10    		"dateValue":"2022-03-02T09:10:00Z",
11    		"objectValue":{
12    			"arrayValue":[1,2,3,4]
13    		},
14    		"nullStringValue":null,
15    		"nullIntValue":null,
16    		"extraValue":4321
17    	}
18    `
19
20    ...
21}

一旦你添加了 JSON 数据,保存你的文件并使用 go run 运行它:

1go run main.go

你的输出将看起来像这样:

1[secondary_label Output]
2json struct: &main.myJSON{IntValue:1234, BoolValue:true, StringValue:"hello!", DateValue:time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC), ObjectValue:(*main.myObject)(0x14000126180), NullStringValue:(*string)(nil), NullIntValue:(*int)(nil), EmptyString:""}
3dateValue: time.Date(2022, time.March, 2, 9, 10, 0, 0, time.UTC)
4objectValue: &main.myObject{ArrayValue:[]int{1, 2, 3, 4}}

您不应该看到此输出和以前的输出之间的任何差异,因为Go 将忽略 JSON 数据中的额外值字段并继续。

在本节中,您更新了您的程序,以使用您之前定义的struct类型来解析您的 JSON 数据. 您看到 Go 如何为您解析一个time.Time值,并且忽略了在struct类型上定义的EmptyString字段,但不是在 JSON 数据中。

结论

在本教程中,您创建了一种新的程序,以在 Go 的标准库中使用编码/json包。 首先,您使用了json.Marshal函数与map[string]interface{}类型以灵活地创建 JSON 数据。 然后,您更新了您的程序,以使用struct类型与json结构标签,以与json.Marshal一贯可靠的方式生成 JSON 数据。 之后,您使用了json.Unmarshal函数与map[string] interface{}类型来解码 JSON 字符串到数据中。 最后,您使用了您以前用json.Unmarshal函数定义的struct类型,让 Go 根据这些

使用编码/json包,您将能够与互联网上许多可用的API进行交互,以创建您自己的集成与流行的网站。

除了您在本教程中使用的函数外, coding/json包还包含其他有用的函数和类型,可用于与 JSON 交互。

本教程也是 DigitalOcean How to Code in Go系列的一部分,该系列涵盖了许多 Go 主题,从首次安装 Go 到如何使用语言本身。

Published At
Categories with 技术
Tagged with
comments powered by Disqus