Go 如何從零編寫 ProtoBuf 插件

go 精通 protobuf 連載五:如何從零編寫 ProtoBuf 插件
本期的主要內容將手把手教會大家,編寫 probuf 的 go 插件,以我自己編寫的一個生成結構圖的插件爲例子。項目位於 https://github.com/hisheng/protoc-gen-go-struct

一、自定義 ProtoBuf 插件介紹

我們常用的 go 支持 protobuf 插件有

cXguFW

基本上該有的插件,都已經有寫過了,所以我們可以查看別人的源碼,來觀察怎麼來寫一個插件。

二、手把手寫自定義插件(以 protoc-gen-go-struct 爲例)

2.1 新建 protoc-gen-go-struct 文件夾, 並且進入到這個文件夾裏。

mkdir protoc-gen-go-struct && cd protoc-gen-go-struct

2.2 新建 go module 項目

我們執行 go mod init modName 命名來生成項目如下:

go mod init github/hisheng/protoc-gen-go-struct

此時我們查看文件夾發現生產了一個 go.mod 文件,查看一下代碼如下:

module github/hisheng/protoc-gen-go-struct

go 1.19

2.3 寫 main 函數

我們在項目根目錄,寫一個 main.go 如下

touch main.go

此時我們發現項目根目錄下,生成了一個 main.go 文件。
我們寫一個 main 函數,代碼如下:

package main

import (
    "google.golang.org/protobuf/compiler/protogen"
)

func main() {
    protogen.Options{}.Run(func(gen *protogen.Plugin) error {
        for _, f := range gen.Files {
            if !f.Generate {
                continue
            }
            generateFile(gen, f)
        }
        return nil
    })
}

這個 main() 函數大家可以直接複製,基本所有的插件都是這樣的格式,當然這個 main 也可以接受參數,這裏我們簡化,先不介紹,感興趣的人,可以參考 protoc-gen-go 的 main 函數來寫。
我們自己寫的方法主要是 generateFile(gen, f) 這個函數。
這個函數用來讀取. proto 文件,並且生產 go 文件。

2.4 自定義 generateFile(gen, f) 函數

這個函數全稱是 generateFile(gen _protogen.Plugin, file _protogen.File),接受的兩個參數

gen *protogen.Plugin 爲生成的插件,主要用來生成go文件
file *protogen.File 爲.proto文件對他的file對象

我這裏的主要代碼是:

// 生成.struct.go文件,參數爲 輸出插件gen,以及讀取的文件file
func generateFile(gen *protogen.Plugin, file *protogen.File) {
    filename := file.GeneratedFilenamePrefix + ".struct.go"
    g := gen.NewGeneratedFile(filename, file.GoImportPath)
    // 輸出 package packageName
    g.P("package ", file.GoPackageName)
    g.P() // 換行

    for _, m := range file.Messages {
        // 輸出 type m.GoIdent struct {
        g.P("type ", m.GoIdent, " struct {")
        for _, field := range m.Fields {
            leadingComment := field.Comments.Leading.String()
            trailingComment := field.Comments.Trailing.String()

            line := fmt.Sprintf("%s %s `json:\"%s\"` %s", field.GoName, field.Desc.Kind(), field.Desc.JSONName(), trailingComment)
            // 輸出 行首註釋
            g.P(leadingComment)
            // 輸出 行內容
            g.P(line)
        }
        // 輸出 }
        g.P("}")
    }
    g.P() // 換行
}

就是如此簡單的 10 幾行代碼,就可以讀取. proto 文件,並生成. go 文件了。接下來我們詳細的介紹一下里面主要的變量以及對象。

2.4.1 第一步先生成 GeneratedFile 對象

filename := file.GeneratedFilenamePrefix + ".struct.go"
g := gen.NewGeneratedFile(filename, file.GoImportPath)

2.4.2 第二步生成 go 代碼的 package

// 輸出 package packageName
g.P("package ", file.GoPackageName)
g.P() // 換行

2.4.3 第三步便利 proto 文件的 message
因爲我們這裏是找 message 然後生成 struct,所以使用 file.Messages 來獲取所有的 message,然後遍歷

    for _, m := range file.Messages {
        // 輸出 type m.GoIdent struct {
        g.P("type ", m.GoIdent, " struct {")
        for _, field := range m.Fields {
            leadingComment := field.Comments.Leading.String()
            trailingComment := field.Comments.Trailing.String()

            line := fmt.Sprintf("%s %s `json:\"%s\"` %s", field.GoName, field.Desc.Kind(), field.Desc.JSONName(), trailingComment)
            // 輸出 行首註釋
            g.P(leadingComment)
            // 輸出 行內容
            g.P(line)
        }
        // 輸出 }
        g.P("}")
    }

這一步代碼主要生產 go 的 struct,生成後的樣子如下:

type User struct {
    // xingming
    Name string `json:"name"` // 姓名
    // age
    Age int64 `json:"age"` // 年齡
}

三、編譯插件

我們把這個代碼,在本地編譯安裝,在項目根目錄執行 go install

go install

此時我們到自己的 GOPATH 目錄查看是否生成
我們 cd 到 $GOPATH 的 bin 目錄,一般 go install 安裝的命令都在這裏

cd  $GOPATH/bin && ls -al

我們看到了 protoc-gen-go-struct 二進制命令。

四、protoc 使用 protoc-gen-go-struct 插件

我們在其他地方寫測試方法,寫一個 pt.proto 文件

mkdir protoc_struct && cd protoc_struct && touch pt.proto

然後把 pt.ptoto 裏面寫入代碼

syntax = "proto3";
package pt;
option go_package = "./protoc_struct";

message User {
  // xingming
  string name = 1;// 姓名
  // age
  int64 age = 2;// 年齡
}

最後我們在執行 protoc 命令

protoc --go-struct_out=./ pt.proto

此時我們發現生產了 pt.struct.go 文件
文件裏面生成的 go 代碼如下

package protoc_struct

type User struct {
    // xingming
    Name string `json:"name"` // 姓名
    // age
    Age int64 `json:"age"` // 年齡
}

和上面的. proto 文件一一對應。
寫到這裏,我們自己寫一個 go 對應的 protobuf 插件就完成。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Ixr6EpErZ2Fj7wp4rb8XcA