Protobuf基本特性
官方文档
基本类型
典型的Protobuf文件内容如下。
syntax = "proto3";
// 定义一个消息体
message SearchRequest {
// 这是注释
// 数字表示参数序号,而非参数值
string query = 1;
int32 page_number = 2;
float results_per_page = 3;
// 可重复字段,表示一个列表类型的参数
repeated string courses = 4;
// 定义一个map键值对
map<string, string> tags = 5;
}
// 消息体嵌套
message RequestBody {
SearchRequest request = 1;
}
Protobuf的标量数值类型。
.proto类型 | 说明 | Go对应类型 |
---|---|---|
double | float64 | |
float | float32 | |
int32 | 使用变长编码,编码负数效率低,如果字段可能出现负值,则使用sint32 | int32 |
int64 | 同上,对于使用sint64 | int64 |
uint32 | 使用变长编码 | uint32 |
uint64 | 使用变长编码 | uint64 |
sint32 | 使用变长编码,带符号的int值,它比常规int32能更有效地编码负数 | int32 |
sint64 | 同上,对应int64 | int64 |
fixed32 | 总是4个字节,如果值经常大于2^28,则比uint32更有效 | uint32 |
fixed64 | 总是8个字节,如果值经常大于2^56,则比uint64更有效 | uint64 |
sfixed32 | 总是4个字节 | int32 |
sfixed64 | 总是8个字节 | int64 |
bool | bool | |
string | 字符串必须始终包含UTF-8编码或7位ASCII文本,长度不能超过2^32 | string |
bytes | 可以包含任何长度不超过2^32的任意字节序列 | []byte |
对于字符串
string
,默认值是空字符串""
。对于字节
bytes
,默认值是空的bytes
。对于布尔
bool
,默认值是false
。对于数值类型,默认值是
0
。对于枚举
enum
,默认值是第一个定义的枚举值,它一定是0
。对于
Message
字段,该字段未设置,它的确切值取决于对应生成它的语言,可参考generated code guide。对于可重复字段(
repeated
),它的默认值是空(通常对应于语言中的空列表[]
)。
默认值
syntax = "proto3";
option go_package="./;default";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
int32 age = 2;
bool ismember = 3;
repeated string tags = 4;
}
message HelloReply {
string reply = 1;
}
客户端调用时不传HelloRequest
中的任何值。
......
// 可以这样调用
reply, err := client.SayHello(context.Background(), &proto.HelloRequest{
Name: "lixingyun",
Age: 18,
Ismember: true,
Tags: []string{"java", "python"},
})
// 或者这样调用
reply, err := client.SayHello(context.Background(), &proto.HelloRequest{})
......
服务端调用代码。
......
func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
return &proto.HelloReply{
Result: "hello " + request.Name + " - " + fmt.Sprintf("%d", request.Age) + " - " + fmt.Sprintf("%v", request.Ismember) + " - " + fmt.Sprintf("%v", request.Courses),
}, nil
}
......
当不传任何参数时,服务端打印出来的值是这样的。
hello - 0 - false - []
option xx_package
用于指明生成的Protobuf文件应该放在哪个包/目录之中。
例如,有如下的文件结构。
proto/
|
└-- helloworld.proto
|
└-- common/
|
└-- handler/
|
└-- v1/
|
└-- helloworld.pb.go
|
└-- helloworld_grpc.pb.go
如果想让生成的.go
文件放在proto/common/handler/v1
目录中,那么helloworld.proto
中的option go_package
就应该这样写。
option go_package = "common/handler/v1";
即使proto/common/handler/v1
目录不存在也会自动创建,而且生成的.go
文件的包名就是package v1
。
导入proto文件
有一个通用的common.proto
文件,内容如下。
syntax = "proto3";
option go_package = ".;proto";
message Empty {}
message Pong {
string result = 1;
}
可以在其他Protobuf文件中引用它,做到最大程度的代码复用。
在common.proto
文件所在的目录中有另一个goods.proto
文件。
syntax = "proto3";
// 导入自定义的proto文件
import "common.proto";
// 导入google内置的proto文件(都在`${GO_HOME}/pkg/mod/github.com/golang/protobuf@v1.2.0/ptypes`包中)
import "google/protobuf/empty.proto";
option go_package = ".;proto";
service Goods {
// 使用自定义的proto文件
rpc Ping1 (Empty) returns (Pong) {}
// 使用google的proto文件
rpc Ping2 (google.protobuf.Empty) returns (Pong) {}
}
接下来通过命令来生成.go
源代码文件。在生成.go
源代码文件时,不要忘了生成被引用的common.proto
文件,不然生成的目标Protobuf文件会报错。
-- 先生成被引用proto文件的源代码
> protoc --go_out=. --go-grpc_out=. common.proto
-- 再生成目标proto文件的源代码
> protoc --go_out=. --go-grpc_out=. goods.proto
然后,在server.go
文件中实现这两个方法。
package main
import (
"context"
"google.golang.org/grpc"
"go-project/new_grpc/proto" // 这是自定义的Empty
"google.golang.org/protobuf/types/known/emptypb" // 这是google的Empty,这里的路径就是google.protobuf.Empty文件中`go_package`的路径
"net"
)
type Server struct {
}
func (s *Server) Ping1(ctx context.Context, request *proto.Empty) (*proto.Pong, error) {
return &proto.Pong{
Result: "I'm OK from Ping1",
}, nil
}
func (s *Server) Ping2(ctx context.Context, empty *emptypb.Empty) (*proto.Pong, error) {
return &proto.Pong{
Result: "I'm OK from Ping2",
}, nil
}
......
最后,在client.go
文件中可以这样用。
package main
import (
"context"
"google.golang.org/grpc"
"go-project/new_grpc/proto" // 这是自定义的Empty
"google.golang.org/protobuf/types/known/emptypb" // 这是google的Empty,这里的路径就是google.protobuf.Empty文件中`go_package`的路径
)
......
client := proto.NewGoodsClient(conn)
// 使用自定义的Empty
reply1, _ := client.Ping1(context.Background(), &proto.Empty{})
// 使用google的Empty
reply2, _ := client.Ping2(context.Background(), &emptypb.Empty{})
println(reply1.GetResult())
println(reply2.GetResult())
总的来说,Protobuf架起了客户端和服务端之间通信的桥梁。

上图显示的很清楚,Protobuf的意义可以用两句话讲清楚。
嵌套的message
Protobuf可以在message
中嵌套其他的message
。
syntax = "proto3";
option go_package = ".;proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
int32 age = 2;
bool ismember = 3;
repeated string courses = 4;
}
message HelloReply {
// 单独为HelloReply生成嵌套message
message Result {
string code = 1;
string msg = 2;
}
string result = 1;
repeated Result data = 2;
}
根据这个Protobuf文件生成.go
源代码。
然后,在server.go
文件中实现这个方法。
package main
import (
"context"
"go-project/new_grpc2/proto"
"google.golang.org/grpc"
"net"
)
type Server struct {
}
func (s *Server) SayHello(ctx context.Context, in *proto.HelloRequest) (*proto.HelloReply, error) {
return &proto.HelloReply{
// Result: "hello, " + in.Name + " - " + fmt.Sprintf("%d", in.Age) + " - " + fmt.Sprintf("%v", in.Ismember) + " - " + fmt.Sprintf("%v", in.Courses),
Result: "hello, ",
// Data: make([]*proto.HelloReply_Result, 0),
// 如果Result中还有嵌套,可以按这个套路使用:[]*proto.HelloReply_Result_XXX_YYY_ZZZ_...
Data: []*proto.HelloReply_Result{
{
Code: "1",
Msg: "java",
},
{
Code: "2",
Msg: "go",
},
},
}, nil
}
......
最后,在client.go
文件中可以这样用。
package main
import (
"context"
"fmt"
"go-project/new_grpc2/proto"
"google.golang.org/grpc"
)
......
client := proto.NewGoodsClient(conn)
reply, _ := client.SayHello(context.Background(), &proto.HelloRequest{})
println(reply.Result + fmt.Sprintf("%v", reply.Data))
enum、map和Timestamp
有如下hello.proto
文件。
syntax = "proto3";
// 引入google扩展的timestamp
import "google/protobuf/timestamp.proto";
option go_package = ".;proto";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// 性别枚举
enum Gender {
MALE = 0;
FEMALE = 1;
UNKNOWN = 2;
}
message HelloRequest {
string name = 1;
Gender gender = 2;
// 定义一个map字段
map<string, string> extra = 3;
// 引入时间戳类型
google.protobuf.Timestamp timestamp = 4;
}
message HelloReply {
string result = 1;
}
将之生成对应的.go
源代码文件。
然后再来开发对应的服务端实现。
......
func (s *Server) SayHello(ctx context.Context, in *proto.HelloRequest) (*proto.HelloReply, error) {
return &proto.HelloReply{
Result: "hello, " + in.Name + " - " + in.GetGender().String() + " - " + fmt.Sprintf("%v", in.Extra) + " - " + fmt.Sprintf("%v", in.Sendtime),
}, nil
}
......
以及对应的客户端调用。
package main
import (
"context"
"go-project/new_grpc3/proto"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/timestamppb"
"time"
)
......
client := proto.NewGreeterClient(conn)
reply, _ := client.SayHello(context.Background(), &proto.HelloRequest{
Name: "小明",
Gender: proto.Gender_MALE,
Extra: map[string]string{"level": "英勇黄金"},
Sendtime: timestamppb.New(time.Now()),
})
println(reply.Result)
虽然map
很方便,但不建议使用它。
因为message
中的字段完全可以替代map
中的键,而且结构也很清晰,也能添加注释形成文档,更有利于开发。
感谢支持
更多内容,请移步《超级个体》。