咋一看标题, 这个有啥用?
你别说还真有用, 当你想重构你的项目的时候, 或者你想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; }