# 《Go语言四十二章经》第三十八章 数据编码(encoding)
作者:李骁
## 38.1 序列化与反序列化
数据结构要在网络中传输或保存到文件,就需要对其编码和解码,目前存在很多编码格式:json,XML,Gob,Google Protocol Buffer等,Go 语言当然也支持所有这些编码格式。
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。通过从存储区中读取对象的状态,重新创建该对象,则为反序列化。
简单地说把某种数据结构转为指定格式为 序列化 或 编码(传输之前);而把“指定格式”转为某种数据结构则为 反序列化 或 解码(传输之后)。比如:把数据结构转换成指定格式(data -> string),或者(string -> data structure)。
在Go语言中,encoding/json标准库处理json数据的序列化/反序列化问题。
json数据序列化函数主要有:
json.Marshal()
```go
func Marshal(v interface{}) ([]byte, error) {
e := newEncodeState()
err := e.marshal(v, encOpts{escapeHTML: true})
if err != nil {
return nil, err
}
buf := append([]byte(nil), e.Bytes()...)
e.Reset()
encodeStatePool.Put(e)
return buf, nil
}
```
从上面的Marshal()函数我们可以看到,数据结构序列化后返回的是字节数组,而字节数组很容易通过网络传输或写入文件存储。而且在Go中,Marshal()默认是设置escapeHTML = true的,会自动把 '<', '>', 以及 '&' 等转化为"\u003c" , "\u003e"以及 "\u0026"。
json数据反序列化函数主要有:
UnMarshal()
```go
func Unmarshal(data []byte, v interface{}) error // 把 json 解码为数据结构
```
从上面的UnMarshal()函数我们可以看到,反序列化是读取字节数组,进而解析为对应的数据结构。
注意:
不是所有的数据都可以编码为 json 格式,只有验证通过的数据结构才能被编码:
* json 对象只支持字符串类型的 key;要编码一个 Go map 类型,map 必须是 map[string]T(T是 json 包中支持的任何类型)
* channel,复杂类型和函数类型不能被编码
* 不支持循环数据结构;它将引起序列化进入一个无限循环
* 指针可以被编码,实际上是对指针指向的值进行编码(或者指针是 nil)
而在Go中,json 与 Go 类型对应如下:
* bool 对应 json 的 booleans
* float64 对应 json 的 numbers
* string 对应 json 的 strings
* nil 对应 json 的 null
所以在解析 JSON 格式数据时,若以 interface{} 接收数据,需要按照以上规则进行解析。
## 38.2 json格式处理
前面我们说了序列化和反序列化,下面我们开始了解下有关json数据转换的几种情况。
(一)解码数据到结构体(struct):
这种情况在Web开发中最常见了,如果我们事先知道 json 数据结构,我们可以定义一个适当的结构体并对 json 数据反序列化。下面的例子中,我们将定义:
```go
type FamilyMember struct {
Name string
Age int
Parents []string
}
```
并对其反序列化:
```go
var m FamilyMember
err := json.Unmarshal(b, &m)
```
程序实际上是分配了一个新的切片。这是一个典型的反序列化引用类型(指针、切片和 map)的例子。
我们通过实际访问一个公共接口来具体看看:
```go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
type Response struct {
Status int `json:"status"`
Content interface{} `json:"content"`
Address string `json:"address"`
Province string `json:"province"`
}
func main() {
var bytesData []byte
postBytesReader := bytes.NewReader(bytesData)
httpReq, err := http.NewRequest("GET", "http://api.map.baidu.com/location/ip?ip=x.x.x.x&ak=百度申请key", postBytesReader)
if err != nil {
fmt.Println(err.Error())
}
httpReq.Header.Set("Content-Type", "application/json")
httpClient := &http.Client{Timeout: 300 * time.Second}
httpResp, err := httpClient.Do(httpReq)
if err != nil {
fmt.Println("http get error=%s\n", err.Error())
}
defer httpResp.Body.Close()
body, err := ioutil.ReadAll(httpResp.Body)
var r Response
err1 := json.Unmarshal(body, &r)
if err1 != nil {
fmt.Printf("err was %v", err1)
}
fmt.Println("解析结果:", r.Status)
fmt.Println("解析结果:", r.Address)
fmt.Println("解析结果:", r.Content)
fmt.Println("解析结果:", r.Province)
}
```
```go
解析结果: 0
解析结果: CN|北京|北京|None|CHINANET|0|0
解析结果: map[address:北京市 point:map[x:1111.11 y:1111.11] address_detail:map[district: street: street_number: city_code:131 province:北京市 city:北京市]]
解析结果:
```
这个例子需要注意的是:对于status,接口返回的是int,所以在结构体Response中定义时不能写为string。而Content定义为interface{}是因为它是一个object,也可另外定义一个struct结构体内嵌到Response,这样在使用具体字段时会更方便。具体这里就不进一步阐述,有兴趣可以进一步尝试。
(二)解码任意的数据:
json 包使用 map[string]interface{} 和 []interface{} 储存任意的 json 对象和数组;其可以被反序列化为任何的 json blob 存储到接口值中。
来看这个 json 数据,被存储在变量 b 中:
```go
b := []byte(`{"Name": "Wednesday", "Age": 6, "Parents": ["Gomez", "Morticia"]}`)
```
不用理解这个数据的结构,我们可以直接使用 Unmarshal 把这个数据编码并保存在接口值中:
```go
var f interface{}
err := json.Unmarshal(b, &f)
```
f 指向的值是一个 map,key 是一个字符串,value 是自身存储作为空接口类型的值:
```go
map[string]interface{} {
"Name": "Wednesday",
"Age": 6,
"Parents": []interface{} {
"Gomez",
"Morticia",
},
}
```
要访问这个数据,我们可以使用类型断言
```go
m := f.(map[string]interface{})
```
我们可以通过 for range 语法和 type switch 来访问其实际类型:
```go
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 []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")
}
}
```
通过这种方式,你可以处理未知的 json 数据,同时可以确保类型安全。
(三)Dynamic json
动态json数据处理是一个难点,困难之处在于你在使用之前不知道json数据的确切数据结构。
这里有两种方式处理动态的json数据:
1.灵活类型检查
```go
package main
import (
"encoding/json"
"fmt"
)
type PersonFlexible struct {
Name interface{}
}
type Person struct {
Name string
}
func main() {
thejson := `{"Name": 123}`
var personFlexible PersonFlexible
json.Unmarshal([]byte(thejson), &personFlexible)
if _, ok := personFlexible.Name.(string); !ok {
panic("Name must be a string.")
}
// When validation passes we can use the real object and types.
// This code will never be reached because the above will panic()...
// But if you make the Name above a string it will run the following:
var person Person
json.Unmarshal([]byte(thejson), &person)
fmt.Printf("%+v\n", person)
}
```
上面代码会panic。
2. 穷举类型检查
这个方法有点类似于JavaScript的处理方式,即大量使用typeof函数进行类型检查。你可以使用Go的switch语句优雅的实现这一方式。
```go
package main
import (
"encoding/json"
"fmt"
)
func main() {
thejson := `123`
var anything interface{}
json.Unmarshal([]byte(thejson), &anything)
switch v := anything.(type) {
case float64:
// v is an float64
fmt.Printf("NUMBER: %f\n", v)
case string:
// v is a string
fmt.Printf("STRING: %s\n", v)
default:
panic("I don't know how to handle this!")
}
}
```
程序输出:
NUMBER: 123.000000
Go使用固定的数据类型来编解码json键值。类似于123这样的数据将被解码为float64类型而不是int类型。这种实现方式简化了switch的结构,但是需要你实现对数据的二次加工。
Validating json Schemas
如果你有一个结构复杂的json数据,更为简单的方式是使用”json Schema”。
仔细查看代码中的四种情况
1 将json反序列化成struct对象
2 将json反序列化到可以存储struct的slice中
3 将json 反序列化到map中
4 将json反序列化到slice中
```go
package main
import (
"encoding/json"
"fmt"
)
func main() {
type Person struct {
Name string
Age int
Gender bool
}
//unmarshal to struct
var p Person
var str = `{"Name":"Jane", "Age":21, "Gender":true}`
json.Unmarshal([]byte(str), &p)
fmt.Println(p.Name, ":", p.Age, ":", p.Gender)
// unmarshal to slice-struct
var ps []Person
var ajson = `[{"Name":"Jane", "Age":21, "Gender":true},
{"Name":"Rob", "Age":29, "Gender":false}]`
json.Unmarshal([]byte(ajson), &ps)
fmt.Println(ps, "len is", len(ps))
// unmarshal to map[string]interface{}
var obj interface{} // var obj map[string]interface{}
json.Unmarshal([]byte(str), &obj)
m := obj.(map[string]interface{})
fmt.Println(m["Name"], ":", m["Age"], ":", m["Gender"])
//unmarshal to slice
var strs string = `["Go", "Java", "C", "Php"]`
var aStr []string
json.Unmarshal([]byte(strs), &aStr)
//result --> [Go Java C Php] len is 4
fmt.Println(aStr, " len is", len(aStr))
}
```
```go
程序输出:
Jane : 21 : true
[{Jane 21 true} {Rob 29 false}] len is 2
Jane : 21 : true
[Go Java C Php] len is 4
```
(四)编码和解码流:
json 包提供 Decoder 和 Encoder 类型来支持常用 json 数据流读写。NewDecoder 和 NewEncoder 函数分别封装了 io.Reader 和 io.Writer 接口。
```go
func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder
```
如果要想把 json 直接写入文件,可以使用 json.NewEncoder 初始化文件(或者任何实现 io.Writer 的类型),并调用 Encode();反过来与其对应的是使用 json.Decoder 和 Decode() 函数:
```go
func NewDecoder(r io.Reader) *Decoder
func (dec *Decoder) Decode(v interface{}) error
```
来看下接口是如何对实现进行抽象的:数据结构可以是任何类型,只要其实现了某种接口,目标或源数据要能够被编码就必须实现 io.Writer 或 io.Reader 接口。由于 Go 语言中很多包都实现了 Reader 和 Writer,因此 Encoder 和 Decoder 可被应用的场景非常广泛,例如读取或写入 HTTP 连接、websockets 或文件。
方法 NewDecode 和 Decode 方法:
```go
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"strings"
)
type User struct {
UserName string `json:"username"`
Password string `json:"password"`
}
var jsonString string = `{
"username": "Gopher@gmail.com",
"password": "xxxxxx"
}`
func Decode(r io.Reader) (u *User, err error) {
u = new(User)
err = json.NewDecoder(r).Decode(u)
if err != nil {
return
}
return
}
func main() {
user, err := Decode(strings.NewReader(jsonString))
if err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", user)
}
```
```go
程序输出:
&main.User{UserName:"Gopher@gmail.com", Password:"xxxxxx"}
```
我们定义了一个 Decode 函数,在这个函数进行 json 字串的解析。然后调用 json 的 NewDecoder 方法构造一个 Decode 对象,最后使用这个对象的 Decode 方法赋值给定义好的结构对象。
对于字串,可是使用 strings.NewReader 方法,让字串变成一个 Stream 对象。
(五)延迟解析
下面代码的 UserName 字段,实际上是在使用的时候,才会用到他的具体类型,因此我们可以延迟解析。使用 json.RawMessage 方式,将 json 的字串继续以 byte 数组方式存在。
```go
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"strings"
)
type User struct {
UserName json.RawMessage `json:"username"`
Password string `json:"password"`
Email string
Phone int64
}
var jsonString string = `{
"username": "Gopher@gmail.com",
"password": "xxxxxx"
}`
func Decode(r io.Reader) (u *User, err error) {
u = new(User)
if err = json.NewDecoder(r).Decode(u); err != nil {
return
}
var email string
if err = json.Unmarshal(u.UserName, &email); err == nil {
u.Email = email
return
}
var phone int64
if err = json.Unmarshal(u.UserName, &phone); err == nil {
u.Phone = phone
}
return
}
func main() {
user, err := Decode(strings.NewReader(jsonString))
if err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", user)
}
```
```go
程序输出:
&main.User{UserName:json.RawMessage{0x22, 0x47, 0x6f, 0x70, 0x68, 0x65, 0x72, 0x40, 0x67, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x22}, Password:"xxxxxx", Email:"Gopher@gmail.com", Phone:0}
```
(六)不定字段解析
对于未知 json 结构的解析,不同的数据类型可以映射到接口或者使用延迟解析。有时候,会遇到 json 的数据字段都不一样的情况。例如需要解析下面一个 json 字串:
```go
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
type Place struct {
City string `json:"city"`
Country string `json:"country"`
}
func decode(jsonStr []byte) (persons []Person, places []Place) {
var data map[string][]map[string]interface{}
err := json.Unmarshal(jsonStr, &data)
if err != nil {
fmt.Println(err)
return
}
for i := range data["things"] {
item := data["things"][i]
if item["name"] != nil {
persons = addPerson(persons, item)
} else {
places = addPlace(places, item)
}
}
return
}
func addPerson(persons []Person, item map[string]interface{}) []Person {
name := item["name"].(string)
age := item["age"].(float64)
person := Person{name, int(age)}
persons = append(persons, person)
return persons
}
func addPlace(places []Place, item map[string]interface{}) []Place {
city := item["city"].(string)
country := item["country"].(string)
place := Place{City: city, Country: country}
places = append(places, place)
return places
}
var jsonString string = `{
"things": [
{
"name": "Alice",
"age": 37
},
{
"city": "Ipoh",
"country": "Malaysia"
},
{
"name": "Bob",
"age": 36
},
{
"city": "Northampton",
"country": "England"
}
]
}`
func main() {
personA, placeA := decode([]byte(jsonString))
fmt.Printf("%+v\n", personA)
fmt.Printf("%+v\n", placeA)
}
```
```go
程序输出:
[{Name:Alice Age:37} {Name:Bob Age:36}]
[{City:Ipoh Country:Malaysia} {City:Northampton Country:England}]
```
除了Go标准库外,还有很多的第三方库也能较好解析json数据。这里我推荐一个第三方库:https://github.com/buger/jsonparser
如同 encoding/json 包一样,在Go语言中XML也有 Marshal() 和 UnMarshal() 从 XML 中编码和解码数据;也可以从文件中读取和写入(或者任何实现了 io.Reader 和 io.Writer 接口的类型)。和 json 的方式一样,XML 数据可以序列化为结构,或者从结构反序列化为 XML 数据。
## 38.3 Protocol Buffer数据格式
Protocol Buffer 简单称为protobuf(Pb),是Google开发出来的一个语言无关、平台无关的数据序列化工具,在rpc或tcp通信等很多场景都可以使用。在服务端定义一个数据结构,通过protobuf转化为字节流,再传送到客户端解码,就可以得到对应的数据结构。它的通信效率极高,同一条消息数据,用protobuf序列化后的大小是json的10分之一左右。
为了正常使用protobuf,我们需要做一些准备工作。
1、下载protobuf的编译器protoc,地址:https://github.com/google/protobuf/releases
window用户下载: protoc-3.6.1-win32.zip,然后解压,把protoc.exe文件复制到GOPATH/bin下。
linux 用户下载:protoc-3.6.1-linux-x86_64.zip 或 protoc-3.6.1-linux-x86_32.zip,然后解压,把protoc文件复制到GOPATH/bin下。
2、获取protobuf的编译器插件protoc-gen-go。
在命令行运行 go get -u github.com/golang/protobuf/protoc-gen-go
会在GOPATH/bin下生成protoc-gen-go.exe文件,如果没有请自行build。GOPATH/bin目录建议加入path,以便后续操作方便。
接下来我们可以正式开始尝试怎么使用protobuf了。我们需要创建一个.proto 结尾的文件,这个文件需要按照一定规则编写。
具体请见官方说明:https://developers.google.com/protocol-buffers/docs/proto
也可参考:https://gowalker.org/github.com/golang/protobuf/proto
protobuf的使用方法是将数据结构写入到.proto文件中,使用protoc编译器编译(通过调用protoc-gen-go)得到一个新的go包,里面包含go中可以使用的数据结构和一些辅助方法。
下面我们先创建一个msg.proto文件
```go
syntax = "proto3";
package learn;
message UserInfo {
int32 UserType = 1; //必选字段
string UserName = 2; //必选字段
string UserInfo = 3; //必选字段
}
```
运行如下命令
```go
> protoc --go_out=. msg.proto
```
会生成一个msg.pb.go的文件,代码如下。
```go
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: msg.proto
package learn
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type UserInfo struct {
UserType int32 `protobuf:"varint,1,opt,name=UserType,proto3" json:"UserType,omitempty"`
UserName string `protobuf:"bytes,2,opt,name=UserName,proto3" json:"UserName,omitempty"`
UserInfo string `protobuf:"bytes,3,opt,name=UserInfo,proto3" json:"UserInfo,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *UserInfo) Reset() { *m = UserInfo{} }
func (m *UserInfo) String() string { return proto.CompactTextString(m) }
func (*UserInfo) ProtoMessage() {}
func (*UserInfo) Descriptor() ([]byte, []int) {
return fileDescriptor_c06e4cca6c2cc899, []int{0}
}
func (m *UserInfo) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_UserInfo.Unmarshal(m, b)
}
func (m *UserInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_UserInfo.Marshal(b, m, deterministic)
}
func (m *UserInfo) XXX_Merge(src proto.Message) {
xxx_messageInfo_UserInfo.Merge(m, src)
}
func (m *UserInfo) XXX_Size() int {
return xxx_messageInfo_UserInfo.Size(m)
}
func (m *UserInfo) XXX_DiscardUnknown() {
xxx_messageInfo_UserInfo.DiscardUnknown(m)
}
var xxx_messageInfo_UserInfo proto.InternalMessageInfo
func (m *UserInfo) GetUserType() int32 {
if m != nil {
return m.UserType
}
return 0
}
func (m *UserInfo) GetUserName() string {
if m != nil {
return m.UserName
}
return ""
}
func (m *UserInfo) GetUserInfo() string {
if m != nil {
return m.UserInfo
}
return ""
}
func init() {
proto.RegisterType((*UserInfo)(nil), "learn.UserInfo")
}
func init() { proto.RegisterFile("msg.proto", fileDescriptor_c06e4cca6c2cc899) }
var fileDescriptor_c06e4cca6c2cc899 = []byte{
// 100 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xcc, 0x2d, 0x4e, 0xd7,
0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xcd, 0x49, 0x4d, 0x2c, 0xca, 0x53, 0x8a, 0xe3, 0xe2,
0x08, 0x2d, 0x4e, 0x2d, 0xf2, 0xcc, 0x4b, 0xcb, 0x17, 0x92, 0x82, 0xb0, 0x43, 0x2a, 0x0b, 0x52,
0x25, 0x18, 0x15, 0x18, 0x35, 0x58, 0x83, 0xe0, 0x7c, 0x98, 0x9c, 0x5f, 0x62, 0x6e, 0xaa, 0x04,
0x93, 0x02, 0xa3, 0x06, 0x67, 0x10, 0x9c, 0x0f, 0x93, 0x03, 0x99, 0x21, 0xc1, 0x8c, 0x90, 0x03,
0xf1, 0x93, 0xd8, 0xc0, 0xb6, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x7e, 0x5d, 0xad, 0x78,
0x7a, 0x00, 0x00, 0x00,
}
```
接下来,我们在Go语言程序中使用protobuf。
```go
package main
import (
"github.com/golang/protobuf/proto"
"fmt"
"ind/pb"
)
func main() {
//初始化message UserInfo
usermsg := &pb.UserInfo{
UserType: 1,
UserName: "Jok",
UserInfo: "I am a woker!",
}
//序列化
userdata, err := proto.Marshal(usermsg)
if err != nil {
fmt.Println("Marshaling error: ", err)
}
//反序列化
encodingmsg := &pb.UserInfo{}
err = proto.Unmarshal(userdata, encodingmsg)
if err != nil {
fmt.Println("Unmarshaling error: ", err)
}
fmt.Printf("GetUserType: %d\n", encodingmsg.GetUserType())
fmt.Printf("GetUserName: %s\n", encodingmsg.GetUserName())
fmt.Printf("GetUserInfo: %s\n", encodingmsg.GetUserInfo())
}
```
```go
程序输出:
GetUserType: 1
GetUserName: Jok
GetUserInfo: I am a woker!
```
通过上面的介绍,我们已经学会了怎么使用protobuf来处理我们的数据。
## 38.4 用 Gob 传输数据
Gob 是 Go 自己的以二进制形式序列化和反序列化程序数据的格式;可以在 encoding 包中找到。这种格式的数据简称为 Gob (即 Go binary 的缩写)。
Gob 通常用于远程方法调用
用于参数和结果的传输,以及应用程序和机器之间的数据传输。 它和 json 或 XML 有什么不同呢?Gob 特定地用于纯 Go 的环境中,例如,两个用 Go 写的服务之间的通信。这样的话服务可以被实现得更加高效和优化。 Gob 不是可外部定义,语言无关的编码方式。因此它的首选格式是二进制,而不是像 json 和 XML 那样的文本格式。 Gob 并不是一种不同于 Go 的语言,而是在编码和解码过程中用到了 Go 的反射。
Gob 文件或流是完全自描述的:里面包含的所有类型都有一个对应的描述,并且总是可以用 Go 解码,而不需要了解文件的内容。
只有可导出的字段会被编码,零值会被忽略。在解码结构体的时候,只有同时匹配名称和可兼容类型的字段才会被解码。当源数据类型增加新字段后,Gob 解码客户端仍然可以以这种方式正常工作:解码客户端会继续识别以前存在的字段。并且还提供了很大的灵活性,比如在发送者看来,整数被编码成没有固定长度的可变长度,而忽略具体的 Go 类型。
>本书《Go语言四十二章经》内容在github上同步地址:https://github.com/ffhelicopter/Go42
>本书《Go语言四十二章经》内容在简书同步地址: https://www.jianshu.com/nb/29056963
>虽然本书中例子都经过实际运行,但难免出现错误和不足之处,烦请您指出;如有建议也欢迎交流。
有疑问加站长微信联系(非本文作者))