彻底搞清protobuf和gRPC的文件生成位置

你在学习protobufgRPC时是否遇到过这些问题:

  • 生成的pb.go文件的位置奇奇怪怪,搞不清楚规则
  • 不知道protoc --go_opt=paths=source_relative有什么用,但加了它,代码好像就生成到正确的位置了

如果是的,那这篇文章可以帮你解决你心中的疑惑。

本文主要介绍在protobuf-go中,paths参数和proto文件中的option go_package=xxx是如何共同配合,影响pb.go的生成位置的。

文章大量引用了protobuf-go的源码,懒得看分析过程的同学可以直接下拉到最后的结论部分。

背景知识

现在假定我们有如下所示的目录结构

text 复制代码
proto-example
├── api
│   └── protobuf
│       └── foo.proto
└── internal
    └── foo

foo.proto的内容如下:

protoc 复制代码
syntax = "proto3";

package foo;

option go_package = "github.com/bootun/proto-example/internal/foo";

// ...

我们执行如下命令:

sh 复制代码
protoc \
  --proto_path=api/protobuf/foo.proto \
  --go_out=internal/foo \
  --go_opt=paths=source_relative

你是否会有以下疑惑?

  1. 代码会生成在go_out指定的地方吗?
  2. paths=source_relative有什么用?
  3. proto文件中的go_package有什么用?
  4. 他们之间有什么关系吗?

反正我初学protobuf的时候是被这些奇怪的参数搞得头疼,这次带你彻底缕清他们之间的关系。

开始吧

我们执行protoc --xx_out=yyy -xx_opt=yyy时,其实是告诉protoc,你要执行protoc-gen-xx这个程序。protoc会在你系统定义的PATH环境变量中寻找这个可执行文件。

在背景里定义的例子中,xx就是go。在我们传入--go_out时,protoc会在$PATH中寻找protoc-gen-go这个可执行文件(gRPC同理)。因此我们有问题直接去查看protoc-gen-go的源码即可。

我们从protobuf-go的main函数中开始梳理,发现命令行参数处理的逻辑位于protobuf-go/compiler/protogen/protogen.go中,

go 复制代码
for _, param := range strings.Split(req.GetParameter(), ",") {
    // ...
    // 处理参数
    switch param {
    ...
    case "paths":
        switch value {
        case "import":
            gen.pathType = pathTypeImport
        case "source_relative":
            gen.pathType = pathTypeSourceRelative
        default:
            return nil, fmt.Errorf(`unknown path type %q: want "import" or "source_relative"`, value)
   }
   // ...
}

上面这段代码的逻辑很简单明了,不多做解释。这里如果不传递paths参数,gen.pathType默认为零值。

我们一起来看下,gen.pathType的零值是什么呢?

go 复制代码
type pathType int

const (
    pathTypeImport pathType = iota
    pathTypeSourceRelative
)

如果我们不传paths=source_relativepathType的默认值就是pathTypeImport,等同于--go_opt=paths=import参数。

这两个参数有什么区别呢?我们暂且按下不表,现在我们来看看proto文件里的内容。

option go_package = "xxx"

还记得吗?我们的proto文件里定义了

proto 复制代码
option go_package = "github.com/bootun/proto-example/internal/foo";

这个go-package有什么用呢?我们来看看google\protobuf\descriptor.proto中给go_package的定义:

go 复制代码
// Sets the Go package where structs generated from this .proto will be
// placed. If omitted, the Go package will be derived from the following:
//   - The basename of the package import path, if provided.
//   - Otherwise, the package statement in the .proto file, if present.
//   - Otherwise, the basename of the .proto file, without extension.
optional string go_package = 11;

大概意思就是: 它决定了这个proto文件生成的pb.go应该放在哪个go包中,如果设置了,那生成的.pb.go文件的import路径就是go_package里所指定的路径。如果没有设置,那则由.proto文件的package语句或.proto文件的名称来决定。

可能不是特别好理解,我们来看看这部分实际的逻辑,这是protobuf-go的代码中对go_package的处理方式:

go 复制代码
for _, fdesc := range gen.Request.ProtoFile {
    // fdesc.GetOptions().GetGoPackage() 里就是proto文件中 option go_package设置的内容
    impPath, pkgName := splitImportPathAndPackageName(fdesc.GetOptions().GetGoPackage())
    // ...
}

// 附上splitImportPathAndPackageName的定义
func splitImportPathAndPackageName(s string) (GoImportPath, GoPackageName) {
    if i := strings.Index(s, ";"); i >= 0 {
        return GoImportPath(s[:i]), GoPackageName(s[i+1:])
    }
    return GoImportPath(s), ""
}

go_package里设置的参数被分为了两个部分: impPathpkgName,在本例中,impPath是github.com/bootun/proto-example/internal/foo,pkgName是空字符串。

你可能见过这种使用分号分隔 的写法: option go_package = "xxxx/xxxx;yyy"; 这样写的话我们上边得到的pkgName就是yyy了。

我们来看一下impPath会被如何使用?

go 复制代码
// prefix决定了最终生成的.pb.go文件的前缀
// 此时prefix等于文件名(foo.proto)
prefix := p.GetName()
if ext := path.Ext(prefix); ext == ".proto" || ext == ".protodevel" {
    // 去掉后缀
    prefix = prefix[:len(prefix)-len(ext)]
}
// 此时prefix等于foo

// 😉看!上文提到的gen.pathType出场了
switch gen.pathType {
case pathTypeImport:
    // 如果paths=import(或者没传),
    // 就会给prefix拼上刚刚得到的impPath(f.GoImportPath)
    prefix = path.Join(string(f.GoImportPath), path.Base(prefix))
    // 此时prefix = github.com/bootun/proto-example/internal/foo/foo
case pathTypeSourceRelative:
    // 如果paths=source_relative则什么也不加前缀
    // 此时prefix依旧等于foo
}

f.GoDescriptorIdent = GoIdent{
    GoName:       "File_" + strs.GoSanitized(p.GetName()),
    GoImportPath: f.GoImportPath,
}

// 😲嘿,看这里
// 文件的GeneratedFilenamePrefix被设为了prefix。
// 在我们的例子中就是foo
f.GeneratedFilenamePrefix = prefix

为什么我要你注意GeneratedFilenamePrefix呢?

我们来看看它的注释:

go 复制代码
// GeneratedFilenamePrefix is used to construct filenames for generated
    // files associated with this source file.
    //
    // For example, the source file "dir/foo.proto" might have a filename prefix
    // of "dir/foo". Appending ".pb.go" produces an output file of "dir/foo.pb.go".
    GeneratedFilenamePrefix string

注释中说最终生成的文件就是GeneratedFilenamePrefix加上.pb.go,在我们的例子中,最终生成的文件名就是foo.pb.go

如果你忘记写paths=source_relative或写的是paths=import,那最终生成的文件名就是github.com/bootun/proto-example/internal/foo/foo.pb.go。也就是说,会在--go_out指定的目录下,再创建github.com/bootun/proto-example/internal/foo等一系列的目录!我猜这肯定不是你想要的结果。

结论

总结一下,--go_out参数指定了.go文件要输出的目录,生成的文件会以这个目录为基准。

--go_opt=paths=source_relative参数会告诉protobuf-go生成的文件前不要加前缀。也就是直接在--go_out指定目录下生成.pb.go文件,最终的文件会生成在:

sh 复制代码
${--go_out}/${proto文件名}.pb.go

如果不加--go_opt=paths=source_relative,效果等同于加了--go_opt=paths=import,会在生成的文件前加上proto文件内option go_package的内容,最终的路径就变成了:

sh 复制代码
${--go_out}/${option go_package}/${proto文件名}.pb.go

--go_out--go_opt以及proto文件中的option go_package共同控制着生成文件的最终位置。

本文首发于微信公众号 梦真日记,转载请注明出处。

相关推荐
Piper蛋窝6 小时前
深入 Go 语言垃圾回收:从原理到内建类型 Slice、Map 的陷阱以及为何需要 strings.Builder
后端·go
DemonAvenger11 小时前
高性能 TCP 服务器的 Go 语言实现技巧:从原理到实践
网络协议·架构·go
Code季风11 小时前
深入理解微服务中的服务注册与发现(Consul)
java·运维·微服务·zookeeper·架构·go·consul
zhuyasen13 小时前
定义即代码!这个框架解决了90%的Go开发者还在低效开发项目的问题
架构·go·gin
Lemon程序馆20 小时前
搞懂 GO 的垃圾回收机制
后端·go
程序员爱钓鱼20 小时前
Go 语言泛型 — 泛型语法与示例
后端·面试·go
GO兔1 天前
开篇:GORM入门——Go语言的ORM王者
开发语言·后端·golang·go
Lemon程序馆2 天前
速通 GO 垃圾回收机制
后端·go