Go语言使用protobuf快速入门

简介: protobuf 即 Protocol Buffers,是一种轻便高效的结构化数据存储格式,与语言、平台无关,可扩展可序列化。 protobuf 性能和效率大幅度优于 JSON、XML 等其他的结构化数据格式。 protobuf 是以二进制方式存储的,占用空间小,但也带来了可读性差的缺点。protobuf 在通信协议和数据存储等领域应用广泛。

前言

protobuf 即 Protocol Buffers,是一种轻便高效的结构化数据存储格式,与语言、平台无关,可扩展可序列化。
protobuf 性能和效率大幅度优于 JSON、XML 等其他的结构化数据格式。
protobuf 是以二进制方式存储的,占用空间小,但也带来了可读性差的缺点。protobuf 在通信协议和数据存储等领域应用广泛。

Protobuf 在 .proto 定义需要处理的结构化数据,可以通过 protoc 工具,将 .proto 文件转换为 C、C++、Golang、Java、Python 等多种语言的代码,兼容性好,易于使用。

参考文献

本文文章持续更新于:https://github.com/mailjobblog/dev_go/tree/master/220115_protobuf
protobuf3 官方文档:https://link.jianshu.com/?t=https://developers.google.com/protocol-buffers/docs/proto3
Protocol Buffer 编码:https://developers.google.com/protocol-buffers/docs/encoding?hl=zh-cn#packed
proto service grpc 生成插件:https://github.com/protocolbuffers/protobuf/blob/master/docs/third_party.md
本文代码下载:https://github.com/mailjobblog/dev_go/tree/master/220115_protobuf

安装

安装 protoc
Protobuf Releases 下载最先版本的发布包安装。

brew intall protoc

安装 protoc-gen-go
我们需要在 Golang 中使用 protobuf,还需要安装 protoc-gen-go,这个工具用来将 .proto 文件转换为 Golang 代码。

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

Tips:
这儿有个小小的坑,github.com/golang/protobuf/protoc-gen-gogoogle.golang.org/protobuf/cmd/protoc-gen-go是不同的。
区别在于前者是旧版本,后者是google接管后的新版本,他们之间的API是不同的,也就是说用于生成的命令,以及生成的文件都是不一样的。

检查是否安装成功

$ protoc --version
libprotoc 3.19.3

$ protoc-gen-go --version
protoc-gen-go v1.27.1

protobuf生成代码

快速上手

接下来,我们创建一个非常简单的示例,student.proto

syntax = "proto3";
package main;

// this is a comment
message Student {
  string name = 1;
  bool male = 2;
  repeated int32 scores = 3;
}

在当前目录下执行代码生成命令

$ protoc --go_out=. *.proto

$ ls
student.pb.go  student.proto

执行此生成命令是将该目录下的所有的 .proto 文件转换为 Go 代码,我们可以看到该目录下多出了一个 Go 文件 student.pb.go。这个文件内部定义了一个结构体 Student,以及相关的方法:

type Student struct {
   
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Name   string  `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    Male   bool    `protobuf:"varint,2,opt,name=male,proto3" json:"male,omitempty"`
    Scores []int32 `protobuf:"varint,3,rep,packed,name=scores,proto3" json:"scores,omitempty"`
}

得到生成的 student.pb.go 文件后,可以在项目代码中直接使用了。
以下是一个例子,即证明被序列化的和反序列化后的实例,包含相同的数据。

func main() {
   
    test := &Student{
   
        Name: "geektutu",
        Male:  true,
        Scores: []int32{
   98, 85, 88},
    }
    data, err := proto.Marshal(test)
    if err != nil {
   
        log.Fatal("marshaling error: ", err)
    }
    newTest := &Student{
   }
    err = proto.Unmarshal(data, newTest)
    if err != nil {
   
        log.Fatal("unmarshaling error: ", err)
    }
    // Now test and newTest contain the same data.
    if test.GetName() != newTest.GetName() {
   
        log.Fatalf("data mismatch %q != %q", test.GetName(), newTest.GetName())
    }
}

枚举(Enumerations)

枚举类型适用于提供一组预定义的值,选择其中一个。例如我们将性别(gender)定义为枚举类型。

message StudentEnum {
  string name = 1;
  enum Gender {
    FEMALE = 0;
    MALE = 1;
  }
  Gender gender = 2;
  repeated int32 scores = 3;
}

生成的Go代码主要信息如下:

type StudentEnum_Gender int32

const (
    StudentEnum_FEMALE StudentEnum_Gender = 0
    StudentEnum_MALE   StudentEnum_Gender = 1
)

type StudentEnum struct {
   
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Name   string             `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    Gender StudentEnum_Gender `protobuf:"varint,2,opt,name=gender,proto3,enum=main.StudentEnum_Gender" json:"gender,omitempty"`
    Scores []int32            `protobuf:"varint,3,rep,packed,name=scores,proto3" json:"scores,omitempty"`
}

枚举类型的第一个选项的标识符必须是0,这也是枚举类型的默认值。

别名(Alias)

允许为不同的枚举值赋予相同的标识符,称之为别名,需要打开allow_alias选项。

message StudentAlias {
  enum Status {
    option allow_alias = true;
    UNKOWN = 0;
    STARTED = 1;
    RUNNING = 1;
  }
}

生成的Go语言主要代码如下:

type StudentAlias_Status int32

const (
    StudentAlias_UNKOWN  StudentAlias_Status = 0
    StudentAlias_STARTED StudentAlias_Status = 1
    StudentAlias_RUNNING StudentAlias_Status = 1
)

使用其他消息类型

Result是另一个消息类型,在 SearchReponse 作为一个消息字段类型使用。

message StudentResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

嵌套写也是支持的:

message Student2Response {
  message Result2 {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result2 results2 = 1;
}

如果定义在其他文件中,可以导入其他消息类型来使用:

import "myproject/other_protos.proto";

任意类型(Any)

在使用 GRPC 时,常规的操作是将 message 定义好后进行数据传输,但总会遇到某些数据结构进行组合的操作,采用默认的定义 message 方式,造成代码量的激增。
为了解决这个问题 protobuf 提供类型 any 解决 GRPC 中泛型的处理方式

message StudentAny {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

生成的Go语言主要代码如下:

type StudentAny struct {
   
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Message string       `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
    Details []*anypb.Any `protobuf:"bytes,2,rep,name=details,proto3" json:"details,omitempty"`
}

oneof

如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof特性节省内存。
Oneof字段就像可选字段, 除了它们会共享内存, 至多一个字段会被设置。 设置其中一个字段会清除其它字段。

message StudentOneOf {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

oneof的特性

  • 设置oneof会自动清楚其它oneof字段的值. 所以设置多次后,只有最后一次设置的字段有值
  • 如果解析器遇到同一个oneof中有多个成员,只有最后一个会被解析成消息
  • oneof不支持repeated

map

message StudentMap {
  map<string, int32> points = 1;
}

定义服务(Services)

如果消息类型是用来远程通信的(Remote Procedure Call, RPC),可以在 .proto 文件中定义 RPC 服务接口。
例如我们定义了一个名为 SearchService 的 RPC 服务,提供了 Search 接口,入参是 SearchRequest 类型,返回类型是 SearchResponse

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

官方仓库也提供了一个 插件列表,帮助开发基于 Protocol Buffer 的 RPC 服务。

生成go代码和grpc代码:

# 由proto生成go代码
protoc --go_out=. *.proto

# 由proto生成go的grpc代码
protoc --go-grpc_out=. *.proto

protoc 命令参数

protoc --proto_path=IMPORT_PATH --<lang>_out=DST_DIR path/to/file.proto

--proto_path=IMPORT_PATH: 可以在 .proto 文件中 import 其他的 .proto 文件,proto_path 即用来指定其他 .proto 文件的查找目录。如果没有引入其他的 .proto 文件,该参数可以省略。
--_out=DST_DIR: 指定生成代码的目标文件夹,例如 –go_out=. 即生成 GO 代码在当前文件夹,另外支持 cpp/java/python/ruby/objc/csharp/php 等语言

推荐风格

文件(Files)

  • 文件名使用小写下划线的命名风格,例如 lower_snake_case.proto
  • 每行不超过 80 字符
  • 使用 2 个空格缩进

包(Packages)

  • 包名应该和目录结构对应,例如文件在my/package/目录下,包名应为 my.package

消息和字段(Messages & Fields)

  • 消息名使用首字母大写驼峰风格(CamelCase),例如message StudentRequest { ... }
  • 字段名使用小写下划线的风格,例如 string status_code = 1
  • 枚举类型,枚举名使用首字母大写驼峰风格,例如 enum FooBar,枚举值使用全大写下划线隔开的风格(CAPITALS_WITH_UNDERSCORES ),例如 FOO_DEFAULT=1

服务(Services)

  • RPC 服务名和方法名,均使用首字母大写驼峰风格,例如service FooService{ rpc GetSomething() }

protobuf文件规范

syntax

protobuf 有2个版本,默认版本是 proto2,如果需要 proto3,则需要在非空非注释第一行使用 syntax = "proto3" 标明版本。

package

package,即包名声明符是可选的,用来防止不同的消息类型有命名冲突。

option go_package

option go_package="./proto/pb;pb";

这部分的内容是关于最后生成的go文件是处在哪个目录哪个包中,./proto/pb 代表在当前目录生成,pb 代表了生成的go文件的包名是 pb。

message

消息类型 使用 message 关键字定义,Student 是类型名,name, male, scores 是该类型的 3 个字段,类型分别为 string, bool 和 []int32。字段可以是标量类型,也可以是合成类型。
相当于Go语言中的 struct 结构体。一个 .proto 文件中可以写多个消息类型,即对应多个结构体(struct)。

修饰符

每个字段的修饰符默认是 singular,一般省略不写,repeated 表示字段可重复,即用来表示 Go 语言中的切片类型。

标识符

每个字符 = 后面的数字称为标识符,每个字段都需要提供一个唯一的标识符。标识符用来在消息的二进制格式中识别各个字段,一旦使用就不能够再改变,标识符的取值范围为 [1, 2^29 - 1] 。

文件注释

.proto 文件可以写注释,单行注释 //,多行注释 / ... /

标量类型(Scalar)

.proto Type Notes C++ Type Java Type Python Type[2] Go Type Ruby Type C# Type PHP Type
double double double float float64 Float double float
float float float float float32 Float float float
int32 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
uint32 使用变长编码 uint32 int int/long uint32 Fixnum 或者 Bignum(根据需要) uint integer
uint64 使用变长编码 uint64 long int/long uint64 Bignum ulong integer/string
sint32 使用变长编码,这些编码在负值时比int32高效的多 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
sint64 使用变长编码,有符号的整型值。编码时比通常的int64高效。 int64 long int/long int64 Bignum long integer/string
fixed32 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 uint32 int int uint32 Fixnum 或者 Bignum(根据需要) uint integer
fixed64 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 uint64 long int/long uint64 Bignum ulong integer/string
sfixed32 总是4个字节 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
sfixed64 总是8个字节 int64 long int/long int64 Bignum long integer/string
bool bool boolean bool bool TrueClass/FalseClass bool boolean
string 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 string String str/unicode string String (UTF-8) string string
bytes 可能包含任意顺序的字节数据。 string ByteString str []byte String (ASCII-8BIT) ByteString string

标量类型如果没有被赋值,则不会被序列化,解析时,会赋予默认值

  • strings:空字符串
  • bytes:空序列
  • bools:false
  • 数值类型:0

常见问题

go_package报错

Please specify either:
• a "go_package" option in the .proto source file, or
• a "M" argument on the command line.

在go的1.14版本以后,proto文件中不添加go_package 会报错。
解决方法: option go_package = "./"
或者填写自己的包路径也行如option go_package = "http://github.com/package/name"

安装protoc-gen-go报错

can't load package: package google.golang.org/protobuf/cmd/protoc-gen-go: cannot find package "google.golang.org/protobuf/cmd/protoc-gen-go" in any of:
        C:\Go\src\google.golang.org\protobuf\cmd\protoc-gen-go (from $GOROOT)
        C:\Users\peikai\go\src\google.golang.org\protobuf\cmd\protoc-gen-go (from $GOPATH)

解决方法:
go get google.golang.org/protobuf/cmd/protoc-gen-go 然后再 install 安装。

相关文章
|
21小时前
|
缓存 测试技术 持续交付
Golang深入浅出之-Go语言中的持续集成与持续部署(CI/CD)
【5月更文挑战第5天】本文介绍了Go语言项目中的CI/CD实践,包括持续集成与持续部署的基础知识,常见问题及解决策略。测试覆盖不足、版本不一致和构建时间过长是主要问题,可通过全面测试、统一依赖管理和利用缓存优化。文中还提供了使用GitHub Actions进行自动化测试和部署的示例,强调了持续优化CI/CD流程以适应项目需求的重要性。
28 1
|
22小时前
|
Kubernetes Cloud Native Go
Golang深入浅出之-Go语言中的云原生开发:Kubernetes与Docker
【5月更文挑战第5天】本文探讨了Go语言在云原生开发中的应用,特别是在Kubernetes和Docker中的使用。Docker利用Go语言的性能和跨平台能力编写Dockerfile和构建镜像。Kubernetes,主要由Go语言编写,提供了方便的客户端库与集群交互。文章列举了Dockerfile编写、Kubernetes资源定义和服务发现的常见问题及解决方案,并给出了Go语言构建Docker镜像和与Kubernetes交互的代码示例。通过掌握这些技巧,开发者能更高效地进行云原生应用开发。
28 1
|
22小时前
|
负载均衡 监控 Go
Golang深入浅出之-Go语言中的服务网格(Service Mesh)原理与应用
【5月更文挑战第5天】服务网格是处理服务间通信的基础设施层,常由数据平面(代理,如Envoy)和控制平面(管理配置)组成。本文讨论了服务发现、负载均衡和追踪等常见问题及其解决方案,并展示了使用Go语言实现Envoy sidecar配置的例子,强调Go语言在构建服务网格中的优势。服务网格能提升微服务的管理和可观测性,正确应对问题能构建更健壮的分布式系统。
17 1
|
1天前
|
消息中间件 Go API
Golang深入浅出之-Go语言中的微服务架构设计与实践
【5月更文挑战第4天】本文探讨了Go语言在微服务架构中的应用,强调了单一职责、标准化API、服务自治和容错设计等原则。同时,指出了过度拆分、服务通信复杂性、数据一致性和部署复杂性等常见问题,并提出了DDD拆分、使用成熟框架、事件驱动和配置管理与CI/CD的解决方案。文中还提供了使用Gin构建HTTP服务和gRPC进行服务间通信的示例。
13 0
|
1天前
|
Prometheus 监控 Cloud Native
Golang深入浅出之-Go语言中的分布式追踪与监控系统集成
【5月更文挑战第4天】本文探讨了Go语言中分布式追踪与监控的重要性,包括追踪的三个核心组件和监控系统集成。常见问题有追踪数据丢失、性能开销和监控指标不当。解决策略涉及使用OpenTracing或OpenTelemetry协议、采样策略以及聚焦关键指标。文中提供了OpenTelemetry和Prometheus的Go代码示例,强调全面可观测性对微服务架构的意义,并提示选择合适工具和策略以确保系统稳定高效。
13 5
|
1天前
|
监控 算法 Go
Golang深入浅出之-Go语言中的服务熔断、降级与限流策略
【5月更文挑战第4天】本文探讨了分布式系统中保障稳定性的重要策略:服务熔断、降级和限流。服务熔断通过快速失败和暂停故障服务调用来保护系统;服务降级在压力大时提供有限功能以保持整体可用性;限流控制访问频率,防止过载。文中列举了常见问题、解决方案,并提供了Go语言实现示例。合理应用这些策略能增强系统韧性和可用性。
15 0
|
1天前
|
负载均衡 算法 Go
Golang深入浅出之-Go语言中的服务注册与发现机制
【5月更文挑战第4天】本文探讨了Go语言中服务注册与发现的关键原理和实践,包括服务注册、心跳机制、一致性问题和负载均衡策略。示例代码演示了使用Consul进行服务注册和客户端发现服务的实现。在实际应用中,需要解决心跳失效、注册信息一致性和服务负载均衡等问题,以确保微服务架构的稳定性和效率。
11 3
|
1天前
|
中间件 Go
Go语言中的中间件设计与实现
【5月更文挑战第4天】Go语言中的中间件在HTTP请求处理中扮演重要角色,提供了一种插入逻辑层的方式,便于实现日志、认证和限流等功能,而不增加核心代码复杂性。中间件遵循`http.Handler`接口,通过函数组合实现。常见问题包括错误处理(确保中间件能正确处理并传递错误)和请求上下文管理(使用`context.Context`共享数据以避免并发问题)。通过理解中间件机制和最佳实践,可以构建更健壮的Web应用。
18 0
|
3天前
|
前端开发 Go
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
【5月更文挑战第3天】Go语言通过goroutines和channels实现异步编程,虽无内置Future/Promise,但可借助其特性模拟。本文探讨了如何使用channel实现Future模式,提供了异步获取URL内容长度的示例,并警示了Channel泄漏、错误处理和并发控制等常见问题。为避免这些问题,建议显式关闭channel、使用context.Context、并发控制机制及有效传播错误。理解并应用这些技巧能提升Go语言异步编程的效率和健壮性。
14 5
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
|
3天前
|
监控 负载均衡 算法
Golang深入浅出之-Go语言中的协程池设计与实现
【5月更文挑战第3天】本文探讨了Go语言中的协程池设计,用于管理goroutine并优化并发性能。协程池通过限制同时运行的goroutine数量防止资源耗尽,包括任务队列和工作协程两部分。基本实现思路涉及使用channel作为任务队列,固定数量的工作协程处理任务。文章还列举了一个简单的协程池实现示例,并讨论了常见问题如任务队列溢出、协程泄露和任务调度不均,提出了解决方案。通过合理设置缓冲区大小、确保资源释放、优化任务调度以及监控与调试,可以避免这些问题,提升系统性能和稳定性。
15 6