1. 背景
在工程开发中,经常会遇到频繁的代码编写、上线以及维护的问题。如果能够减轻这些痛点,将大幅提高工作效率。因此,此时此刻,代码自动生成便产生了重要的意义和必要性。同时,在互联网技术日益快速发展的今天,相信大家都知道Go语言具有高并发、高性能等优点,因此,本文就会基于Go语言进行代码自动生成的实践。
2. 具体实践
2.1 项目的依赖
为了实现代码自动生成,需要先安装Go语言中的部分依赖。其中,有一个被称为“go generate”的工具,可以和Go程序进行一起编译,并将这种预处理过程作为对源代码的一部分,用于执行类似代码自动生成之类的操作。
在此前提下,需要在项目目录下安装"go-bindata"和"go-bindata-assetfs"两个依赖,安装命令如下:
go get -u github.com/jteeuwen/go-bindata/...
go get -u github.com/elazarl/go-bindata-assetfs/...
在这里,最终会得到两个程序,一个是将文件转换成Go代码的go-bindata,另一个是将bindata代码转换成http.FileSystem接口的go-bindata-assetfs。
2.2 给Go程序中的结构体打标记
在我们需要自动生成代码的Go语言结构体中,需要进行特别的处理。一般来说,我们需要在结构体中打上一个标记,以示这个结构体需要自动生成代码。假设我们有下面这个结构体:
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
}
我们需要在结构体上添加tag,并打上一个特殊的标记约定“generate”,像下面这样:
type User struct {
ID int64 `json:"id" generate:"id"`
Name string `json:"name" generate:"name"`
Email string `json:"email" generate:"email"`
CreatedAt *time.Time `json:"createdAt" generate:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt" generate:"updatedAt"`
}
在这个标记中,我们为每个字段命名,并设置了一个特殊的标记“generate”,这个标记在处理时会将其和字段一起处理。
2.3 编写模板文件
我们需要编写一些代码模板,以便生成代码时使用。这里,我们将使用Go语言的文本模板,即"text/template"包。首先,我们创建一个名为“template.go”文件,将其放在project/app/utils目录下:
package utils
import (
"os"
"text/template"
)
func GenerateFile(path, tmpl string, data interface{}) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
t, err := template.New("").Parse(tmpl)
if err != nil {
return err
}
return t.Execute(f, data)
}
在上述代码中,我们定义了一个名为GenerateFile的函数,它将生成一个文件并且将模板与数据传递给其以生成输出内容。其中,我们使用了text/template包,它是一个可进行文本模板处理的包,使用类似于许多其它语言的模板方式,因此,这样的话,我们可以非常方便地直接将模板和数据合并后输出。
此外,我们还需要一个默认的代码模板(项目名称为"awesome-app"):
package main
var codeTemplate = `package {{.PackageName}}
import (
{{- range .Imports }}
"{{.}}"
{{- end }}
)
{{ range .Types -}}
type I{{.Name}}Service interface {
Index(ctx context.Context) ([]{{.Name}}, error)
Create(ctx context.Context, input *{{.Name}}) (*{{.Name}}, error)
}
type {{.Name}}Service struct {
{{- range .Fields }}
{{.Name}} {{.Package}}.{{.TypeName}}
{{- end }}
}
func (s *{{.Name}}Service) Index(ctx context.Context) ([]{{.Name}}, error) {
{{- /* implementation goes here */ }}
}
func (s *{{.Name}}Service) Create(ctx context.Context, input *{{.Name}}) (*{{.Name}}, error) {
{{- /* implementation goes here */ }}
}
{{ end -}}`
在这个模板中,我们定义了一个包名,一些必要的导入和类型。我们还定义了一些公共类型和方法,并将其映射到创建的服务结构体上。使用文本模板,我们就可以直接将这些属性合并到结构体中。
2.4 编写自动生成代码的脚本
现在,我们已经具备了自动生成Go代码的基础:一个标记结构体的程序,一个可用的文本模板,以及将它们合并到一起的功能。
接下来,我们需要编写一个自动生成代码的脚本。下面是一个示例:
package main
import (
"flag"
"fmt"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"github.com/urfave/cli/v2"
)
var (
packageFlag = flag.String("package", "", "package name")
)
func main() {
app := cli.NewApp()
app.Name = os.Args[0]
app.Usage = "generate boilerplate code for types with `generate` tag"
app.Version = "1.0.0"
app.Commands = []*cli.Command{
{
Name: "generate",
Usage: "generate boilerplate code for types with `generate` tag",
Action: Generate,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "package",
Aliases: []string{"p"},
Usage: "package name",
Required: true,
Destination: packageFlag,
},
},
},
}
err := app.Run(os.Args)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func Generate(ctx *cli.Context) error {
if *packageFlag == "" {
return fmt.Errorf("package required")
}
args := ctx.Args()
if len(args) == 0 {
return fmt.Errorf("no files provided")
}
fset := token.NewFileSet()
for _, arg := range args.Slice() {
err := filepath.Walk(arg, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !(strings.HasSuffix(path, ".go") && !info.IsDir()) {
return nil
}
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return err
}
types := CollectTypes(file)
if len(types) > 0 {
err := GenerateCode(file, types)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
}
return nil
}
上述程序使用了一些常见的软件包——filepath,go/parser,以及go/token。首先,它解析每个传递的Go源文件,并检查其中的结构体是否被标记过。如果有哪个结构体被标记,则会使用之前定义的代码模板为这个结构体生成服务和接口。我们使用walk函数来迭代指定的目录,然后找到Go文件,检查每一个文件,最后生成代码。
3. 总结
到这里,我们已经实现了使用Go语言进行代码自动生成的实践。在实践中,我们使用了Go语言自带的go generate工具,利用文本模板对标记结构体生成代码,最后通过遍历Go源文件来定位并执行生成代码的过程。通过这样的方法,我们已经可以实现快速编写、上线以及维护代码的需求。当然,这里我们只是提供了一个比较简单的实现方式,实践中还需要根据具体业务的需求来进行适当的组织与实现。