Go Protobuf 資源的可讀化
工作上有大量協議採用 Google Protocol Buffer,關於 Protobuf 的簡單介紹可以看 IBM 的ofollow,noindex">《Google Protocol Buffer 的使用和原理》 這篇介紹。簡單來說,Protobuf 的優點是(相比 XML)更小、更快、更簡單,同時可以向後相容。缺點的話,對我日常工作影響比較大的就是可讀性較差,因為 Protobuf 壓縮的時候會做序列化,生成 pb 檔案,這個檔案是二進位制的,無法做到 human readable。但在日常工作中,尤其是排查問題是,經常需要看資原始檔內容是否正確、上下游服務收發包內容是否正確、偽造 pb 資源等等,這些內容都是 pb 的,需要經過轉換才能讀懂,由此就用 Go 寫了利用 JSON 偽造 pb 資源和反序列化 pb 列印成人類可讀的文字的兩段程式。
JSON 轉 pb
這個感覺起來是件很麻煩的事情,但是有了jsonpb 這個庫之後,事情就變得很簡單了。
首先定義 user.proto 。
syntax = "proto3"; package user_info; message UserInfo { message User { string username = 1; uint32 age= 2; string graduate = 3; } repeated User user_list = 1; }
然後再轉換生成 user.pb.go 檔案。
protoc --go_out=. user.proto
編寫 JSON 檔案,注意 key 的名字需要遵循 user.pb.go 中的名字,例如:
type UserInfo struct { UserList []*UserInfo_User `protobuf:"bytes,1,rep,name=user_list,json=userList" json:"user_list,omitempty"` } type UserInfo_User struct { Username string `protobuf:"bytes,1,opt,name=username" json:"username,omitempty"` Ageuint32 `protobuf:"varint,2,opt,name=age" json:"age,omitempty"` Graduate string `protobuf:"bytes,3,opt,name=graduate" json:"graduate,omitempty"` }
user.pb.go 已經指定了一個 field 在 JSON 中的命名,直接按照這個編寫 JSON 檔案即可。
{ "userList": [ { "username": "lawrencelin", "age": 28, "graduate": "Tongji University" }, { "username": "findingsea", "age": 28, "graduate": "Fudan University" } ] }
編寫主程式碼:
package main import ( "github.com/golang/protobuf/proto" "io/ioutil" "os" "fmt" "github.com/golang/protobuf/jsonpb" "user_proto" ) func main(){ jsonFilePath := "/home/lawrence/GoglandProjects/JsonToPbIntro/json/user_info.json" pbFilePath := "/home/lawrence/GoglandProjects/JsonToPbIntro/pb/user_info.pb" buf, err := ioutil.ReadFile(jsonFilePath) if err != nil { fmt.Println("Read file err: ", err) os.Exit(0) } userInfo := &user_info.UserInfo{} if err = jsonpb.UnmarshalString(string(buf), userInfo); err != nil { fmt.Println("jsonpb UnmarshalString fail: ", err) os.Exit(0) } fmt.Println("user info pb: ", userInfo.String()) data, err := proto.Marshal(userInfo) if err != nil { fmt.Println("proto Marshal fail: ", err) os.Exit(0) } if err = ioutil.WriteFile(pbFilePath, data, os.ModePerm); err != nil { fmt.Println("Write file err: ", err) } }
核心函式就是UnmarshalString ,輸入是 JSON 字串,輸出 Protobuf 物件。
func UnmarshalString(str string, pb proto.Message) error
執行一下 main.go,就生成好了 user_info.pb 檔案,列印如下:
user info pb:user_list:<username:"lawrencelin" age:28 graduate:"Tongji University" > user_list:<username:"findingsea" age:28 graduate:"Fudan University" >
列印 Protobuf 物件
這一邊本來應該很簡單的,因為 Protobuf 庫就提供了字串轉換函式,像 C++ 版 Protobuf 直接提供了DebugString()
方法,可以直接輸出可讀的列印字串。但是 Go 裡面,我直覺反應呼叫了一下String()
方法,fmt.Println("user info pb: ", userInfo.String())
,發現只能列印成一行。
user_list:<username:"lawrencelin" age:28 graduate:"Tongji University" > user_list:<username:"findingsea" age:28 graduate:"Fudan University" >
看了一下String()
方法的實現,直接呼叫了CompactTextString
方法:
func (m *UserInfo) String() string{ return proto.CompactTextString(m) } // CompactText writes a given protocol buffer in compact text format (one line). func CompactText(w io.Writer, pb Message) error { return compactTextMarshaler.Marshal(w, pb) } // CompactTextString is the same as CompactText, but returns the string directly. func CompactTextString(pb Message) string { return compactTextMarshaler.Text(pb) }
註釋裡說明了這個介面只能返回壓縮過的文字,這個可讀性就很差了,那如何輸出可讀的 Protobuf 物件呢?
看了文件之後,發現應該使用MarshalTextString
介面,就可以直接返回可讀的文字格式 Protobuf 物件。其介面原始碼和註釋如下:
// MarshalText writes a given protocol buffer in text format. // The only errors returned are from w. func MarshalText(w io.Writer, pb Message) error { return defaultTextMarshaler.Marshal(w, pb) } // MarshalTextString is the same as MarshalText, but returns the string directly. func MarshalTextString(pb Message) string { return defaultTextMarshaler.Text(pb) }
呼叫的方法很簡單,fmt.Println(proto.MarshalTextString(userInfo))
,輸出:
user_list: < username: "lawrencelin" age: 28 graduate: "Tongji University" > user_list: < username: "findingsea" age: 28 graduate: "Fudan University" >