How to Use stringer for Generating String Methods

Generate String() methods for integer types by running go run golang.org/x/tools/cmd/stringer -type=YourTypeName.

The debugging trap

You are debugging a network protocol parser. The logs show packet.flags = 13. You know 13 means FlagUrgent | FlagPush, but the terminal does not care. You spend twenty minutes writing a switch statement that maps integers to human-readable names. You add a new flag tomorrow. You forget to update the switch. The logs go back to printing numbers. This is the exact problem stringer solves.

Go has a built-in contract for turning values into text. Any type that implements the fmt.Stringer interface gets automatic formatting support in fmt.Println, log.Printf, and fmt.Sprintf. The interface requires a single method: String() string. Writing that method by hand is repetitive. The stringer tool from golang.org/x/tools reads your source code, finds integer types backed by iota constants, and writes the String() method for you. It generates a new .go file in your package. You commit the generated file alongside your source. The tool handles the mapping logic. You handle the domain logic.

How stringer works

stringer is a code generator. It does not run at runtime. It runs during your build process, usually triggered by go generate. The tool parses your package's abstract syntax tree, locates the type you specify, and inspects every constant declaration that uses iota. It extracts the constant names, maps them to their underlying integer values, and writes a new Go file containing a String() method. The generated method satisfies fmt.Stringer automatically.

The naming convention for the output file is deterministic. If your type is Status, the tool creates status_string.go. If your type is LogLevel, it creates loglevel_string.go. The file lives in the same package directory. You treat it like any other source file. You do not edit it manually. You regenerate it whenever the constants change.

The minimal setup

Start with a simple enum-like type. Define the type and its constants in a regular .go file.

package main

// Status represents the lifecycle state of a background job.
type Status int

const (
    // Pending means the job is waiting in the queue.
    Pending Status = iota
    // Running means the worker has picked up the job.
    Running
    // Completed means the job finished successfully.
    Completed
    // Failed means the job returned an error.
    Failed
)

Add a go:generate directive at the package level. This tells the go toolchain exactly which command to run.

//go:generate stringer -type=Status

Run the generator from your terminal.

go generate ./...

The command scans the current directory, finds the directive, and executes stringer -type=Status. It produces status_string.go. The generated file contains a String() method that switches on the integer value and returns the constant name. It satisfies fmt.Stringer. You can now pass Status values directly to fmt.Println and see Pending instead of 0.

Goroutines are cheap. Generated code is free. Let the compiler handle the mapping.

Under the hood

When stringer runs, it uses the go/ast and go/parser packages to read your source files. It walks the AST looking for GenDecl nodes with Token == token.CONST. It filters for constants whose type matches your -type flag. It extracts the constant names and their iota-derived values. The tool then writes a new file containing a String() method with a receiver named after the first letter of the type, following Go's convention for short receiver names.

The generated method uses a switch statement under the hood. It maps each integer value to its corresponding string. If the value falls outside the known range, it returns a fallback format like Status(99). This prevents panics when you print uninitialized or corrupted values. The generated code is already formatted with gofmt. You do not need to run a formatter on it. Most editors run gofmt on save, which keeps the generated file consistent with the rest of your package.

The go generate command runs before go build. This means the generated file exists when the compiler links your package. If you skip go generate, the compiler rejects the program with undefined: Status.String or does not implement fmt.Stringer. The error message points directly to the missing method. Running go generate ./... fixes it immediately.

Real-world configuration

Production code rarely uses a single type. You will have multiple enums in one package. You will want cleaner output names. You will want to use comments instead of constant names. stringer supports flags to handle these cases.

package config

// Mode controls how the application handles incoming requests.
type Mode int

const (
    // Development enables verbose logging and hot reloading.
    Development Mode = iota
    // Production disables debug endpoints and enables rate limiting.
    Production
    // Testing mocks external dependencies and skips validation.
    Testing
)

//go:generate stringer -type=Mode -trimprefix=Mode -linecomment

The -trimprefix flag removes the type name from the generated strings. Without it, the output would be ModeDevelopment. With it, the output is Development. The -linecomment flag tells stringer to use the comment directly above each constant as the string value. This decouples your public API names from your debug output. You can rename constants without breaking log parsing scripts.

Run the generator again.

go generate ./...

The tool produces mode_string.go. The String() method now returns Development, Production, or Testing. It handles the mapping cleanly. You can pass Mode values to any logging framework and get readable output.

Accept interfaces, return structs. Implement fmt.Stringer when you need debug visibility. Let the generator handle the boilerplate.

Common pitfalls

Generated code introduces a specific set of workflow challenges. The first is forgetting to regenerate after changing constants. If you add a new constant and run go build, the compiler rejects the program with cannot use x (untyped int constant) as string value in argument if you try to pass it to a function expecting fmt.Stringer, or it simply prints the raw number because the switch statement does not cover the new value. Run go generate before every build. Many teams add a pre-commit hook that runs go generate ./... && git diff --exit-code to catch uncommitted changes.

The second pitfall is editing the generated file manually. The file contains a comment header warning against manual edits. If you modify it, your changes disappear the next time you run stringer. Treat the file as an artifact. Keep your domain logic in the source file. Keep the mapping logic in the generated file.

The third pitfall is using stringer on non-integer types. The tool only supports int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, and their aliases. If you try to run it on a string or struct type, the tool exits with a type must be an integer type error. Use a manual switch or a map for non-integer types.

The worst goroutine bug is the one that never logs. The worst enum bug is the one that prints 42 instead of FlagReadOnly. Generate the strings. Commit the output. Keep your logs readable.

When to use stringer

Use stringer when you have an integer type backed by iota constants and you need automatic fmt.Stringer implementation. Use a manual switch statement when you need custom formatting logic, conditional branching, or runtime configuration that a static generator cannot express. Use a map[int]string when you need to load string mappings from an external configuration file or database at runtime. Use fmt.Sprintf when you only need one-off formatting and do not want to implement an interface. Use plain sequential code when you don't need concurrency: the simplest thing that works is usually the right thing.

Where to go next