Golang编解码JSON与HTTP请求

本文基于实际工作加之诸多前辈的博文结晶拼合而来,由以下部分组成:

  1. 概念
  2. 解码
    2.1 解析到结构体
    2.2 解析到interface
    2.3 SimpleJSON
  3. 编码
  4. json.NewEncoder与json.NewDecoder
  5. HTTP请求

文里的例子可以直接在the go playground里跑,已测试。https://play.golang.org/

概念

JSON语法

先来明确一下JSON语法:

  1. JSON语法是JavaScript语法的子集。JSON语法是JavaScript对象表示法语法的子集。
  • 数据在名称/值对中
  • 数据由逗号分隔
  • 大括号保存对象
  • 中括号保存数组
  1. JSON data is written as name/value pairs.
    A name/value pair consists of a field name (in double quotes), followed by a colon, followed by a value:
    "name":"John"

  2. In JSON, values must be one of the following data types:

  • a string(在双引号中。在JS里可以单括号,但JSON需是双括号)
  • a number(整数或浮点数)
  • an object (JSON object,在大括号中)
  • an array(在中括号中)
  • a boolean(True或False)
  • null

JSON对象:

  • JSON 对象在大括号({})中书写
  • 对象可以包含多个名称/值对
{
    "name": "runoob",
    "alexa": 10000,
    "sites": {
        "site1": "www.runoob.com",
        "site2": "m.runoob.com",
        "site3": "c.runoob.com"
    }
}

JSON数组:

  • JSON 数组在中括号中书写
  • 数组可包含多个对象
{
    "sites": [
        {
            "name": "菜鸟教程",
            "url": "www.runoob.com"
        },
        {
            "name": "google",
            "url": "www.google.com"
        },
        {
            "name": "微博",
            "url": "www.weibo.com"
        }
    ]
}

序列化与反序列化

序列化相关也可移步至:https://sowhatbigfatloser.com/xu-lie-hua-yu-fan-xu-lie-hua/
序列化:就是将对象转化成二进制序列的过程。
反序列化:就是讲二进制序列转化成对象的过程。

我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flattening等等,都是一个意思。
序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。
反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling。

解码

实际工作中运用到,解析返回JSON目前看来有三种方式:

  • 解析到结构体:如果知道被解析的JSON的结构
  • 解析到interface:用map[string]interface{}先接过来
  • SimpleJSON:单纯数据解析,可使用第三方库

解析到结构体

Go的JSON包中有如下函数:
func Unmarshal(data []byte, v interface{}) error
比较直接的情况, 注意需要unmarshal到&变量地址,而不是变量:

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	resp := `{	"code": "00",
 			"message": "SUCCESS",
 			"describe": "成功",
 			"resultInfo": { "uniqueNumber": "20180816" } 
			 }`
             
    //定义结构体
	type JsonResp struct {
		Code       int               `json:"code"`
		Message    string            `json:"message"`
		Describe   string            `json:"describe"`
		ResultInfo map[string]string `json:"resultInfo"`
	}
	var smsresp JsonResp
    
	temp := []byte(resp)
    
    //注意需要unmarshal到&smsresp,而不是smsresp
	json.Unmarshal(temp, &smsresp)
	fmt.Println(smsresp.Code)
	fmt.Println(smsresp.Describe)
	fmt.Println(smsresp.Message)
	fmt.Println(smsresp.ResultInfo["uniqueNumber"])
}

复杂一些的结构,可以尝试在JSON中嵌套JSON:

package main

import (
	"encoding/json"
	"fmt"
	"time"
)

