前言
在本篇文章中,我们将要学习一下Go语言的代码生成的玩法。Go语言代码生成主要还是用来解决编程泛型的问题,泛型编程主要解决的问题是因为静态类型语言有类型,所以,相关的算法或是对数据处理的程序会因为类型不同而需要复制一份,这样导致数据类型和算法功能耦合的问题。泛型编程可以解决这样的问题,就是说,在写代码的时候,不用关心处理数据的类型,只需要关心相当处理逻辑。泛型编程是静态语言中非常非常重要的特征,如果没有泛型,我们很难做到多态,也很难完成抽象,会导致我们的代码冗余量很大。
更新历史
2020 年 12 月 23 日 - 初稿
扩展阅读
我们并不需要自己手写 gen.sh
这样的工具类,已经有很多第三方的已经写好的可以使用。下面是一个列表:
现实中的类比
举个现实当中的例子,用螺丝刀来做具比方,螺丝刀本来就是一个拧螺丝的动作,但是因为螺丝的类型太多,有平口的,有十字口的,有六角的……螺丝还有大小尺寸,导致我们的螺丝刀为了要适配各种千奇百怪的螺丝类型(样式和尺寸),导致要做出各种各样的螺丝刀。
![]() |
![]() |
---|---|
而真正的抽象是螺丝刀不应该关心螺丝的类型,只要关注好自己的功能是否完备,并让自己可以适配于不同类型的螺丝,如下所示,这就是所谓的泛型编程要解决的实际问题。
Go语方的类型检查
因为Go语言目前并不支持真正的泛型,所以,只能用 interface{}
这样的类似于 void*
这种过度泛型来玩这就导致了我们在实际过程中就需要进行类型检查。Go语言的类型检查有两种技术,一种是 Type Assert,一种是Reflection。
Type Assert
这种技术,一般是对某个变量进行 .(type)
的损人和,其会返回两个值, variable, error
,第一个返回值是被转换好的类型,第二个是如果不能转换类型,则会报错。
比如下面的示例,我们有一个通用类型的容器,可以进行 Put(val)
和 Get()
,注意,其使用了 interface{}
作泛型
1 | //Container is a generic container, accepting anything. |
在使用中,我们可以这样使用
1 | intContainer := &Container{} |
但是,在把数据取出来时,因为类型是 interface{}
,所以,你还要做一个转型,如果转型成功能才能进行后续操作(因为 interface{}
太泛了,泛到什么类型都可以放)下在是一个Type Assert的示例:
1 | // assert that the actual type is int |
Reflection
对于反射,我们需要把上面的代码修改如下:
1 | type Container struct { |
上面的代码并不难读,这是完全使用 reflection的玩法,其中
- 在
NewContainer()
会根据参数的类型初始化一个Slice - 在
Put()
时候,会检查val
是否和Slice的类型一致。 - 在
Get()
时,我们需要用一个入参的方式,因为我们没有办法返回reflect.Value
或是interface{}
,不然还要做Type Assert - 但是有类型检查,所以,必然会有检查不对的总理 ,因些,需要返回
error
于是在使用上面这段代码的时候,会是下面这个样子:
1 | f1 := 3.1415926 |
我们可以看到,Type Assert是不用了,但是用反射写出来的代码还是有点复杂的。那么有没有什么好的方法?
它山之石
对于泛型编程最牛的语言 C++ 来说,这类的问题都是使用 Template来解决的。
//用 |
int i=5, j=6, k; //生成int类型的函数k=GetMax<int>(i,j); long l=10, m=5, n; //生成long类型的函数n=GetMax<long>(l,m); |
---|---|
C++的编译器会在编译时分析代码,根据不同的变量类型来自动化的生成相关类型的函数或类。C++叫模板的具体化。
这个技术是编译时的问题,所以,不需要我们在运行时进行任何的运行的类型识别,我们的程序也会变得比较的干净。
那么,我们是否可以在Go中使用C++的这种技术呢?答案是肯定的,只是Go的编译器不帮你干,你需要自己动手。
Go Generator
要玩 Go的代码生成,你需要三件事:
- 一个函数模板,其中设置好相应的占位符。
- 一个脚本,用于按规则来替换文本并生成新的代码。
- 一行注释代码。
函数模板
我们把我们之前的示例改成模板。取名为 container.tmp.go
放在 ./template/
下
1 | package PACKAGE_NAME |
我们可以看到函数模板中我们有如下的占位符:
PACKAGE_NAME
– 包名GENERIC_NAME
– 名字GENERIC_TYPE
– 实际的类型
其它的代码都是一样的。
函数生成脚本
然后,我们有一个叫gen.sh
的生成脚本,如下所示:
1 | #!/bin/bash |
其需要4个参数:
- 模板源文件
- 包名
- 实际需要具体化的类型
- 用于构造目标文件名的后缀
然后其会用 sed
命令去替换我们的上面的函数模板,并生成到目标文件中。
生成代码
接下来,我们只需要在代码中打一个特殊的注释:
1 | //go:generate ./gen.sh ./template/container.tmp.go gen uint32 container |
其中,
- 第一个注释是生成包名为
gen
类型为uint32
目标文件名以container
为后缀 - 第二个注释是生成包名为
gen
类型为string
目标文件名以container
为后缀
然后,在工程目录中直接执行 go generate
命令,就会生成如下两份代码,
一份文件名为uint32_container.go
1 | package gen |
另一份的文件名为 string_container.go
1 | package gen |
这两份代码可以让我们的代码完全编译通过,所付出的代价就是需要多执行一步 go generate
命令。
新版Filter
现在我们再回头看看我们之前《Go编程模式:Map-Reduce》中的那些个用反射整出来的例子,有了这样的技术,我就不必在代码里用那些晦涩难懂的反射来做运行时的类型检查了。我们可以写下很干净的代码,让编译器在编译时检查类型对不对。下面是一个Fitler的模板文件 filter.tmp.go
:
1 | package PACKAGE_NAME |
于是我们可在需要使用这个的地方,加上相关的 go generate 的注释
1 | type Employee struct { |