背景
在日常的 Go 微服務開發中,Protocol Buffers(protobuf) 是廣泛使用的數據交換格式。其配套工具 protoc-gen-go
會根據 .proto
文件生成 Go 結構體代碼,但默認生成的字段名、JSON tag 命名風格往往不能滿足所有團隊或項目的代碼規范需求。
比如,團隊可能有以下規范或訴求:
- Go 結構體字段名需要使用特定的 PascalCase 命名規則;
- JSON tag 必須統一為
snake_case
,以與前端規范對齊; - 字段名稱如
id
、user_id
等在 Go 代碼中必須轉換為ID
、UserID
,以保持一致性和清晰性。
遺憾的是,protoc-gen-go
并沒有提供原生的機制來滿足這些細致的定制需求。因此,我們選擇自定義 protoc-gen-go
插件的生成邏輯,以實現結構體字段命名的精細控制。
實現步驟
1. 獲取protobuf-go源碼
首先克隆指定版本的protobuf-go倉庫:
git clone github.com/protocolbuffers/protobuf-go@v1.36.6
2. 定義全局參數
在cmd/protoc-gen-go/internal_gengo/main.go
文件中定義全局變量和常量:
var (IsCustomField bool // 標識是否使用自定義字段命名TagJSONStyle string // JSON標簽命名風格
)const (SnakeCaseStyle = "snake_case"CamelCaseStyle = "camel_case"
)
3. 聲明命令行參數
在cmd/protoc-gen-go/main.go
中添加參數解析邏輯:
// 定義option參數
isCustomField = flags.Bool("is_custom_field", false, "struct field naming style setting, default is false, indicates no modification, "+"if true indicates custom hump style, for example, message field name suffix "+"is _id or Id, struct field name suffix of generated go code is ID")tagJSONStyle = flags.String("tag_json_style", "", "struct field tag json naming style setting, default is empty, indicates no modification, "+"if set to 'snake_case', indicates snake case style, if set to 'camelCase', indicates camel case style. "+"NOTE: this option overrides protobuf's json_name option")// 判斷參數是否合法
if *isCustomField {gengo.IsCustomField = true
}
if *tagJSONStyle != "" {if *tagJSONStyle != gengo.SnakeCaseStyle && *tagJSONStyle != gengo.CamelCaseStyle {return fmt.Errorf("protoc-gen-go: invalid tag_json_style value: %q, "+"must be 'camel_case' or'snake_case'", *tagJSONStyle)}gengo.TagJSONStyle = *tagJSONStyle
}
4. 添加命名轉換工具
從nameFormat.go復制代碼到cmd/protoc-gen-go/internal_gengo
目錄,并將函數名xstrings.ToCamelCase
修改為xstrings.ToPascalCase
。
5. 修改字段生成邏輯
在generateOneFile
函數中添加自定義字段命名邏輯:
func generateOneFile(gen *protogen.Plugin, file *protogen.File, f *fileInfo, variant string) *protogen.GeneratedFile {// ......for _, message := range f.allMessages {// 添加的自定義字段名風格設置if IsCustomField {for _, field := range message.Fields {field.GoName = toCamel(field.GoName)}}genMessage(g, f, message)}// ......
}
6. 修改JSON標簽生成邏輯
更新fieldJSONTagValue
函數以支持自定義JSON標簽風格:
func fieldJSONTagValue(field *protogen.Field) string {switch TagJSONStyle {case SnakeCaseStyle:return customToSnake(string(field.Desc.Name())) + ",omitempty"case CamelCaseStyle:return customToCamel(string(field.Desc.Name())) + ",omitempty"}return string(field.Desc.Name()) + ",omitempty" // default
}
使用示例
測試proto文件
使用以下user.proto
文件進行測試:
syntax = "proto3";package api.user.v1;option go_package = "user/api/user/v1;v1";service user {// Login 登錄rpc Login(LoginRequest) returns (LoginReply) {}
}message LoginRequest {string email = 1;string password = 2;
}message LoginReply {uint64 user_id =1;uint64 communityId=2;repeated uint64 roleIDs =3;string token =4;
}
生成命令對比
- 默認生成方式(與原始protobuf-go行為一致):
protoc --go_out=. --go_opt=paths=source_relative user.proto
- 自定義字段命名:
protoc --go_out=. --go_opt=paths=source_relative --go_opt=is_custom_field=true user.proto
- 自定義字段命名+蛇形JSON標簽:
protoc --go_out=. --go_opt=paths=source_relative --go_opt=is_custom_field=true --go_opt=tag_json_style=snake_case user.proto
效果對比
默認生成的代碼
type LoginReply struct {state protoimpl.MessageStatesizeCache protoimpl.SizeCacheunknownFields protoimpl.UnknownFieldsUserId uint64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`CommunityId uint64 `protobuf:"varint,2,opt,name=communityId,json=communityId,proto3" json:"communityId,omitempty"`RoleIDs []uint64 `protobuf:"varint,3,rep,packed,name=roleIDs,json=roleIDs,proto3" json:"roleIDs,omitempty"`Token string `protobuf:"bytes,4,opt,name=token,proto3" json:"token,omitempty"`
}
自定義字段命名后的代碼
type LoginReply struct {state protoimpl.MessageStatesizeCache protoimpl.SizeCacheunknownFields protoimpl.UnknownFieldsUserID uint64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`CommunityID uint64 `protobuf:"varint,2,opt,name=communityId,json=communityId,proto3" json:"communityId,omitempty"`RoleIDs []uint64 `protobuf:"varint,3,rep,packed,name=roleIDs,json=roleIDs,proto3" json:"roleIDs,omitempty"`Token string `protobuf:"bytes,4,opt,name=token,proto3" json:"token,omitempty"`
}
自定義字段命名+蛇形JSON標簽后的代碼
type LoginReply struct {state protoimpl.MessageStatesizeCache protoimpl.SizeCacheunknownFields protoimpl.UnknownFieldsUserID uint64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`CommunityID uint64 `protobuf:"varint,2,opt,name=communityId,json=communityId,proto3" json:"community_id,omitempty"`RoleIDs []uint64 `protobuf:"varint,3,rep,packed,name=roleIDs,json=roleIDs,proto3" json:"role_ids,omitempty"`Token string `protobuf:"bytes,4,opt,name=token,proto3" json:"token,omitempty"`
}
總結
通過修改protoc-gen-go源碼,我們實現了以下功能:
- 統一結構體字段命名風格(如將ID后綴統一大寫)
- 控制JSON標簽的命名風格(蛇形或駝峰)
- 通過命令行參數靈活控制生成行為
這種定制化特別適合需要嚴格遵循特定代碼規范的團隊,可以確保生成的代碼風格一致,減少人工修改的工作量。
注意事項
- 此修改基于protobuf-go v1.36.6版本,其他版本可能需要相應調整
- 自定義JSON標簽風格會覆蓋protobuf原生的json_name選項
- 建議團隊內部統一使用規范,避免風格不一致
希望本文對需要自定義protobuf代碼生成的開發者有所幫助!