proto语法

虽然语法繁多但是每一个都很简单

syntax

语法,目前主要设置为proto3

syntax = "proto3";

package

用于指定生成的go文件所在文件夹的名称

go_package

因为go插件是通过proto文件是生成对应的go代码,所以go_package相当于要生成的文件的package属性,用于指定包名

如:

option go_package="proto-learn/pb/person;person";

分号隔开的是别名

// 写了别名
import (
	user "proto-learn/pb/user"
)

// 没写别名
import (
	"proto-learn/pb/user"
)

message

定义结构体

message MyStruct {
	... // 字段名
}

定义变量,语法:类型 变量名 = 标识符[一般为数字,递增];

其实我觉得有一点点像C++语法

类型含义
string字符串
int3232位整型[2^31]
int6464位整型[2^63]
float32位精度浮点型[2^127]
double64位精度浮点型[2^1023]
bool布尔
repeated切片
map<K,V>map
reversed保留字
enum枚举
oneof多选一

service

这是定义方法的地方,主要分为四种

  • 即刻响应
  • 输入流
  • 输出流
  • 输入输出流
service PersonService {
    rpc GetPerson (Person) returns (Person); // 即刻响应
    rpc GetPersonIn (stream Person) returns (Person); // 输入以流的形式
    rpc GetPersonOut (Person) returns (stream Person); // 输出以流的形式
    rpc GetPersonIO (stream Person) returns (stream Person); // 输入输出都以流的形式
}

示例代码

syntax = "proto3";

package person;
option go_package="proto-learn/pb/person;person";

import "pb/user/user.proto";

message Person{
    string name = 1;
    int64 age = 2;
    float height = 3;
    double weight = 4;
    bool isMale = 5;
    repeated string lessons = 6;
    message Friend {
        string name = 7;
    }
    Friend friend = 8;
    map<string,int64> scores = 9;

    reserved "reversed_field"; // 保留字段
    reserved 10; // 保留唯一标识
    
    // 枚举类型
    enum Subjects{
        Chinese = 0;
        Math = 1;
        English = 2;
    }

    Subjects subject = 11;

    // oneof
    oneof Teacher{
        string Teacher1 = 12;
        string Teacher2 = 13;
        string Teacher3 = 14;
    }

    user.UserReq req = 15;
}

service PersonService {
    rpc GetPerson (Person) returns (Person); // 即刻响应
    rpc GetPersonIn (stream Person) returns (Person); // 输入以流的形式
    rpc GetPersonOut (Person) returns (stream Person); //输出以流的形式
    rpc GetPersonIO (stream Person) returns (stream Person); // 输入输出都以流的形式
}

命令行生成

编写makefile脚本

.PHONY: gen-user gen-person

gen-user:
	protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./pb/user/user.proto

gen-person:
	protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./pb/person/person.proto

执行make gen-person即可生成

现在我来解释一下命令行的各个参数,在学习的时候也踩了很多坑

  • --go_out: pb.go生成的相对目录
  • --go-grpc_out: grpc.pb.go生成的相对目录
  • --go_opt=paths=source_relative: 如果不写的话,默认生成的是go_package的目录,这样写的话就和proto定义的位置一致
  • --go-grpc_opt=paths=source_relative: 如果不写的话,默认生成的是go_package的目录,这样写的话就和proto定义的位置一致

oneof语法

在proto语法当中oneof关键字比较特殊,代表多选一

实际上就是定义proto时不确定使用什么字段,设置oneof,本质上类似Go语法当中的枚举类型

举个例子

如果我的博客文章更新了,既可以选择短信推送也可以选择邮件推送

可以这样定义proto

syntax = "proto3";

option go_package="hello/pb;pb";

package pb;

message NoticeRequest {
    string msg = 1;
    oneof msg_type {
        string phone = 2;
        string email = 3;
    }
}

message NoticeResponse {
    string msg = 1;
}

service NoticeService {
    rpc Notice (NoticeRequest) returns (NoticeResponse);
}

定义server端代码

type NoticeServer struct {
	pb.UnimplementedNoticeServiceServer
}

func (s *NoticeServer) Notice(ctx context.Context, req *pb.NoticeRequest) (*pb.NoticeResponse, error) {
	send_msg := req.Msg
	switch req.MsgType.(type) { // 类型断言
	case *pb.NoticeRequest_Email:
		send_msg += " send by email:" + req.GetEmail()
	case *pb.NoticeRequest_Phone:
		send_msg += " send by phone" + req.GetPhone()
	}

	return &pb.NoticeResponse{
		Msg: send_msg,
	}, nil
}

根据传过来的值进行类型断言后继续处理

定义client端代码

// notice
noticeResp, err := noticeClient.Notice(ctx, &pb.NoticeRequest{Msg: *msg, MsgType: &pb.NoticeRequest_Phone{
	Phone: "15811111111",
}})
if err != nil {
	log.Fatalf("failed to call Notice[phone]: %v", err)
}
log.Printf("Response: %s", noticeResp.Msg)

noticeResp, err = noticeClient.Notice(ctx, &pb.NoticeRequest{Msg: *msg, MsgType: &pb.NoticeRequest_Email{
	Email: "email@codepzj.cn",
}})
if err != nil {
	log.Fatalf("failed to call Notice[email]: %v", err)
}
log.Printf("Response: %s", noticeResp.Msg)

可以定义phone和email两种不同的请求信息

WrapperValue和optional

对于Go语言来说,如果对于int32类型来说,传参为0和不传参得到的零值都为0,怎么判断这个参数传没传呢?

答案是使用结构体和指针

在protobuf当中对应WrapperValueoptional两个关键字

protobuf v3在删除required的同时把optional也一起删除了(v3.15.0又加回来了),这使得我们没办法轻易判断某些字段究竟是未赋值还是其被赋值为零值。

WrapperValue适用于v3.15之前的版本,optional适合v3.15之后的版本,不过现在的protoc都v33了😂

WrapperValue位于protoc的include目录下的google/protobuf/wrappers.proto

示例代码

proto

syntax = "proto3";

option go_package = "hello/pb;pb";

import "google/protobuf/wrappers.proto";

message BookRequest {
    string name = 1;
    google.protobuf.Int32Value price = 2;
    optional string author = 3;
}

message BookResponse {
    string msg = 1;
}

service BookService {
    rpc Buy (BookRequest) returns (BookResponse);
}

server

type BookServer struct {
	pb.UnimplementedBookServiceServer
}

func (s *BookServer) Buy(ctx context.Context, req *pb.BookRequest) (*pb.BookResponse, error) {
	if req.Price == nil {
		return &pb.BookResponse{Msg: "price没传, 可能是无价之宝"}, nil
	}
	msg := fmt.Sprintf("%s这本书价格为:%d元", req.Name, req.GetPrice().GetValue())
	if req.Author != nil {
		msg += ",作者为:" + req.GetAuthor()
	}
	return &pb.BookResponse{Msg: msg}, nil
}

client

// book
author := "codepzj"
bookResp, err := bookClient.Buy(ctx, &pb.BookRequest{Name: "《Go语言圣经》", Price: &wrapperspb.Int32Value{Value: 99}, Author: &author})
if err != nil {
	log.Fatalf("failed to call Buy: %v", err)
}
log.Printf("Response: %s", bookResp.Msg)

bookResp, err = bookClient.Buy(ctx, &pb.BookRequest{Name: "《如何让富婆爱上你》"})
if err != nil {
	log.Fatalf("failed to call Buy: %v", err)
}
log.Printf("Response: %s", bookResp.Msg)

输出结果

2025/12/12 00:33:26 Response: 《Go语言圣经》这本书价格为:99元,作者为:codepzj
2025/12/12 00:33:26 Response: price没传, 可能是无价之宝

本质上就是把字段拿出来判断指针是否为nil,如果为nil,说明字段没有赋值,不为nil,说明字段有值

代码仓库

本教程都是一步步手敲出来的,对应的代码仓库链接为:

https://cnb.cool/codepzj/goplayground/-/tree/main/rpc-learn/proto-learn

参考文章: