The Stringer Interface and Generating String Methods
Give an enum a String method by hand or generate it with stringer and go generate. See the generated code, the flags and the Go 1.24 tool directive.

Anything that implements String() string prints as itself. That is the whole fmt.Stringer interface, one method that fmt looks for when it formats a value, and it is why fmt.Println(time.Second) prints 1s and not 1000000000. For an enum you could write the method by hand, as enums done properly does, with a names array and a bounds check. The Go project's own answer is a code generator, and it is a good small introduction to go generate. This how-to uses it on a Status type, reads the generated code, covers the flags you will want and shows the Go 1.24 way to keep the tool's version pinned. It belongs in the Go basics path and follows constants and iota.
Tested with Go 1.25.6, golang.org/x/tools v0.38.0 (stringer), macOS on Apple Silicon (arm64)
What the interface is
type Stringer interface {
String() string
}It lives in the fmt package. Any type with that method satisfies it, and fmt uses it for the %v and %s verbs and for Print, Println and their relatives. The %d verb on an integer type still prints the number, and %q quotes the string. If the method has a pointer receiver, only pointers get the behavior, which is the trap described in the pointer receiver error. A String method that calls fmt.Sprintf("%v", s) on itself recurses forever, so convert to the underlying type before formatting. Printing a struct that contains such a value calls the method too, so a log line of Account{Status: StatusActive} shows {Active} in %v and Status:Active in %+v, which is the main practical payoff: every place the value appears is readable without a single call to a helper.
Generating it
Start with the type and its constants, and a comment above the type that tells go generate what to run:
package main
import "fmt"
//go:generate stringer -type=Status -trimprefix=Status
type Status int
const (
StatusUnknown Status = iota
StatusActive
StatusSuspended
StatusClosed
)
func main() {
fmt.Println(StatusActive, StatusClosed, Status(9))
fmt.Printf("%d %v %s %q\n", StatusSuspended, StatusSuspended, StatusSuspended, StatusSuspended)
}Install the tool, run the generator and look at what it wrote:
go install golang.org/x/tools/cmd/[email protected]
go generate ./...
lsgo.mod
main.go
status_string.goNow read the generated file:
cat status_string.go// Code generated by "stringer -type=Status -trimprefix=Status"; DO NOT EDIT.
package main
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[StatusUnknown-0]
_ = x[StatusActive-1]
_ = x[StatusSuspended-2]
_ = x[StatusClosed-3]
}
const _Status_name = "UnknownActiveSuspendedClosed"
var _Status_index = [...]uint8{0, 7, 13, 22, 28}
func (i Status) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_Status_index)-1 {
return "Status(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Status_name[_Status_index[idx]:_Status_index[idx+1]]
}And run the program, which has not changed at all:
go run .Active Closed Status(9)
2 Suspended Suspended "Suspended"The first command put stringer in the Go binary directory, and go generate ran the line in the comment, which called it and wrote status_string.go beside main.go. The program then printed Active Closed Status(9): the method exists although nobody wrote it. The %d verb shows 2, and %v, %s and %q show the name without its prefix, because -trimprefix=Status removed it.
Reading the generated code
The generated file is short, and worth reading once because it shows how the names are stored. All the names are concatenated into one string constant, "UnknownActiveSuspendedClosed", and a small array of offsets, _Status_index, says where each name starts. String slices that string, so it allocates nothing and does no map lookup. Out-of-range values print as Status(9), a bounds check that a hand-written method has to remember.
At the top of the file is a function that is never called:
func _() {
var x [1]struct{}
_ = x[StatusUnknown-0]
_ = x[StatusActive-1]
...
}It is a compile-time check. Each line indexes a one-element array with a constant, which compiles only if the constant has the value the generator saw. If you later change StatusActive to 5 or insert a constant above it without regenerating, the build fails with an invalid argument: index out of bounds error that comes with a comment telling you to run the command again. That check is why a stale generated file cannot slip through unnoticed when the values shift.
The flags you will use
| Flag | Effect |
|---|---|
-type=A,B | Generate for one or more types |
-trimprefix=Status | Strip a prefix from every name |
-linecomment | Use the trailing // text on each constant as its name |
-output=file.go | Choose the output file name |
-tags=... | Build tags when parsing the package |
-linecomment is the answer when the name should not be a valid Go identifier: StatusInProgress // in progress prints in progress. It is also how you localize or shorten names. For consecutive values stringer produces the compact table shown above, and for values that are scattered it falls back to a map, which you can read in the output.
Pinning the tool with Go 1.24
go install tool@version leaves the tool outside your module, so two developers can run different versions and produce different files. Go 1.24 added a tool directive to go.mod that fixes this. Add the tool as a dependency of the module, and run it through the go tool command:
package main
import "fmt"
//go:generate go tool stringer -type=Status -trimprefix=Status
type Status int
const (
StatusUnknown Status = iota
StatusActive
StatusSuspended
StatusClosed
)
func main() {
fmt.Println(StatusActive, StatusClosed, Status(9))
}go get -tool golang.org/x/tools/cmd/[email protected]
go generate ./...
go run .
grep -n "tool" go.modActive Closed Status(9)
5:tool golang.org/x/tools/cmd/stringer
10: golang.org/x/tools v0.38.0 // indirectThe go get -tool command recorded a tool golang.org/x/tools/cmd/stringer line in go.mod, with the tool's module version in the require block and its checksums in go.sum. The generator comment became go tool stringer, so go generate finds the pinned version without anything on the PATH. A new contributor runs go generate ./... after a clone and gets the same output as everyone else, and Dependabot or Renovate can update the tool like any other dependency. I use this form in new projects and go install in older ones.
Keeping generated files honest
Generated code is committed, since a build should not need the generator, and it needs a guard against going stale. A CI step regenerates and fails if anything changed:
go generate ./...
git diff --exit-codeTogether with the compile-time check inside the generated file, that makes both ways of drifting visible: a changed value fails the build, and a new constant that was never regenerated fails the diff. Name the generated files with the _string.go suffix, which stringer does by default, and exclude them from your linter's complaints, since they carry the Code generated ... DO NOT EDIT. header that tools recognize.
When to write it by hand
For an enum of three values in one package, a hand-written String method with a names array is shorter than a generator setup, and it is what the enum article builds, together with Parse and marshaling. Stringer does not generate Parse or text marshaling, so you write those anyway. It also does nothing for string-backed enums, which do not need it. The generator pays off when you have many enums, constants that change often or names that follow a pattern, and in a code base with several of them, a go generate step is one more habit that pays back. The tool's flags are documented at pkg.go.dev/golang.org/x/tools/cmd/stringer, and the interface is defined in the fmt package.


