protoc-gen-mutable

command module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 12, 2026 License: MIT Imports: 8 Imported by: 0

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.Duration
  • google.protobuf.Timestamp
  • google.protobuf.Any
  • google.protobuf.Empty

这样可以避免为这些标准类型重复生成包装层。

适用场景

这个生成器适合下面几类代码:

  • 频繁构造 protobuf 树状对象,希望减少 nil 判断和初始化模板代码。
  • 业务里大量使用 repeated / map / oneof,希望调用侧更接近普通 Go API。
  • 需要把 protobuf 数据快照以只读形式传出,避免被下游意外修改。
  • 需要配合 slogTypeID 做日志或运行时类型分发。

注意事项

  • 生成的 Readonly_XXX 是深拷贝快照,不是零成本视图。
  • Getter 返回的 map / slice 本身不是只读容器;约束主要依赖“数据已深拷贝”和“message 已转只读包装”。
  • ReverseIfNilXxx 的命名保持与当前生成器实现一致;语义上更接近“按容量预留并初始化”。
  • 生成器不会为纯标量单值字段生成 MutableXxx()

依赖

生成的代码依赖以下包:

  • google.golang.org/protobuf/proto
  • google.golang.org/protobuf/reflect/protoreflect
  • log/slog
  • github.com/atframework/atframe-utils-go

Documentation

The Go Gopher

There is no documentation for this package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL