Skip to main content

Go

Overview

Go (a.k.a. Golang) is a programming language designed by folks at Google.

This guide provides some general resources for working in Go. Web Application in Go provides some specifics to web development.

Learning Resources

References

Tours/Lessons

Testing

General

  • Use table-driven tests where appropriate.
  • Make judicious use of helper functions so that the intent of a test is not lost in a sea of error checking and boilerplate.
  • Comments delineating the 3 or 4 phases of your tests can help with comprehension.
  • Use t.Helper() in your test helper functions to keep stack traces clean.
  • Use t.Parallel() to speed up tests.
  • Trend away from using testify/suite. (It used to address some shortcomings in the standard library testing tools that have since been addressed.)
  • Lightweight assertion packages can help with expressiveness. Consider using testify/assert or is

Coverage

  • Always test exported functions. Exported functions should be treated as an API layer for other packages. Cover the expected behavior and error scenarios as a user of that API.
    • Consider using the _test package suffix to simulate calling the package under test from an external package
  • Try not to test unexported functions. Unexported functions are implementation details of exported ones and should not change the intended usage. If you find that an unexported function is complex and needs testing, it might mean it needs to be refactored as it's exported function elsewhere.

Context

Function First Parameter

As a general rule, context.Context should be passed down through the layers of your program, as this is the conventional Go way to address "cross cutting concerns", e.g. cancellation, logging, distributed tracing, or other types of instrumentation (which in other languages might be addressed via Thread Locals or similar constructs).

At Google, they found this pattern to be so useful that they require it.

Errors

FYI, Go's error handling had a bit of a shift and update as of 1.13 in ~2019.

Wrap Errors

Prior to Go 1.13, people were forced to make a hard choice: either add descriptive context around an error that was received, or to just pass the error through in case it was a "sentinel error" like the canonical io.EOF example. Nowadays you can both add context to an error, and still ensure the caller can examine the error to see if it is a sentinel.

err := ...
if err != nil {
if oldStyle == true {
// this is the pre-1.13 error encapsulation.
// the problem here is that this created a whole new error,
// which hid the underlying error type.
return fmt.Errorf("<richer contextual information>: %v", err)
}

// using the "%w" (wrap) verb means that the error chain is returned
// and can be examined by the caller using the
// `errors.Unrwap(...)` or `errors.Is(...)` functions
return fmt.Errorf("<richer contextual information>: %w", err)
}

Since these capabilities changed fairly recently, it's common to see many libraries and legacy code not using the wrapping pattern, but going forward it is preferable to opt for wrapping errors where possible.

Handle Errors Once

This advice is mostly stolen from the second-to-last section of Dave Cheney's somewhat dated blog post.

If there are 0 side effects from an error received in your code, this means you are swallowing it, and this will generally make someone's life (maybe even yours) more difficult in the future. Please don't ignore errors.

If there are 2+ side effects from receiving an error, you are adding too much noise to the system. This usually manifests as doing BOTH logging of the error AND returning the error back up the call stack, but if every layer were to do this you'd fill your logs with unnecessary duplication.

Eliminate Errors

This section is ispired by another Dave Cheney missive.

It is possible to reduce error handling overhead by considering if your functions absolutely need to return errors. Consider the following:

func Logger(ctx context.Context) (*zap.Logger, bool) {
logger, ok := ctx.Value(loggerKey).(*zap.Logger)
return logger, ok
}

This function means that every place that wants a *zap.Logger has to deal with the !ok condition, creating a lot of boilerplate checking at callsites who likely have no idea what the proper fallback is if there is no logger defined.

As a consumer, wouldn't the following function signature be easier to deal with?:

func Logger(ctx context.Context) *zap.Logger {
logger, ok := ctx.Value(loggerKey).(*zap.Logger)
if ok {
return logger
}
fmt.Fprintf(os.Stderr, "no logger configured")
return zap.NewNoopLogger()
}

Callers can now be sure that they will always get a logger of some type. And we've centralized, rather than distributed, the fallback behavior. (This implementation chose to communicate the configuration problem to the system operators via stderr, but you might also choose to panic(), or do something else, YMMV.)

Packages

Dependency Management

  • Go Modules has become the standard way to manage your dependencies
  • Before Go Modules were solidified, dep was frequently used for dependency management.
    • This might be helpful if you are dealing with an older project: Daily Dep documentation (common tasks you’ll encounter with the dependency manager)

Prefer Standard Libraries

In general, when selecting new packages, highly consider standard libraries over third party dependencies. One of the strengths of Go is its core packages, such as http, json, and sql. These libraries also use vocabulary and patterns easily accessible via popular public Go resources, which are often translatable to modern programming approaches in their respective areas. This creates an easier bridge for Engineers new to Go or domain areas (such as relational databases) to adjust and onboard.

Since the third party ecosystem is still new, packages may have little community support, follow opinionated patterns inconsistent with Go idioms, or lack long term support. When choosing a third party package, carefully weigh the cost adoption and support contributions. Document the decision and any shortcomings of a comparable standard library along with a rollback plan.

Third Party Packages

Some examples of third party packages we've found to be helpful and stable are:

If you're exploring a new package, Awesome Go is a good place to start.

Time

Clock Dependency

time.Now() can cause a lot of side effects in a codebase. One example is that you can't test the "current" time that happened in a function you called in the past

For example, let's say we have the following:

package mypackage

import "time"

func MyTimeFunc() time.Time {
return time.Now()
}

func TestMyTimeFunc(t *testing.T) {
if MyTimeFunc() != time.Now() {
// This will error!
// The time in the function and the test happen at different times
t.Errorf("time was not now")
}
}

How do we test the contents of the return here? If we want to assert the time we need a way to know what time.Now() was when the function was called.

Instead of directly using the time package, we can pass a clock as a dependency and call .Now() on that. Then in our tests, we can assert against that clock! The clock can be anything as long as it adheres to the clock.Clock interface as defined in the facebookgo clock package. We could, for example, make the clock always return the year 0, or the 2019 New Year, or maybe your birthday! In this clock package, there are two clocks.

  • The real clock where clock.Now() will call time.Now().
  • A mock clock where clock.Now() always returns epoch time. We'll show later how to change that!

Let's look at the example above with the clock package.

package mypackage

import "fmt"
import "time"

import "github.com/facebookgo/clock"

func MyTimeFunc(clock clock.Clock) time.Time {
return clock.Now()
}

// Then our caller
func main() {
// clock.New() creates a clock that uses the time package
// it will output current time when .Now() is called
fmt.Print(MyTimeFunc(clock.New()))
}

Then in our tests we can use a mock clock that freezes .Now() at epoch time:

func TestMyTimeFunc(t *testing.T) {
testClock := clock.NewMock()
if MyTimeFunc(testClock) != testClock.Now() {
// both should equal epoch time, we won't hit this error
t.Errorf("time was not now")
}
}

Cool, but what if I want to use a different date? Say my test relies on our TestYear constant. The clock.Mock clock allows us to add durations to the clock and set the current time. Note that the clock.Clock interface does not allow this, it needs to happen before passing the mock clock through the interface parameter.

Setting the Mock Clock

Here's an example using the test above and setting the time to September 30 of TestYear:

func TestMyTimeFunc(t *testing.T) {
testClock := clock.NewMock()
dateToTest := time.Date(TestYear, time.September, 30, 0, 0, 0, 0, time.UTC)
timeDiff := dateToTest.Sub(c.Now())
testClock.Add(timeDiff)
if MyTimeFunc(testClock) != testClock.Now() {
// both will now be September 30 of TestYear
// we'll pass the test again
t.Errorf("time was not now")
}
}

http.ResponseWriter

HTTP Status Codes & Bytes Written

There are a surprising number of footguns associated with trying to gather statistics about an HTTP request, having to do with several obscure and optional ("smuggled"?) http.ResponseWriter interfaces. This is a regularly occurring challenge for anyone trying to implement a request- logging middleware.

The recommended package for this task is called httpsnoop, and the author has a great explaination of "Why this package exists".