《Go语言四十二章经》第三十八章 数据序列化

《Go语言四十二章经》第三十八章 数据序列化

作者:李骁

38.1 序列化与反序列化

我们的数据对象要在网络中传输或保存到文件,就需要对其编码和解码动作,目前存在很多编码格式:json,XML,Gob,Google Protocol Buffer等,Go 语言当然也支持所有这些编码格式。

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。通过从存储区中读取对象的状态,重新创建该对象,则为反序列化。

简单地说把某种数据结构转为指定数据格式为“序列化”或“编码”(传输之前);而把“指定数据格式”转为某种数据结构则为“反序列化”或“解码”(传输之后)。

在Go语言中,encoding/json标准包处理json数据的序列化与反序列化问题。

json数据序列化函数主要有:

json.Marshal()

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()

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数据格式

在Go语言中,利用encoding/json标准包将数据序列化为json数据格式这个过程简单直接,直接使用json.Marshal(v)来处理任意类型,序列化成功后得到一个字节数组。

反过来我们将一个json数据来反序列化或解码,则就不那么容易了,下面我们一一来说明。

(一)将json数据反序列化到结构体:

这种需求是最常见的,在我们知道 json 的数据结构前提情况下,我们完全可以定义一个或几个适当的结构体并对 json 数据反序列化。例如:

package main

import (
    "encoding/json"
    "fmt"
)

type Human struct {
    name   string `json:"name"` // 姓名
    Gender  string `json:"s"`    // 性别,性别的tag表明在json中为s字段
    Age    int    `json:"Age"`  // 年龄
    Lesson
}

type Lesson struct {
    Lessons []string `json:"lessons"`
}

func main() {
    jsonStr := `{"Age": 18,"name": "Jim" ,"s": "男",
    "lessons":["English","History"],"Room":201,"n":null,"b":false}`

    var hu Human
    if err := json.Unmarshal([]byte(jsonStr), &hu); err == nil {
        fmt.Println("\n结构体Human")
        fmt.Println(hu)
    }

    var le Lesson
    if err := json.Unmarshal([]byte(jsonStr), &le); err == nil {
        fmt.Println("\n结构体Lesson")
        fmt.Println(le)
    }

    jsonStr = `["English","History"]`

    var str []string
    if err := json.Unmarshal([]byte(jsonStr), &str); err == nil {
        fmt.Println("\n字符串数组")
        fmt.Println(str)
    } else {
        fmt.Println(err)
    }
}

程序输出:
结构体Human
{ 男 18 {[English History]}}

结构体Lesson
{[English History]}

字符串数组
[English History 

我们定义了2个结构体Human和Lesson,结构体Human的Gender字段tag标签为:json:"s",表明这个字段在json中的名字对应为s。而且结构体Human中嵌入了Lesson结构体。

jsonStr 我们可以认作为一个json数据,通过json.Unmarshal,我们可以把json中的数据反序列化到了对应结构体,由于结构体Human的name字段不能导出,所以并不能实际得到json中的值,这是我们在定义结构体时需要注意的,字段首字母大写。

对json中的Age,在结构体Human对应Age int,不能是string。另外,如果是json数组,可以把数据反序列化给一个字符串数组。

总之,知道json的数据结构很关键,有了这个前提做反序列化就容易多了。而且结构体的字段并不需要和json中所有数据都一一对应,定义的结构体字段可以是json中的一部分。

(二)反序列化任意json数据:

encoding/json 包使用 map[string]interface{} 和 []interface{} 储存任意的 json 对象和数组;其可以被反序列化为任何的 json blob 存储到接口值中。

直接使用 Unmarshal 把这个数据反序列化,并保存在map[string]interface{} 中,要访问这个数据,我们可以使用类型断言:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    jsonStr := `{"Age": 18,"name": "Jim" ,"s": "男","Lessons":["English","History"],"Room":201,"n":null,"b":false}`

    var data map[string]interface{}
    if err := json.Unmarshal([]byte(jsonStr), &data); err == nil {
        fmt.Println("map结构")
        fmt.Println(data)
    }

    for k, v := range data {
        switch vv := v.(type) {
        case string:
            fmt.Println(k, "是string", vv)
        case bool:
            fmt.Println(k, "是bool", vv)
        case float64:
            fmt.Println(k, "是float64", vv)
        case nil:
            fmt.Println(k, "是nil", vv)
        case []interface{}:
            fmt.Println(k, "是array:")
            for i, u := range vv {
                fmt.Println(i, u)
            }
        default:
            fmt.Println(k, "未知数据类型")
        }
    }
}

程序输出:
map结构
map[n:<nil> b:false Age:18 name:Jim s:男 Lessons:[English History] Room:201]
name 是string Jim
s 是string 男
Lessons 是array:
0 English
1 History
Room 是float64 201
n 是nil <nil>
b 是bool false
Age 是float64 18

通过这种方式,即使是未知 json 数据结构,我们也可以反序列化,同时可以确保类型安全。在switch-type中,我们可以根据表16-3 json与Go数据类型对照表来做选择。比如Age是float64而不是int类型,另外json的booleans、null类型在json也常常出现,在这里都做了case。

(三)json数据编码和解码:

json 包提供 Decoder 和 Encoder 类型来支持常用 json 数据流读写。NewDecoder 和 NewEncoder 函数分别封装了 io.Reader 和 io.Writer 接口。

func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder

如果要想把 json 直接写入文件,可以使用 json.NewEncoder 初始化文件(或者任何实现 io.Writer 的类型),并调用 Encode();反过来与其对应的是使用 json.Decoder 和 Decode() 函数:

func NewDecoder(r io.Reader) *Decoder
func (dec *Decoder) Decode(v interface{}) error

由于 Go 语言中很多标准包都实现了 Reader 和 Writer接口,因此 Encoder 和 Decoder 使用起来非常方便。

例如,下面例子使用 Decode方法解码一段json格式数据,同时使用Encode方法将我们的结构体数据保存到文件t.json中:

package main

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

type Human struct {
    name   string `json:"name"` // 姓名
    Gender string `json:"s"`    // 性别,性别的tag表明在json中为s字段
    Age    int    `json:"Age"`  // 年龄
    Lesson
}

type Lesson struct {
    Lessons []string `json:"lessons"`
}

func main() {
    // json数据的字符串
    jsonStr := `{"Age": 18,"name": "Jim" ,"s": "男",
    "lessons":["English","History"],"Room":201,"n":null,"b":false}`
    strR := strings.NewReader(jsonStr)
    h := &Human{}

    // Decode 解码json数据到结构体Human中
    err := json.NewDecoder(strR).Decode(h)

    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(h)

    // 定义Encode需要的Writer
    f, err := os.Create("./t.json")

    // 把保存数据的Human结构体对象编码为json保存到文件
    json.NewEncoder(f).Encode(h)

}

程序输出:
&{ 男 18 {[English History]}}

我们调用json.NewDecoder 函数构造了 Decoder 对象,使用这个对象的 Decode方法解码给定义好的结构体对象h。对于字符串,使用 strings.NewReader 方法,让字符串变成一个 Reader。

类似解码过程,我们通过json.NewEncoder()函数来构造Encoder对象,由于os中文件操作已经实现了Writer接口,所以可以直接使用,把h结构体对象编码为json数据格式保存在文件t.json中。

文件t.json中内容为:

{"s":"男","Age":18,"lessons":["English","History"]}

(四)json数据延迟解析

Human.Name字段,由于可以等到使用的时候,再根据具体数据类型来解析,因此我们可以延迟解析。当结构体Human的Name字段的类型设置为 json.RawMessage 时,它将在解码后继续以 byte 数组方式存在。

package main

import (
    "encoding/json"
    "fmt"
)

type Human struct {
    Name   json.RawMessage `json:"name"` // 姓名,json.RawMessage 类型不会进行解码
    Gender string          `json:"s"`    // 性别,性别的tag表明在json中为s字段
    Age    int             `json:"Age"`  // 年龄
    Lesson
}

type Lesson struct {
    Lessons []string `json:"lessons"`
}

func main() {
    jsonStr := `{"Age": 18,"name": "Jim" ,"s": "男",
    "lessons":["English","History"],"Room":201,"n":null,"b":false}`

    var hu Human
    if err := json.Unmarshal([]byte(jsonStr), &hu); err == nil {
        fmt.Printf("\n 结构体Human \n")
        fmt.Printf("%+v \n", hu) // 可以看到Name字段未解码,还是字节数组
    }

    // 对延迟解码的Human.Name进行反序列化
    var UName string
    if err := json.Unmarshal(hu.Name, &UName); err == nil {
        fmt.Printf("\n Human.Name: %s \n", UName)
    }
}

程序输出:

 结构体Human 
{Name:[34 74 105 109 34] Gender:男 Age:18 Lesson:{Lessons:[English History]}} 

 Human.Name: Jim 

在对json数据第一次解码后,保存在Human的hu.Name的值还是二进制数组,在后面对hu.Name进行解码后才真正发序列化为string类型的真实字符串对象。

除了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文件

syntax = "proto3";

package learn;

message UserInfo {
    int32 UserType = 1;     //必选字段
    string UserName = 2;    //必选字段
    string UserInfo = 3;    //必选字段
}

运行如下命令

> protoc --go_out=.  msg.proto

会生成一个msg.pb.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。

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())
}
程序输出:

GetUserType: 1
GetUserName: Jok
GetUserInfo: I am a woker!

通过上面的介绍,我们已经学会了怎么使用protobuf来处理我们的数据。

本书《Go语言四十二章经》内容在github上同步地址:https://github.com/ffhelicopter/Go42
本书《Go语言四十二章经》内容在简书同步地址: //www.greatytc.com/nb/29056963

虽然本书中例子都经过实际运行,但难免出现错误和不足之处,烦请您指出;如有建议也欢迎交流。
联系邮箱:roteman@163.com

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 从右往左写 墨迹未干怕毁了这纸 小心翼翼地写 第一次写心经 留个黑历史哈哈哈
    伐伐Eve阅读 896评论 8 12
  • 最近她和他赌气的次数越来越多了,想抛弃他的想法越来越强烈,他似乎也有所感应。她不知道为什么仅仅一年多他就变成这样。...
    L君爱吃柠檬阅读 244评论 0 0