Golang 中实现注解功能的思路分析

2023-11-15

注解的作用

提到注解,需要短暂的说明其前世今生。在注解兴起之前,各个框架为了灵活性,基本都是基于 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 函数式编程的特性,将行为先存储在容器中,然后后续再去调用这些行为

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Golang 中实现注解功能的思路分析 的相关文章

  • go字符串详解

    文章目录 摘要 1 byte和rune类型 2 字符串 string 3 练习 反转字符串 摘要 go字符串结构体包含 指向底层存储数组的指针 字符串长度 字符串按utf 8将字符编码成二进制数 然后存储在byte数组中 因为utf 8编码
  • Go Web编程实战(6)----反射

    目录 反射 反射的3大原则 接口类型变量 转换为 反射类型对象 反射类型对象 转换为 接口类型变量 反射类型对象 修改 值必 可写的 反射 与其他语言一样 Go语言的反射同样是指 计算机程序在运行时 可以访问 检测和修改它本身状态或行为的一
  • 带你使用Golang快速构建出命令行应用程序

    在日常开发中 大家对命令行工具 CLI 想必特别熟悉了 如果说你不知道命令工具 那你可能是个假开发 每天都会使用大量的命令行工具 例如最常用的Git Go Docker等 不管是做技术开发还是业务开发 都会有开发命令行程序的场景 例如如果是
  • Go_关键字、编译、转义字符

    关键字 关键字是指被go语言赋予了特殊含义的单词 共25个 关键字不能用于自定义名字 只能在特定语法结构中使用 break default func interface select case defer go map struct cha
  • Jenkins系列:3、wsl/ubuntu安装Jenkins及Jenkins构建可交叉编译的go程序

    Jenkins系列 3 wsl ubuntu安装Jenkins及Jenkins构建可交叉编译的go程序 文章目录 Jenkins系列 3 wsl ubuntu安装Jenkins及Jenkins构建可交叉编译的go程序 1 前言 2 wsl
  • Go切片排序

    Go 语言标准库提供了sort包 用于对切片和用户定义的集合进行排序 具体示例如下 基本排序 package main import fmt sort func main float 从小到大排序 f float64 5 2 1 3 0 7
  • golang:环境变量GOPROXY和GO111MODULE设置

    我们安装完golang后 我们在windows的cmd命令下就可以直接查看和使用go命令和环境变量了 同样的在linux下可以在控制台使用 如下图所示 C Users lijie1 gt go env set GO111MODULE set
  • Golang连接Jenkins获取Job Build状态及相关信息

    文章目录 1 连接Jenkins 2 controller 3 module 4 router 5 效果展示 第三方包 gojenkins 方法文档 gojenkins docs 实现起来很简单 利用第三方库 连接jenkins 调用相关方
  • 掌握 Go 语言中的循环结构:从基础到高级

    一 if else 分支结构 1 if 条件判断基本写法 package main import fmt func main score 65 if score gt 90 fmt Println A else if score gt 75
  • 【go语言开发】编写单元测试

    本文主要介绍使用go语言编写单元测试用例 首先介绍如何编写单元测试 然后介绍基本命令的使用 最后给出demo示例 文章目录 前言 命令 示例 前言 在go语言中编写单元测试时 使用说明 测试文件命名 在 Go 语言中 测试文件的命名应与被测
  • go-zero目录结构和说明

    code of conduct md 行为准则 CONTRIBUTING md 贡献指南 core 框架的核心组件 bloom 布隆过滤器 用于检测一个元素是否在一个集合中 breaker 熔断器 用于防止过多的请求导致系统崩溃 cmdli
  • go-zero开发入门之网关往rpc服务传递数据1

    go zero 的网关往 rpc 服务传递数据时 可以使用 headers 但需要注意前缀规则 否则会发现数据传递不过去 或者对方取不到数据 go zero 的网关对服务的调用使用了第三方库 grpcurl 入口函数为 InvokeRPC
  • go-zero开发入门-API网关鉴权开发示例

    本文是 go zero开发入门 API网关开发示例 一文的延伸 继续之前请先阅读此文 在项目根目录下创建子目录 middleware 在此目录下创建文件 auth go 内容如下 鉴权中间件 package middleware impor
  • GoLong的学习之路,进阶,Viper(yaml等配置文件的管理)

    本来有今天是继续接着上一章写微服务的 但是这几天有朋友说 再写Web框架的时候 遇到一个问题 就是很多的中间件 redis 微信 mysql mq 的配置信息写的太杂了 很不好管理 希望我能写一篇有管理配置文件的 所以这篇就放到今天写吧 微
  • 【golang】go执行shell命令行的方法( exec.Command )

    所需包 import os exec cmd 的用法 cmd exec Command ls lah ls是命令 后面是参数 e cmd Run 多个参数的要分开传入 如 ip link show bond0 cmd
  • [每周一更]-(第55期):Go的interface

    参考地址 https juejin cn post 6978322067775029261 https gobyexample com interfaces https go dev tour methods 9 介绍下Go的interfa
  • Golang拼接字符串性能对比

    g o l a n g golang g o l an g
  • 这套Go语言开发框架组合真的非常高效

    我尝试过很多框架 从Django Flask和Laravel到NextJS和SvelteKit 到目前为止 这是我唯一可以使用的不会让我感到疯狂或者放弃项目的堆栈 框架 我喜欢所有这些框架 但我只是不太适应它们的设计方式 实际上 我是一个弱
  • 【go语言】结构体数据填充生成md错误码文件

    这里使用pongo2这个模版引擎库进行md文件渲染 GitHub flosch pongo2 Django syntax like template engine for Go package main import fmt github
  • 【go语言】读取toml文件

    一 简介 TOML 全称为Tom s Obvious Minimal Language 是一种易读的配置文件格式 旨在成为一个极简的数据序列化语言 TOML的设计原则之一是保持简洁性 易读性 同时提供足够的灵活性以满足各种应用场景 TOML

随机推荐

  • 最长上升子序列(C语言 动态规划)

    描述 一个数的序列bi 当b1 lt b2 lt lt bS的时候 我们称这个序列是上升的 对于给定的一个序列 a1 a2 aN 我们可以得到一些上升的子序列 ai1 ai2 aiK 这里1 i1 lt i2 lt lt iK N 比如 对
  • linux设备利用率,linux – 对dm设备100%利用率的影响

    我们这里有一台RHEL 5 6服务器 有4条到单个LUN的活动路径 我们怀疑它无法将管道中的足够的IO塞进到另一端的XIV mpath0 XXXXXXXXXXXXXXX dm 9 IBM 2810XIV size 1 6T features
  • WARN Received a PartitionLeaderEpoch assignment for an epoch < latestEpoch

    Topic 处于 under replicated 状态 server log 充满 2020 11 30 19 00 00 006 WARN Received a PartitionLeaderEpoch assignment for a
  • MySQL---创建表和连表、分组、排序等操作

    首先这里有两张表做测试用 代码均在命令提示符终端完成 创建部门表 create table department table department id int auto increment primary key department v
  • TCP三次握手如果使用二次握手代替则会出现的问题

    简述 第一次握手 发送SYN报文 传达信息 你好 我想建立连接 第二次握手 回传SYN ACK报文 传达信息 好的 可以建立链接 第三次握手 回传ACK报文 传到信息 好的 我知道了 那我们连接 然后就建立连接了 在发送报文之前各方都要确认
  • Java基础——Math类、Random类、System类

    目录 1 Math类 2 Random类 3 System类 1 Math类 Math 类包含用于执行基本数学运算的方法 如初等指数 对数 平方根和三角函数 成员变量 public static final double E 自然底数 pu
  • java设计模式——代理模式(Proxy Pattern)

    概述 在某些情况下 一个客户不想或者不能直接引用一个对 象 此时可以通过一个称之为 代理 的第三者来实现 间接引用 代理对象可以在客户端和目标对象之间起到 中介的作用 并且可以通过代理对象去掉客户不能看到 的内容和服务或者添加客户需要的额外
  • 在vscode中创建vue自定义模板

    首先找到用户代码片段的位置 https img blog csdnimg cn 20201105094443610 png pic center 在输入框中输入vue 然后选择vue json或vue 打开之后是这样的 这些注释就是介绍怎么
  • SQL拼接存在的误区

    这两天修复一个SQL拼接的问题 给自己挖了个坑 对于指定的字符串作为查询条件时 当在SQL语句中使用单引号包裹查询条件时 需要注意以下几点 1 SQL注入攻击 如果接受外部输入的值直接拼接到SQL语句中 在查询条件中包含特殊字符如单引号或双
  • Java内部类总结

    Java内部类其实在J2EE编程中使用较少 不过在窗口应用编程中特别常见 主要用来事件的处理 其实 做非GUI编程 内部类完全可以不用 内部类的声明 访问控制等于外部类有所不同 要灵活使用内部类来编写程序 还是有相当难度的 Java发明了这
  • Web后台快速开发框架

    Web后台快速开发框架 Coldairarrow 目录 目录 第1章 目录 1 第2章 简介 3 第3章 基础准备 4 3 1 开发环境要求 4 3 2 基础数据库构建 4 3 3 运行 5 第4章 详细教程 6 4 1 代码架构 6 4
  • PC: 市场寒冬剖析

    市场调研机构Canalys数据显示 今年一季度 中国市场整体PC出货量同比下降24 至890万台 已是连续第五个季度下跌 今年截至618结束 都没有一家主要的PC厂商愿意发战报 PC市场怎样走出寒冬 谈谈你的理解和看法 PC 一 2022年
  • Rxjava学习(一)简单分析Rxjava调用流程

    本篇以Rxjava最简短的调用流程为例来分析 下面是要分析的实例代码 Observable create new ObservableOnSubscribe
  • 1140 石子游戏 II

    题目描述 亚历克斯和李继续他们的石子游戏 许多堆石子 排成一行 每堆都有正整数颗石子 piles i 游戏以谁手中的石子最多来决出胜负 亚历克斯和李轮流进行 亚历克斯先开始 最初 M 1 在每个玩家的回合中 该玩家可以拿走剩下的 前 X 堆
  • C++中传送函数指针

    随时随地阅读更多技术实战干货 获取项目源码 学习资料 请关注源代码社区公众号 ydmsq666 函数指针是一种非常好的类型 因此 可以编写一个函数 它的一个参数是函数指针 然后 在 外部 函数使用其函数指针参数时 就间接地调用在调用函数时对
  • vue路由

    路由 理解 一个路由 route 就是一组映射关系 key value 多个路由需要路由器 router 进行管理 前端路由 key是路径 value是组件 路由标签
  • 基于共享内存 实现Python 和c++ 传输图片

    需求 c 将图片写入共享内存 python读取 将c 写的共享内存的操作封装为一个so库 c 和python共同调用这个库 便于双方的操作 省去信号量的管理操作 一 c 写入端 int main int argc char argv Sha
  • GDB的使用方法

    GDB的使用方法 一 GDB的静态调试启动方法 1 当需要在命令行通过gdb来启动可执行程序的时候 可使用一下命令 gdb lt 可执行程序名 gt 这个时候gdb会加载可执行程序的符号表和堆栈 并为启动程序作好准备 接下来 需要设置可执行
  • Knight Moves_dfs_2018_3_10

    A friend of you is doing research on the Traveling Knight Problem TKP where you are to find the shortest closed tour of
  • Golang 中实现注解功能的思路分析

    文章目录 注解的作用 一些实现注解的开源 Golang 工程 Golang 中实现注解的基本思路 第一步 源码词法分析 第二步 代码生成 第三步 自动执行 番外 Golang 中一种代替注解的方案 注解的作用 提到注解 需要短暂的说明其前世