切换语言为:繁体

根据go结构体反编译生成proto的message

  • 爱糖宝
  • 2024-08-27
  • 2065
  • 0
  • 0

咋一看标题, 这个有啥用?

你别说还真有用, 当你想重构你的项目的时候, 或者你想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;
}


0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.