func main() {
	resp := `{	"code": "00",
 			"message": "SUCCESS",
 			"describe": "成功",
 			"resultInfo": [
				{"uniqueNumber": 20180816,
				 "created_at": "2019-05-24T11:33:18+08:00",
                 "updated_at": "2021-03-01T23:40:20+08:00",
				 "last_updated_by": "service_centerA"				
				},
				{"uniqueNumber": 20180817,
				 "created_at": "2019-05-24T11:33:19+08:00",
                 "updated_at": "2021-03-01T23:40:21+08:00",
				 "last_updated_by": "service_centerB"				
				},
				{"uniqueNumber": 20180818,
				 "created_at": "2019-05-24T11:33:20+08:00",
                 "updated_at": "2021-03-01T23:40:22+08:00",
				 "last_updated_by": "service_centerC"				
				}
] 
}`
    //定义结构体, 里面是json.RawMessage
	type JsonResp struct {
		Code       int               `json:"code"`
		Message    string            `json:"message"`
		Describe   string            `json:"describe"`
		ResultInfo json.RawMessage `json:"resultInfo"`
	}
	
    //定义内部的结构体
	type Result struct {
		Uniquenumber  int       `json:"uniqueNumber"`
		CreatedAt     time.Time `json:"created_at"`
		UpdatedAt     time.Time `json:"updated_at"`
		LastUpdatedBy string    `json:"last_updated_by"`
	}

    //一度unmarshal拿到response
	var smsresp JsonResp
	temp := []byte(resp)
	json.Unmarshal(temp, &smsresp)
	
    //二度unmarshal拿到result
	var smsresults []Result
	json.Unmarshal(smsresp.ResultInfo, &smsresults)
	
	
	fmt.Println("========Response========")
	fmt.Println(smsresp.Code)
	fmt.Println(smsresp.Describe)
	fmt.Println(smsresp.Message)
	fmt.Println("========Result========")
	fmt.Println(smsresults[1].Uniquenumber)
	fmt.Println(smsresults[1].CreatedAt)
	fmt.Println(smsresults[1].LastUpdatedBy)
}

输出结果:

========Response========
0
成功
SUCCESS
========Result========
20180817
2019-05-24 11:33:19 +0800 +0800
service_centerB

注意: 能够被赋值的字段必须是可导出字段,即首字母大写。 同时JSON解析的时候只会解析能找得到的字段,找不到的字段会被忽略。我们在实际使用的过程中一定要随时警惕这一点。

其实与这个潜在的坑相比,它的优势非常明显:当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候,你只需将你想要的数据对应的字段名大写,即可轻松解决。

解析到interface

我们知道interface{}可以用来存储任意数据类型的对象,这种数据结构正好用于存储解析的未知结构的json数据的结果。JSON包中采用map[string]interface{}[]interface{}结构来存储任意的JSON对象和数组。Go类型和JSON类型的对应关系如下:

类型 JSON类型
bool JSON booleans
float64 JSON numbers
string JSON strings
nil JSON null

现在我们假设有如下的JSON数据
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
如果在我们不知道他的结构的情况下,我们把他解析到interface{}里面

var f interface{}
err := json.Unmarshal(b, &f)

这个时候f里面存储了一个map类型,他们的key是string,值存储在空的interface{}里

f = map[string]interface{}{
	"Name": "Wednesday",
	"Age":  6,
	"Parents": []interface{}{
		"Gomez",
		"Morticia",
	},
}

那么如何来访问这些数据呢?通过断言的方式:
m := f.(map[string]interface{})
通过断言之后,你就可以通过如下方式来访问里面的数据了

for k, v := range m {
	switch vv := v.(type) {
	case string:
		fmt.Println(k, "is string", vv)
	case int:
		fmt.Println(k, "is int", vv)
	case float64:
		fmt.Println(k,"is float64",vv)
	case []interface{}:
		fmt.Println(k, "is an array:")
		for i, u := range vv {
			fmt.Println(i, u)
		}
	default:
		fmt.Println(k, "is of a type I don't know how to handle")
	}
}

综合实践的程序如下:

package main

import (
	"fmt"
	"encoding/json"
)

func main() {
		resp := `{"code": "00",
                  "message": "SUCCESS",
                  "describe": "成功",
                  "resultInfo": {"uniqueNumber": "201808161"} 
  }`

	var x interface{}
	_ = json.Unmarshal([]byte(resp), &x)
	
	fmt.Println("x contains: ", x)
	fmt.Println("===============")
	m := x.(map[string]interface{})

	for k, v := range m {
		switch vv := v.(type) {
		case string:
			fmt.Println(k, "is string", vv)
		case int:
			fmt.Println(k, "is int", vv)
		case float64:
			fmt.Println(k, "is float64", vv)
		case []interface{}:
			fmt.Println(k, "is an array:")
			for i, u := range vv {
				fmt.Println(i, u)
			}
		case map[string]interface{}:
			fmt.Println(k, "is an map[string]string:")
			for i, u := range vv {
				fmt.Println(i, u)
			}
		default:
			fmt.Println(k, "is of a type didn't handle")
		}
	}
}

输出结果:

x contains:  map[code:00 describe:成功 message:SUCCESS resultInfo:map[uniqueNumber:201808161]]
===============
code is string 00
message is string SUCCESS
describe is string 成功
resultInfo is an map[string]string:
uniqueNumber 201808161

SimpleJSON

单纯用第三方库github.com/bitly/go-simplejson解析

