In Go, there is an unwritten habit that many people like to use generated code, for example, the directory structure of the project, the stub code of grpc are generated by tools, and small ones such as static files embedded in the code, automatically generated enum type String form, etc. Anyway, the pattern of generated code can always be seen.

In fact, this may be related to the Go official is also to promote this way, for example, Go from version 1.4 onwards go generate function, although I started to write the draft of this article more than 2 years ago, but so long actually not how seriously to understand this function. Recently, I’ve been trying to use this feature, so I’ve really gotten to know go genreate, and in this article I’ll try to summarize what go generate is all about.

How it works

The way go generate works is when you type go generate ./..., go looks in your current directory for places that contain the //go:generate comment, which is usually followed by a command, such as this comment.

1
[root@liqiang.io]# //go:generate mygentool arg1 arg2 -on

It’s actually the same as if you run it in this directory: mygentool arg1 arg2 -on, but the difference is that you don’t get any additional metadata if you run it locally, but if you run it through go generate, go will add some additional properties to you by default, which can be verified with this program (note that the generate tool needs to be in your PATH directory, if it’s in the current directory, don’t forget to add the current directory to your PATH environment variable).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[root@liqiang.io]# go generate ./...
args[0]: mygentool
args[1]: arg1
args[2]: arg2
args[3]: -on
GO111MODULE=auto
GOPATH=/xxxx/software/go/gopath
GOROOT=/xxxx/software/go/goroot/go
GOARCH=amd64
GOOS=linux
GOFILE=sample.go
GOLINE=3
GOPACKAGE=main
cwd: /xxxx/blog_codes/golang/tools/generator/example1

As you can see, Go passes in a lot of environment variables for us by default, such as what file this comment is in, what line it’s on, what the package name is, and then what directory you’re executing the go generate command from. With these parameters, we can do a lot of interesting things.

stringer

Now take a look at an example mentioned in the official Go blog: Go Generate, an example of a stringer, which is actually a method that adds a string to the enum type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[root@liqiang.io]# cat example.go
package painkiller
//go:generate stringer -type=Pill
type Pill int
const (
    Placebo Pill = iota
    Aspirin
    Ibuprofen
    Paracetamol
    Acetaminophen = Paracetamol
)

Then execute the command.

1
2
[root@liqiang.io]# go get golang.org/x/tools/cmd/stringer
[root@liqiang.io]# go generate

You can see that the current directory will have one more file: pill_string.go , then try to test it.

1
2
3
4
5
6
7
[root@liqiang.io]# cat pill_test.go
func TestPill_String(t *testing.T) {
    var p painkiller.Pill
    if p.String() != "Placebo" {
        t.Fatalf("p should equal to Placebo")
    }
}

You can find that the enum type has a String method, and the return value of this method is the String value of the Enum.

By looking at the stringer code, we can see that stringer is an executable program that supports the following parameters.

1
2
3
4
5
6
7
8
[root@liqiang.io]# cat stringer.go
var (
    typeNames   = flag.String("type", "", "comma-separated list of type names; must be set")
    output      = flag.String("output", "", "output file name; default srcdir/<type>_string.go")
    trimprefix  = flag.String("trimprefix", "", "trim the `prefix` from the generated constant names")
    linecomment = flag.Bool("linecomment", false, "use line comment text as printed text when present")
    buildTags   = flag.String("tags", "", "comma-separated list of build tags to apply")
)

Its implementation is to parse your file by ast (ast.Inspect(file.file, file.genDecl)), then find the specified name, iterate over its values, then merge those values into an array, and finally construct the structure of stringer.

yacc

Finally, an advanced use is to call yacc to automatically generate code via go generate. This is actually a metaprogramming idea, where we specify a metadata and then create code from that metadata (e.g. create a struct, which then automatically generates a lot of built-in methods, somewhat similar to proto -> go code, but more advanced and feature-rich).

I’m not familiar with yacc, I just know it’s a compilation tool, and I don’t usually know about it or use it. So here’s an introduction based on the official documentation, starting with installing the Go version of yacc.

1
[root@liqiang.io]# go get golang.org/x/tools/cmd/goyacc

Then edit your yacc file, for example I copied one from repo, and create the go file containing the go generate command.

1
2
3
4
5
[root@liqiang.io]# cat main.go
package main
//go:generate goyacc -o calc.go calc.y
func main() {
}

Then run go generate . /... command, and you find a local file calc.go. I’m not familiar with yacc, but from the code it looks like a syntax rule defined by clac, which should be used to parse the syntax of a specific rule.

I won’t expand on this because I’m not good at it, but the function is still used in this way, without departing from the basic method of operation.

Sample code

All the code for this article can be found in this repo: https://github.com/liuliqiang/blog_codes/tree/master/golang/tools/generator

Reference

  • https://github.com/golang/tools/blob/master/cmd/stringer/stringer.go
  • https://eli.thegreenplace.net/2021/a-comprehensive-guide-to-go-generate/
  • https://liqiang.io/post/using-go-generate-with-go-a10df252