Saltar a contenido

Week 15 - go generate and AST-Based Code Generation

15.1 Conceptual Core

  • go generate is 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 reflect cost 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. gopls and 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 the DO NOT EDIT line on generated files), gocritic: dupArg. Configure golangci-lint to skip generated files for most lints (exclude-files or per-linter exclude-rules).

15.5 Production Hardening Slice

  • Add a CI step make generate && git diff --exit-code that fails when generated code is stale relative to its inputs. This catches the "I forgot to regenerate" PR antipattern.

Comments