前段时间把公司某项目依赖的 github.com/golang/protobuf
的版本从 v1.3.3 升级到了 v1.4.2,本文记录了升级过程中遇到的一些问题。
Google 对 Go 的 protobuf 库的底层进行了大的改进,新版本的包路径转移到了 google.golang.org/protobuf
.
同时,这些改进也被带进了 github.com/golang/protobuf
:从 v1.4
版本起,github.com/golang/protobuf
会在 google.golang.org/protobuf
的基础上实现,但会保证接口兼容,这也表明当前依赖 github.com/golang/protobuf
的项目可以直接升级版本,而无需对上层代码进行改动。
然而,新版的 protobuf-gen-go
使用了 google.golang.org/protobuf/protoreflect
,编译出的 message 结构体与之前完全不同,这给我们的升级工作带来了一些麻烦。
1. 代码中对 XXX_Unmarshal
的直接调用
老版的 protoc-gen-go
会暴露一个 XXX_Unmarshal
接口,用于在 proto.Unmarshal
时进行调用,所以有一些同事选择会直接调用 message.XXX_Unmarshal
方法。新版的 proto 通过 ProtoReflect
接口暴露 message 内部信息,编译 pb.go
时也没有了 XXX_Unmarshal
方法,所以会导致编译时报错 message.XXX_Unmarshal undefined
.
解决方案很简单:改用 proto.Unmarshal
即可。
2. 结构体内部结构变化导致测试出错
针对同一个 message,老版本编译后的结构体结构如下:
1 | type HealthCheckResponse struct { |
而新版本编译后的结构如下:
1 | type HealthCheckResponse struct { |
可以看到,新版本中添加了三个未导出字段,而这三个字段为我们的测试代码带来了一些麻烦。
- cmp.Equal 时 panic
我们的测试中使用了github.com/google/go-cmp/cmp.Equal
来对 proto 结构体进行比较,而结构体中的未导出字段会让cmp.Equal
和cmp.Diff
panic:
1 | panic: cannot handle unexported field at {*pkg.SomeRequest}.state: |
go-cmp
推荐的方式是使用 IgnoreUnexported
,但这种方式需要传递所有需要忽略的类型,对含有多层嵌套的 message 非常不友好。
经过一番搜索,发现 protocmp.Transform
可以将所有的 protobuf message 转换成自定义的 map[string]interface{}
进行比较,所以我们可以用 Transform()
来解决问题:
1 | import "google.golang.org/protobuf/testing/protocmp" |
- assert 卡死并占满内存
相比上面的问题,下面的问题更加奇怪:使用github.com/stretchr/testify/assert.Equal
比较某些特殊的 proto message 时会卡死,同时内存占用会暴涨。
尝试用 pprof 取样,取出来的 CPU 和堆采样图长这样:
可以看到 spew.Dump
中存在无限递归,这导致了程序卡死以及持续的内存分配。
随后搜到了testify 的 issue,相关评论中提出了几种绕过的方案,然而这个问题至今没有解决。
个人推荐的解决方式有两种:
- 使用
marshalTextString()
将 message 转换成 proto text,然后再进行比较; - 使用
cmp.Equal
,结合protocmp.Transform
.
3. lint 报错 copylocks
处理完业务代码处理测试,处理完测试代码还有 lint 要处理。
我们的项目在升级完后,go vet 会报 copylocks 错误:assignment copies lock value to got: .../message_go_protos.Message contains google.golang.org/protobuf/internal/impl.MessageState contains sync.Mutex
解决方式也比较简单:将所有 proto message 改为指针传递即可。