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 函数式编程的特性,将行为先存储在容器中,然后后续再去调用这些行为

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WindTerm是一个基于Web的终端模拟器,支持通过SSH连接远程服务器,并提供rz/sz文件传输功能。rz/sz是Linux/Unix系统上的一个工具,可以将本地文件传输到远程服务器。在Windows系统,可以使用PuTTY等工具实现类似功能。 在golang,我们可以使用os/exec包来执行命令并获取输出结果,然后通过WebSocket将结果返回到前端。以下是一个简单的示例代码: ```go package main import ( "io/ioutil" "os" "os/exec" "github.com/gorilla/websocket" ) func rzHandler(conn *websocket.Conn, params map[string]string) { // 获取上传的文件名和大小 filename := params["filename"] filesize, _ := strconv.Atoi(params["filesize"]) // 执行rz命令,并将文件写入stdin cmd := exec.Command("rz", "-q", "-e", "-b", "-") stdin, _ := cmd.StdinPipe() go func() { defer stdin.Close() conn.SetReadLimit(int64(filesize)) conn.SetReadDeadline(time.Now().Add(30 * time.Second)) conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(30 * time.Second)) return nil }) for { _, message, err := conn.ReadMessage() if err != nil { break } stdin.Write(message) } }() // 读取rz命令的输出并发送到前端 stdout, _ := cmd.StdoutPipe() go func() { defer stdout.Close() for { output, err := ioutil.ReadAll(stdout) if err != nil { break } conn.WriteMessage(websocket.BinaryMessage, output) } }() // 启动rz命令 cmd.Start() cmd.Wait() } ``` 在这个示例代码,我们使用了gorilla/websocket包来处理WebSocket连接。首先从前端获取上传的文件名和大小,然后执行rz命令,并将文件写入stdin。接着读取rz命令的输出并发送到前端。 需要注意的是,在执行rz命令时,我们需要设置一些参数,例如-q表示关闭交互模式,-e表示启用转义字符,-b表示二进制模式,-表示使用stdin进行文件传输。 当然,这只是一个简单的示例代码,实际使用还需要考虑一些安全和错误处理的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值