Skip to main content

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:

  1. A pointer to some type-specific information. You can think of this as "type."
  2. 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
BadGood
type Handler struct {
// ...
}



func (h *Handler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
...
}
type Handler struct {
// ...
}

var _ http.Handler = (*Handler)(nil)

func (h *Handler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}

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
}

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.

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.

BadGood
mu := new(sync.Mutex)
mu.Lock()
var mu sync.Mutex
mu.Lock()

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.

BadGood
type SMap struct {
sync.Mutex

data map[string]string
}

func NewSMap() *SMap {
return &SMap{
data: make(map[string]string),
}
}

func (m *SMap) Get(k string) string {
m.Lock()
defer m.Unlock()

return m.data[k]
}
type SMap struct {
mu sync.Mutex

data map[string]string
}

func NewSMap() *SMap {
return &SMap{
data: make(map[string]string),
}
}

func (m *SMap) Get(k string) string {
m.mu.Lock()
defer m.mu.Unlock()

return m.data[k]
}

The Mutex field, and the Lock and Unlock methods are unintentionally part of the exported API of SMap.

The mutex and its methods are implementation details of SMap hidden from its callers.

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
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}

trips := ...
d1.SetTrips(trips)

// Did you mean to modify d1.trips?
trips[0] = ...
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
}

trips := ...
d1.SetTrips(trips)

// We can now modify trips[0] without affecting d1.trips.
trips[0] = ...

Returning Slices and Maps

Similarly, be wary of user modifications to maps or slices exposing internal state.

BadGood
type Stats struct {
mu sync.Mutex
counters map[string]int
}

// Snapshot returns the current stats.
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()

return s.counters
}

// snapshot is no longer protected by the mutex, so any
// access to the snapshot is subject to data races.
snapshot := stats.Snapshot()
type Stats struct {
mu sync.Mutex
counters map[string]int
}

func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()

result := make(map[string]int, len(s.counters))
for k, v := range s.counters {
result[k] = v
}
return result
}

// Snapshot is now a copy.
snapshot := stats.Snapshot()

Defer to Clean Up

Use defer to clean up resources such as files and locks.

BadGood
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// easy to miss unlocks due to multiple returns
p.Lock()
defer p.Unlock()

if p.count < 10 {
return p.count
}

p.count++
return p.count

// more readable

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.

BadGood
// Ought to be enough for anybody!
c := make(chan int, 64)
// Size of one
c := make(chan int, 1) // or
// Unbuffered channel, size of zero
c := make(chan int)

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.

BadGood
type Operation int

const (
Add Operation = iota
Subtract
Multiply
)

// Add=0, Subtract=1, Multiply=2
type Operation int

const (
Add Operation = iota + 1
Subtract
Multiply
)

// Add=1, Subtract=2, Multiply=3

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.

  1. A day has 24 hours
  2. An hour has 60 minutes
  3. A week has 7 days
  4. A year has 365 days
  5. 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.

BadGood
func isActive(now, start, stop int) bool {
return start <= now && now < stop
}
func isActive(now, start, stop time.Time) bool {
return (start.Before(now) || start.Equal(now)) && now.Before(stop)
}

Use time.Duration for periods of time

Use time.Duration when dealing with periods of time.

BadGood
func poll(delay int) {
for {
// ...
time.Sleep(time.Duration(delay) * time.Millisecond)
}
}

poll(10) // was it seconds or milliseconds?
func poll(delay time.Duration) {
for {
// ...
time.Sleep(delay)
}
}

poll(10*time.Second)

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:

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.

BadGood
// {"interval": 2}
type Config struct {
Interval int `json:"interval"`
}
// {"intervalMillis": 2000}
type Config struct {
IntervalMillis int `json:"intervalMillis"`
}

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

