- Introduction
- Guidelines
- Pointers to Interfaces
- Verify Interface Compliance
- Receivers and Interfaces
- Zero-value Mutexes are Valid
- Copy Slices and Maps at Boundaries
- Defer to Clean Up
- Channel Size is One or None
- Start Enums at One
- Use
"time"
to handle time - Errors
- Handle Type Assertion Failures
- Don't Panic
- Use go.uber.org/atomic
- Avoid Mutable Globals
- Avoid Embedding Types in Public Structs
- Avoid Using Built-In Names
- Avoid
init()
- Exit in Main
- Use field tags in marshaled structs
- Don't fire-and-forget goroutines
- Performance
- Style
- Avoid overly long lines
- Be Consistent
- Group Similar Declarations
- Import Group Ordering
- Package Names
- Function Names
- Import Aliasing
- Function Grouping and Ordering
- Reduce Nesting
- Unnecessary Else
- Top-level Variable Declarations
- Prefix Unexported Globals with _
- Embedding in Structs
- Local Variable Declarations
- nil is a valid slice
- Reduce Scope of Variables
- Avoid Naked Parameters
- Use Raw String Literals to Avoid Escaping
- Initializing Structs
- Initializing Maps
- Format Strings outside Printf
- Naming Printf-style Functions
- Patterns
- Linting
Introduction
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:
We aim for the code samples to be accurate for the two most recent minor versions of Go releases.
All code should be error-free when run through golint
and go vet
. We
recommend setting up your editor to:
- Run
goimports
on save - Run
golint
andgo vet
to check for errors
You can find information in editor support for Go tools here: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
Guidelines
Pointers to Interfaces
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:
- A pointer to some type-specific information. You can think of this as "type."
- Data pointer. If the data stored is a pointer, it’s stored directly. If the data stored is a value, then a pointer to the value is stored.
If you want interface methods to modify the underlying data, you must use a pointer.
Verify Interface Compliance
Verify interface compliance at compile time where appropriate. This includes:
- Exported types that are required to implement specific interfaces as part of their API contract
- Exported or unexported types that are part of a collection of types implementing the same interface
- Other cases where violating an interface would break users
Bad | Good |
---|---|
|
|
The statement var _ http.Handler = (*Handler)(nil)
will fail to compile if
*Handler
ever stops matching the http.Handler
interface.
The right hand side of the assignment should be the zero value of the asserted
type. This is nil
for pointer types (like *Handler
), slices, and maps, and
an empty struct for struct types.
type LogHandler struct {
h http.Handler
log *zap.Logger
}
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
Receivers and Interfaces
Methods with value receivers can be called on pointers as well as values. Methods with pointer receivers can only be called on pointers or addressable 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
}
// We cannot get pointers to values stored in maps, because they are not
// addressable values.
sVals := map[int]S{1: {"A"}}
// We can call Read on values stored in the map because Read
// has a value receiver, which does not require the value to
// be addressable.
sVals[1].Read()
// We cannot call Write on values stored in the map because Write
// has a pointer receiver, and it's not possible to get a pointer
// to a value stored in a map.
//
// sVals[1].Write("test")
sPtrs := map[int]*S{1: {"A"}}
// You can call both Read and Write if the map stores pointers,
// because pointers are intrinsically addressable.
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.
Zero-value Mutexes are Valid
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 should be a non-pointer field on it. Do not embed the mutex on the struct, even if the struct is not exported.
Bad | Good |
---|---|
|
|
The | The mutex and its methods are implementation details of |
Copy Slices and Maps at Boundaries
Slices and maps contain pointers to the underlying data so be wary of scenarios when they need to be copied.
Receiving Slices and Maps
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 |
---|---|
|
|
Returning Slices and Maps
Similarly, be wary of user modifications to maps or slices exposing internal state.
Bad | Good |
---|---|
|
|
Defer to Clean Up
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
.
Channel Size is One or None
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 |
---|---|
|
|
Start Enums at One
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
Use "time"
to handle time
Time is complicated. Incorrect assumptions often made about time include the following.
- A day has 24 hours
- An hour has 60 minutes
- A week has 7 days
- A year has 365 days
- And a lot more
For example, 1 means that adding 24 hours to a time instant will not always yield a new calendar day.
Therefore, always use the "time"
package when dealing with time because it
helps deal with these incorrect assumptions in a safer, more accurate manner.
Use time.Time
for instants of time
Use time.Time
when dealing with instants of time, and the methods on
time.Time
when comparing, adding, or subtracting time.
Bad | Good |
---|---|
|
|
Use time.Duration
for periods of time
Use time.Duration
when dealing with periods of time.
Bad | Good |
---|---|
|
|
Going back to the example of adding 24 hours to a time instant, the method we
use to add time depends on intent. If we want the same time of the day, but on
the next calendar day, we should use Time.AddDate
. However, if we want an
instant of time guaranteed to be 24 hours after the previous time, we should
use Time.Add
.
newDay := t.AddDate(0 /* years */, 0 /* months */, 1 /* days */)
maybeNewDay := t.Add(24 * time.Hour)
Use time.Time
and time.Duration
with external systems
Use time.Duration
and time.Time
in interactions with external systems when
possible. For example:
- Command-line flags:
flag
supportstime.Duration
viatime.ParseDuration
- JSON:
encoding/json
supports encodingtime.Time
as an RFC 3339 string via itsUnmarshalJSON
method - SQL:
database/sql
supports convertingDATETIME
orTIMESTAMP
columns intotime.Time
and back if the underlying driver supports it - YAML:
gopkg.in/yaml.v2
supportstime.Time
as an RFC 3339 string, andtime.Duration
viatime.ParseDuration
.
When it is not possible to use time.Duration
in these interactions, use
int
or float64
and include the unit in the name of the field.
For example, since encoding/json
does not support time.Duration
, the unit
is included in the name of the field.
Bad | Good |
---|---|
|
|
When it is not possible to use time.Time
in these interactions, unless an
alternative is agreed upon, use string
and format timestamps as defined in
RFC 3339. This format is used by default by Time.UnmarshalText
and is
available for use in Time.Format
and time.Parse
via time.RFC3339
.
Although this tends to not be a problem in practice, keep in mind that the
"time"
package does not support parsing timestamps with leap seconds
(8728), nor does it account for leap seconds in calculations (15190). If
you compare two instants of time, the difference will not include the leap
seconds that may have occurred between those two instants.
Errors
Error Types
There are few options for declaring errors. Consider the following before picking the option best suited for your use case.
- Does the caller need to match the error so that they can handle it?
If yes, we must support the
errors.Is
orerrors.As
functions by declaring a top-level error variable or a custom type. - Is the error message a static string,
or is it a dynamic string that requires contextual information?
For the former, we can use
errors.New
, but for the latter we must usefmt.Errorf
or a custom error type. - Are we propagating a new error returned by a downstream function? If so, see the section on error wrapping.
Error matching? | Error Message | Guidance |
---|---|---|
No | static | errors.New |
No | dynamic | fmt.Errorf |
Yes | static | top-level var with errors.New |
Yes | dynamic | custom error type |
For example,
use errors.New
for an error with a static string.
Export this error as a variable to support matching it with errors.Is
if the caller needs to match and handle this error.
No error matching | Error matching |
---|---|
|
|
For an error with a dynamic string,
use fmt.Errorf
if the caller does not need to match it,
and a custom error
if the caller does need to match it.
No error matching | Error matching |
---|---|
|
|
Note that if you export error variables or types from a package, they will become part of the public API of the package.
Error Wrapping
There are three main options for propagating errors if a call fails:
- return the original error as-is
- add context with
fmt.Errorf
and the%w
verb - add context with
fmt.Errorf
and the%v
verb
Return the original error as-is if there is no additional context to add. This maintains the original error type and message. This is well suited for cases when the underlying error message has sufficient information to track down where it came from.
Otherwise, add context to the error message 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".
Use fmt.Errorf
to add context to your errors,
picking between the %w
or %v
verbs
based on whether the caller should be able to
match and extract the underlying cause.
- Use
%w
if the caller should have access to the underlying error. This is a good default for most wrapped errors, but be aware that callers may begin to rely on this behavior. So for cases where the wrapped error is a knownvar
or type, document and test it as part of your function's contract. - Use
%v
to obfuscate the underlying error. Callers will be unable to match it, but you can switch to%w
in the future if needed.
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.
Error Naming
For error values stored as global variables,
use the prefix Err
or err
depending on whether they're exported.
This guidance supersedes the Prefix Unexported Globals with _.
var (
// The following two errors are exported
// so that users of this package can match them
// with errors.Is.
ErrBrokenLink = errors.New("link is broken")
ErrCouldNotOpen = errors.New("could not open")
// This error is not exported because
// we don't want to make it part of our public API.
// We may still use it inside the package
// with errors.Is.
errNotFound = errors.New("not found")
)
For custom error types, use the suffix Error
instead.
// Similarly, this error is exported
// so that users of this package can match it
// with errors.As.
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
// And this error is not exported because
// we don't want to make it part of the public API.
// We can still use it inside the package
// with errors.As.
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("resolve %q", e.Path)
}
Handle Errors Once
When a caller receives an error from a callee, it can handle it in a variety of different ways depending on what it knows about the error.
These include, but not are limited to:
- if the callee contract defines specific errors,
matching the error with
errors.Is
orerrors.As
and handling the branches differently - if the error is recoverable, logging the error and degrading gracefully
- if the error represents a domain-specific failure condition, returning a well-defined error
- returning the error, either wrapped or verbatim
Regardless of how the caller handles the error, it should typically handle each error only once. The caller should not, for example, log the error and then return it, because its callers may handle the error as well.
For example, consider the following cases:
Description | Code |
---|---|
Bad: Log the error and return it Callers further up the stack will likely take a similar action with the error. Doing so causing a lot of noise in the application logs for little value. |
|
Good: Wrap the error and return it Callers further up the stack will handle the error.
Use of |
|
Good: Log the error and degrade gracefully If the operation isn't strictly necessary, we can provide a degraded but unbroken experience by recovering from it. |
|
Good: Match the error and degrade gracefully If the callee defines a specific error in its contract, and the failure is recoverable, match on that error case and degrade gracefully. For all other cases, wrap the error and return it. Callers further up the stack will handle other errors. |
|
Handle Type Assertion Failures
The single return value form of a type assertion will panic on an incorrect type. Therefore, always use the "comma ok" idiom.
Bad | Good |
---|---|
|
|
Don't Panic
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 |
---|---|
|
|
Use go.uber.org/atomic
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 |
---|---|
|
|
Avoid Mutable Globals
Avoid mutating global variables, instead opting for dependency injection. This applies to function pointers as well as other kinds of values.
Bad | Good |
---|---|
|
|
|
|
Avoid Embedding Types in Public Structs
These embedded types leak implementation details, inhibit type evolution, and obscure documentation.
Assuming you have implemented a variety of list types using a shared
AbstractList
, avoid embedding the AbstractList
in your concrete list
implementations.
Instead, hand-write only the methods to your concrete list that will delegate
to the abstract list.
type AbstractList struct {}
// Add adds an entity to the list.
func (l *AbstractList) Add(e Entity) {
// ...
}
// Remove removes an entity from the list.
func (l *AbstractList) Remove(e Entity) {
// ...
}
Bad | Good |
---|---|
|
|
Go allows type embedding as a compromise between inheritance and composition. The outer type gets implicit copies of the embedded type's methods. These methods, by default, delegate to the same method of the embedded instance.
The struct also gains a field by the same name as the type. So, if the embedded type is public, the field is public. To maintain backward compatibility, every future version of the outer type must keep the embedded type.
An embedded type is rarely necessary. It is a convenience that helps you avoid writing tedious delegate methods.
Even embedding a compatible AbstractList interface, instead of the struct, would offer the developer more flexibility to change in the future, but still leak the detail that the concrete lists use an abstract implementation.
Bad | Good |
---|---|
|
|
Either with an embedded struct or an embedded interface, the embedded type places limits on the evolution of the type.
- Adding methods to an embedded interface is a breaking change.
- Removing methods from an embedded struct is a breaking change.
- Removing the embedded type is a breaking change.
- Replacing the embedded type, even with an alternative that satisfies the same interface, is a breaking change.
Although writing these delegate methods is tedious, the additional effort hides an implementation detail, leaves more opportunities for change, and also eliminates indirection for discovering the full List interface in documentation.
Avoid Using Built-In Names
The Go language specification outlines several built-in, predeclared identifiers that should not be used as names within Go programs.
Depending on context, reusing these identifiers as names will either shadow the original within the current lexical scope (and any nested scopes) or make affected code confusing. In the best case, the compiler will complain; in the worst case, such code may introduce latent, hard-to-grep bugs.
Bad | Good |
---|---|
|
|
|
|
Note that the compiler will not generate errors when using predeclared
identifiers, but tools such as go vet
should correctly point out these and
other cases of shadowing.
Avoid init()
Avoid init()
where possible. When init()
is unavoidable or desirable, code
should attempt to:
- Be completely deterministic, regardless of program environment or invocation.
- Avoid depending on the ordering or side-effects of other
init()
functions. Whileinit()
ordering is well-known, code can change, and thus relationships betweeninit()
functions can make code brittle and error-prone. - Avoid accessing or manipulating global or environment state, such as machine information, environment variables, working directory, program arguments/inputs, etc.
- Avoid I/O, including both filesystem, network, and system calls.
Code that cannot satisfy these requirements likely belongs as a helper to be
called as part of main()
(or elsewhere in a program's lifecycle), or be
written as part of main()
itself. In particular, libraries that are intended
to be used by other programs should take special care to be completely
deterministic and not perform "init magic".
Bad | Good |
---|---|
|
|
|
|
Considering the above, some situations in which init()
may be preferable or
necessary might include:
- Complex expressions that cannot be represented as single assignments.
- Pluggable hooks, such as
database/sql
dialects, encoding type registries, etc. - Optimizations to Google Cloud Functions and other forms of deterministic precomputation.
Exit in Main
Go programs use os.Exit
or log.Fatal*
to exit immediately. (Panicking
is not a good way to exit programs, please don't panic.)
Call one of os.Exit
or log.Fatal*
only in main()
. All other
functions should return errors to signal failure.
Bad | Good |
---|---|
|
|
Rationale: Programs with multiple functions that exit present a few issues:
- Non-obvious control flow: Any function can exit the program so it becomes difficult to reason about the control flow.
- Difficult to test: A function that exits the program will also exit the test
calling it. This makes the function difficult to test and introduces risk of
skipping other tests that have not yet been run by
go test
. - Skipped cleanup: When a function exits the program, it skips function calls
enqueued with
defer
statements. This adds risk of skipping important cleanup tasks.
Exit Once
If possible, prefer to call os.Exit
or log.Fatal
at most once in your
main()
. If there are multiple error scenarios that halt program execution,
put that logic under a separate function and return errors from it.
This has the effect of shortening your main()
function and putting all key
business logic into a separate, testable function.
Bad | Good |
---|---|
|
|
The example above uses log.Fatal
, but the guidance also applies to
os.Exit
or any library code that calls os.Exit
.
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
You may alter the signature of run()
to fit your needs.
For example, if your program must exit with specific exit codes for failures,
run()
may return the exit code instead of an error.
This allows unit tests to verify this behavior directly as well.
func main() {
os.Exit(run(args))
}
func run() (exitCode int) {
// ...
}
More generally, note that the run()
function used in these examples
is not intended to be prescriptive.
There's flexibility in the name, signature, and setup of the run()
function.
Among other things, you may:
- accept unparsed command line arguments (e.g.,
run(os.Args[1:])
) - parse command line arguments in
main()
and pass them ontorun
- use a custom error type to carry the exit code back to
main()
- put business logic in a different layer of abstraction from
package main
This guidance only requires that there's a single place in your main()
responsible for actually exiting the process.
Use field tags in marshaled structs
Any struct field that is marshaled into JSON, YAML, or other formats that support tag-based field naming should be annotated with the relevant tag.
Bad | Good |
---|---|
|
|
Rationale: The serialized form of the structure is a contract between different systems. Changes to the structure of the serialized form--including field names--break this contract. Specifying field names inside tags makes the contract explicit, and it guards against accidentally breaking the contract by refactoring or renaming fields.
Don't fire-and-forget goroutines
Goroutines are lightweight, but they're not free: at minimum, they cost memory for their stack and CPU to be scheduled. While these costs are small for typical uses of goroutines, they can cause significant performance issues when spawned in large numbers without controlled lifetimes. Goroutines with unmanaged lifetimes can also cause other issues like preventing unused objects from being garbage collected and holding onto resources that are otherwise no longer used.
Therefore, do not leak goroutines in production code. Use go.uber.org/goleak to test for goroutine leaks inside packages that may spawn goroutines.
In general, every goroutine:
- must have a predictable time at which it will stop running; or
- there must be a way to signal to the goroutine that it should stop
In both cases, there must be a way code to block and wait for the goroutine to finish.
For example:
Bad | Good |
---|---|
|
|
There's no way to stop this goroutine. This will run until the application exits. | This goroutine can be stopped with |
Wait for goroutines to exit
Given a goroutine spawned by the system, there must be a way to wait for the goroutine to exit. There are two popular ways to do this:
-
Use a
sync.WaitGroup
. Do this if there are multiple goroutines that you want to wait forvar wg sync.WaitGroup
for i := 0; i < N; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// ...
}()
}
// To wait for all to finish:
wg.Wait() -
Add another
chan struct{}
that the goroutine closes when it's done. Do this if there's only one goroutine.done := make(chan struct{})
go func() {
defer close(done)
// ...
}()
// To wait for the goroutine to finish:
<-done
No goroutines in init()
init()
functions should not spawn goroutines.
See also Avoid init().
If a package has need of a background goroutine,
it must expose an object that is responsible for managing a goroutine's
lifetime.
The object must provide a method (Close
, Stop
, Shutdown
, etc)
that signals the background goroutine to stop, and waits for it to exit.
Bad | Good |
---|---|
|
|
Spawns a background goroutine unconditionally when the user exports this package. The user has no control over the goroutine or a means of stopping it. | Spawns the worker only if the user requests it. Provides a means of shutting down the worker so that the user can free up resources used by the worker. Note that you should use |
Performance
Performance-specific guidelines apply only to the hot path.
Prefer strconv over fmt
When converting primitives to/from strings, strconv
is faster than
fmt
.
Bad | Good |
---|---|
|
|
|
|
Avoid string-to-byte conversion
Do not create byte slices from a fixed string repeatedly. Instead, perform the conversion once and capture the result.
Bad | Good |
---|---|
|
|
|
|
Prefer Specifying Container Capacity
Specify container capacity where possible in order to allocate memory for the container up front. This minimizes subsequent allocations (by copying and resizing of the container) as elements are added.
Specifying Map Capacity Hints
Where possible, provide capacity hints when initializing
maps with make()
.
make(map[T1]T2, hint)
Providing a capacity hint to make()
tries to right-size the
map at initialization time, which reduces the need for growing
the map and allocations as elements are added to the map.
Note that, unlike slices, map capacity hints do not guarantee complete, preemptive allocation, but are used to approximate the number of hashmap buckets required. Consequently, allocations may still occur when adding elements to the map, even up to the specified capacity.
Bad | Good |
---|---|
|
|
|
|
Specifying Slice Capacity
Where possible, provide capacity hints when initializing slices with make()
,
particularly when appending.
make([]T, length, capacity)
Unlike maps, slice capacity is not a hint: the compiler will allocate enough
memory for the capacity of the slice as provided to make()
, which means that
subsequent append()
operations will incur zero allocations (until the length
of the slice matches the capacity, after which any appends will require a resize
to hold additional elements).
Bad | Good |
---|---|
|
|
|
|
Style
Avoid overly long lines
Avoid lines of code that require readers to scroll horizontally or turn their heads too much.
We recommend a soft line length limit of 99 characters. Authors should aim to wrap lines before hitting this limit, but it is not a hard limit. Code is allowed to exceed this limit.
Be Consistent
Some of the guidelines outlined in this document can be evaluated objectively; others are situational, contextual, or subjective.
Above all else, be consistent.
Consistent code is easier to maintain, is easier to rationalize, requires less cognitive overhead, and is easier to migrate or update as new conventions emerge or classes of bugs are fixed.
Conversely, having multiple disparate or conflicting styles within a single codebase causes maintenance overhead, uncertainty, and cognitive dissonance, all of which can directly contribute to lower velocity, painful code reviews, and bugs.
When applying these guidelines to a codebase, it is recommended that changes are made at a package (or larger) level: application at a sub-package level violates the above concern by introducing multiple styles into the same code.
Group Similar Declarations
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 |
---|---|
|
|
Exception: Variable declarations, particularly inside functions, should be grouped together if declared adjacent to other variables. Do this for variables declared together even if they are unrelated.
Bad | Good |
---|---|
|
|
Import Group Ordering
There should be two import groups:
- Standard library
- Everything else
This is the grouping applied by goimports by default.
Bad | Good |
---|---|
|
|
Package Names
When naming packages, choose a name that is:
- All lower-case. No capitals or underscores.
- Does not need to be renamed using named imports at most call sites.
- Short and succinct. Remember that the name is identified in full at every call site.
- Not plural. For example,
net/url
, notnet/urls
. - Not "common", "util", "shared", or "lib". These are bad, uninformative names.
See also Package Names and Style guideline for Go packages.
Function Names
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
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 |
---|---|
|
|
Function Grouping and Ordering
- Functions should be sorted in rough call order.
- Functions in a file should be grouped by receiver.
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 |
---|---|
|
|
Reduce Nesting
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 |
---|---|
|
|
Unnecessary Else
If a variable is set in both branches of an if, it can be replaced with a single if.
Bad | Good |
---|---|
|
|
Top-level Variable Declarations
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 Globals with _
Prefix unexported top-level var
s and const
s with _
to make it clear when
they are used that they are global symbols.
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 |
---|---|
|
|
Exception: Unexported error values may use the prefix err
without the underscore.
See Error Naming.
Embedding in Structs
Embedded types 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 |
---|---|
|
|
Embedding should provide tangible benefit, like adding or augmenting functionality in a semantically-appropriate way. It should do this with zero adverse user-facing effects (see also: Avoid Embedding Types in Public Structs).
Exception: Mutexes should not be embedded, even on unexported types. See also: Zero-value Mutexes are Valid.
Embedding should not:
- Be purely cosmetic or convenience-oriented.
- Make outer types more difficult to construct or use.
- Affect outer types' zero values. If the outer type has a useful zero value, it should still have a useful zero value after embedding the inner type.
- Expose unrelated functions or fields from the outer type as a side-effect of embedding the inner type.
- Expose unexported types.
- Affect outer types' copy semantics.
- Change the outer type's API or type semantics.
- Embed a non-canonical form of the inner type.
- Expose implementation details of the outer type.
- Allow users to observe or control type internals.
- Change the general behavior of inner functions through wrapping in a way that would reasonably surprise users.
Simply put, embed consciously and intentionally. A good litmus test is, "would all of these exported inner methods/fields be added directly to the outer type"; if the answer is "some" or "no", don't embed the inner type - use a field instead.
Bad | Good |
---|---|
|
|
|
|
|
|
Local Variable Declarations
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 used. Declaring Empty Slices, for example.
Bad | Good |
---|---|
|
|