Code Generation with go generate

While reflection provides runtime flexibility, code generation offers a complementary approach by moving complexity from runtime to build time. Instead of inspecting and manipulating types dynamically, code generation creates type-specific implementations during the build process, resulting in code that’s both type-safe and performant.

The go generate Tool

Go’s built-in go generate command provides a standardized way to integrate code generation into the build process. Unlike other build tools, go generate doesn’t run automatically during compilation. Instead, it’s explicitly invoked before the build, allowing developers to control when generation happens.

The tool works by scanning Go source files for special comments that specify commands to run:

//go:generate command argument...

When go generate is executed, it runs these commands in the context of the package directory, enabling a wide range of code generation workflows.

Template-Based Code Generation

One of the most common approaches to code generation is using Go’s powerful text/template package to generate code from templates. This approach is particularly useful for generating repetitive code patterns:

package main

import (
	"fmt"
	"os"
	"strings"
	"text/template"
)

// Define the template for generating typed collection operations
const tmpl = `// Code generated by collection-gen; DO NOT EDIT.
package {{.Package}}

// {{.TypeName}}Collection provides type-safe collection operations for {{.TypeName}}
type {{.TypeName}}Collection struct {
	items []{{.TypeName}}
}

// New{{.TypeName}}Collection creates a new collection from the given items
func New{{.TypeName}}Collection(items ...{{.TypeName}}) *{{.TypeName}}Collection {
	return &{{.TypeName}}Collection{items: items}
}

// Add adds an item to the collection
func (c *{{.TypeName}}Collection) Add(item {{.TypeName}}) {
	c.items = append(c.items, item)
}

// Filter returns a new collection containing only items that satisfy the predicate
func (c *{{.TypeName}}Collection) Filter(predicate func({{.TypeName}}) bool) *{{.TypeName}}Collection {
	result := New{{.TypeName}}Collection()
	for _, item := range c.items {
		if predicate(item) {
			result.Add(item)
		}
	}
	return result
}

// Map transforms each item using the provided function
func (c *{{.TypeName}}Collection) Map(transform func({{.TypeName}}) {{.TypeName}}) *{{.TypeName}}Collection {
	result := New{{.TypeName}}Collection()
	for _, item := range c.items {
		result.Add(transform(item))
	}
	return result
}

// Items returns the underlying slice
func (c *{{.TypeName}}Collection) Items() []{{.TypeName}} {
	return c.items
}
`

// TemplateData holds the data for the template
type TemplateData struct {
	Package  string
	TypeName string
}

func main() {
	if len(os.Args) != 4 {
		fmt.Println("Usage: collection-gen <package> <type> <output-file>")
		os.Exit(1)
	}

	packageName := os.Args[1]
	typeName := os.Args[2]
	outputFile := os.Args[3]

	// Parse the template
	t, err := template.New("collection").Parse(tmpl)
	if err != nil {
		fmt.Printf("Error parsing template: %v\n", err)
		os.Exit(1)
	}

	// Create the output file
	file, err := os.Create(outputFile)
	if err != nil {
		fmt.Printf("Error creating output file: %v\n", err)
		os.Exit(1)
	}
	defer file.Close()

	// Execute the template
	err = t.Execute(file, TemplateData{
		Package:  packageName,
		TypeName: typeName,
	})
	if err != nil {
		fmt.Printf("Error executing template: %v\n", err)
		os.Exit(1)
	}

	fmt.Printf("Generated %s collection for type %s in package %s\n",
		outputFile, typeName, packageName)
}

This generator can be used with go generate by adding a comment like this to a Go file:

//go:generate go run collection-gen.go main User user_collection.go

When go generate runs, it will create a file called user_collection.go with type-safe collection operations for the User type.

Stringer Example: Generating String Methods

One of the most common use cases for code generation is implementing the String() method for custom types, especially for enums. The Go team provides a tool called stringer that does exactly this:

package status

//go:generate stringer -type=Status
type Status int

const (
	StatusPending Status = iota
	StatusActive
	StatusSuspended
	StatusCancelled
)

Running go generate will create a file called status_string.go with an efficient implementation of the String() method:

// Code generated by "stringer -type=Status"; DO NOT EDIT.

package status

import "strconv"

func _() {
	// An "invalid array index" compiler error signifies that the constant values have changed.
	// Re-run the stringer command to generate them again.
	var x [1]struct{}
	_ = x[StatusPending-0]
	_ = x[StatusActive-1]
	_ = x[StatusSuspended-2]
	_ = x[StatusCancelled-3]
}

const _Status_name = "StatusPendingStatusActiveStatusSuspendedStatusCancelled"

var _Status_index = [...]uint8{0, 13, 25, 40, 55}

func (i Status) String() string {
	if i < 0 || i >= Status(len(_Status_index)-1) {
		return "Status(" + strconv.FormatInt(int64(i), 10) + ")"
	}
	return _Status_name[_Status_index[i]:_Status_index[i+1]]
}

This generated code is both efficient and type-safe, avoiding the runtime overhead and potential errors of a reflection-based approach.

Generating Mock Implementations for Testing

Code generation is particularly valuable for testing, where it can automatically create mock implementations of interfaces. The popular mockgen tool from the gomock package demonstrates this approach:

package service

//go:generate mockgen -destination=mock_repository.go -package=service github.com/example/myapp/service Repository

// Repository defines the data access interface
type Repository interface {
	FindByID(id string) (*User, error)
	Save(user *User) error
	Delete(id string) error
}

// UserService provides user management operations
type UserService struct {
	repo Repository
}

// NewUserService creates a new user service
func NewUserService(repo Repository) *UserService {
	return &UserService{repo: repo}
}

// GetUser retrieves a user by ID
func (s *UserService) GetUser(id string) (*User, error) {
	return s.repo.FindByID(id)
}

// CreateUser creates a new user
func (s *UserService) CreateUser(user *User) error {
	return s.repo.Save(user)
}

// DeleteUser deletes a user by ID
func (s *UserService) DeleteUser(id string) error {
	return s.repo.Delete(id)
}

Running go generate will create a mock implementation of the Repository interface that can be used in tests:

package service

import (
	"testing"

	"github.com/golang/mock/gomock"
)

func TestUserService_GetUser(t *testing.T) {
	// Create a new mock controller
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	// Create a mock repository
	mockRepo := NewMockRepository(ctrl)

	// Set up expectations
	user := &User{ID: "123", Name: "Test User"}
	mockRepo.EXPECT().FindByID("123").Return(user, nil)

	// Create the service with the mock repository
	service := NewUserService(mockRepo)

	// Call the method being tested
	result, err := service.GetUser("123")

	// Assert the results
	if err != nil {
		t.Errorf("Expected no error, got %v", err)
	}
	if result.ID != "123" || result.Name != "Test User" {
		t.Errorf("Expected user {ID:123, Name:Test User}, got %v", result)
	}
}

This approach enables thorough testing of code that depends on interfaces without having to manually create mock implementations.

Protocol Buffers and gRPC

One of the most powerful applications of code generation is in the realm of service definitions and serialization. Protocol Buffers (protobuf) and gRPC use code generation to create efficient, type-safe client and server code from service definitions:

// user_service.proto
syntax = "proto3";

package user;

option go_package = "github.com/example/myapp/user";

service UserService {
  rpc GetUser(GetUserRequest) returns (User) {}
  rpc CreateUser(CreateUserRequest) returns (User) {}
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) {}
}

message GetUserRequest {
  string id = 1;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

message ListUsersRequest {
  int32 page_size = 1;
  string page_token = 2;
}

message ListUsersResponse {
  repeated User users = 1;
  string next_page_token = 2;
}

message User {
  string id = 1;
  string name = 2;
  string email = 3;
  UserStatus status = 4;
  
  enum UserStatus {
    ACTIVE = 0;
    INACTIVE = 1;
    SUSPENDED = 2;
  }
}

With a single go generate directive, you can create client libraries, server interfaces, and serialization code:

//go:generate protoc --go_out=. --go-grpc_out=. user_service.proto

This generates comprehensive, type-safe code for both client and server implementations, including:

  1. Struct definitions for all message types
  2. Serialization/deserialization methods
  3. Client interface and implementation
  4. Server interface for implementing the service

The generated code handles all the low-level details of network communication, serialization, and type conversion, allowing developers to focus on business logic.

Code Generation Best Practices

When working with code generation, following these best practices will help maintain a clean, maintainable codebase:

  1. Mark generated files clearly: Always include a comment indicating that the file is generated and should not be edited manually.

  2. Version control generated files: While it might seem counterintuitive, checking in generated files can simplify builds and reduce dependencies.

  3. Separate generated code: Keep generated code in separate files from hand-written code to maintain a clear distinction.

  4. Document generation commands: Include clear documentation on how to regenerate code when needed.

  5. Automate generation: Use build scripts or Makefiles to ensure code is regenerated when needed.

  6. Test generated code: Even though the code is generated, it should still be covered by tests.

Code generation complements reflection by providing a compile-time alternative that offers better performance and type safety. In the next section, we’ll explore AST manipulation, which enables even more sophisticated code generation techniques.