Use github.com/go-faster/errors package instead of errors and fmt.Errorf.

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 or errors.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 use errors.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 MessageGuidance
Nostaticerrors.New
Nodynamicerrors.Errorf
Yesstatictop-level var with errors.New
Yesdynamiccustom 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 matchingError matching
// package foo

func Open() error {
return errors.New("could not open")
}

// package bar

if err := foo.Open(); err != nil {
// Can't handle the error.
panic("unknown error")
}
// package foo

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
return ErrCouldNotOpen
}

// package bar

if err := foo.Open(); err != nil {
if errors.Is(err, foo.ErrCouldNotOpen) {
// handle the error
} else {
panic("unknown error")
}
}

For an error with a dynamic string, use [errors.Wrap] if the caller does not need to match it, and a custom error if the caller does need to match it.

No error matchingError matching
// package foo

func Open(file string) error {
return errors.Errorf("file %q not found", file)
}

// package bar

if err := foo.Open("testfile.txt"); err != nil {
// Can't handle the error.
panic("unknown error")
}
// package foo

type NotFoundError struct {
File string
}

func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}

func Open(file string) error {
return &NotFoundError{File: file}
}


// package bar

if err := foo.Open("testfile.txt"); err != nil {
var notFound *NotFoundError
if errors.As(err, &notFound) {
// handle the error
} else {
panic("unknown error")
}
}

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 two main options for propagating errors if a call fails:

  • return the original error as-is
  • add context with errors.Wrap or errors.Wrapf

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 errors.Wrap to add context to your errors.

Be aware that callers may begin to rely on this behavior. So for cases where the wrapped error is a known var or type, document and test it as part of your function's contract.

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:

BadGood
s, err := store.New()
if err != nil {
return errors.Wrap(err, "failed to create new store")
}
s, err := store.New()
if err != nil {
return errors.Wrap(err, "new store")
}
failed to x: failed to y: failed to create new store: the error
x: y: new store: the error

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 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.

BadGood
t := i.(string)
t, ok := i.(string)
if !ok {
// handle the error gracefully
}

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.

BadGood
func run(args []string) {
if len(args) == 0 {
panic("an argument is required")
}
// ...
}

func main() {
run(os.Args[1:])
}
func run(args []string) error {
if len(args) == 0 {
return errors.New("an argument is required")
}
// ...
return nil
}

func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

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.

BadGood
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
panic("failed to set up test")
}
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
t.Fatal("failed to set up test")
}

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.

BadGood
type foo struct {
running int32 // atomic
}

func (f* foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
// already running…
return
}
// start the Foo
}

func (f *foo) isRunning() bool {
return f.running == 1 // race!
}
type foo struct {
running atomic.Bool
}

func (f *foo) start() {
if f.running.Swap(true) {
// already running…
return
}
// start the Foo
}

func (f *foo) isRunning() bool {
return f.running.Load()
}

Avoid Mutable Globals

Avoid mutating global variables, instead opting for dependency injection. This applies to function pointers as well as other kinds of values.

BadGood
// sign.go

var _timeNow = time.Now

func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
}
// sign.go

type signer struct {
now func() time.Time
}

func newSigner() *signer {
return &signer{
now: time.Now,
}
}

func (s *signer) Sign(msg string) string {
now := s.now()
return signWithTime(msg, now)
}
// sign_test.go

func TestSign(t *testing.T) {
oldTimeNow := _timeNow
_timeNow = func() time.Time {
return someFixedTime
}
defer func() { _timeNow = oldTimeNow }()

assert.Equal(t, want, sign(give))
}
// sign_test.go

func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time {
return someFixedTime
}

assert.Equal(t, want, s.Sign(give))
}

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) {
// ...
}
BadGood
// ConcreteList is a list of entities.
type ConcreteList struct {
*AbstractList
}
// ConcreteList is a list of entities.
type ConcreteList struct {
list *AbstractList
}

// Add adds an entity to the list.
func (l *ConcreteList) Add(e Entity) {
l.list.Add(e)
}

// Remove removes an entity from the list.
func (l *ConcreteList) Remove(e Entity) {
l.list.Remove(e)
}

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.

BadGood
// AbstractList is a generalized implementation
// for various kinds of lists of entities.
type AbstractList interface {
Add(Entity)
Remove(Entity)
}

// ConcreteList is a list of entities.
type ConcreteList struct {
AbstractList
}
// ConcreteList is a list of entities.
type ConcreteList struct {
list AbstractList
}

// Add adds an entity to the list.
func (l *ConcreteList) Add(e Entity) {
l.list.Add(e)
}

// Remove removes an entity from the list.
func (l *ConcreteList) Remove(e Entity) {
l.list.Remove(e)
}

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.

BadGood
var error string
// `error` shadows the builtin

// or

func handleErrorMessage(error string) {
// `error` shadows the builtin
}
var errorMessage string
// `error` refers to the builtin

// or

func handleErrorMessage(msg string) {
// `error` refers to the builtin
}
type Foo struct {
// While these fields technically don't
// constitute shadowing, grepping for
// `error` or `string` strings is now
// ambiguous.
error error
string string
}

func (f Foo) Error() error {
// `error` and `f.error` are
// visually similar
return f.error
}

func (f Foo) String() string {
// `string` and `f.string` are
// visually similar
return f.string
}
type Foo struct {
// `error` and `string` strings are
// now unambiguous.
err error
str string
}

func (f Foo) Error() error {
return f.err
}

func (f Foo) String() string {
return f.str
}

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:

  1. Be completely deterministic, regardless of program environment or invocation.
  2. Avoid depending on the ordering or side-effects of other init() functions. While init() ordering is well-known, code can change, and thus relationships between init() functions can make code brittle and error-prone.
  3. Avoid accessing or manipulating global or environment state, such as machine information, environment variables, working directory, program arguments/inputs, etc.
  4. 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".

BadGood
type Foo struct {
// ...
}

var _defaultFoo Foo

func init() {
_defaultFoo = Foo{
// ...
}
}
var _defaultFoo = Foo{
// ...
}

// or, better, for testability:

var _defaultFoo = defaultFoo()

func defaultFoo() Foo {
return Foo{
// ...
}
}
type Config struct {
// ...
}

var _config Config

func init() {
// Bad: based on current directory
cwd, _ := os.Getwd()

// Bad: I/O
raw, _ := ioutil.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)

yaml.Unmarshal(raw, &_config)
}
type Config struct {
// ...
}

func loadConfig() Config {
cwd, err := os.Getwd()
// handle err

raw, err := ioutil.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// handle err

var config Config
yaml.Unmarshal(raw, &config)

return config
}

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.

BadGood
func main() {
body := readFile(path)
fmt.Println(body)
}

func readFile(path string) string {
f, err := os.Open(path)
if err != nil {
log.Fatal(err)
}

b, err := ioutil.ReadAll(f)
if err != nil {
log.Fatal(err)
}

return string(b)
}
func main() {
body, err := readFile(path)
if err != nil {
log.Fatal(err)
}
fmt.Println(body)
}

func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}

b, err := ioutil.ReadAll(f)
if err != nil {
return "", err
}

return string(b), nil
}

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.

BadGood
package main

func main() {
args := os.Args[1:]
if len(args) != 1 {
log.Fatal("missing file")
}
name := args[0]

f, err := os.Open(name)
if err != nil {
log.Fatal(err)
}
defer f.Close()

// If we call log.Fatal after this line,
// f.Close will not be called.

b, err := ioutil.ReadAll(f)
if err != nil {
log.Fatal(err)
}

// ...
}
package main

func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}

func run() error {
args := os.Args[1:]
if len(args) != 1 {
return errors.New("missing file")
}
name := args[0]

f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()

b, err := ioutil.ReadAll(f)
if err != nil {
return err
}

// ...
}