go-codegen
is a simple template-based code generation system for go. By
annotating structs with specially tagged fields, go-codegen will generate code
based upon templates provided alongside your packages. The templates have
access to the full, compiled structure of your program, allowing for
sophisticated meta-programming when you need it.
This project is based off of the original go-codegen
project by nullstyle.
It's essentially a complete rewrite of the codebase, only drawing on some of the
key concepts such as embedding structs to trigger code generation, and as such
should be considered a separate project. A new name is in the works to
differentiate installs of nullstyle's project vs this one.
go install github.com/CyborgMaster/go-codegen/cmd/go-codegen@latest
go-codegen works by scanning your file for structs with fields annotated with a
codegen
tag, and then will run the corresponding template with the same name
as the field type in the same folder where the field type is defined.
Take for example, the following go file called commands.go
:
package main
import "fmt"
//go:generate go-codegen $GOFILE
// cmdGen is a template. Blank structs are good to use for targeting templates
// as they do not affect the compiled package.
type cmdGen struct{}
type cmd interface {
Execute() (interface{}, error)
MustExecute() interface{}
}
type HelloCommand struct {
// HelloCommand needs to have the `cmd` template invoked upon it.
// By mixing in cmd, we tell go-codegen so.
cmdGen `codegen:""`
Name string
}
func (cmd *HelloCommand) Execute() (interface{}, error) {
return "Hello, " + cmd.Name, nil
}
type GoodbyeCommand struct {
cmdGen `codegen:""`
Name string
}
func (cmd *GoodbyeCommand) Execute() (interface{}, error) {
return "Goodbye, " + cmd.Name, nil
}
func main() {
var c cmd
c = &HelloCommand{Name: "You"}
fmt.Println(c.MustExecute())
c = &GoodbyeCommand{Name: "You"}
fmt.Println(c.MustExecute())
}
Notice that HelloCommand
doesn't have a MustExecute
method. This code will
be generated by go-codegen
. Now we need to write the cmdGen
template.
Create a new file named cmdGen.tmpl
in the same package where the cmdGen
struct is defined:
// MustExecute behaves like Execute, but panics if an error occurs.
func (cmd *{{ .StructName }}) MustExecute() interface{} {
result, err := cmd.Execute()
if err != nil {
panic(err)
}
return result
}
Notice the {{ .StructName }}
expression: It's just normal go template code.
Now, given both files, lets generate the code. Run go generate .
in the
package directory, and you'll see a file called commands_generated.go
whose
content looks like:
// Code generated by go-codegen; DO NOT EDIT.
package main
// MustExecute behaves like Execute, but panics if an error occurs.
func (cmd *GoodbyeCommand) MustExecute() interface{} {
result, err := cmd.Execute()
if err != nil {
panic(err)
}
return result
}
// MustExecute behaves like Execute, but panics if an error occurs.
func (cmd *HelloCommand) MustExecute() interface{} {
result, err := cmd.Execute()
if err != nil {
panic(err)
}
return result
}
Note that these are my opinions on when code generation is a good solution. Your millage may vary.
The primary reason that code generation is a good fit for Go is that Go doesn't have generics. In fact, it is likely that when generics are officially released for Go, the majority of code generated by go-codegen would be better if rewritten as generic Go code. Until that time, our only generic options in Go are interfaces and reflection. Code that can use those tools should, as generic code is "better" than generated code.
In many cases, interfaces can be used to provide generic functionality. One can
define a utility function with the generic behavior that accepts objects of an
interface type that houses the object-specific behavior. The go
sort package is a good example of that. It uses
an interface to allow a generic sort
function to be able to sort arbitrary collections of different types.
However, this interface paradigm tends to break down in two cases:
-
Code that must return a specific type.
While accepting an interface tends to work well, returning an interface tends to be more problematic. If you have code that follows the same pattern that must return a type related to your input type, often times your only option is codegen.
-
Code that must accept a generic slice.
Because
[]ConcreteType
cannot be used as an argument of[]InterfaceType
even thoughConcreteType
implementsInterfaceType
, your only recourse is to create a new slice ofInterfaceType
and copy the entries over before calling the function, or have your generic function accept an emptyinterface{}
and lose all type safety. Another option is to codegen a wrapper that does the slice conversion and copy for you, or if the generic function is simple enough, codegen the type specific version of it.
Templates are invoked by annotating a field with a codegen
struct tag.
The "data" available in a template invocation is represented with a
TemplateContext
struct. You can see it
here.
String arguments may be passed to the template by including them in the struct
tag as key=value
pairs, comma separated. e.g.
type HelloArgs struct {
// The argsGen template will have available to it a "hello" arg with the
// value "there", and a "cool" arg with the value "stuff".
argsGen `codegen:"hello=there,cool=stuff"`
// other fields ...
}
These are available inside the template by calling $.Arg
, and their presence
can be detected by calling $.HasArg
{{ if $.HasArg "hello" }}
var hello := {{ $.Arg "hello" }}
{{ end }}
Templates may be recursive. If the type which defines a template is itself a
struct with a field with a codegen
tag, then both the outer and the nested
template will be invoked on the root calling struct.
type FooGen struct {
BarGen `codegen:""`
}
type BarGen struct{}
// Baz will have both the `FooGen` and the `BarGen` template invoked on it.
type Baz struct {
FooGen `codgen:""`
}
Variables passed to the outer template invocation will be forwarded to the inner invocation as well.
A template is expected to be found within the same directory where the type
referenced by the field is defined, using a name of the form TypeName.tmpl
.
A template may add additional imports into the generated go file by calling the
AddImport
method on the TemplateContext
like so:
{{ $.AddImport "net/http" }}
The Sprig template library is available
as global functions within a template. In addition, the singular
and plural
methods from jinzhu/inflection, are
available.
The entire type system of your program is parsed using the go compiler and is
inspectable from within the template. The struct that is the target of the
invocation is available at .Struct
and is a
types.Struct
. This can be used to check
nearly all properties of your struct and its fields. A couple of helper
functions are available to make it easier:
typeName
converts atypes.Type
to a string in the format"package.Name"
structField
gets a field from a struct by name.pointerType
wraps an existingtypes.Type
in a new one representing a pointer to that type.$.Implements
returns true if atypes.Type
implements an interface, given by fully qualified name.
This lets you do all sorts of things like find the field in your struct that embeds a type from a specific package:
{{ range $fieldNum := until .Struct.NumFields }}
{{ $f := $.Struct.Field $fieldNum }}
{{ $type := $f.Type | typeName }}
{{ if $f.Embedded | and ($type | hasPrefix "mypkg.") }}
// codgen here ...
{{ end }}
{{ end }}
or if a pointer to a given type implements an interface defined elsewhere:
{{ if $.Implements
(pointerType $foundType)
"github.com/user/project/package/DefinedInterface" }}
// code here...
{{ end }}
or only assign a field if it exists:
func (s *{{ .StructName }}) Apply(value *some.Type) {
{{ if structField .Struct "Name" }}
s.Name = value.Name
{{ end }}
// more code...
}