|
| 1 | +--- |
| 2 | +title: "高级注解" |
| 3 | +type: docs |
| 4 | +--- |
| 5 | + |
| 6 | +# 高级注解 |
| 7 | + |
| 8 | +作为一位 Go 程序员,你会发现身边的同事大多都拥有其他语言的编写经验。那势必就会遇到一点,要把新学到的知识和以前的知识建立连接。 |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | +特殊在于,Go 有些特性是其他语言有,他没有的。最经典的就是 N 位 Java 同学寻找 Go 语言的注解在哪里,总要解释。 |
| 13 | + |
| 14 | +为此,今天煎鱼就带大家了解一下 Go 语言的注解的使用和情况。 |
| 15 | + |
| 16 | +## 什么是注解 |
| 17 | + |
| 18 | +### 了解历史 |
| 19 | + |
| 20 | +注解(Annotation)最早出现自何处,翻了一圈并没有找到。但可以明确,在注解的使用中,Java 注解最为经典,为了便于理解,因此我们基于 Java 做初步的注解理解。 |
| 21 | + |
| 22 | + |
| 23 | + |
| 24 | +在 2002 年,JSR-175 提出了 《[A Metadata Facility for the Java Programming Language](https://jcp.org/en/jsr/detail?id=175)》,也就是为 Java 编程语言提供元数据工具。 |
| 25 | + |
| 26 | +这就是现在使用最广泛地注解(Annotation)的来源。 |
| 27 | +示例如下: |
| 28 | + |
| 29 | +```golang |
| 30 | +// @annotation1 |
| 31 | +// @annotation2 |
| 32 | +func Hello() string { |
| 33 | + return "" |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +在格式上均以 “@” 作为注解标识来使用。 |
| 38 | + |
| 39 | +### 注解例子 |
| 40 | + |
| 41 | +摘抄自 @wikipedia 的一个注解例子: |
| 42 | + |
| 43 | +```java |
| 44 | + //等同于 @Edible(value = true) |
| 45 | + @Edible(true) |
| 46 | + Item item = new Carrot(); |
| 47 | + |
| 48 | + public @interface Edible { |
| 49 | + boolean value() default false; |
| 50 | + } |
| 51 | + |
| 52 | + @Author(first = "Oompah", last = "Loompah") |
| 53 | + Book book = new Book(); |
| 54 | + |
| 55 | + public @interface Author { |
| 56 | + String first(); |
| 57 | + String last(); |
| 58 | + } |
| 59 | + |
| 60 | + // 该标注可以在运行时通过反射访问。 |
| 61 | + @Retention(RetentionPolicy.RUNTIME) |
| 62 | + // 该标注只用于类内方法。 |
| 63 | + @Target({ElementType.METHOD}) |
| 64 | + public @interface Tweezable { |
| 65 | + } |
| 66 | + |
| 67 | +``` |
| 68 | + |
| 69 | +在上述例子中,通过注解去做了一系列的定义、声明、赋值等。若是对语言既有注解不熟,或是做的比较复杂的注解,就会有一定的理解成本。 |
| 70 | + |
| 71 | +在业内也常常会说,**注解就是 “在源码上进行编码”**,注解的存在,有着明确的优缺点。你觉得呢? |
| 72 | + |
| 73 | +## 注解的作用 |
| 74 | + |
| 75 | +在注解的的作用上,分为如下几点: |
| 76 | +1. 为编译器提供信息:注释可以被编译器用来检测错误或支持警告。 |
| 77 | +2. 编译时和部署时处理:软件工具可以处理注释信息以生成代码、XML文件等。 |
| 78 | +3. 运行时处理:有些注解可以在运行时检查,并用于其他用途。 |
| 79 | + |
| 80 | +## Go 注解在哪里 |
| 81 | + |
| 82 | +### 现状 |
| 83 | + |
| 84 | +Go 语言本身并没有原生支持强大的注解,仅限于以下两种: |
| 85 | +- 编译时生成:go:generate |
| 86 | +- 编译时约束:go:build |
| 87 | + |
| 88 | +但这先按不足以作为一个函数注解来使用,也无法形成像 Python 那样的装饰器行为。 |
| 89 | + |
| 90 | +### 为什么不支持 |
| 91 | + |
| 92 | +Go issues 上有人提过类似的提案: |
| 93 | + |
| 94 | + |
| 95 | + |
| 96 | + |
| 97 | +Go Contributor @ianlancetaylor 给出了明确的答复,Go在设计上更倾向于明确的、显式的编程风格。 |
| 98 | + |
| 99 | +思考的优缺点如下: |
| 100 | +- 优势:不知道 Go 能从添加装饰器中得到什么好处,没能在 issues 上明确论证。 |
| 101 | +- 缺点:是明确的,会存在意外设置的情况。 |
| 102 | + |
| 103 | +因如下原因,没有接受注解: |
| 104 | +- 对比现有代码方法,这种装饰器的新的方法没有提供比现有方法更多的优势,大到足矣推翻原有的设计思路。 |
| 105 | +- 社区内的投票,支持的也很少(基于表情符号的投票),用户反馈不多。 |
| 106 | + |
| 107 | +可能有小伙伴会说了,有注解做装饰器了,代码会简洁不少。 |
| 108 | + |
| 109 | +对此 Go 团队的态度很明确: |
| 110 | + |
| 111 | + |
| 112 | + |
| 113 | +Go 认为**可读性更重要**,如果只是额外多写一点代码,在权衡后,还是可以接受的。 |
| 114 | + |
| 115 | +## 用 Go 实现注解 |
| 116 | + |
| 117 | +虽然 Go 语言官方没有原生的完整支持,但开源社区中也有小伙伴已经放出了大招,借助各项周边工具和库来实现特定的函数注解功能。 |
| 118 | + |
| 119 | + |
| 120 | +GitHub 项目分别如下: |
| 121 | +- [MarcGrol/golangAnnotations](github.com/MarcGrol/golangAnnotations) |
| 122 | +- [u2takey/go-annotation](https://github.com/u2takey/go-annotation) |
| 123 | + |
| 124 | +使用示例如下: |
| 125 | + |
| 126 | +```golang |
| 127 | +package tourdefrance |
| 128 | + |
| 129 | +//go:generate golangAnnotations -input-dir . |
| 130 | + |
| 131 | +// @RestService( path = "/api/tour" ) |
| 132 | +type TourService struct{} |
| 133 | + |
| 134 | +type EtappeResult struct{ ... } |
| 135 | + |
| 136 | +// @RestOperation( method = "PUT", path = "/{year}/etappe/{etappeUid}" ) |
| 137 | +func (ts *TourService) addEtappeResults(c context.Context, year int, etappeUid string, results EtappeResult) error { |
| 138 | + return nil |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +对 Go 注解的使用感兴趣的小伙伴可以自行查阅使用手册。 |
| 143 | + |
| 144 | +我们更多的关心,Go 原生都没支持,那么开源库都是如何实现的呢?在此我们借助 [MarcGrol/golangAnnotations](github.com/MarcGrol/golangAnnotations) 项目所提供的思路来讲解。 |
| 145 | + |
| 146 | +分为三个步骤: |
| 147 | +1. 解析代码。 |
| 148 | +2. 模板处理。 |
| 149 | +3. 生成代码。 |
| 150 | + |
| 151 | +### 解析 AST |
| 152 | + |
| 153 | +首先,我们需要用用 go/ast 标准库获取代码所生成的 AST Tree 中需要的内容和结构。 |
| 154 | + |
| 155 | +示例代码如下: |
| 156 | + |
| 157 | +```shell |
| 158 | +parsedSources := ParsedSources{ |
| 159 | + PackageName: "tourdefrance", |
| 160 | + Structs: []model.Struct{ |
| 161 | + { |
| 162 | + DocLines: []string{"// @RestService( path = "/api/tour" )"}, |
| 163 | + Name: "TourService", |
| 164 | + Operations: []model.Operation{ |
| 165 | + { |
| 166 | + DocLines: []string{"// @RestOperation( method = "PUT", path = "/{year}/etappe/{etappeUid}"}, |
| 167 | + ... |
| 168 | + }, |
| 169 | + }, |
| 170 | + }, |
| 171 | + }, |
| 172 | +} |
| 173 | +``` |
| 174 | +我们可以看到,在 AST Tree 中能够获取到在示例代码中所定义的注解内容,我们就可以依据此去做很多奇奇怪怪的事情了。 |
| 175 | +
|
| 176 | +### 模板生成 |
| 177 | +
|
| 178 | +紧接着,在知道了注解的输入是什么后,我们只需要根据实际情况,编写对应的模板生成器 code-generator 就可以了。 |
| 179 | +
|
| 180 | +我们会基于 text/template 标准库来实现,比较经典的像是 [kubernetes/code-generator](https://github.com/kubernetes/code-generator) 是一个可以参考的实现。 |
| 181 | +
|
| 182 | +代码实现完毕后,将其编译成 go plugin,便于我们在下一步调用就可以了。 |
| 183 | +
|
| 184 | +### 代码生成 |
| 185 | +
|
| 186 | +最后,万事俱备只欠东风。差的就是告诉工具,哪些 Go 文件中包含注解,需要我们去生成的。 |
| 187 | +
|
| 188 | +这时候我们可以使用 `//go:generate` 在 Go 文件声明。就像前面的项目中所说的: |
| 189 | +
|
| 190 | +``` |
| 191 | +//go:generate golangAnnotations -input-dir . |
| 192 | +``` |
| 193 | +
|
| 194 | +声明该 Go 文件需要生成,并调用前面编写好的 golangAnnotations 二进制文件,就可以实现基本的 Go 注解生成了。 |
| 195 | +
|
| 196 | +## 总结 |
| 197 | +
|
| 198 | +我们介绍了注解(Annotation)的历史背景。同时我们针对 Go 语言目前原生的注解支持情况进行了说明。 |
| 199 | +
|
| 200 | +也面向为什么 Go 没有像 Java 那样支持强大的注解进行了基于 Go 官方团队的原因解释。如果希望在 Go 实现注解的,也提供了相应的开源技术方案。 |
| 201 | +
|
| 202 | +你觉得 Go 语言是否需要有强大的注解支持? |
0 commit comments