Go developers love code generation. Where C# uses reflection, attributes, and source generators, Go often uses tools that generate code before compilation.
This sounds backwards—writing code that writes code? But it has advantages: generated code is inspectable, debuggable, and has zero runtime overhead.
The go generate Command
Add a magic comment to your source file:
//go:generate stringer -type=Status
type Status int
const (
StatusPending Status = iota
StatusActive
StatusCompleted
)
Run:
go generate ./...
This runs stringer -type=Status in the package directory, which generates status_string.go:
// Code generated by "stringer -type=Status"; DO NOT EDIT.
func (i Status) String() string {
switch i {
case StatusPending:
return "StatusPending"
case StatusActive:
return "StatusActive"
case StatusCompleted:
return "StatusCompleted"
}
return "Status(" + strconv.FormatInt(int64(i), 10) + ")"
}
The generated code is committed to your repo. It’s just Go code.
Common Code Generators
stringer
Generates String() methods for constants:
//go:generate stringer -type=LogLevel
We covered this in the enums post.
mockgen
Generates mock implementations:
//go:generate mockgen -source=repository.go -destination=mock_repository.go -package=repo
Creates mock structs for testing.
sqlc
Generates type-safe database code from SQL:
-- queries/users.sql
-- name: GetUser :one
SELECT id, name, email FROM users WHERE id = $1;
-- name: ListUsers :many
SELECT id, name, email FROM users ORDER BY name;
//go:generate sqlc generate
Produces:
func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) {
row := q.db.QueryRowContext(ctx, getUserSQL, id)
var i User
err := row.Scan(&i.ID, &i.Name, &i.Email)
return i, err
}
Type-safe queries with no reflection.
protoc (Protocol Buffers)
Generates Go code from .proto files:
//go:generate protoc --go_out=. --go-grpc_out=. api.proto
oapi-codegen
Generates code from OpenAPI specs:
//go:generate oapi-codegen -package api -generate types,server openapi.yaml
ent
Generates ORM code:
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
Writing Your Own Generator
Generators are just programs. You can write your own:
// gen/main.go
package main
import (
"os"
"text/template"
)
var tmpl = `// Code generated; DO NOT EDIT.
package {{.Package}}
var Version = "{{.Version}}"
var BuildTime = "{{.BuildTime}}"
`
func main() {
t := template.Must(template.New("version").Parse(tmpl))
data := map[string]string{
"Package": os.Getenv("GOPACKAGE"),
"Version": os.Getenv("VERSION"),
"BuildTime": time.Now().Format(time.RFC3339),
}
f, _ := os.Create("version_gen.go")
defer f.Close()
t.Execute(f, data)
}
Use it:
//go:generate go run gen/main.go
The go:generate Directive
The comment format:
//go:generate command arg1 arg2
Rules:
- Must start with
//go:generate(no space after //) - The command runs in the package directory
- Environment variables available:
$GOPACKAGE,$GOFILE,$GOLINE - Output to stderr shows during generation
Multiple directives are fine:
//go:generate stringer -type=Status
//go:generate mockgen -source=service.go -destination=mock_service.go
Comparing to C# Source Generators
C# has source generators (Roslyn) that run during compilation:
[Generator]
public class MyGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// Generate code based on syntax trees
}
}
| Aspect | C# Source Generators | Go go:generate |
|---|---|---|
| When runs | During compilation | Before compilation |
| Output | In-memory | Files in repo |
| Debugging | Harder | Easy (it’s just files) |
| IDE support | Integrated | Manual regeneration |
| Dependencies | Part of build | External tools |
| Incremental | Yes | Manual |
C#’s generators are more integrated. Go’s are more transparent.
Best Practices
1. Commit generated code
Generated code should be in your repo:
- CI doesn’t need generator tools
- Code review includes generated changes
- git blame works
2. Mark generated files
Start with the standard header:
// Code generated by TOOL; DO NOT EDIT.
Linters and editors recognize this and treat the file specially.
3. Regenerate in CI
Verify generated code is up-to-date:
- name: Check generated code
run: |
go generate ./...
git diff --exit-code
Fails if generated code differs from committed code.
4. Document generation
In your README or Makefile:
generate:
go generate ./...
.PHONY: generate
5. Keep generators fast
go generate runs serially. Slow generators hurt iteration speed.
Real-World Example
A typical project might have:
// internal/domain/status.go
//go:generate stringer -type=Status
// internal/repository/repository.go
//go:generate mockgen -source=repository.go -destination=mock_repository.go -package=repository
// internal/database/queries.sql
//go:generate sqlc generate
// api/openapi.yaml
//go:generate oapi-codegen -package api -generate types,server,spec -o api_gen.go openapi.yaml
Run go generate ./... and all these tools produce code.
The Honest Take
Code generation is more prominent in Go than C#. It’s a cultural thing—Go prefers explicit, inspectable code over runtime magic.
What Go does well:
- Generated code is transparent
- Easy to debug (it’s just files)
- No runtime reflection overhead
- Tools are standalone programs
What C# does better:
- Source generators are integrated
- IDE understands generated code immediately
- Incremental generation
- No manual regeneration step
The verdict: If you’re not used to code generation, it feels weird. But once you embrace it, you appreciate the transparency.
sqlc in particular is wonderful—type-safe database access without ORM magic, because the queries are analyzed at generation time.
Learn to love go generate ./.... It’s part of the Go workflow.
Next up: debugging with Delve—getting productive with dlv after Visual Studio’s polished debugging experience.