README
¶
protoc-gen-mutable
这个项目提供一个 protoc Go 插件,用来为 protobuf 生成额外的便捷接口,输出文件名为 *_mutable.pb.go。
生成代码的目标主要有三类:
- 为 message 字段补齐惰性初始化的
MutableXxx()访问器。 - 为 repeated、map、oneof 提供更直接的构建接口。
- 为 message 生成只读包装
Readonly_XXX,用于安全地向外暴露数据快照。
安装
在项目目录执行:
go install github.com/yousongyang/protoc-gen-mutable@latest
安装后会得到插件可执行文件 protoc-gen-mutable,即可在 protoc 中通过 --go_mutable_out 使用。
使用方式
protoc \
--go_out=. \
--mutable_out=. \
your.proto
生成结果会落在与 *.pb.go 同目录的 *_mutable.pb.go 文件中。
生成内容概览
假设 proto 中存在:
message Profile {
string name = 1;
}
message User {
Profile profile = 1;
repeated string tags = 2;
map<string, Profile> friends = 3;
oneof contact {
string email = 4;
Profile card = 5;
}
}
会额外生成以下几类接口。
Message 级接口
每个普通 message 都会生成:
func (m *User) Clone() *User
func (m *User) Merge(src *User)
func (m *User) LogValue() slog.Value
func (_ *User) GetTypeID() lu.TypeID
func (m *User) ToReadonly() *Readonly_User
说明:
Clone()基于proto.Clone返回完整副本;当接收者为nil时返回空对象。Merge(src)基于proto.Merge合并消息。LogValue()便于直接写入slog。GetTypeID()依赖github.com/atframework/atframe-utils-go/lang_utility中的TypeID。ToReadonly()会先克隆 message,再转换成只读包装,因此适合生成稳定快照,而不是零拷贝视图。
普通字段的 Mutable 接口
message 字段
对于 message 字段:
func (m *User) MutableProfile() *Profile
行为:
- 当字段为
nil时自动new(Profile)。 - 返回可直接修改的子 message 指针。
这类接口适合把原先的判空初始化逻辑压缩成一行:
user.MutableProfile().Name = "alice"
repeated 字段
对于 repeated 字段,会生成:
func (m *User) MutableTags() []string
func (m *User) ReverseIfNilTags(l int32) []string
func (m *User) AppendTags(d string)
func (m *User) MergeTags(d []string) []string
func (m *User) RemoveLastTags()
如果 repeated 的元素是 message,还会额外生成:
func (m *User) AddChildren() *Child
说明:
MutableXxx()保证切片非nil。ReverseIfNilXxx(l)会在切片为nil时按给定容量初始化。AppendXxx()和MergeXxx()封装了追加逻辑。RemoveLastXxx()在切片非空时删除最后一个元素。AddXxx()仅对 message 元素生效,会新建一个元素、追加到切片并返回该元素。
map 字段
对于 map 字段:
func (m *User) MutableFriends() map[string]*Profile
行为:
- 当 map 为
nil时自动初始化。 - 返回 map 本身,便于直接写入。
示例:
user.MutableFriends()["bob"] = &Profile{Name: "Bob"}
oneof 接口
生成器会同时为 oneof 生成三层能力。
1. case 枚举
例如 User.contact 会生成:
type User_EnContactID int32
const (
User_EnContactID_NONE User_EnContactID = 0
User_EnContactID_Email User_EnContactID = 4
User_EnContactID_Card User_EnContactID = 5
)
2. 外层查询接口
func (m *User) GetContactOneofCase() User_EnContactID
func (m *User) GetContactOneofName() string
func (m *User) GetContactTypeID() lu.TypeID
说明:
GetContactOneofCase()返回当前命中的 case。GetContactOneofName()返回 proto 字段名。GetContactTypeID()返回当前 oneof 实际承载结构体的TypeID。
3. oneof option 的 Mutable 接口
对于每个 oneof 字段,都会生成对应的切换/初始化方法。
如果字段是基础类型:
func (m *User) MutableEmail() *User_Email
如果字段是 message:
func (m *User) MutableCard() *Profile
说明:
- 调用时如果当前 oneof 不是该分支,会自动切换到该分支。
- 对于 message 型 oneof,返回的是内部 message 字段本身,而不是外层 oneof 包装结构。
- 生成器也会为 oneof 包装结构生成
GetTypeID()等辅助接口,供 case/type 查询逻辑使用。
Readonly 包装接口
每个普通 message 都会生成一个只读包装:
type Readonly_User struct {
// 内部字段由生成器维护
}
并生成以下常用方法:
func (r *Readonly_User) ReadonlyProtoReflect() innerXXXReadonlyMessage
func (r *Readonly_User) CloneMessage() *User
func (r *Readonly_User) ToMessage() *User
func (r Readonly_User) LogValue() slog.Value
同时为所有字段生成只读 Getter:
func (r *Readonly_User) GetProfile() *Readonly_Profile
func (r *Readonly_User) GetTags() []string
func (r *Readonly_User) GetFriends() map[string]*Readonly_Profile
func (r *Readonly_User) GetEmail() string
func (r *Readonly_User) GetCard() *Readonly_Profile
func (r *Readonly_User) GetContactOneofCase() User_EnContactID
func (r *Readonly_User) GetContactTypeID() lu.TypeID
设计特点:
ToReadonly()会先深拷贝,再构造只读对象,避免后续原对象修改污染快照。- message 字段会尽量递归转换成
Readonly_XXX。 bytes字段在拷贝和读取时都会复制,避免调用方通过切片共享底层数组。- repeated / map 字段会创建新容器;其中 message 元素会递归转成只读包装。
- oneof 会生成只读接口和只读 option 结构体,用于安全读取当前分支值。
Readonly 的特殊处理
以下 protobuf well-known types 不会继续包装成 Readonly_XXX,而是保留原类型指针:
google.protobuf.Durationgoogle.protobuf.Timestampgoogle.protobuf.Anygoogle.protobuf.Empty
这样可以避免为这些标准类型重复生成包装层。
适用场景
这个生成器适合下面几类代码:
- 频繁构造 protobuf 树状对象,希望减少
nil判断和初始化模板代码。 - 业务里大量使用 repeated / map / oneof,希望调用侧更接近普通 Go API。
- 需要把 protobuf 数据快照以只读形式传出,避免被下游意外修改。
- 需要配合
slog和TypeID做日志或运行时类型分发。
注意事项
- 生成的
Readonly_XXX是深拷贝快照,不是零成本视图。 - Getter 返回的 map / slice 本身不是只读容器;约束主要依赖“数据已深拷贝”和“message 已转只读包装”。
ReverseIfNilXxx的命名保持与当前生成器实现一致;语义上更接近“按容量预留并初始化”。- 生成器不会为纯标量单值字段生成
MutableXxx()。
依赖
生成的代码依赖以下包:
google.golang.org/protobuf/protogoogle.golang.org/protobuf/reflect/protoreflectlog/sloggithub.com/atframework/atframe-utils-go
Documentation
¶
There is no documentation for this package.
Click to show internal directories.
Click to hide internal directories.