Styles are the conventions that govern our code. The term style is a bit of a misnomer, since these conventions cover far more than just source file formatting—gofmt handles that for us.
The goal of this guide is to manage this complexity by describing in detail the Dos and Don'ts of writing Go code at Uber. These rules exist to keep the code base manageable while still allowing engineers to use Go language features productively.
This guide was originally created by Prashant Varanasi and Simon Newton as a way to bring some colleagues up to speed with using Go. Over the years it has been amended based on feedback from others.
This documents idiomatic conventions in Go code that we follow at Uber. A lot of these are general guidelines for Go, while others extend upon external resources:
All code should be error-free when run through golint
and go vet
. We
recommend setting up your editor to:
goimports
on savegolint
and go vet
to check for errorsYou can find information in editor support for Go tools here: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
You almost never need a pointer to an interface. You should be passing interfaces as values—the underlying data can still be a pointer.
An interface is two fields:
If you want interface methods to modify the underlying data, you must use a pointer.
Methods with value receivers can be called on pointers as well as values.
For example,
type S struct {
data string
}
func (s S) Read() string {
return s.data
}
func (s *S) Write(str string) {
s.data = str
}
sVals := map[int]S{1: {"A"}}
// You can only call Read using a value
sVals[1].Read()
// This will not compile:
// sVals[1].Write("test")
sPtrs := map[int]*S{1: {"A"}}
// You can call both Read and Write using a pointer
sPtrs[1].Read()
sPtrs[1].Write("test")
Similarly, an interface can be satisfied by a pointer, even if the method has a value receiver.
type F interface {
f()
}
type S1 struct{}
func (s S1) f() {}
type S2 struct{}
func (s *S2) f() {}
s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}
var i F
i = s1Val
i = s1Ptr
i = s2Ptr
// The following doesn't compile, since s2Val is a value, and there is no value receiver for f.
// i = s2Val
Effective Go has a good write up on Pointers vs. Values.
The zero-value of sync.Mutex
and sync.RWMutex
is valid, so you almost
never need a pointer to a mutex.
Bad | Good |
---|---|
|
|
If you use a struct by pointer, then the mutex can be a non-pointer field.
Unexported structs that use a mutex to protect fields of the struct may embed the mutex.
|
|
Embed for private types or types that need to implement the Mutex interface. | For exported types, use a private field. |
Slices and maps contain pointers to the underlying data so be wary of scenarios when they need to be copied.
Keep in mind that users can modify a map or slice you received as an argument if you store a reference to it.
Bad | Good |
---|---|
|
|
Similarly, be wary of user modifications to maps or slices exposing internal state.
Bad | Good |
---|---|
|
|
Use defer to clean up resources such as files and locks.
Bad | Good |
---|---|
|
|
Defer has an extremely small overhead and should be avoided only if you can
prove that your function execution time is in the order of nanoseconds. The
readability win of using defers is worth the miniscule cost of using them. This
is especially true for larger methods that have more than simple memory
accesses, where the other computations are more significant than the defer
.
Channels should usually have a size of one or be unbuffered. By default, channels are unbuffered and have a size of zero. Any other size must be subject to a high level of scrutiny. Consider how the size is determined, what prevents the channel from filling up under load and blocking writers, and what happens when this occurs.
Bad | Good |
---|---|
|
|
The standard way of introducing enumerations in Go is to declare a custom type
and a const
group with iota
. Since variables have a 0 default value, you
should usually start your enums on a non-zero value.
Bad | Good |
---|---|
|
|
There are cases where using the zero value makes sense, for example when the zero value case is the desirable default behavior.
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
There are various options for declaring errors:
errors.New
for errors with simple static stringsfmt.Errorf
for formatted error stringsError()
method"pkg/errors".Wrap
When returning errors, consider the following to determine the best choice:
errors.New
should suffice.Error()
method.fmt.Errorf
is okay.If the client needs to detect the error, and you have created a simple error
using errors.New
, use a var for the error.
Bad | Good |
---|---|
|
|
If you have an error that clients may need to detect, and you would like to add more information to it (e.g., it is not a static string), then you should use a custom type.
Bad | Good |
---|---|
|
|
Be careful with exporting custom error types directly since they become part of the public API of the package. It is preferable to expose matcher functions to check the error instead.
// package foo
type errNotFound struct {
file string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
}
func IsNotFoundError(err error) bool {
_, ok := err.(errNotFound)
return ok
}
func Open(file string) error {
return errNotFound{file: file}
}
// package bar
if err := foo.Open("foo"); err != nil {
if foo.IsNotFoundError(err) {
// handle
} else {
panic("unknown error")
}
}
There are three main options for propagating errors if a call fails:
"pkg/errors".Wrap
so that the error message provides
more context and "pkg/errors".Cause
can be used to extract the original
error.fmt.Errorf
if the callers do not need to detect or handle that
specific error case.It is recommended to add context where possible so that instead of a vague error such as "connection refused", you get more useful errors such as "call service foo: connection refused".
When adding context to returned errors, keep the context succinct by avoiding phrases like "failed to", which state the obvious and pile up as the error percolates up through the stack:
Bad | Good |
---|---|
|
|
|
|
However once the error is sent to another system, it should be clear the
message is an error (e.g. an err
tag or "Failed" prefix in logs).
See also Don't just check errors, handle them gracefully.
The single return value form of a type assertion will panic on an incorrect type. Therefore, always use the "comma ok" idiom.
Bad | Good |
---|---|
|
|
Code running in production must avoid panics. Panics are a major source of cascading failures. If an error occurs, the function must return an error and allow the caller to decide how to handle it.
Bad | Good |
---|---|
|
|
Panic/recover is not an error handling strategy. A program must panic only when something irrecoverable happens such as a nil dereference. An exception to this is program initialization: bad things at program startup that should abort the program may cause panic.
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
Even in tests, prefer t.Fatal
or t.FailNow
over panics to ensure that the
test is marked as failed.
Bad | Good |
---|---|
|
|
Atomic operations with the sync/atomic package operate on the raw types
(int32
, int64
, etc.) so it is easy to forget to use the atomic operation to
read or modify the variables.
go.uber.org/atomic adds type safety to these operations by hiding the
underlying type. Additionally, it includes a convenient atomic.Bool
type.
Bad | Good |
---|---|
|
|
Performance-specific guidelines apply only to the hot path.
When converting primitives to/from strings, strconv
is faster than
fmt
.
Bad | Good |
---|---|
|
|
|
|
Do not create byte slices from a fixed string repeatedly. Instead, perform the conversion once and capture the result.
Bad | Good |
---|---|
|
|
|
|
Go supports grouping similar declarations.
Bad | Good |
---|---|
|
|
This also applies to constants, variables, and type declarations.
Bad | Good |
---|---|
|
|
Only group related declarations. Do not group declarations that are unrelated.
Bad | Good |
---|---|
|
|
Groups are not limited in where they can be used. For example, you can use them inside of functions.
Bad | Good |
---|---|
|
|
There should be two import groups:
This is the grouping applied by goimports by default.
Bad | Good |
---|---|
|
|
When naming packages, choose a name that is:
net/url
, not net/urls
.See also Package Names and Style guideline for Go packages.
We follow the Go community's convention of using [MixedCaps for function
names]. An exception is made for test functions, which may contain underscores
for the purpose of grouping related test cases, e.g.,
TestMyFunction_WhatIsBeingTested
.
Import aliasing must be used if the package name does not match the last element of the import path.
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
In all other scenarios, import aliases should be avoided unless there is a direct conflict between imports.
Bad | Good |
---|---|
|
|
Therefore, exported functions should appear first in a file, after
struct
, const
, var
definitions.
A newXYZ()
/NewXYZ()
may appear after the type is defined, but before the
rest of the methods on the receiver.
Since functions are grouped by receiver, plain utility functions should appear towards the end of the file.
Bad | Good |
---|---|
|
|
Code should reduce nesting where possible by handling error cases/special conditions first and returning early or continuing the loop. Reduce the amount of code that is nested multiple levels.
Bad | Good |
---|---|
|
|
If a variable is set in both branches of an if, it can be replaced with a single if.
Bad | Good |
---|---|
|
|
At the top level, use the standard var
keyword. Do not specify the type,
unless it is not the same type as the expression.
Bad | Good |
---|---|
|
|
Specify the type if the type of the expression does not match the desired type exactly.
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()
// F returns an object of type myError but we want error.
Prefix unexported top-level var
s and const
s with _
to make it clear when
they are used that they are global symbols.
Exception: Unexported error values, which should be prefixed with err
.
Rationale: Top-level variables and constants have a package scope. Using a generic name makes it easy to accidentally use the wrong value in a different file.
Bad | Good |
---|---|
|
|
Embedded types (such as mutexes) should be at the top of the field list of a struct, and there must be an empty line separating embedded fields from regular fields.
Bad | Good |
---|---|
|
|
You should almost always specify field names when initializing structs. This is
now enforced by go vet
.
Bad | Good |
---|---|
|
|
Exception: Field names may be omitted in test tables when there are 3 or fewer fields.
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
Short variable declarations (:=
) should be used if a variable is being set to
some value explicitly.
Bad | Good |
---|---|
|
|
However, there are cases where the default value is clearer when the var
keyword is use. Declaring Empty Slices, for example.
Bad | Good |
---|---|
|
|
nil
is a valid slice of length 0. This means that,
You should not return a slice of length zero explicitly. Return nil
instead.
Bad | Good |
---|---|
|
|
To check if a slice is empty, always use len(s) == 0
. Do not check for
nil
.
Bad | Good |
---|---|
|
|
The zero value (a slice declared with var
) is usable immediately without
make()
.
Bad | Good |
---|---|
|
|
Where possible, reduce scope of variables. Do not reduce the scope if it conflicts with Reduce Nesting.
Bad | Good |
---|---|
|
|
If you need a result of a function call outside of the if, then you should not try to reduce the scope.
Bad | Good |
---|---|
|
|
Naked parameters in function calls can hurt readability. Add C-style comments
(/* ... */
) for parameter names when their meaning is not obvious.
Bad | Good |
---|---|
|
|
Better yet, replace naked bool
types with custom types for more readable and
type-safe code. This allows more than just two states (true/false) for that
parameter in the future.
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
StatusReady = iota + 1
StatusDone
// Maybe we will have a StatusInProgress in the future.
)
func printInfo(name string, region Region, status Status)
Go supports raw string literals, which can span multiple lines and include quotes. Use these to avoid hand-escaped strings which are much harder to read.
Bad | Good |
---|---|
|
|
Use &T{}
instead of new(T)
when initializing struct references so that it
is consistent with the struct initialization.
Bad | Good |
---|---|
|
|
If you declare format strings for Printf
-style functions outside a string
literal, make them const
values.
This helps go vet
perform static analysis of the format string.
Bad | Good |
---|---|
|
|
When you declare a Printf
-style function, make sure that go vet
can detect
it and check the format string.
This means that you should use pre-defined Printf
-style function
names if possible. go vet
will check these by default. See Printf family
for more information.
If using the pre-defined names is not an option, end the name you choose with
f: Wrapf
, not Wrap
. go vet
can be asked to check specific Printf
-style
names but they must end with f.
$ go vet -printfuncs=wrapf,statusf
See also go vet: Printf family check.
Use table-driven tests with subtests to avoid duplicating code when the core test logic is repetitive.
Bad | Good |
---|---|
|
|
Test tables make it easier to add context to error messages, reduce duplicate logic, and add new test cases.
We follow the convention that the slice of structs is referred to as tests
and each test case tt
. Further, we encourage explicating the input and output
values for each test case with give
and want
prefixes.
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
Functional options is a pattern in which you declare an opaque Option
type
that records information in some internal struct. You accept a variadic number
of these options and act upon the full information recorded by the options on
the internal struct.
Use this pattern for optional arguments in constructors and other public APIs that you foresee needing to expand, especially if you already have three or more arguments on those functions.
Bad | Good |
---|---|
|