golang代碼生成

  • 2020 年 2 月 11 日
  • 筆記

代碼生成

makefile在make all之前會先generated_files去進行代碼生成,所以首先要理解代碼生成的原理,然後才可以很好的知道 這個過程都幹啥了.

all: generated_files    hack/make-rules/build.sh $(WHAT)

首先,為什麼需要代碼生成?答案是「懶」。寫過代碼的都知道很多時候有大量結構重複的代碼需要去寫,勞心勞力還沒什麼技術含量,所以為了解決這個問題代碼生成它來了。

代碼生成的場景有很多如:

  • protobuf 根據一個協議字段配置文件生成客戶端和服務端的.go代碼
  • IDE中的自動測試用例和接口實現函數代碼生成
  • 一些web框架自動生成RESTFUL接口代碼
  • operator腳手架工具生成k8s controller代碼等

在kubernetes中主要生成代碼有這些:

  • deep-copy generator, kubernetes中的對象都需要實現該方法,每個對象都自己手動去寫很累,因為kubernetes需要把期望狀態與實際狀態進行比對,所以需要把對象深拷貝一份再比對
  • defaulter generator 不重要,可以用來填充些靜態內容的文件
  • go-to-protobuf generator, 組件之間都是通過protobuf進行通信

代碼生成原理

所以我們的目的就是根據源代碼再生成一些源代碼,那問題就分成三步走:

  1. 解析我們寫的源碼,提取我們所需要的內容,如包名,結構體名,等
  2. 渲染模板文件
  3. 生成源碼文件

下面用個簡單的例子來幫助理解這一過程。

安裝stringer

stringer可以幫助枚舉類型自動生成String()方法

go install golang.org/x/tools/cmd/stringer

編碼

cd $GOPATH/src  mkdir gen && cd gen && touch main.go

我們在main.go里輸入以下內容:

package main    import "fmt"    //go:generate stringer -type=Pill  type Pill int    const (    Placebo Pill = iota    Aspirin    Ibuprofen    Paracetamol    Acetaminophen = Paracetamol  )    func main() {    fmt.Println(Placebo.String())  }

注意現在只有一個main.go 且裏面並沒有實現String()方法,String方法輸出葯的名字,而main裏面卻調用了,那自然編譯不過:

# github.com/fanux/gen  ./main.go:17:21: Placebo.String undefined (type Pill has no field or method String)

如果我們自己要去實現String()方法枯燥無味且還需要對葯進行判斷,葯越多switch case越多,假設有10000種葯,那麼我們直接就崩潰了

代碼生成

因為有這個注釋,go generate時就會調用stringer工具進行代碼生成

//go:generate stringer -type=Pill# go generate && go build && ls  gen            go.mod         main.go        pill_string.go

可以看到就生成了一個pill_string.go文件,裏面實現了我們想要的String方法:

func _() {    // An "invalid array index" compiler error signifies that the constant values have changed.    // Re-run the stringer command to generate them again.    var x [1]struct{}    _ = x[Placebo-0]    _ = x[Aspirin-1]    _ = x[Ibuprofen-2]    _ = x[Paracetamol-3]  }    const _Pill_name = "PlaceboAspirinIbuprofenParacetamol"    var _Pill_index = [...]uint8{0, 7, 14, 23, 34}    func (i Pill) String() string {    if i < 0 || i >= Pill(len(_Pill_index)-1) {      return "Pill(" + strconv.FormatInt(int64(i), 10) + ")"    }    return _Pill_name[_Pill_index[i]:_Pill_index[i+1]]  }

函數實現成什麼樣子不用糾結,知道生成了我們需要的代碼就行,運行一下:

# ./gen  Placebo

原理

stringer是如何做到的,很簡單,讓我們一起去看一下其源碼,其中最重要的就是go/ast語法樹解析和go/parser解析庫的運用

首先我們需要生成代碼肯定需要知道包名是什麼:

packages庫的LOAD函數可以幫助我們做到

golang.org/x/tools/go/packages  pkgs, err := packages.Load(cfg, patterns...)

並且獲取到package下面的所有文件信息:

for i, file := range pkg.Syntax {    g.pkg.files[i] = &File{      file:        file,      pkg:         g.pkg,      trimPrefix:  g.trimPrefix,      lineComment: g.lineComment,    }  }

ast庫中的Inspect函數可以深度優先的去遍歷語法樹,樹的每個節點會傳遞給file.genDecl函數去處理

ast.Inspect(file.file, file.genDecl)

遍歷聲明的元素,每個元素都是一個ValueSpec:

for _, spec := range decl.Specs {    vspec := spec.(*ast.ValueSpec)

最終遍歷得到枚舉的名字並保存下來,這也是生成代碼所需要的

for _, name := range vspec.Names {      ...    v := Value{      originalName: name.Name, // 我們想要的      value:        u64,      signed:       info&types.IsUnsigned == 0,      str:          value.String(),    }      ...  }

最後用一些模板把代碼渲染出來:

const stringOneRun = `func (i %[1]s) String() string {    if %[3]si >= %[1]s(len(_%[1]s_index)-1) {      return "%[1]s(" + strconv.FormatInt(int64(i), 10) + ")"    }    return _%[1]s_name[_%[1]s_index[i]:_%[1]s_index[i+1]]  }`    const stringOneRunWithOffset = `func (i %[1]s) String() string {    i -= %[2]s    if %[4]si >= %[1]s(len(_%[1]s_index)-1) {      return "%[1]s(" + strconv.FormatInt(int64(i + %[2]s), 10) + ")"    }    return _%[1]s_name[_%[1]s_index[i] : _%[1]s_index[i+1]]  }

所以就沒什麼神奇的了

如果覺得上面還是不太懂,那麼看個更簡單的例子:

func main() {      // 這是我們需要進行語法樹分析的代碼    src := `  package p  const c = 1.0  var X = f(3.14)*2 + c  `        // 創建一個AST    fset := token.NewFileSet()    f, err := parser.ParseFile(fset, "src.go", src, 0)    if err != nil {      panic(err)    }        // 遍歷語法樹節點    ast.Inspect(f, func(n ast.Node) bool {      var s string      switch x := n.(type) {      case *ast.BasicLit:        s = x.Value      case *ast.Ident:        s = x.Name      }      if s != "" {        fmt.Printf("%s:t%sn", fset.Position(n.Pos()), s)      }      return true    })    }

會輸出:

src.go:2:9:     p  src.go:3:7:     c  src.go:3:11:    1.0  src.go:4:5:     X  src.go:4:9:     f  src.go:4:11:    3.14  src.go:4:17:    2  src.go:4:21:    c

可以看到變量名,變量值都被輸出了,然後我們就可以拿着這些結果去渲染代碼了

我們還可以輸出完整的語法樹看一下:

    src := `  package main  func main() {    println("Hello, World!")  }  `    fset := token.NewFileSet()    f, err := parser.ParseFile(fset, "", src, 0)    if err != nil {      panic(err)    }      ast.Print(fset, f)     0  *ast.File {       1  .  Package: 2:1       2  .  Name: *ast.Ident {       3  .  .  NamePos: 2:9       4  .  .  Name: "main"       5  .  }       6  .  Decls: []ast.Decl (len = 1) {       7  .  .  0: *ast.FuncDecl {       8  .  .  .  Name: *ast.Ident {       9  .  .  .  .  NamePos: 3:6      10  .  .  .  .  Name: "main"      11  .  .  .  .  Obj: *ast.Object {      12  .  .  .  .  .  Kind: func      13  .  .  .  .  .  Name: "main"      14  .  .  .  .  .  Decl: *(obj @ 7)      15  .  .  .  .  }      16  .  .  .  }      17  .  .  .  Type: *ast.FuncType {      18  .  .  .  .  Func: 3:1      19  .  .  .  .  Params: *ast.FieldList {      20  .  .  .  .  .  Opening: 3:10      21  .  .  .  .  .  Closing: 3:11      22  .  .  .  .  }      23  .  .  .  }      24  .  .  .  Body: *ast.BlockStmt {      25  .  .  .  .  Lbrace: 3:13      26  .  .  .  .  List: []ast.Stmt (len = 1) {      27  .  .  .  .  .  0: *ast.ExprStmt {      28  .  .  .  .  .  .  X: *ast.CallExpr {      29  .  .  .  .  .  .  .  Fun: *ast.Ident {      30  .  .  .  .  .  .  .  .  NamePos: 4:2      31  .  .  .  .  .  .  .  .  Name: "println"      32  .  .  .  .  .  .  .  }      33  .  .  .  .  .  .  .  Lparen: 4:9      34  .  .  .  .  .  .  .  Args: []ast.Expr (len = 1) {      35  .  .  .  .  .  .  .  .  0: *ast.BasicLit {      36  .  .  .  .  .  .  .  .  .  ValuePos: 4:10      37  .  .  .  .  .  .  .  .  .  Kind: STRING      38  .  .  .  .  .  .  .  .  .  Value: ""Hello, World!""      39  .  .  .  .  .  .  .  .  }      40  .  .  .  .  .  .  .  }      41  .  .  .  .  .  .  .  Ellipsis: -      42  .  .  .  .  .  .  .  Rparen: 4:25      43  .  .  .  .  .  .  }      44  .  .  .  .  .  }      45  .  .  .  .  }      46  .  .  .  .  Rbrace: 5:1      47  .  .  .  }      48  .  .  }      49  .  }      50  .  Scope: *ast.Scope {      51  .  .  Objects: map[string]*ast.Object (len = 1) {      52  .  .  .  "main": *(obj @ 11)      53  .  .  }      54  .  }      55  .  Unresolved: []*ast.Ident (len = 1) {      56  .  .  0: *(obj @ 29)      57  .  }      58  }

想拿啥都能拿到了

kubernetes中的代碼生成

kubernetes/gengo 項目專門用來做代碼生成的,有了上面基礎我就直接說結果了

咱打開kubernetes源碼, pkg/apis/core/types.go 里有Pod結構體的定義

// Pod is a collection of containers, used as either input (create, update) or as output (list, get).  type Pod struct {    metav1.TypeMeta    // +optional    metav1.ObjectMeta      // Spec defines the behavior of a pod.    // +optional    Spec PodSpec      // Status represents the current information about a pod. This data may not be up    // to date.    // +optional    Status PodStatus  }

注意同目錄下zz_generated.deepcopy.go 文件就是生成的,為Pod生成深拷貝方法 這裏面生成會有很多細節的處理,如鏈表的處理,指針的處理等,細節部分有興趣可以參考 此處代碼

func (in *Pod) DeepCopy() *Pod  func (in *Pod) DeepCopyObject() runtime.Object

如此是不是學到了什麼,比如我們在自定義資源時是不是可以用同樣的方式來減少我們的工作量,當然現在如kubebuilder operator framework都幫我們這樣做了,我們就不需要自己去實現代碼生成器了。但是學會了AST我們自己寫重複的業務邏輯時就會非常有用