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

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

你别说还真有用, 当你想重构你的项目的时候, 或者你想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;
}
在WPF中,可以使用数据模板和数据绑定机制来根据结构体属性自动生成控件。具体来说,可以为结构体定义一个数据模板,用于描述结构体的属性应该如何呈现为控件,并将结构体的实例作为数据上下文设置到该数据模板中,这样WPF就会自动根据数据模板生成对应的控件。 假设我们有一个包含三个属性的结构体Person,如下所示: ```csharp public struct Person { public string Name { get; set; } public int Age { get; set; } public bool IsMale { get; set; } } ``` 现在,我们想要根据这个结构体的属性自动生成控件,可以使用以下代码定义一个数据模板: ```xml <DataTemplate DataType="{x:Type local:Person}"> <StackPanel> <TextBlock Text="Name:"/> <TextBox Text="{Binding Path=Name, Mode=TwoWay}"/> <TextBlock Text="Age:"/> <TextBox Text="{Binding Path=Age, Mode=TwoWay}"/> <TextBlock Text="Is Male:"/> <CheckBox IsChecked="{Binding Path=IsMale, Mode=TwoWay}"/> </StackPanel> </DataTemplate> ``` 然后,在代码中,我们需要将Person结构体的实例作为数据上下文设置到该数据模板中,以便WPF能够根据数据模板自动生成控件。假设我们有一个名为person的Person结构体变量,可以使用以下代码将它设置为数据上下文: ```csharp ContentControl contentControl = new ContentControl(); contentControl.Content = person; contentControl.ContentTemplate = FindResource(typeof(Person)) as DataTemplate; ``` 这样,WPF就会根据数据模板自动生成一个包含三个TextBox控件和一个CheckBox控件的StackPanel控件,用于显示和修改Person结构体的属性。当用户在界面中修改Name、Age或IsMale属性时,Person结构体中的相应属性值也会自动更新。反之,当Person结构体中的属性值发生变化时,界面中显示这些属性的控件的值也会自动更新。 需要注意的是,为了实现自动更新,必须将数据绑定的Mode属性设置为TwoWay,这样绑定才能够在控件的值发生变化时更新数据源。另外,如果Person结构体中的属性发生变化时,界面中的控件没有自动更新,可以使用INotifyPropertyChanged接口来通知界面更新控件的值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ambition!6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值