Go Protobuf 资源的可读化

工作上有大量协议采用 Google Protocol Buffer,关于 Protobuf 的简单介绍可以看 IBM 的《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"`
    Age      uint32 `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"
>

1.1k 声望
63 粉丝
0 条评论
推荐阅读
关于 C++ vector 的两个小 tips
本来这篇文章标题我想起成《关于 vector 的两个小坑》,后来想想,其实也不算是坑,还是自己对原理性的东西理解的没做那么透彻。工作中遇到的很多问题,后来归根到底都是基础不牢靠。

findingea阅读 1.4k

前端如何入门 Go 语言
类比法是一种学习方法,它是通过将新知识与已知知识进行比较,从而加深对新知识的理解。在学习 Go 语言的过程中,我发现,通过类比已有的前端知识,可以更好地理解 Go 语言的特性。

robin23阅读 3.2k评论 6

封面图
Golang 中 []byte 与 string 转换
string 类型和 []byte 类型是我们编程时最常使用到的数据结构。本文将探讨两者之间的转换方式,通过分析它们之间的内在联系来拨开迷雾。

机器铃砍菜刀24阅读 58k评论 2

年度最佳【golang】map详解
这篇文章主要讲 map 的赋值、删除、查询、扩容的具体执行过程,仍然是从底层的角度展开。结合源码,看完本文一定会彻底明白 map 底层原理。

去去100216阅读 11.5k评论 2

年度最佳【golang】GMP调度详解
Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱, 虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻底的. 这篇文章将通过分析...

去去100215阅读 11.9k评论 4

万字详解,吃透 MongoDB!
MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的。MongoDB 提供了 面向文档 的存储方式,操作起来比较简单和容易,支持“无模式”的数据建模,可以存储比较复杂的数据类型,是一款非常...

JavaGuide8阅读 1.7k

封面图
数据结构与算法:二分查找
一、常见数据结构简单数据结构(必须理解和掌握)有序数据结构:栈、队列、链表。有序数据结构省空间(储存空间小)无序数据结构:集合、字典、散列表,无序数据结构省时间(读取时间快)复杂数据结构树、 堆图二...

白鲸鱼9阅读 5.3k

1.1k 声望
63 粉丝
宣传栏