如何使用Go语言进行代码自动生成实践

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源文件来定位并执行生成代码的过程。通过这样的方法,我们已经可以实现快速编写、上线以及维护代码的需求。当然,这里我们只是提供了一个比较简单的实现方式,实践中还需要根据具体业务的需求来进行适当的组织与实现。

后端开发标签