Golang 中實現註解功能的思路分析

註解的作用

提到註解,需要短暫的說明其前世今生。在註解興起之前,各個框架爲了靈活性,基本都是基於 XML/JSON/YAML 之類的配置文件來做模塊間的解耦。

因爲配置文件可以理解爲代碼對外的一種特殊的接口,需要先進行設計、代碼實現,然後才能對外使用。所以,一般而言配置文件對解耦可以做的比較徹底,但是開發維護成本會比較高。爲了解決這個問題,註解這種方式就沒提出來了,相對於配置文件它在耦合性上做了一定的讓步,換來了更改的易維護性。

例如,著名的 Java 就在 Java 5 中引入了註解,支持對源碼中的類、方法、變量、參數和包進行註解,虛擬機通過反射技術,可以在運行時獲取到註解內容,並將其相關功能動態加入到目標程序的字節碼中。例如,下面就是維基百科中給出的一個 Java 中的註解的例子:

 //等同於 @Edible(value = true)
  @Edible(true)
  Item item = new Carrot();

  public @interface Edible {
    boolean value() default false;
  }

  @Author(first = "Oompah", last = "Loompah")
  Book book = new Book();

  public @interface Author {
    String first();
    String last();
  }

通常註解會被用於:格式檢查、減少配置文件試用、減少重複工作,常見於各類框架如 Junit、xUtils 等等。

一些實現註解的開源 Golang 工程

由於註解有其獨特的作用,因此,雖然至今(版本<=1.13)Golang 原生版本不支持註解功能,依然有不少的開源項目基於自己的需求實現了註解,其中比較著名的有:

beego中的註解路由實現:https://beego.me/docs/mvc/controller/router.md

Golang 中實現註解的基本思路

參考:https://github.com/MarcGrol/golangAnnotations/wiki

第一步:源碼詞法分析

Golang 在編譯時候涉及到的詞法分析和語法分析,其大致過程如下:

  • Scanner(掃描器)將源代碼轉換爲一系列的 token,以供 Parser 使用。
  • Parser(語法分析器)將這些 token 轉換爲 AST(Abstract Syntax Tree, 抽象語法樹),以供代碼生成。
  • 將 AST 轉換爲機器碼。

這些相關功能的核心代碼在了標準庫 go/ast 中,可以直接使用。

例如,我們可以通過 github 中的開源工具 goast-viewer 快速分析以下代碼段:

package main

import (
	"fmt"
)

// main entry
func main() {
	fmt.Printf("Hello, Golang\n")
}

通過 go ast 工具就可以被構建出如下的抽象語法樹:

0  *ast.File {
     1  .  Doc: nil
     2  .  Package: foo:1:1
     3  .  Name: *ast.Ident {
     4  .  .  NamePos: foo:1:9
     5  .  .  Name: "main"
     6  .  .  Obj: nil
     7  .  }
     8  .  Decls: []ast.Decl (len = 2) {
     9  .  .  0: *ast.GenDecl {
    10  .  .  .  Doc: nil
    11  .  .  .  TokPos: foo:3:1
    12  .  .  .  Tok: import
    13  .  .  .  Lparen: foo:3:8
    14  .  .  .  Specs: []ast.Spec (len = 1) {
    15  .  .  .  .  0: *ast.ImportSpec {
    16  .  .  .  .  .  Doc: nil
    17  .  .  .  .  .  Name: nil
    18  .  .  .  .  .  Path: *ast.BasicLit {
    19  .  .  .  .  .  .  ValuePos: foo:4:2
    20  .  .  .  .  .  .  Kind: STRING
    21  .  .  .  .  .  .  Value: "\"fmt\""
    22  .  .  .  .  .  }
    23  .  .  .  .  .  Comment: nil
    24  .  .  .  .  .  EndPos: -
    25  .  .  .  .  }
    26  .  .  .  }
    27  .  .  .  Rparen: foo:5:1
    28  .  .  }
    29  .  .  1: *ast.FuncDecl {
    30  .  .  .  Doc: *ast.CommentGroup {
    31  .  .  .  .  List: []*ast.Comment (len = 1) {
    32  .  .  .  .  .  0: *ast.Comment {
    33  .  .  .  .  .  .  Slash: foo:7:1
    34  .  .  .  .  .  .  Text: "// main entry"
    35  .  .  .  .  .  }
    36  .  .  .  .  }
    37  .  .  .  }
    38  .  .  .  Recv: nil
    39  .  .  .  Name: *ast.Ident {
    40  .  .  .  .  NamePos: foo:8:6
    41  .  .  .  .  Name: "main"
    42  .  .  .  .  Obj: *ast.Object {
    43  .  .  .  .  .  Kind: func
    44  .  .  .  .  .  Name: "main"
    45  .  .  .  .  .  Decl: *(obj @ 29)
    46  .  .  .  .  .  Data: nil
    47  .  .  .  .  .  Type: nil
    48  .  .  .  .  }
    49  .  .  .  }
    50  .  .  .  Type: *ast.FuncType {
    51  .  .  .  .  Func: foo:8:1
    52  .  .  .  .  Params: *ast.FieldList {
    53  .  .  .  .  .  Opening: foo:8:10
    54  .  .  .  .  .  List: nil
    55  .  .  .  .  .  Closing: foo:8:11
    56  .  .  .  .  }
    57  .  .  .  .  Results: nil
    58  .  .  .  }
    59  .  .  .  Body: *ast.BlockStmt {
    60  .  .  .  .  Lbrace: foo:8:13
    61  .  .  .  .  List: []ast.Stmt (len = 1) {
    62  .  .  .  .  .  0: *ast.ExprStmt {
    63  .  .  .  .  .  .  X: *ast.CallExpr {
    64  .  .  .  .  .  .  .  Fun: *ast.SelectorExpr {
    65  .  .  .  .  .  .  .  .  X: *ast.Ident {
    66  .  .  .  .  .  .  .  .  .  NamePos: foo:9:2
    67  .  .  .  .  .  .  .  .  .  Name: "fmt"
    68  .  .  .  .  .  .  .  .  .  Obj: nil
    69  .  .  .  .  .  .  .  .  }
    70  .  .  .  .  .  .  .  .  Sel: *ast.Ident {
    71  .  .  .  .  .  .  .  .  .  NamePos: foo:9:6
    72  .  .  .  .  .  .  .  .  .  Name: "Printf"
    73  .  .  .  .  .  .  .  .  .  Obj: nil
    74  .  .  .  .  .  .  .  .  }
    75  .  .  .  .  .  .  .  }
    76  .  .  .  .  .  .  .  Lparen: foo:9:12
    77  .  .  .  .  .  .  .  Args: []ast.Expr (len = 1) {
    78  .  .  .  .  .  .  .  .  0: *ast.BasicLit {
    79  .  .  .  .  .  .  .  .  .  ValuePos: foo:9:13
    80  .  .  .  .  .  .  .  .  .  Kind: STRING
    81  .  .  .  .  .  .  .  .  .  Value: "\"Hello, Golang\\n\""
    82  .  .  .  .  .  .  .  .  }
    83  .  .  .  .  .  .  .  }
    84  .  .  .  .  .  .  .  Ellipsis: -
    85  .  .  .  .  .  .  .  Rparen: foo:9:30
    86  .  .  .  .  .  .  }
    87  .  .  .  .  .  }
    88  .  .  .  .  }
    89  .  .  .  .  Rbrace: foo:10:1
    90  .  .  .  }
    91  .  .  }
    92  .  }
    93  .  Scope: *ast.Scope {
    94  .  .  Outer: nil
    95  .  .  Objects: map[string]*ast.Object (len = 1) {
    96  .  .  .  "main": *(obj @ 42)
    97  .  .  }
    98  .  }
    99  .  Imports: []*ast.ImportSpec (len = 1) {
   100  .  .  0: *(obj @ 15)
   101  .  }
   102  .  Unresolved: []*ast.Ident (len = 1) {
   103  .  .  0: *(obj @ 65)
   104  .  }
   105  .  Comments: []*ast.CommentGroup (len = 1) {
   106  .  .  0: *(obj @ 30)
   107  .  }
   108  }

例如通過這個工具我們就可以看到我們可以分析出每一段註釋所對應的,註釋內容以及位置:

    29  .  .  1: *ast.FuncDecl {
    30  .  .  .  Doc: *ast.CommentGroup {
    31  .  .  .  .  List: []*ast.Comment (len = 1) {
    32  .  .  .  .  .  0: *ast.Comment {
    33  .  .  .  .  .  .  Slash: foo:7:1
    34  .  .  .  .  .  .  Text: "// main entry"
    35  .  .  .  .  .  }
    36  .  .  .  .  }
    37  .  .  .  }
    38  .  .  .  Recv: nil
    39  .  .  .  Name: *ast.Ident {
    40  .  .  .  .  NamePos: foo:8:6
    41  .  .  .  .  Name: "main"
    42  .  .  .  .  Obj: *ast.Object {
    43  .  .  .  .  .  Kind: func
    44  .  .  .  .  .  Name: "main"
    45  .  .  .  .  .  Decl: *(obj @ 29)
    46  .  .  .  .  .  Data: nil
    47  .  .  .  .  .  Type: nil
    48  .  .  .  .  }
    49  .  .  .  }

具體的細節可以仔細閱讀這篇文章的解釋:Go 程序到機器碼的編譯之旅Go 程序到機器碼的編譯之旅

第二步:代碼生成

當我們通過代碼分析找到需要生成的代碼之後,可以考慮將代碼按照類似的方式進行存儲:

Structs: []model.Struct{
        {
      	     DocLines: []string{""},
             Name: "",
             Operations: []model.Operation{
                {
              	    DocLines: []string{""},
		            Name:       "",
              	    InputArgs:  []model.Field{
              	        { Name: "", TypeName: "" },
              	        { Name: "", TypeName: "" },
              	        { Name: "", TypeName: "" },
              	        { Name: "", TypeName: "" },
              	    },
              	    OutputArgs: []model.Field{
              	        { Name: "", TypeName: "" },
              	    },
                },
            },
        },
    }

之後,再按照意圖基於模塊進行中間代碼自動生成,這裏可以直接藉助 golang 中 text/template 包的模板渲染能力進行:

具體使用方式可以參考官方文檔:https://golang.org/pkg/text/template/

第三步:自動執行

爲了將上述步驟自動化,我們需要藉助 golang 提供的另外一個工具: go generate:

go generate 命令是 Golang 1.4 版本引入的一個新命令,當運行 go generate時,它將掃描與當前包相關的源代碼文件,找出所有包含"//go:generate"的特殊註釋,提取並執行該特殊註釋後面的命令,命令爲可執行程序,形同shell下面執行。

在我們的需求中,我們在需要處理的源碼 package 中增加 "//go:generate"相關命令,作用於僅爲當前 package,該命令僅檢查當前 package 中是否存在有滿足定義的註解,如果有就會進行處理,如果沒有則不會改變原有源碼內容。

需要注意的是:

  1. “//go:generate” 特殊註釋必須在.go源碼文件中,且僅當顯示運行 go generate 命令時,纔會執行特殊註釋後面的命令。
  2. 命令串行執行的,如果出錯,就終止後面的執行。

更多關於 go generate 的資料可以參考官方材料:https://blog.golang.org/generate

番外:Golang 中一種代替註解的方案

參考:https://mritd.me/2018/10/23/golang-code-plugin

“基礎代碼不變,後續使用者可以將自己的實際需求的需求以熱插拔的形式注入進來,Caddy 框架提供了一種解決思路。

// RegisterPlugin plugs in plugin. All plugins should register
// themselves, even if they do not perform an action associated
// with a directive. It is important for the process to know
// which plugins are available.
//
// The plugin MUST have a name: lower case and one word.
// If this plugin has an action, it must be the name of
// the directive that invokes it. A name is always required
// and must be unique for the server type.
func RegisterPlugin(name string, plugin Plugin) {
	if name == "" {
		panic("plugin must have a name")
	}
	if _, ok := plugins[plugin.ServerType]; !ok {
		plugins[plugin.ServerType] = make(map[string]Plugin)
	}
	if _, dup := plugins[plugin.ServerType][name]; dup {
		panic("plugin named " + name + " already registered for server type " + plugin.ServerType)
	}
	plugins[plugin.ServerType][name] = plugin
}

套路就是定義一個 map,map 裏用於存放一種特定形式的 func,並且暴露出一個方法用於向 map 內添加指定 func,然後在合適的時機遍歷這個 map,並執行其中的 func。這種套路利用了 Go 函數式編程的特性,將行爲先存儲在容器中,然後後續再去調用這些行爲

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章