Saturday, July 13, 2024
Google search engine
HomeUncategorizedGolang’s best-kept secret: ‘executable examples’

Golang’s best-kept secret: ‘executable examples’

As we saw in Test names should be sentences, there’s a lot of valuable information our Go tests can communicate to readers, starting with the test names, and continuing with the logic of the tests themselves. By using the system in a test, we can not just verify things about its behaviour, but also show users how they can call the system, and how they should expect it to behave.

That’s great, but sometimes these two aims conflict a little. For example, many tests require a fair amount of code to set up an environment that resembles the one in which it would really be used. But users don’t need to do that, because they’re writing real programs, not tests. So the communication aspect gets slightly obscured by the verification aspect.

Similarly, while we might write wonderful documentation comments on each of our exported functions, our tests won’t appear as part of the resulting autogenerated documentation. So someone reading about your package on pkg.go.dev, for example, will see your documentation, but not your code or tests.

What we’d like is a way to write code that will be included in the autogenerated documentation, and whose behaviour can be automatically verified in the same way as the tests. That’s exactly what Go provides, with a feature called executable examples.

A good example can be a more succinct or intuitive way to convey the behavior of a library function than its prose description, especially when used as a reminder or quick reference.

And unlike examples within comments, example functions are real Go code, subject to compile-time checking, so they don’t become stale as the code evolves.

—Alan Donovan & Brian Kernighan, “The Go Programming Language”

An executable example is like a test function, but even simpler. Its name must begin with the word Example, but it doesn’t take any parameters, or return anything:

func ExampleDouble() {
    fmt.Println(double.Double(2))
    // Output:
    // 4
}

(Listing double/2)

Very straightforward. In this example, we call Double(2) and print the result. A comment shows that the expected output is 4.

So what? Well, the neat thing is that this example function is automatically included in the documentation for your package. Here’s what that might look like on pkg.go.dev:

We can see that the program has been reformatted to put the example code inside a main function, and that the expected output is shown separately.

Indeed, you can run the example directly in your browser by clicking the Run button on the website. Try it with the examples from the script package:

When you click Run, the example program is built, its dependencies downloaded, the resulting binary run, and the output displayed, all right in the browser. You can also edit the example to do whatever you like, and try out your modified program in the same way.

In fact, the examples are being run using the Go Playground, and you can do the same kind of thing by browsing to:

and entering whatever program you like.

There are some common-sense restrictions on code that runs in the playground (and thus on executable examples running on pkg.go.dev). For example, there’s no networking available, and the amounts of CPU, memory, and execution time are limited to prevent abuse.

And, interestingly, you’ll find that the time and date returned the first time you call time.Now in playground programs is always the same: 11pm on Tuesday, November 10, 2009. This is a significant date for Gophers, naturally, but it also means that the behaviour of time-related code is deterministic.

When you run your examples locally, on your own machine, using go test, these restrictions don’t apply, but just be aware that they will apply to people running them on go.pkg.dev.

Your executable examples are not only included in the documentation for your package, but they’re also run whenever you run go test. What exactly is being tested here, then, you might wonder? And how could such a test fail?

Let’s try changing the comment showing the expected output, and see what happens. For example, suppose we make it “5”, instead of “4”:

func ExampleDouble() {
    fmt.Println(double.Double(2))
    // Output:
    // 5
}

Running go test produces this result:

--- FAIL: ExampleDouble (0.00s)
got:
4
want:
5
FAIL

So that Output comment is not just a comment, it turns out. It’s also an expectation that the test machinery will verify.

We check the behaviour of the function, in other words, by asserting something about its standard output, ignoring any leading and trailing whitespace.

We can use examples to test functions that print to standard output by just calling them directly. Indeed, it’s easier to test such functions using executable examples than it would be with a regular test:

func ExamplePrintHello() {
    PrintHello()
    // Output:
    // Hello
}

If the function doesn’t happen to print anything, but instead returns a result, we can use fmt.Println to print that result, as in the Double example, and then verify it with the Output comment.

We can also match multiline output, if necessary:

func ExamplePrintHello() {
    PrintHello()
    PrintHello()
    // Output:
    // Hello
    // Hello
}

If we know what lines should be present in the output, but not in what order they will appear, we can use a different version of the Output comment:

func ExamplePrintMap() {
    m := map[int]bool{
        1: true,
        3: true,
        2: false,
    }
    for k, v := range m {
        fmt.Println(k, v)
    }
    // Unordered output:
    // 1 true
    // 2 false
    // 3 true
}

By specifying the example’s unordered output, we’re saying that each of these lines must be present in the output, but they can appear in a different order to that given. This will likely be true when ranging over a map, for example.

You might also be wondering how Go knows that ExampleDouble, for instance, should be attached to the documentation for the Double function. That’s easy: whatever follows the word Example in the example name is taken to be the name of the function it documents.

So if your function is called Greet, and you want to provide an example for it, you would name that example function ExampleGreet. The documentation tool will then automatically associate the example with the Greet function.

What if you want to give more than one example for the same function? You can do that by adding a suffix after the function name:

func ExampleDouble_with2() {
    fmt.Println(double.Double(2))
    // Output:
    // 4
}

func ExampleDouble_with3() {
    fmt.Println(double.Double(3))
    // Output:
    // 6
}

(Listing double/2)

In fact, you can supply as many examples as you want, provided each has a unique name, whose suffix begins with a lowercase letter (for example, with2).

If you want to write an example for the whole package, rather than any specific function, just name it Example:

func Example() {
    // this demonstrates how to use the entire package
    ...
}

Otherwise, Go will assume that whatever follows the word Example is the name of the thing you’re documenting.

This works with types, too. Suppose you define some type User:

type User struct {
    Name string
}

(Listing user/1)

As part of your documentation, you can supply an example in your test file:

func ExampleUser() {
    u := user.User{ Name: "Gopher" }
    fmt.Println(u)
    // Output:
    // 
}

(Listing user/1)

What about methods on types? We can give examples for those too. Suppose you add a method on User named NameString, to return the user’s name as a string:

func (u User) NameString() string {
    return u.Name
}

(Listing user/1)

You can add an example to your test file that shows what this method does:

func ExampleUser_NameString() {
    u := user.User{Name: "Gopher"}
    fmt.Println(u.NameString())
    // Output:
    // Gopher
}

(Listing user/1)

The convention is simple: the word Example is followed by the name of the type we’re documenting (in this case User), then an underscore, then the name of the specific method (here it’s NameString).

It’s always a great idea to use code examples as part of our documentation anyway, but if they’re just text, there’s a danger that they’ll become out of date and stop working. Indeed, they might never have worked in the first place.

We’ve probably all had the unpleasant experience of copying and pasting an example code snippet from someone’s documentation, and finding not only that it doesn’t behave the way we expected, but that it doesn’t even compile. Not a good look for the project.

So it’s nice that Go provides a way for us to supply examples that are automatically included in our documentation, that users can edit and run in a web browser without even needing Go installed, and that are also automatically checked every time we run go test.

Go itself uses executable examples quite widely in the standard library documentation (see the strings package documentation, for instance). But most third-party packages don’t bother to include examples, which I think is a shame. It’s an easy way to add a lot of value to your package, and also lends a rather professional appearance to its documentation.

Read More

RELATED ARTICLES

1 COMMENT

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments