Go語言中的數據格式(json、xml 、msgpack、protobuf)


 在分布式的系統中,因為涉及到數據的傳輸,所以一定會進行數據的交換,此時就要定義數據交換的格式,例如二進制、Json、Xml等等。本篇文章就是總結一下常用的幾種數據格式。

 一、Json格式

如果想使用Json數據格式,可以借助於encoding/json這個包。

利用json包里的 json.Marshal(xxx) 和 json.Unmarshal(data, &xxx) 進行序列化和反序列化。

 下面舉個例子:

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "math/rand"
)

type Student struct {
    Name string
    Age  int
    Sex  string
}

//寫入json數據
func writeJson(filename string) (err error) {
    var students []*Student
    //隨機生成10個學生數據
    for i := 0; i < 10; i++ {
        p := &Student{
            Name: fmt.Sprintf("name%d", i),
            Age:  rand.Intn(100),
            Sex:  "Man",
        }

        students = append(students, p)
    }

    //執行序列化操作
    data, err := json.Marshal(students)
    if err != nil {
        fmt.Printf("=marshal failed, err:%v\n", err)
        return
    }

    //將數據寫到一個文件當中
    err = ioutil.WriteFile(filename, data, 0755)
    if err != nil {
        fmt.Printf("write file failed, err:%v\n", err)
        return
    }

    return
}

//讀取json數據
func readJson(filename string) (err error) {
    var students []*Student
    data, err := ioutil.ReadFile(filename)
    if err != nil {
        return
    }

    err = json.Unmarshal(data, &students)
    if err != nil {
        return
    }

    for _, v := range students {
        fmt.Printf("%#v\n", v)
    }
    return
}

 執行:

func main() {
    filename := "C:/tmp/Students.txt"
    err := writeJson(filename)
    if err != nil {
        fmt.Printf("write json failed, err:%v\n", err)
        return
    }

    err = readJson(filename)
    if err != nil {
        fmt.Printf("read json failed, err:%v\n", err)
        return
    }
}

執行結果:

1.可以看到在C:/tmp/下面生成了一個Students.txt文件,打開里面存放是剛剛隨機生成的10個學生數據

2.執行結果可以看到控制台打印:

二、Xml格式

Xml格式也是我們常用的數據格式,同樣要使用Xml格式,可以使用encoding/xml這個包。

像上面json一樣,同樣存在 xml.Marshal(xxx) 和 xml.Unmarshal(data, &xxx) 兩個方法。此外還有方法xml.MarshalIndent(xxx) 可以格式化xml

先熟悉一下XML對應 標簽怎么寫:

- XMLName字段,如上所述,會省略

- 具有標簽"-"的字段會省略

- 具有標簽"name,attr"的字段會成為該XML元素的名為name的屬性

- 具有標簽",attr"的字段會成為該XML元素的名為字段名的屬性

- 具有標簽",chardata"的字段會作為字符數據寫入,而非XML元素

- 具有標簽",innerxml"的字段會原樣寫入,而不會經過正常的序列化過程

- 具有標簽",comment"的字段作為XML注釋寫入,而不經過正常的序列化過程,該字段內不能有"--"字符串

- 標簽中包含"omitempty"選項的字段如果為空值會省略

  空值為false、0、nil指針、nil接口、長度為0的數組、切片、映射

- 匿名字段(其標簽無效)會被處理為其字段是外層結構體的字段

- 如果一個字段的標簽為"a>b>c",則元素c將會嵌套進其上層元素a和b中。如果該字段相鄰的字段標簽指定了同樣的上層元素,則會放在同一個XML元素里。

原文鏈接:https://blog.csdn.net/yuyinghua0302/article/details/84568531

下面舉個例子:

例如我想創建一個如下的xml數據:

<Servers version="2.0">
    <server>
        <serverName>Server0</serverName>
        <serverIP>192.168.1.0</serverIP>
    </server>
    <server>
        <serverName>Server1</serverName>
        <serverIP>192.168.1.1</serverIP>
    </server>
</Servers>

我就可以創建下面這樣的結構體:

//最外層的xml
type Servers struct {
    XMLName xml.Name  `xml:"Servers"`
    Version string    `xml:"version,attr"`
    Servers []*Server `xml:"server"`
}

//具體的server
type Server struct {
    ServerName string `xml:"serverName"`
    ServerIP   string `xml:"serverIP"`
}

寫文件方法:

func writeXml(fileName string) (err error) {
    //創建一個*Server類型的數組
    var serverList []*Server
    for i := 0; i < 2; i++ {
        s := &Server{
            ServerName: fmt.Sprintf("Server%d", i),
            ServerIP:   fmt.Sprintf("192.168.1.%d", i),
        }
        serverList = append(serverList, s)
    }

    var myServers *Servers = &Servers{
        Version: "2.0",
        Servers: serverList,
    }

    //執行序列化操作
    data, err := xml.MarshalIndent(myServers, "", "    ")
    if err != nil {
        fmt.Printf("=marshal failed, err:%v\n", err)
        return
    }

    //將數據寫到一個文件當中
    err = ioutil.WriteFile(fileName, data, 0755)
    if err != nil {
        fmt.Printf("write file failed, err:%v\n", err)
        return
    }

    return
}

如上代碼,使用了MarshalIndent方法,第一個參數是需要序列化的數據,第二參數是前綴,第三個是縮進的字符串(這里是四個空格),然后在main方法中調用一下即可(代碼略)。

這里主要想說明一下結構體里面的標簽:

XmlName可以省略不寫,不寫的話最外層就是用的結構體的名稱,例如第一個結構體是Servers,那么xml最外層的節點名稱就是Servers。

 

讀的話,使用 xml.Unmarshal(data, &xxx) 就可以實現了。

func readXml(fileName string) (err error) {
    var myServers *Servers
    data, err := ioutil.ReadFile(fileName)
    if err != nil {
        return
    }

    err = xml.Unmarshal(data, &myServers)
    if err != nil {
        return
    }

    fmt.Printf("XMLNAME = %v\n", myServers.XMLName)
    fmt.Printf("Version = %v\n", myServers.Version)
    for _, v := range myServers.Servers {
        fmt.Printf("%v\n", v)
    }
    return
}

三、msgPack格式

上面兩種Json和Xml格式,都是文本格式的數據,好處在於能夠方便的閱讀。但是問題在於占用空間比較大。所以又出現了MsgPack這種格式,它是在json基礎上轉換為二進制進行傳輸的。對應關系像下面這個圖:

MsgPack並沒有官方的包,我們需要使用一個第三方的包,項目地址:https://github.com/vmihailenco/msgpack  

實現比較簡單,將 json.Marshal 和 json.Unmarshal 中的【 json】替換為【 maspack】即可,下面是對上面代碼的改造,創建了10000個學生的數據。

 

四、protobuf格式

 

protobuf是Google公司開發出的一種數據格式。官方文檔地址:https://developers.google.cn/protocol-buffers/

簡單講它使用了IDL語言作為中間語言來串聯不同的編程語言。不同的語言可以根據生成的IDL中間語言,生成自己的語言。

這樣做有什么好處? 舉個例子:當我們在協作開發的時候,A部門使用的是Go語言、B部分使用的是Java語言,C部門使用的是C#語言,當他們之間進行數據交換的時候,都要各自維護自己的結構體,才能進行數據的

序列化和反序列化,使用protobuf的好處就是只需要一個IDL描述,然后生成不同的語言的結構,這樣維護一份就可以了。

同時 prototbuf的性能也很好,這也是它的一個優勢。IDL語言使用的變長編碼(根據整數的范圍 0-255 那么這個數字就占用1個字節 ,如果使用定長編碼的話 一個整數可能就是 4個字節)所以它的空間利用率是很好的。

 

那開發流程是怎樣的?

A. IDL編寫

B. 生成只定語言的代碼

C. 序列化和反序列化

 

如何在Go中應用prototbuf

A.安裝protoc編譯器,解壓后拷貝到GOPATH/bin目錄下, 下載地址:https://github.com/google/protobuf/releases

 

然后把bin下面的protoc.exe 這個放到GoPath下的bin中,打開cmd,輸入protoc,應該會出現如下內容:

 

如果不存在,可以將Gopath的bin加入到系統的環境變量path當中。

 

B.安裝生成Go語言的插件

執行命令:go get -u github.com/golang/protobuf/protoc-gen-go

 

C. 創建一個簡單的proto文件 

//指定版本
//注意proto3與proto2的寫法有些不同
syntax = "proto3";

//包名,通過protoc生成時go文件時
package school;

//性別
//枚舉類型第一個字段必須為0
enum Sex {
    male = 0;
    female = 1;
    other =2;
}

//學生
message Student {
    Sex sex = 1;
    string Name = 2;
    int32 Age =3;
}

//班級
message Class{
    repeated Student Students =1;
    string Name; 
}

  message 就可以理解成類, repeated可以理解成數組。

D.利用之前下載好的protoc.exe 生成一個Go的代碼。 第一個【.】代表當前輸出的目錄,后面*.proto則是 proto文件的路徑

protoc--go_out=.  *.proto

protoc --go_out=.\school\ .\school.proto

執行之后會生成如下的文件,這個go文件就可以直接使用了。

 

 

E. 使用生成的Go文件

①使用 proto.Marshal() 執行序列化

func writeProto(filename string) (err error) {
    //創建學生信息
    var students []*school.Student
    for i := 0; i < 30; i++ {

        var sex = (school.Sex)(i % 3)
        student := &school.Student{
            Name: fmt.Sprintf("Student_%d", i),
            Age:  15,
            Sex:  sex,
        }

        students = append(students, student)
    }

    //創建班級信息
    var myClass school.Class
    myClass.Name = "我的班級"
    myClass.Students = students

    data, err := proto.Marshal(&myClass)
    if err != nil {
        fmt.Printf("marshal proto buf failed, err:%v\n", err)
        return
    }

    err = ioutil.WriteFile(filename, data, 0755)
    if err != nil {
        fmt.Printf("write file failed, err:%v\n", err)
        return
    }
    return
}

②使用proto.Unmarshal(data, &mySchool)執行反序列化

func readProto(filename string) (err error) {
    var mySchool school.Class
    data, err := ioutil.ReadFile(filename)
    if err != nil {
        return
    }
    err = proto.Unmarshal(data, &mySchool)
    if err != nil {
        return
    }

    fmt.Printf("proto:%v\n", mySchool)
    return
}

 

Q&A

如果在使用protobuf生成的Go文件,出現了如下的異常:

undefined: proto.ProtoPackageIsVersion3

這個時候可能是由於上面兩步下載的protoc.exe 和 protobuf 的版本不一致導致的。

1. 可以清空下gopath下的 github.com\golang\protobuf 然后重新下載,並在github.com\golang\protobuf\protoc-gen-go 執行 go install 命令。

2. 檢查一下是不是使用了 godep 等包管理工具,里面引用的版本和protoc.exe 不一致造成的


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM