JavaRush /Courses /Go SELF /Example‑tests as documentation

Example‑tests as documentation

Go SELF
Level 36, Lesson 3
Available

1. What are Example‑tests, and why is this documentation

If you open the documentation for many standard Go packages on pkg.go.dev, you’ll see “Example” blocks next to functions and types. These aren’t mood images and aren’t pseudocode “in Go style”. In Go, these examples are real Go code that lives in *_test.go files, can be executed by go test, and therefore guarantees that the documentation won’t rot after refactoring. That’s what makes Example‑tests especially valuable: they both teach the user how to use the API and protect the project from “pretty but lying” examples.

The main idea is simple: we write a function whose name starts with Example, print something to stdout inside it, and add a // Output: comment. During go test, the framework captures stdout and compares it with the expected output. If it differs, the example is treated as a failed test. This behavior is described in the official article about testable examples.

To cement the idea, imagine that an Example is a “demo script” that gets replayed on every test run. If the demo no longer matches reality, the test fails. Documentation literally stops being “the author’s opinion” and becomes a verifiable fact.

Where Example‑tests live and how go test runs them

Example‑tests live in regular test files: any file whose name ends with _test.go qualifies. Projects often create a separate example_test.go to avoid mixing examples with regular unit tests, but technically this is not mandatory—it’s a hygiene choice.

An Example differs from a regular test in that:

  • the function starts with Example...,
  • it has no parameters,
  • it usually uses fmt.Print/Println/Printf (i.e., stdout),
  • and the expected result is specified via a // Output: block.

When you run tests with go test -v, you’ll see that Example functions are executed almost like regular tests, as separate “RUN” entries. The article about examples shows this on real go test output.

It’s important to understand one subtlety: an Example is not “documentation magic”—it’s a test mechanism. That means compilation rules, imports, packages, compiler errors—everything is exactly like normal Go code. No discounts for being “an example for humans”. The compiler isn’t sentimental.

2. // Output: and output checking

Let’s calmly unpack what happens when you write // Output:. While an Example function runs, the test runner intercepts everything printed to standard output and compares it to what you wrote after Output:. If the output matches, the example is considered successful; if not, it fails, and you’ll get a “got / want” style message. This is the key mechanism described in the official “Testable Examples in Go” article.

The most common beginner mistake here is thinking that “Output” works “approximately”. No— it works exactly. Spaces, newlines, quotes—everything matters. And that’s good: the example becomes a reliable contract.

A mini example at the most everyday level (not tied to our app—just to feel the mechanics):

package demo

import "fmt"

func ExampleHello() {
	fmt.Println("hello")
	// Output: hello
}

If you change the string to "Hello" or add an extra space, the example will fail. Sometimes that’s annoying (especially when you really want “just a little bit of prettiness”), but in the long run it saves you: the documentation always matches reality.

4. Example names and binding to GoDoc

Now an important part: why do we bother so much with Example... names? Because GoDoc and pkg.go.dev can infer from the Example function name what this example relates to and show it next to the right API element.

Go has naming conventions for Example functions, and they really work in practice (you’ll see this when you open a package’s documentation). Below is a short table to use as a handy cheat sheet.

Example function name What it is attached to in the docs
Example()
the package as a whole
ExampleFoo()
the function/variable/type Foo
ExampleType()
the type Type
ExampleType_Method()
the method Method of type Type
ExampleFoo_suffix()
Foo, but as a separate scenario (a suffix to distinguish it)

The idea of the suffix part is simple: sometimes one function has multiple “usage modes”, and you want two examples. Then you write ExampleParseID_simple() and ExampleParseID_error()—and both will relate to ParseID, just as separate cases.

This mechanism is heavily used in the standard library. You’ll also regularly see examples like ExampleRegexp_FindString()—this format can be seen in real stories about adding examples to the regexp package.

5. Practical Example variants

Example without // Output:: compile‑only examples

Sometimes you want to show an example, but it can’t be deterministic in terms of stdout. For example, the code hits the network, depends on the current time, or on file order in a directory. In such cases, Go has a compromise: you can write an Example function without // Output:.

What happens then? The example will be compiled, but typically won’t be executed as a stdout check. This is also described in the examples article: the absence of Output turns the Example into a “at least it compiles” check.

This is useful more often than it seems. Such an example still:

  • shows up in the documentation,
  • guarantees that function signatures and package names haven’t changed in a way that makes the example invalid,
  • and doesn’t force you to invent “stable output” where there objectively isn’t any.

A small example in the style of “we just demonstrate a call and don’t promise stdout”:

package todo

func ExampleParseID_compilesOnly() {
	_, _ = ParseID("10")
	// no Output: the example still “doesn’t rot” syntactically
}

From a learning perspective, this is also psychologically convenient: you can first learn to write examples “as code”, and only then add // Output: where it makes sense.

Determinism: how not to make an Example brittle

Example tests have one nasty trap: you can write an example that sometimes prints one thing and sometimes prints another. Then your tests will be green one moment and red the next, and you’ll feel like the main character in a horror movie titled “The Curse of Non-Determinism”.

The most common sources of non-determinism for beginners are time and maps (map), because map iteration order is not guaranteed. Another classic is random numbers if you don’t fix the seed (and even with a seed you often don’t want to drag rand into an example).

So a good Example usually prints something simple and stable. If you need ordering—sort. If you need time—don’t print time.Now() in the example. If you want to demonstrate formatting—print a fixed string.

You can think of it as a kitchen rule: an Example test is a recipe that should turn out the same every time. If you add “a pinch of randomness to taste”, the test runner won’t appreciate that taste.

Whole‑file examples: when one func Example...() isn’t enough

Sometimes, in an example, you want to declare a type and a method to show how your API works with interfaces or custom structs. And here a beginner runs into a harsh truth: you can’t declare a method inside a function (methods are declared at the package level). That means a regular func Example...() may not be enough.

For these situations Go has “whole‑file examples”: that’s when an entire _test.go file becomes an example, and it’s allowed to declare helper types/methods at the package level, and only then write Example...().

In a learning context, it’s convenient to show this with a simple scenario: suppose we have Status and String() for a task status, and we want an example where the status prints nicely.

6. Example: adding Examples to the todo package

So we don’t make examples about nothing, let’s assume we already have a small todo package that we’re building throughout the course. It doesn’t have to be huge; a few functions/types are enough to demonstrate documentation examples.

Let’s imagine that the todo package already has:

  • ParseID(s string) (int, error) — parses an ID,
  • ErrEmptyTitle and ValidateTitle(title string) error — validation,
  • Status with String() — a human-readable state.

Now let’s create example_test.go in the todo package. We’ll start with an example for ValidateTitle. We want to show a “typical failure”, because that’s usually more important than the happy path.

package todo

import (
	"fmt"
)

func ExampleValidateTitle() {
	err := ValidateTitle("")
	fmt.Println(err == ErrEmptyTitle) // true
	// Output: true
}

Notice the style: we don’t print the full error text (it can change); instead, we check the contract we want to lock in. If you’ve agreed that ValidateTitle("") returns exactly ErrEmptyTitle, then such an Example protects that contract better than any lecture.

Now let’s add an example for ParseID, but make two scenarios: success and failure. For this, we’ll use a suffix.

package todo

import (
	"fmt"
)

func ExampleParseID_ok() {
	id, err := ParseID("42")
	fmt.Println(id, err) // 42 <nil>
	// Output: 42 <nil>
}

And now the negative scenario. Here it’s important not to try to “guess” the full error text from strconv.Atoi, because it may not be what you want to cement into your contract. It’s better to show that err != nil.

package todo

import (
	"fmt"
)

func ExampleParseID_bad() {
	_, err := ParseID("nope")
	fmt.Println(err != nil) // true
	// Output: true
}

This is very Go-like: the example locks in the behavior, but doesn’t make your API hostage to the wording of a system error.

Now let’s make an example that binds to the String() method on the Status type. This is where the ExampleType_Method() convention becomes visible:

package todo

import (
	"fmt"
)

func ExampleStatus_String() {
	fmt.Println(StatusDone) // done
	// Output: done
}

There’s an important point here: for this to work, the package must actually have the Status type, the StatusDone value, and the String() string method so that fmt.Println prints the string representation. And that’s great: an Example forces us to keep the docs and the code in sync.

An example for the whole package: Example() as a “quick start”

Sometimes you want the package documentation to show the simplest “how to use this” scenario, without tying it to a specific function. For that, you write Example() with nothing appended. Such an example will land in the package’s examples section.

Let’s do a “quick start”: validate a title and parse an ID.

package todo

import (
	"fmt"
)

func Example() {
	fmt.Println(ValidateTitle("buy milk") == nil) // true
	id, _ := ParseID("7")
	fmt.Println(id) // 7
	// Output:
	// true
	// 7
}

Notice how multi-line expected output is formatted: after // Output:, each output line is repeated as a separate comment. This is the same style you’ll see in the standard library (for example, in examples for regexp).

7. How an Example differs from a unit test

Sometimes beginner developers confuse Example tests with regular unit tests and try to cram all checks into an Example. To make it easier on your brain, keep this simple model:

flowchart TD
    A[Unit test TestXxx] --> B[Check many cases]
    A --> C[Can use t.Fatalf/t.Errorf]
    A --> D[Focus: internal correctness]

    E[Example test ExampleXxx] --> F[Show one usage scenario]
    E --> G[Usually print stdout]
    E --> H[Focus: how to use the API + a stable result]

A unit test answers “does it work?”, while an Example answers “how do I use it correctly and predictably?”. In a good project, they complement each other instead of competing.

8. Common mistakes

Mistake #1: the example depends on unstable ordering or time.
This is the most unpleasant category: the example may pass locally for you and fail for a colleague. Often the cause is printing a map without sorting or using time.Now(). The remedy is simple: in examples, print only deterministic things, and if you need an order—sort the data before printing.

Mistake #2: // Output: is written “approximately”, but doesn’t match character-for-character.
What breaks beginners most often isn’t the logic, but a single extra space or newline. Keep in mind that the runner compares stdout strictly. Keep output short, don’t print extra explanations, and if you need to show a complex structure, print boolean checks (true/false) or compact values.

Mistake #3: trying to lock in someone else’s error text as part of the contract.
If your ParseID uses strconv.Atoi internally, the error text may not be what you want to promise users. An example like // Output: strconv.Atoi: parsing "nope": invalid syntax will be brittle. Usually it’s better to check “there is an error” or compare to your own error / error type, if that’s what you truly guarantee.

Mistake #4: the Example turns into a 50-line mini-tutorial.
An Example works best when it carries one idea. If you tried to show task creation, sorting, formatting, and handling five errors in a single example, you most likely wrote poor documentation. Split it into multiple examples using suffixes: ExampleFoo_basic, ExampleFoo_error, ExampleFoo_customFormat.

Mistake #5: the example is written in the wrong package and “accidentally sees internals”.
If you write examples in package todo, they have access to all unexported names in the package. Sometimes that tempts you, and the example starts using things a package user won’t see. If you want examples specifically as “external usage”, write them in package todo_test and import todo as an external package. This adds discipline and makes examples more honest.

1
Task
Go SELF, level 36, lesson 3
Locked
Greeting card
Greeting card
1
Task
Go SELF, level 36, lesson 3
Locked
Example without running
Example without running
1
Task
Go SELF, level 36, lesson 3
Locked
Status printing
Status printing
1
Task
Go SELF, level 36, lesson 3
Locked
Stable frequencies
Stable frequencies
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION