You are on page 1of 6

Hey Ivandro, it's Nate from newline, and today I'll teach you how to perform

benchmark tests for Golang code. Go promises fast performance, but how you
implement your code plays a big role in this. Improving the performance of your code
is worth your attention because it leads to lower server costs and faster-loading user
interfaces.
Below, I'm going to show you:
• how benchmarking works
• how we create a simple benchmark in Go
• steps to create and run a real-life, more complete, benchmark test
If you want to learn Golang quickly through building multiple real-world projects,
here's just about the fastest way to do so. The newline Guide to Reliable
Webservers with Golang is our comprehensive immersion in building practical
Golang web servers and other highly useful apps. It's taught by former Google and
Etsy engineers, Nat Welch and Steve McCarthy. You can get it here.

Benchmarks In A Nutshell
Benchmarking is a testing technique that helps you answer questions such as:
• "How fast does my code run?"
• "And if I make these changes to my code, will I get a significant performance
improvement?"
Benchmarks give us actual numbers that quantify the performance of our code:
• Execution Time - how much time does it take to run?
• Total Amount of Memory Allocated - how much memory must be allocated
for it to run?
• Rate of Memory Allocation - how many times must memory be allocated to
meet the total memory requirement?
Go's testing package contains benchmarking utilities for measuring these metrics.

A Simple Benchmarking Example


Here's a straightforward benchmark test example.
Suppose we want to benchmark a simple Sum function. First, we write the function.

// Returns the sum of two ints


func Sum(a int, b int) {
// Add the two input numbers
// return their sum
return a + b
}

Next, here's how we write the benchmark test for the Sum function.
// Import testing library
import (
"testing"
)

// Benchmark for `Sum` function


func BenchmarkSumFunc(b *testing.B) {
// Run the function `b.N` times
for i := 0; i < b.N; i++ {
Sum(100, i)
}
}

Similar to unit tests, benchmarks go in a file whose name ends with _test.go. To run
the benchmark, you invoke the go test command with the -bench=. flag.

go test -bench=.

Here, . tells Go to run all benchmarks within the package (i.e., main).

Take a look at the benchmark test output.

This benchmark runs the Sum function for b.N iterations. In this case, the benchmark
ran for one billion iterations. On average, each iteration ran 0.6199 ns.

A More Featureful Benchmark Example


I have a Go and chi RESTful API project available on GitHub. Clone it into a local
folder and checkout the basic-tests branch using git.

This project hosts five endpoints for working with posts:

• GET /posts - Retrieve a list of posts.


• POST /posts - Create a post.
• GET /posts/{id} - Retrieve post by id.
• PUT /posts/{id} - Update post by id.
• DELETE /posts/{id} - Delete a single post by id.
If you would like to learn how to code this RESTful API, then follow this tutorial blog
post.
After cloning, run the following command to install dependencies.

$ make install_deps

Run the following command to execute the unit tests within


the routes/posts_test.go file.

$ make test

Our new benchmark will test the route handler for the GET /posts route.

Writing A Benchmark For A Go And chi Route Handler


Now let's write our benchmark test in the routes/posts_test.go file.

// routes/posts_test.go
// ...

func BenchmarkGetPostsHandler(
b *testing.B) {
// Group benchmarks
b.Run(
"Endpoint: GET /posts",
func(b *testing.B) {
GetPosts = (
&JsonPlaceholderMock{}).GetPosts

// Define the GET request


// to benchmark
r, _ := http.NewRequest(
"GET", "/posts", nil)

// Create a response recorder


w := httptest.NewRecorder()
// Create an HTTP route handler
handler := http.HandlerFunc(
PostsResource{}.List)

// Turn on memory stats


b.ReportAllocs()
b.ResetTimer()

// Execute the handler


// with a request, `b.N` times
for i := 0; i < b.N; i++ {
handler.ServeHTTP(w, r)
}
})
}
In the code above, we use b.Run to group benchmarks by the functionality they test.
To avoid sending network requests during benchmarking, we mock out
the GetPosts package-scoped variable. We create a new GET request to send
to /posts via the http package's NewRequest method.
We also create a response recorder via httptest.NewRecorder to record the
mutations of ResponseWriter. The PostsResource{}.List method is an ordinary
function. We therefore pass it to the http package's HandlerFunc method to create
an HTTP route handler from it.
We turn on malloc statistics via b.ReportAllocs, which displays an additional two
columns in the output. These columns tell how many bytes of memory, on average,
were allocated per iteration and how many allocations were performed per iteration.
The handler.ServeHTTP method executes the HTTP route handler representation
of PostsResource{}.List using the response recorder w and the created request r.

We loop over handler.ServeHTTP until the benchmark determines how fast it runs.
The value of b.N changes until the benchmark stabilizes and knows how long it must
run to properly time the function.

Run The Benchmark


Now let's run the benchmark.

$ go test -bench=. ./routes -run=^$

Let's take a closer look at the flags in the above go test command.

• -bench=. - Tells go test to run benchmarks and tests within the


project's _test.go files. . is a regular expression that tells go test to match
with everything.
• ./routes - Tells go test the location of the _test.go files with the
benchmarks and tests to run.
• -run=^$ - Tells go test to run tests with names that satisfy the regular
expression ^$. Since none of the tests' names begin with $, go test does not
run any tests and runs only benchmarks.

Here, benchmarks run sequentially.


The benchmark ran a total of 97220 iterations with each iteration running, on
average, 11439 ns. This represents the average time it took for
each handler.ServeHTTP function and PostsResource{}.List call to complete. Each
iteration involved the allocation of, on average, 33299 bytes of memory. Memory was
allocated, on average, 8 times per iteration.
Feel free to look at the GitHub repo for the final version of the route handler
benchmark test.

The Bigger Picture About Benchmarks' Usefulness


What's particularly useful about benchmarks like this is that we're able to code
different implementations of a program and see which one performs best. Open
source code projects often do this, where major upgrades to the project include new
implementations that provide a more efficient set of routines for essential tasks.
Without benchmarks to compare implementations, we'd be stabbing in the dark.
Here's a real world discussion from the Python project community discussing
benchmarks for Python 2 vs Python 3, a big change to that community's
programming language. It goes without saying, benchmarks are an essential
consideration in open source communities when arguing for or against those kinds
of significant code changes.
Till next time.
-- Nate

P.S. Many engineers struggle to learn Golang, not because the language is too
difficult, but because they rely on incomplete, outdated blog posts. The newline
Guide to Reliable Webservers with Golang is the fastest way to proficiency with
Golang. See more here.

By the way, if you don't want to hear from me about Go, click here to opt-out of Go content, but stay
subscribed to the list.

You might also like