最近修改历史项目的时候,有使用到 proto文件生成go代码,有踩一下坑,分享一下。

开始前

如果不熟悉Protobuf语法可以先看这篇-Protobuf语法,如果没有安装生产工具请先执行:

go get -u github.com/golang/protobuf/proto
go get -u github.com/golang/protobuf/protoc-gen-go
go get -u github.com/micro/micro/v2/cmd/protoc-gen-micro

尝试

编写 proto 文件

我们先新建common.proto:

syntax = "proto3";  //语法声明

enum TypeHello {
Unuse = 0;
Morning = 1;
Afernoon = 2;
Evening = 3;
}

protoc 生成 .pb.go

protoc --proto_path=./ --micro_out=. --go_out=. *.proto

proto

错误分析

能看到错误其实是protoc-gen-go报出的,解决方案有两种:

  1. 使用 go_package 参数
  2. 命令行使用–go_opt=M

我比较建议使用 go_package,当多个proto文件有依赖时,使用 go_package 比较清晰,使用 –go_opt=M 可能要麻烦得多,甚至出错。诸如以下:

// common.proto
syntax = "proto3"; //语法声明

package common; //包名
// go_package 使用 go mod 需要的路径即可,也可以是私有 gitlab package
option go_package = "github.com/puresai/go-learing/micro/hello/common";

enum TypeHello {
Unuse = 0;
Morning = 1;
Afernoon = 2;
Evening = 3;
}

生成时务必加上 –go_out=paths=source_relative,具体说明可见文末说明。

protoc --proto_path=. --go_out=paths=source_relative:. -I=../common *.proto

这里我们稍微弄复杂一点,hello.proto 依赖 common.proto:

syntax = "proto3";  //语法声明

import "common.proto"; // 依赖

package hello;
option go_package="github.com/puresai/go-learing/micro/hello/hello";


// 定义服务
service Demo {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// 请求数据格式
message HelloRequest {
string name = 1;
}

// 响应数据格式
message HelloReply {
common.TypeHello hello = 2;
string message = 1;
}

注意这里多了个 micro_out,这时需要 protoc-gen-micro的,会多生成一个.pb.micro.go文件。

protoc --proto_path=. --go_out=paths=source_relative:. --micro_out=paths=source_relative:. -I=../common *.proto

文件生成了,使用go-micro(V2)写一个简单的demo。

package main

import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"

"github.com/puresai/go-learing/micro/hello/common"
"github.com/puresai/go-learing/micro/hello/hello"
"github.com/micro/go-micro/v2"
_ "github.com/micro/go-plugins/registry/kubernetes/v2"
"github.com/sirupsen/logrus"
)

const (
ServiceName = "hello-server"
)

type HelloServer struct{}

func (s *HelloServer) SayHello(ctx context.Context, req *hello.HelloRequest, res *hello.HelloReply) error {
res.Message = "hello " + req.Name
res.Hello = common.TypeHello_Afernoon
return nil
}

func main() {
service := micro.NewService(
// Set service name
micro.Name(ServiceName),
micro.AfterStart(func() error {
fmt.Println("starting...")
return nil
}),
micro.Address(":8089"),
)

service.Init()

hello.RegisterDemoHandler(service.Server(), &HelloServer{})

go func() {
if err := service.Run(); err != nil {
log.Fatal(err)
}
}()

stop := make(chan os.Signal)
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT, os.Interrupt)

go func() {
tick := time.NewTicker(3 * time.Second)

for {
select {
case <-stop:
tick.Stop()
default:
<-tick.C
client()
}
}
}()

select {
case <-stop:
logrus.Infof("got exit signal, shutdown")
}
}

func client() {
service := micro.NewService(micro.Name(ServiceName + "client"))
c := hello.NewDemoService(ServiceName, service.Client())

// 发起RPC调用
rsp, err := c.SayHello(context.TODO(), &hello.HelloRequest{Name: "puresai"})
if err != nil {
fmt.Println(err)
}

// 打印返回值
fmt.Println(rsp.Message)
}


可以看到我们通过protoc生成的代码是没有问题的。

more

paths

生成的文件在输出目录中的.pb.go位置取决于–go_out 标识符。有以下模式:

  • paths=import: 输出文件将放置在以 Go 包的导入路径命名的目录中。例如,protos/buzz.proto 具有 Go 导入路径的输入文件会example.com/project/protos/fizz 导致输出文件位于example.com/project/protos/fizz/buzz.pb.gopaths
  • module=$PREFIX: 输出文件将放置在以 Go 包的导入路径命名的目录中,但从输出文件名中删除指定的目录前缀。例如,protos/buzz.proto 具有 Go 导入路径example.com/project/protos/fizz并 example.com/project指定为module前缀的输入文件会生成位于protos/fizz/buzz.pb.go. 在模块路径之外生成任何 Go 包都会导致错误。此模式对于将生成的文件直接输出到 Go 模块很有用。
  • paths=source_relative: 输出文件与输入文件放在相同的相对目录中。例如,输入文件protos/buzz.proto 导致输出文件位于protos/buzz.pb.go.
    默认是第一种 `paths=import

其实写文章的时候我也尝试了下使用M,执行命令如下:

// 这里写法有点特殊哦,注意,因为我是在文件同一目录运行,所以 common.proto=../common,这样生成的package才会是common,若是 common.proto=./生成就是下划线了
protoc --proto_path=./ --micro_out=. --go_out=. --go_opt=Mcommon.proto=../common *.proto

虽然这样也能生成,但生成代码并不是我想要的(import部分只是个相对路径,或许换成类似example.com/project/protos/fizz也能生成)。如果感兴趣,我更建议可以阅读参考文章,自自己,我用错了也未尝不可能呢?

参考: