go:generate is a build-time code generation tool that lets you automate repetitive tasks, but its real power comes from combining it with Go’s text/template package for dynamic code creation.
Let’s see it in action. Imagine you have a list of API endpoints and you want to generate Go code for making HTTP requests to them.
First, we need a template file, say templates/api.go.tmpl:
// Code generated by go:generate. DO NOT EDIT.
package api
import "net/http"
{{range .Endpoints}}
// {{.Name}} makes a GET request to {{.Path}}.
func (c *Client) {{.Name}}() (*http.Response, error) {
req, err := http.NewRequest("GET", c.baseURL + "{{.Path}}", nil)
if err != nil {
return nil, err
}
return c.httpClient.Do(req)
}
{{end}}
This template defines a Go struct Client and then iterates over a list of Endpoints. For each endpoint, it generates a method on the Client struct that makes a GET request to the specified path.
Next, we need a Go program that will execute this template. Let’s create a file named generate.go:
//go:generate go run -mod=mod generate.go
package main
import (
"log"
"os"
"path/filepath"
"text/template"
)
type Endpoint struct {
Name string
Path string
}
type TemplateData struct {
Endpoints []Endpoint
}
func main() {
// Define the data for our template
data := TemplateData{
Endpoints: []Endpoint{
{Name: "GetUser", Path: "/users/{id}"},
{Name: "ListPosts", Path: "/posts"},
{Name: "CreateComment", Path: "/comments"},
},
}
// Get the directory of the current file
_, filename, _, ok := runtime.Caller(0)
if !ok {
log.Fatal("Could not get current file path")
}
rootDir := filepath.Dir(filepath.Dir(filename)) // Assuming generate.go is in the root directory
// Define the template file path
templatePath := filepath.Join(rootDir, "templates", "api.go.tmpl")
// Parse the template
tmpl, err := template.ParseFiles(templatePath)
if err != nil {
log.Fatalf("Error parsing template file: %v", err)
}
// Define the output file path
outputPath := filepath.Join(rootDir, "api", "generated.go")
outputFile, err := os.Create(outputPath)
if err != nil {
log.Fatalf("Error creating output file: %v", err)
}
defer outputFile.Close()
// Execute the template and write to the output file
err = tmpl.Execute(outputFile, data)
if err != nil {
log.Fatalf("Error executing template: %v", err)
}
log.Printf("Successfully generated code to %s", outputPath)
}
This generate.go file defines the data that will be fed into the template. It has a TemplateData struct which contains a slice of Endpoint structs. Each Endpoint has a Name and a Path. The main function then parses the api.go.tmpl template, populates it with the defined data, and writes the generated Go code to api/generated.go.
To make this work with go:generate, we add a special comment at the top of a Go file, typically in the same package where you want the generated code to reside. Let’s say we have an api package and we want the generated code there. In api/api.go, we’d add:
//go:generate go run generate.go
package api
import (
"net/http"
"net/url"
)
type Client struct {
baseURL string
httpClient *http.Client
}
func NewClient(baseURL string) *Client {
return &Client{
baseURL: baseURL,
httpClient: &http.Client{},
}
}
// This file will contain the generated code.
// It's good practice to have an empty file or a file with package-level declarations
// in the target directory.
Now, when you run go generate in your project’s root directory, it will find the //go:generate go run generate.go comment, execute the generate.go program, and create the api/generated.go file.
The api/generated.go file will look like this after running go generate:
// Code generated by go:generate. DO NOT EDIT.
package api
import "net/http"
// GetUser makes a GET request to /users/{id}.
func (c *Client) GetUser() (*http.Response, error) {
req, err := http.NewRequest("GET", c.baseURL + "/users/{id}", nil)
if err != nil {
return nil, err
}
return c.httpClient.Do(req)
}
// ListPosts makes a GET request to /posts.
func (c *Client) ListPosts() (*http.Response, error) {
req, err := http.NewRequest("GET", c.baseURL + "/posts", nil)
if err != nil {
return nil, err
}
return c.httpClient.Do(req)
}
// CreateComment makes a GET request to /comments.
func (c *Client) CreateComment() (*http.Response, error) {
req, err := http.NewRequest("GET", c.baseURL + "/comments", nil)
if err != nil {
return nil, err
}
return c.httpClient.Do(req)
}
The core of go:generate is its ability to execute arbitrary commands before the go build or go install process. The go run -mod=mod generate.go part in the go:generate directive tells Go to compile and run the generate.go file. The -mod=mod flag ensures that Go modules are used correctly during the execution of the generator.
The text/template package in Go provides a powerful way to construct text files programmatically. It uses a Go-like syntax for control flow (like {{range}}, {{if}}) and variable access ({{.VariableName}}). You pass it data structures, and it renders them into the desired output format, in this case, Go source code.
The most surprising thing about go:generate is that it’s not part of the Go compiler or the go build process itself; it’s a convention that the go generate command looks for. This means you can use it for more than just Go code generation – you could generate documentation, configuration files, or anything else text-based.
The real magic happens when you combine dynamic data sources with your templates. Instead of hardcoding the Endpoints in generate.go, you could read them from a JSON file, a database, or even fetch them from an API that defines your services. This makes your code generation truly dynamic and adaptable.
The // Code generated by go:generate. DO NOT EDIT. comment is crucial. It’s a convention that signals to developers that this file should not be manually edited because it will be overwritten by the generation process.
The next step is to explore how to handle more complex template logic, such as conditional generation, including other template files, and using custom functions within your Go templates.