[Golang] Go Protobuf: The new Opaque API 使用篇

333 阅读3分钟

介绍

今天看「The Go Blog」,发现最新的一篇文章是「Go Protobuf: The new Opaque API」。这篇文章介绍了一种生成.pb.go的新API——「Opaque API」,以这种API产生的Go struct,所有字段都是对外隐藏的,.pb.go文件提供一系列方法(get/set/build)来操作这些隐藏的字段。

较于旧API,Go官方称之为「Open Struct API」,也是我们日常使用得最多的API。顾名思义,它产生的Go struct的字段都是公开的。

文章还介绍了「Open Struct API」的弊端和「Opaque API」的好处,但本文仅介绍使用方法,即如何使用「Opaque API」生成新的.pb.go。

准备

  • 安装 protoc
    • brew install protoc@latest
  • 安装 protoc-gen-go
    • go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
  • 安装 protoc-gen-go-grpc:
    • go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

使用

回顾「Open Struct API」

我的目录结构:

├── client
│   └── main.go
├── pb
├── proto
│   └── test.proto
└── server
    └── main.go

该目录下包含grpc服务端和客户端,.proto文件定义在/proto下

🙋‍♀️🌰: test.proto

syntax = "proto3";
package pb;
option go_package="/pb";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

这个.proto文件通过以下指令编译后,会在/pb下生成.pb.go和_grpc.pb.go:

protoc --go_out=. --go-grpc_out=. proto/test.proto
├── pb
│   ├── test.pb.go
│   └── test_grpc.pb.go

使用「Opaque API」

根据Protocol Buffers的官方文档,使用如下指令可以在不修改.proto文件下生成新的.pb.go

protoc --go_opt=default_api_level=API_HYBRID --go_out=. --go-grpc_out=. proto/test.proto

较于上面的指令,该指令新增了--go_opt=default_api_level=API_HYBRID,指定protoc使用「Opaque API」去编译.proto文件,执行后,会发现在/pb下产生了三个文件:

├── pb
│   ├── test.pb.go
│   ├── test_grpc.pb.go
│   └── test_protoopaque.pb.go

关注test_protoopaque.pb.go,它就是新API的产物!

// ......

// The request message containing the user's name.
type HelloRequest struct {
	state           protoimpl.MessageState `protogen:"opaque.v1"`
	xxx_hidden_Name string                 `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
	unknownFields   protoimpl.UnknownFields
	sizeCache       protoimpl.SizeCache
}

// ......

func (x *HelloRequest) GetName() string {
	if x != nil {
		return x.xxx_hidden_Name
	}
	return ""
}

func (x *HelloRequest) SetName(v string) {
	x.xxx_hidden_Name = v
}

type HelloRequest_builder struct {
	_ [0]func() // Prevents comparability and use of unkeyed literals for the builder.

	Name string
}

func (b0 HelloRequest_builder) Build() *HelloRequest {
	m0 := &HelloRequest{}
	b, x := &b0, m0
	_, _ = b, x
	x.xxx_hidden_Name = b.Name
	return m0
}

// ......

可以看到,HelloRequest struct的Name => xxx_hidden_Name! Name字段被私有化了,下面则是对Name的一系列操作方法。Get/Set没什么好说的,关注下Build:

// 以前的方式
obj := &pb.HelloRequest{Name: "Anson"}

// 现在的方式
obj := pb.HelloRequest_builder{Name: "Anson"}.Build()

细心的朋友可以发现了test.pb.go文件,test.pb.go跟「Open Struct API」产生的.pb.go文件名一样,但内容可就不一样了,Go对这个文件做了向后兼容,有了它,项目可以兼容旧代码,你只需要写新代码时注意不要.公开字段就好,所有对struct的操作都用方法的形式。

而且test.pb.go顶部有一个编译标志://go:build !protoopaque 而test_protoopaque.pb.go 顶部也有一个编译标志://go:build protoopaque,这是一对相反的编译标志。意味着,当你go run或者go build时,如果指定-tags protoopaque,则编译器会编译test_protoopaque.pb.go,反之编译test.pb.go。如果是新项目,就可以编译时指定一下编译标志,如果是旧项目,就别指定编译标志了,在写新代码时注意一下就行。

迁移

手动迁移

如果message的数量少可以选择Manual Migration,即手动改代码,所有操作message对象的地方都要换成新方式。

自动迁移

但如果是企业级项目,最好选择 Automated Migration,使用这个包: google.golang.org/open2opaque,但这种方式我还没试过,大家有兴趣可以试一下

总结

这篇文章属于操作指南,介绍了如何使用新的「Opaque API」生成新的.pb.go和如何使用新的struct。至于Go为什么要推出新API,大家有兴趣就去仔细看看Blog的下半部分,大致是因为私有化字段能减少内存使用和提高运行效率,并且可能是无法忍受公开化字段埋下的坑点了。

参考:

  1. go.dev/blog/protob…
  2. protobuf.dev/reference/g…
  3. protobuf.dev/reference/g…