切換語言為:簡體

根據go結構體反編譯生成proto的message

  • 爱糖宝
  • 2024-08-27
  • 2068
  • 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.