package main

import (
	"fmt"
	"github.com/bitly/go-simplejson"
)

func main() {
	resp := `{"code": "00",
 			  "message": "SUCCESS",
 			  "describe": "成功",
 			  "resultInfo": { "uniqueNumber": "20180816" } 
 			}`
	js, errs := simplejson.NewJson([]byte(resp))
	if errs != nil {
		return
	}
	discount := js.Get("resultInfo").Get("uniqueNumber")
	strcode, _ := js.Get("code").String()
	intcode, _ := js.Get("code").Int()
	path := js.GetPath("resultInfo", "uniqueNumber")
	fmt.Println(discount)
	fmt.Println(strcode)
	fmt.Println(intcode)
	fmt.Println(path)
}

输出结果:

&{201808161133401673324075025000035}
00
0
&{201808161133401673324075025000035}

编码

Marshal()和MarshalIndent()函数可以将数据封装成json数据。

  • struct、slice、array、map都可以转换成json
  • struct转换成json的时候,只有字段首字母大写的才会被转换
  • map转换的时候,key必须为string
  • 封装的时候,如果是指针,会追踪指针指向的对象进行封装

先有数据结构,然后Marshal。JSON包里Marshal函数来处理,函数定义如下:
func Marshal(v interface{}) ([]byte, error)
例子:

package main

import (
	"encoding/json"
	"fmt"
)

type Server struct {
	ServerName string
	ServerIP   string
}

type Serverslice struct {
	Servers []Server
}

func main() {
	var s Serverslice
	s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})
	s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})
	b, err := json.Marshal(s)
	if err != nil {
		fmt.Println("json err:", err)
	}
	fmt.Println(string(b))
}

输出如下内容:

{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}

我们看到上面的输出字段名的首字母都是大写的,如果你想用小写的首字母怎么办呢?把结构体的字段名改成首字母小写的?JSON输出的时候必须注意,只有导出的字段才会被输出,如果修改字段名,那么就会发现什么都不会输出,所以必须通过struct tag定义来实现

type Server struct {
	ServerName string `json:"serverName"`
	ServerIP   string `json:"serverIP"`
}

type Serverslice struct {
	Servers []Server `json:"servers"`
}

通过修改上面的结构体定义,输出的JSON串就和我们最开始定义的JSON串保持一致了。

针对JSON的输出,我们在定义struct tag的时候需要注意的几点是:

  • 字段的tag是"-",那么这个字段不会输出到JSON
  • tag中带有自定义名称,那么这个自定义名称会出现在JSON的字段名中,例如上面例子中serverName
  • tag中如果带有"omitempty"选项,那么如果该字段值为空,就不会输出到JSON串中
  • 如果字段类型是bool, string, int, int64等,而tag中带有",string"选项,那么这个字段在输出到JSON的时候会把该字段对应的值转换成JSON字符串

举例来说:

package main

import (
	"encoding/json"
	"os"
)

type Server struct {
	// ID 不会导出到JSON中
	ID int `json:"-"`

	// ServerName2 的值会进行二次JSON编码
	ServerName  string `json:"serverName"`
	ServerName2 string `json:"serverName2,string"`

	// 如果 ServerIP 为空,则不输出到JSON串中
	ServerIP   string `json:"serverIP,omitempty"`
}

func main() {
	s := Server {
		ID:         3,
		ServerName:  `Go "1.0" `,
		ServerName2: `Go "1.0" `,
		ServerIP:   ``,
	}
	b, _ := json.Marshal(s)
	os.Stdout.Write(b)	
	
}

输出结果:

{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}

Marshal函数只有在转换成功的时候才会返回数据,在转换的过程中我们需要注意几点:

  • JSON对象只支持string作为key,所以要编码一个map,那么必须是map[string]T这种类型(T是Go语言中任意的类型)
  • Channel, complex和function是不能被编码成JSON的
  • 嵌套的数据是不能编码的,不然会让JSON编码进入死循环
  • 指针在编码的时候会输出指针指向的内容,而空指针会输出null

json.NewEncoder与json.NewDecoder

除了marshal和unmarshal函数,Go还提供了Decoder和Encoder对streamJSON进行处理,常见 request中的Body、文件等。
编码

json.NewEncoder(<Writer>).encode(v)
json.Marshal(&v)

解码

json.NewDecoder(<Reader>).decode(&v)
json.Unmarshal([]byte, &v)

例子:

$ cat post.json 
{
  "name":"apple 8P",
  "product_id":10,
  "number":10000,
  "price":6000,
  "is_on_sale":"true"
}
$ cat json9.go 
package main
import (
 "encoding/json"
 "fmt"
 "io"
 "os"
)

type Product struct {
    Name      string
    ProductID int64
    Number    int
    Price     float64
    IsOnSale  bool
}


func main() {
  jsonFile, err := os.Open("post.json")
  if err != nil {
      fmt.Println("Error opening json file:", err)
      return
  }

  defer jsonFile.Close()
  decoder := json.NewDecoder(jsonFile)
  for {
      var post Product
      err := decoder.Decode(&post)
      if err == io.EOF {
	  break
      }

      if err != nil {
	  fmt.Println("error decoding json:", err)
	  return
      }

      fmt.Println(post)
  }
}

运行结果:

$ go  run json9.go
{apple 8P 0 10000 6000 false}

两种编解码比较:

package main

import (
    "encoding/json"
    "fmt"
    "bytes"
    "strings"
)

type Person struct {
    Name string `json:"name"`
    Age int `json:"age"`
}

func main()  {
    // 1. 使用 json.Marshal 编码
    person1 := Person{"张三", 24}
    bytes1, err := json.Marshal(&person1)
    if err == nil {
        // 返回的是字节数组 []byte
        fmt.Println("json.Marshal 编码结果: ", string(bytes1))
    }

    // 2. 使用 json.Unmarshal 解码
    str := `{"name":"李四","age":25}`
    // json.Unmarshal 需要字节数组参数, 需要把字符串转为 []byte 类型
    bytes2 := []byte(str) // 字符串转换为字节数组
    var person2 Person    // 用来接收解码后的结果
    if json.Unmarshal(bytes2, &person2) == nil {
        fmt.Println("json.Unmarshal 解码结果: ", person2.Name, person2.Age)
    }

    // 3. 使用 json.NewEncoder 编码
    person3 := Person{"王五", 30}
    // 编码结果暂存到 buffer
    bytes3 := new(bytes.Buffer)
    _ = json.NewEncoder(bytes3).Encode(person3)
    if err == nil {
        fmt.Print("json.NewEncoder 编码结果: ", string(bytes3.Bytes()))
    }

    // 4. 使用 json.NewDecoder 解码
    str4 := `{"name":"赵六","age":28}`
    var person4 Person
    // 创建一个 string reader 作为参数
    err = json.NewDecoder(strings.NewReader(str4)).Decode(&person4)
    if err == nil {
        fmt.Println("json.NewDecoder 解码结果: ", person4.Name, person4.Age)
    }
}

运行结果:

json.Marshal 编码结果:  {"name":"张三","age":24}
json.Unmarshal 解码结果:  李四 25
json.NewEncoder 编码结果: {"name":"王五","age":30}
json.NewDecoder 解码结果:  赵六 28

HTTP请求

发送GET

//基本的GET请求可以直接使用http.Get方法
package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    resp, err := http.Get("http://httpbin.org/get")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
    fmt.Println(resp.StatusCode)
    if resp.StatusCode == 200 {
        fmt.Println("ok")
    }
}

发送JSON数据的post请求

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    client := &http.Client{}
    data := make(map[string]interface{})
    data["name"] = "zhaofan"
    data["age"] = "23"
    bytesData, _ := json.Marshal(data)
    req, _ := http.NewRequest("POST","http://httpbin.org/post",bytes.NewReader(bytesData))
    resp, _ := client.Do(req)
    defer resp.Body.Close()
    body, _ := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))

}

其他Golang语言中的著名Json库

  • encoding/json
  • jsoniter
  • jsonparser
  • fastjson
  • easyjson
  • GJSON

References:
https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html
https://golang.org/pkg/encoding/json/
https://studygolang.com/articles/18462
https://www.cnblogs.com/Detector/p/9048678.html
https://segmentfault.com/q/1010000008997626
https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/07.2.md
https://www.cnblogs.com/f-ck-need-u/p/10080793.html
https://www.cnblogs.com/zhaof/p/11346412.html
https://zhuanlan.zhihu.com/p/80442700
https://sanyuesha.com/2018/05/07/go-json/
https://blog.csdn.net/weixin_33795743/article/details/87958551?utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromMachineLearnPai2~default-1.baidujs&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromMachineLearnPai2~default-1.baidujs
https://my.oschina.net/u/4360480/blog/4521023
https://www.liaoxuefeng.com/wiki/1016959663602400/1017624706151424

Subscribe to 隅

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe