咋一看标题, 这个有啥用?
你别说还真有用, 当你想重构你的项目的时候, 或者你想rpc调用其他语言时, 这个就不用你一个一个去编写message了,而是根据你的go结构体直接生成, 简洁高效(好吧, 一般人也不会有这种需求, 有也直接发给ai就行, 不过各位看官就当看个乐子, 学习下也可以)
实现原理
思路
随机读取包含go的结构体文件, 然后读取每个结构的内容, 以及结构体名字, 通过fmt.Sprintf嵌入到准备好的模板中, 生成新的可执行go文件, 这样就可以生成proto文件了
实现代码
定义proto结构体, 方便后续调用操作
// Proto 结构体用于生成 Protobuf 描述
type Proto struct {
}
// NewProto 创建一个新的 Proto 实例
func NewProto() *Proto {
return &Proto{}
}
读取go 文件, 根据type 分割 go 结构体,
// readGoFile 读取 Go 文件并提取结构体定义
func (p *Proto) readGoFile(gopath string) ([]string, error) {
data, err := os.ReadFile(gopath)
if err != nil {
return nil, err
}
datas := strings.Split(strings.TrimSpace(string(data)), "type ")
for i := 1; i < len(datas); i++ {
datas[i] = "type " + datas[i]
}
return datas[1:], nil
}
将分割好的每个结构体依次获取它的结构体名字, 构建到GetStruct放法中, 并将其放到st中, 最后插入模板, 也就是template中, template在文章后面
func (p *Proto) writeGoFile(datas []string, outputGoPath string) error {
template := (放在文章后面)
st := make([]string, 0)
for i := 0; i < len(datas); i++ {
structName, err := extractStructName(datas[i])
if err != nil {
return err
}
st = append(st, fmt.Sprintf("st = append(st ,\tGetStruct(new(%s)))", structName))
}
//参数为类型定义占位
create, err := os.Create(outputGoPath)
if err != nil {
return err
}
defer create.Close()
_, err = create.Write([]byte(fmt.Sprintf(template, strings.Join(datas, "\n"), strings.Join(st, "\n"))))
return err
}
template 模板
package main
import (
"fmt"
"os"
"reflect"
"strings"
"unicode"
)
%s
// determineTypeProto 根据 Go 类型确定 Protobuf 类型
func determineTypeProto(t reflect.Type) string {
switch t.Kind() {
case reflect.String:
return "string"
case reflect.Int, reflect.Int32:
return "int32"
case reflect.Int64:
return "int64"
case reflect.Float32:
return "float"
case reflect.Float64:
return "double"
case reflect.Bool:
return "bool"
case reflect.Slice:
return "repeated " + determineTypeProto(t.Elem())
case reflect.Struct:
return t.Name() // 直接使用结构体的名字
default:
return "bytes" // 其他未知类型默认使用 bytes
}
}
func toProtobufName(name string) string {
// Protobuf 字段名通常使用小写字母和下划线
var result string
for i, ch := range name {
if i > 0 && unicode.IsUpper(ch) {
result += "_"
}
result += string(unicode.ToLower(ch))
}
return result
}
func toProtobufMessageName(name string) string {
// Protobuf 消息名通常使用驼峰式命名
return strings.Title(name)
}
func toProtobufFieldName(name string) string {
// Protobuf 字段名不能以数字开头,因此确保首字符是字母
if len(name) > 0 && unicode.IsDigit(rune(name[0])) {
name = "field" + name
}
// 将字段名转换为小写加下划线
return toProtobufName(name)
}
// GetStruct 生成结构体的 Protobuf 格式描述
func GetStruct(s interface{}) string {
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
if t.Kind() == reflect.Ptr {
t = t.Elem()
v = v.Elem()
}
st := make([]string, 0)
for i := 0; i < t.NumField(); i++ {
fieldName := toProtobufFieldName(t.Field(i).Name)
fieldType := determineTypeProto(t.Field(i).Type)
// 构建 Protobuf 字段描述
fieldDesc := fmt.Sprintf("%%s %%s = %%d;", fieldType, fieldName, i+1)
st = append(st, fieldDesc)
}
// 将字段描述连接成一个完整的 message
return fmt.Sprintf("message %%s {\n%%s\n}", toProtobufMessageName(t.Name()), "\n"+strings.Join(st, "\n"))
}
func main() {
//类型定义占位
st := make([]string, 0)
%s
create, err := os.Create("./output.proto")
if err != nil {
fmt.Println(err)
}
defer create.Close()
_, err = create.Write([]byte(strings.Join(st, "\n")))
if err != nil {
fmt.Println(err)
}
}
效果展示
package code_gen
type LoginReq struct {
Username string `json:"username"`
Password string `json:"password"`
Ip string `json:"ip,optional"`
}
type LoginRes struct {
Username string `json:"username"`
Token string `json:"token"`
MemberLevel string `json:"memberLevel"`
RealName string `json:"realName"`
Country string `json:"country"`
Avatar string `json:"avatar"`
PromotionCode string `json:"promotionCode"`
Id int64 `json:"id"`
LoginCount int `json:"loginCount"`
SuperPartner string `json:"superPartner"`
MemberRate int `json:"memberRate"`
}
message LoginReq {
string username = 1;
string password = 2;
string ip = 3;
}
message LoginRes {
string username = 1;
string token = 2;
string member_level = 3;
string real_name = 4;
string country = 5;
string avatar = 6;
string promotion_code = 7;
int64 id = 8;
int32 login_count = 9;
string super_partner = 10;
int32 member_rate = 11;
}