Week 15 - go generate and AST-Based Code Generation¶
15.1 Conceptual Core¶
go generateis a convention, not a feature. It scans source files for//go:generate <command>comments and runs them. The output is normal Go source, committed to the repo.- The pattern is preferred over reflection for performance-critical paths: generate exhaustive code at build time, with no
reflectcost at runtime. - Canonical tools:
stringer-String()method for enum-like int types.mockgen-interface mocks for testing.sqlc-SQL → typed Go from query files.ent-schema → typed Go ORM.buf+protoc-gen-go-grpc-protobuf → Go.
15.2 Mechanical Detail¶
- Writing a generator (template-based):
//go:embed tmpl/api.tmpl var apiTmpl string type binding struct{ Name, Method, Path, Result string } func main() { cfg := &packages.Config{Mode: packages.NeedTypes | packages.NeedSyntax | ...} pkgs, _ := packages.Load(cfg, ".") bindings := extractBindings(pkgs[0]) // walks AST var buf bytes.Buffer template.Must(template.New("api").Parse(apiTmpl)).Execute(&buf, bindings) formatted, _ := format.Source(buf.Bytes()) // gofmt the output os.WriteFile("api_generated.go", formatted, 0644) } format.Source-always run generated bytes through it. Ungofmt'd generated code is an immediate code-review smell.- Token-based building (when templates get unwieldy):
go/ast+go/printer. Construct AST nodes programmatically;printer.Fprint(w, fset, node)writes them out. More verbose, more correct. - Generation hygiene:
- Add
// Code generated by foo. DO NOT EDIT.as the first line.goplsand reviewers honor this convention. - Commit the generated files. Do not run generation in CI by default; verify it is up-to-date via
go generate ./... && git diff --exit-code. - Keep generators small and composable. A 5000-line generator is a sign you should be using a real schema language (protobuf, openapi).
15.3 Lab-"Three Generators"¶
Build three small generators:
1. Enum stringer-a from-scratch reimplementation of stringer for one annotation pattern.
2. Mock generator-for one interface, generate a struct with method recorders and call assertions.
3. JSON marshaler-generate a type-specific MarshalJSON that allocates zero maps. Compare allocations against encoding/json for the same type.
For each: go vet - clean output,gofmt - formatted, with a go generate directive in the consumer file.
15.4 Idiomatic & golangci-lint Drill¶
revive: file-header(require theDO NOT EDITline on generated files),gocritic: dupArg. Configuregolangci-lintto skip generated files for most lints (exclude-filesor per-linterexclude-rules).
15.5 Production Hardening Slice¶
- Add a CI step
make generate && git diff --exit-codethat fails when generated code is stale relative to its inputs. This catches the "I forgot to regenerate" PR antipattern.