跳到正文
awesomego.org

Language

source ↗

Language

Literal formatting

Go has an exceptionally powerful composite literal syntax, with which it is possible to express deeply-nested, complicated values in a single expression. Where possible, this literal syntax should be used instead of building values field-by-field. The gofmt formatting for literals is generally quite good, but there are some additional rules for keeping these literals readable and maintainable.

Field names

Struct literals must specify field names for types defined outside the current package.

  • Include field names for types from other packages.

    // Good:
    // https://pkg.go.dev/encoding/csv#Reader
    r := csv.Reader{
      Comma: ',',
      Comment: '#',
      FieldsPerRecord: 4,
    }

    The position of fields in a struct and the full set of fields (both of which are necessary to get right when field names are omitted) are not usually considered to be part of a struct’s public API; specifying the field name is needed to avoid unnecessary coupling.

    // Bad:
    r := csv.Reader{',', '#', 4, false, false, false, false}
  • For package-local types, field names are optional.

    // Good:
    okay := Type{42}
    also := internalType{4, 2}

    Field names should still be used if it makes the code clearer, and it is very common to do so. For example, a struct with a large number of fields should almost always be initialized with field names.

    // Good:
    okay := StructWithLotsOfFields{
      field1: 1,
      field2: "two",
      field3: 3.14,
      field4: true,
    }

Matching braces

The closing half of a brace pair should always appear on a line with the same amount of indentation as the opening brace. One-line literals necessarily have this property. When the literal spans multiple lines, maintaining this property keeps the brace matching for literals the same as brace matching for common Go syntactic constructs like functions and if statements.

The most common mistake in this area is putting the closing brace on the same line as a value in a multi-line struct literal. In these cases, the line should end with a comma and the closing brace should appear on the next line.

// Good:
good := []*Type{{Key: "value"}}
// Good:
good := []*Type{
    {Key: "multi"},
    {Key: "line"},
}
// Bad:
bad := []*Type{
    {Key: "multi"},
    {Key: "line"}}
// Bad:
bad := []*Type{
    {
        Key: "value"},
}

Cuddled braces

Dropping whitespace between braces (aka “cuddling” them) for slice and array literals is only permitted when both of the following are true.

  • The indentation matches
  • The inner values are also literals or proto builders (i.e. not a variable or other expression)
// Good:
good := []*Type{
    { // Not cuddled
        Field: "value",
    },
    {
        Field: "value",
    },
}
// Good:
good := []*Type{{ // Cuddled correctly
    Field: "value",
}, {
    Field: "value",
}}
// Good:
good := []*Type{
    first, // Can't be cuddled
    {Field: "second"},
}
// Good:
okay := []*pb.Type{pb.Type_builder{
    Field: "first", // Proto Builders may be cuddled to save vertical space
}.Build(), pb.Type_builder{
    Field: "second",
}.Build()}
// Bad:
bad := []*Type{
    first,
    {
        Field: "second",
    }}

Repeated type names

Repeated type names may be omitted from slice and map literals. This can be helpful in reducing clutter. A reasonable occasion for repeating the type names explicitly is when dealing with a complex type that is not common in your project, when the repetitive type names are on lines that are far apart and can remind the reader of the context.

// Good:
good := []*Type{
    {A: 42},
    {A: 43},
}
// Bad:
repetitive := []*Type{
    &Type{A: 42},
    &Type{A: 43},
}
// Good:
good := map[Type1]*Type2{
    {A: 1}: {B: 2},
    {A: 3}: {B: 4},
}
// Bad:
repetitive := map[Type1]*Type2{
    Type1{A: 1}: &Type2{B: 2},
    Type1{A: 3}: &Type2{B: 4},
}

Tip: If you want to remove repetitive type names in struct literals, you can run gofmt -s.

Zero-value fields

Zero-value fields may be omitted from struct literals when clarity is not lost as a result.

Well-designed APIs often employ zero-value construction for enhanced readability. For example, omitting the three zero-value fields from the following struct draws attention to the only option that is being specified.

// Bad:
import (
  "github.com/golang/leveldb"
  "github.com/golang/leveldb/db"
)

ldb := leveldb.Open("/my/table", &db.Options{
    BlockSize: 1<<16,
    ErrorIfDBExists: true,

    // These fields all have their zero values.
    BlockRestartInterval: 0,
    Comparer: nil,
    Compression: nil,
    FileSystem: nil,
    FilterPolicy: nil,
    MaxOpenFiles: 0,
    WriteBufferSize: 0,
    VerifyChecksums: false,
})
// Good:
import (
  "github.com/golang/leveldb"
  "github.com/golang/leveldb/db"
)

ldb := leveldb.Open("/my/table", &db.Options{
    BlockSize: 1<<16,
    ErrorIfDBExists: true,
})

Structs within table-driven tests often benefit from explicit field names, especially when the test struct is not trivial. This allows the author to omit the zero-valued fields entirely when the fields in question are not related to the test case. For example, successful test cases should omit any error-related or failure-related fields. In cases where the zero value is necessary to understand the test case, such as testing for zero or nil inputs, the field names should be specified.

Concise

tests := []struct {
    input      string
    wantPieces []string
    wantErr    error
}{
    {
        input:      "1.2.3.4",
        wantPieces: []string{"1", "2", "3", "4"},
    },
    {
        input:   "hostname",
        wantErr: ErrBadHostname,
    },
}

Explicit

tests := []struct {
    input    string
    wantIPv4 bool
    wantIPv6 bool
    wantErr  bool
}{
    {
        input:    "1.2.3.4",
        wantIPv4: true,
        wantIPv6: false,
    },
    {
        input:    "1:2::3:4",
        wantIPv4: false,
        wantIPv6: true,
    },
    {
        input:    "hostname",
        wantIPv4: false,
        wantIPv6: false,
        wantErr:  true,
    },
}

Nil slices

For most purposes, there is no functional difference between nil and the empty slice. Built-in functions like len and cap behave as expected on nil slices.

// Good:
import "fmt"

var s []int         // nil

fmt.Println(s)      // []
fmt.Println(len(s)) // 0
fmt.Println(cap(s)) // 0
for range s {...}   // no-op

s = append(s, 42)
fmt.Println(s)      // [42]

If you declare an empty slice as a local variable (especially if it can be the source of a return value), prefer the nil initialization to reduce the risk of bugs by callers.

// Good:
var t []string
// Bad:
t := []string{}

Do not create APIs that force their clients to make distinctions between nil and the empty slice.

// Good:
// Ping pings its targets.
// Returns hosts that successfully responded.
func Ping(hosts []string) ([]string, error) { ... }
// Bad:
// Ping pings its targets and returns a list of hosts
// that successfully responded. Can be empty if the input was empty.
// nil signifies that a system error occurred.
func Ping(hosts []string) []string { ... }

When designing interfaces, avoid making a distinction between a nil slice and a non-nil, zero-length slice, as this can lead to subtle programming errors. This is typically accomplished by using len to check for emptiness, rather than == nil.

This implementation accepts both nil and zero-length slices as “empty”:

// Good:
// describeInts describes s with the given prefix, unless s is empty.
func describeInts(prefix string, s []int) {
    if len(s) == 0 {
        return
    }
    fmt.Println(prefix, s)
}

Instead of relying on the distinction as a part of the API:

// Bad:
func maybeInts() []int { /* ... */ }

// describeInts describes s with the given prefix; pass nil to skip completely.
func describeInts(prefix string, s []int) {
  // The behavior of this function unintentionally changes depending on what
  // maybeInts() returns in 'empty' cases (nil or []int{}).
  if s == nil {
    return
  }
  fmt.Println(prefix, s)
}

describeInts("Here are some ints:", maybeInts())

See in-band errors for further discussion.

Indentation confusion

Avoid introducing a line break if it would align the rest of the line with an indented code block. If this is unavoidable, leave a space to separate the code in the block from the wrapped line.

// Bad:
if longCondition1 && longCondition2 &&
    // Conditions 3 and 4 have the same indentation as the code within the if.
    longCondition3 && longCondition4 {
    log.Info("all conditions met")
}

See the following sections for specific guidelines and examples:

Function formatting

The signature of a function or method declaration should remain on a single line to avoid indentation confusion.

Function argument lists can make some of the longest lines in a Go source file. However, they precede a change in indentation, and therefore it is difficult to break the line in a way that does not make subsequent lines look like part of the function body in a confusing way:

// Bad:
func (r *SomeType) SomeLongFunctionName(foo1, foo2, foo3 string,
    foo4, foo5, foo6 int) {
    foo7 := bar(foo1)
    // ...
}

See best practices for a few options for shortening the call sites of functions that would otherwise have many arguments.

Lines can often be shortened by factoring out local variables.

// Good:
local := helper(some, parameters, here)
good := foo.Call(list, of, parameters, local)

Similarly, function and method calls should not be separated based solely on line length.

// Good:
good := foo.Call(long, list, of, parameters, all, on, one, line)
// Bad:
bad := foo.Call(long, list, of, parameters,
    with, arbitrary, line, breaks)

Avoid adding inline comments to specific function arguments where possible. Instead, use an option struct or add more detail to the function documentation.

// Good:
good := server.New(ctx, server.Options{Port: 42})
// Bad:
bad := server.New(
    ctx,
    42, // Port
)

If the API cannot be changed or if the local call is unusual (whether or not the call is too long), it is always permissible to add line breaks if it aids in understanding the call.

// Good:
canvas.RenderHeptagon(fillColor,
    x0, y0, vertexColor0,
    x1, y1, vertexColor1,
    x2, y2, vertexColor2,
    x3, y3, vertexColor3,
    x4, y4, vertexColor4,
    x5, y5, vertexColor5,
    x6, y6, vertexColor6,
)

Note that the lines in the above example are not wrapped at a specific column boundary but are grouped based on vertex coordinates and color.

Long string literals within functions should not be broken for the sake of line length. For functions that include such strings, a line break can be added after the string format, and the arguments can be provided on the next or subsequent lines. The decision about where the line breaks should go is best made based on semantic groupings of inputs, rather than based purely on line length.

// Good:
log.Warningf("Database key (%q, %d, %q) incompatible in transaction started by (%q, %d, %q)",
    currentCustomer, currentOffset, currentKey,
    txCustomer, txOffset, txKey)
// Bad:
log.Warningf("Database key (%q, %d, %q) incompatible in"+
    " transaction started by (%q, %d, %q)",
    currentCustomer, currentOffset, currentKey, txCustomer,
    txOffset, txKey)

Conditionals and loops

An if statement should not be line broken; multi-line if clauses can lead to indentation confusion.

// Bad:
// The second if statement is aligned with the code within the if block, causing
// indentation confusion.
if db.CurrentStatusIs(db.InTransaction) &&
    db.ValuesEqual(db.TransactionKey(), row.Key()) {
    return db.Errorf(db.TransactionError, "query failed: row (%v): key does not match transaction key", row)
}

If the short-circuit behavior is not required, the boolean operands can be extracted directly:

// Good:
inTransaction := db.CurrentStatusIs(db.InTransaction)
keysMatch := db.ValuesEqual(db.TransactionKey(), row.Key())
if inTransaction && keysMatch {
    return db.Error(db.TransactionError, "query failed: row (%v): key does not match transaction key", row)
}

There may also be other locals that can be extracted, especially if the conditional is already repetitive:

// Good:
uid := user.GetUniqueUserID()
if db.UserIsAdmin(uid) || db.UserHasPermission(uid, perms.ViewServerConfig) || db.UserHasPermission(uid, perms.CreateGroup) {
    // ...
}
// Bad:
if db.UserIsAdmin(user.GetUniqueUserID()) || db.UserHasPermission(user.GetUniqueUserID(), perms.ViewServerConfig) || db.UserHasPermission(user.GetUniqueUserID(), perms.CreateGroup) {
    // ...
}

if statements that contain closures or multi-line struct literals should ensure that the braces match to avoid indentation confusion.

// Good:
if err := db.RunInTransaction(func(tx *db.TX) error {
    return tx.Execute(userUpdate, x, y, z)
}); err != nil {
    return fmt.Errorf("user update failed: %s", err)
}
// Good:
if _, err := client.Update(ctx, &upb.UserUpdateRequest{
    ID:   userID,
    User: user,
}); err != nil {
    return fmt.Errorf("user update failed: %s", err)
}

Similarly, don’t try inserting artificial linebreaks into for statements. You can always let the line simply be long if there is no elegant way to refactor it:

// Good:
for i, max := 0, collection.Size(); i < max && !collection.HasPendingWriters(); i++ {
    // ...
}

Often, though, there is:

// Good:
for i, max := 0, collection.Size(); i < max; i++ {
    if collection.HasPendingWriters() {
        break
    }
    // ...
}

switch and case statements should also remain on a single line.

// Good:
switch good := db.TransactionStatus(); good {
case db.TransactionStarting, db.TransactionActive, db.TransactionWaiting:
    // ...
case db.TransactionCommitted, db.NoTransaction:
    // ...
default:
    // ...
}
// Bad:
switch bad := db.TransactionStatus(); bad {
case db.TransactionStarting,
    db.TransactionActive,
    db.TransactionWaiting:
    // ...
case db.TransactionCommitted,
    db.NoTransaction:
    // ...
default:
    // ...
}

If the line is excessively long, indent all cases and separate them with a blank line to avoid indentation confusion:

// Good:
switch db.TransactionStatus() {
case
    db.TransactionStarting,
    db.TransactionActive,
    db.TransactionWaiting,
    db.TransactionCommitted:

    // ...
case db.NoTransaction:
    // ...
default:
    // ...
}

In conditionals comparing a variable to a constant, place the variable value on the left hand side of the equality operator:

// Good:
if result == "foo" {
  // ...
}

Instead of the less clear phrasing where the constant comes first (“Yoda style conditionals”):

// Bad:
if "foo" == result {
  // ...
}

Copying

To avoid unexpected aliasing and similar bugs, be careful when copying a struct from another package. For example, synchronization objects such as sync.Mutex must not be copied.

The bytes.Buffer type contains a []byte slice and, as an optimization for small strings, a small byte array to which the slice may refer. If you copy a Buffer, the slice in the copy may alias the array in the original, causing subsequent method calls to have surprising effects.

In general, do not copy a value of type T if its methods are associated with the pointer type, *T.

// Bad:
b1 := bytes.Buffer{}
b2 := b1

Invoking a method that takes a value receiver can hide the copy. When you author an API, you should generally take and return pointer types if your structs contain fields that should not be copied.

These are acceptable:

// Good:
type Record struct {
  buf bytes.Buffer
  // other fields omitted
}

func New() *Record {...}

func (r *Record) Process(...) {...}

func Consumer(r *Record) {...}

But these are usually wrong:

// Bad:
type Record struct {
  buf bytes.Buffer
  // other fields omitted
}


func (r Record) Process(...) {...} // Makes a copy of r.buf

func Consumer(r Record) {...} // Makes a copy of r.buf

This guidance also applies to copying sync.Mutex.

Don’t panic

Do not use panic for normal error handling. Instead, use error and multiple return values. See the Effective Go section on errors.

Within package main and initialization code, consider [log.Exit] for errors that should terminate the program (e.g., invalid configuration), as in many of these cases a stack trace will not help the reader. Please note that [log.Exit] calls os.Exit and any deferred functions will not be run.

For errors that indicate “impossible” conditions, namely bugs that should always be caught during code review and/or testing, a function may reasonably return an error or call [log.Fatal].

Also see when panic is acceptable.

Note: log.Fatalf is not the standard library log. See [#logging].

Must functions

Setup helper functions that stop the program on failure follow the naming convention MustXYZ (or mustXYZ). In general, they should only be called early on program startup, not on things like user input where normal Go error handling is preferred.

This often comes up for functions called to initialize package-level variables exclusively at package initialization time (e.g. template.Must and regexp.MustCompile).

// Good:
func MustParse(version string) *Version {
    v, err := Parse(version)
    if err != nil {
        panic(fmt.Sprintf("MustParse(%q) = _, %v", version, err))
    }
    return v
}

// Package level "constant". If we wanted to use `Parse`, we would have had to
// set the value in `init`.
var DefaultVersion = MustParse("1.2.3")

The same convention may be used in test helpers that only stop the current test (using t.Fatal). Such helpers are often convenient in creating test values, for example in struct fields of table driven tests, as functions that return errors cannot be directly assigned to a struct field.

// Good:
func mustMarshalAny(t *testing.T, m proto.Message) *anypb.Any {
  t.Helper()
  any, err := anypb.New(m)
  if err != nil {
    t.Fatalf("mustMarshalAny(t, m) = %v; want %v", err, nil)
  }
  return any
}

func TestCreateObject(t *testing.T) {
  tests := []struct{
    desc string
    data *anypb.Any
  }{
    {
      desc: "my test case",
      // Creating values directly within table driven test cases.
      data: mustMarshalAny(t, mypb.Object{}),
    },
    // ...
  }
  // ...
}

In both of these cases, the value of this pattern is that the helpers can be called in a “value” context. These helpers should not be called in places where it’s difficult to ensure an error would be caught or in a context where an error should be checked (e.g., in many request handlers). For constant inputs, this allows tests to easily ensure that the Must arguments are well-formed, and for non-constant inputs it permits tests to validate that errors are properly handled or propagated.

Where Must functions are used in a test, they should generally be marked as a test helper and call t.Fatal on error (see error handling in test helpers for more considerations of using that).

They should not be used when ordinary error handling is possible (including with some refactoring):

// Bad:
func Version(o *servicepb.Object) (*version.Version, error) {
    // Return error instead of using Must functions.
    v := version.MustParse(o.GetVersionString())
    return dealiasVersion(v)
}

Goroutine lifetimes

When you spawn goroutines, make it clear when or whether they exit.

Goroutines can leak by blocking on channel sends or receives. The garbage collector will not terminate a goroutine blocked on a channel even if no other goroutine has a reference to the channel.

Even when goroutines do not leak, leaving them in-flight when they are no longer needed can cause other subtle and hard-to-diagnose problems. Sending on a channel that has been closed causes a panic.

// Bad:
ch := make(chan int)
ch <- 42
close(ch)
ch <- 13 // panic

Modifying still-in-use inputs “after the result isn’t needed” can lead to data races. Leaving goroutines in-flight for arbitrarily long can lead to unpredictable memory usage.

Concurrent code should be written such that the goroutine lifetimes are obvious. Typically this will mean keeping synchronization-related code constrained within the scope of a function and factoring out the logic into synchronous functions. If the concurrency is still not obvious, it is important to document when and why the goroutines exit.

Code that follows best practices around context usage often helps make this clear. It is conventionally managed with a [context.Context]:

// Good:
func (w *Worker) Run(ctx context.Context) error {
    var wg sync.WaitGroup
    // ...
    for item := range w.q {
        // process returns at latest when the context is cancelled.
        wg.Add(1)
        go func() {
            defer wg.Done()
            process(ctx, item)
        }()
    }
    // ...
    wg.Wait()  // Prevent spawned goroutines from outliving this function.
}

There are other variants of the above that use raw signal channels like chan struct{}, synchronized variables, condition variables, and more. The important part is that the goroutine’s end is evident for subsequent maintainers.

In contrast, the following code is careless about when its spawned goroutines finish:

// Bad:
func (w *Worker) Run() {
    // ...
    for item := range w.q {
        // process returns when it finishes, if ever, possibly not cleanly
        // handling a state transition or termination of the Go program itself.
        go process(item)
    }
    // ...
}

This code may look OK, but there are several underlying problems:

  • The code probably has undefined behavior in production, and the program may not terminate cleanly, even if the operating system releases the resources.

  • The code is difficult to test meaningfully due to the code’s indeterminate lifecycle.

  • The code may leak resources as described above.

See also:

Interfaces

Avoid creating interfaces until a real need exists. Focus on the required behavior rather than just abstract named patterns like “service” or “repository” and the like.

  • Do not wrap RPC clients in new manual interfaces just for the sake of abstraction or testing. Use real transports instead (testing RPC).

  • Do not define back doors or export test double implementations of an interface solely for testing. Prefer testing via the public API of the real implementation instead.

Design interfaces to be small for easier implementation and composition (GoTip #78: Minimal Viable Interfaces). Document interfaces appropriately including their contract, edge cases, and expected errors. Keep interface types unexported if they are only used internally within a package.

The consumer of the interface should define it (not the package implementing the interface), ensuring it includes only the methods they actually use. The producer package may export the interface if the interface is the product (a common protocol) to prevent interface redefinition bloat.

There is an adage: Functions should take interfaces as arguments but return concrete types (GoTip #49: Accept Interfaces, Return Concrete Types). Returning concrete types allows the caller to have access to every public method and field of that specific implementation, not just the subset of methods defined in a pre-chosen interface. The caller can still pass that concrete result into any other function that expects an interface. Sometimes returning an interface is acceptable for encapsulation (e.g., error interface), and certain constructs like command, chaining, factory, and strategy patterns.

Deeper discussion on interfaces exists in the Best Practices’ section on interfaces.

Generics

Generics (formally called “Type Parameters”) are allowed where they fulfill your business requirements. In many applications, a conventional approach using existing language features (slices, maps, interfaces, and so on) works just as well without the added complexity, so be wary of premature use. See the discussion on least mechanism.

When introducing an exported API that uses generics, make sure it is suitably documented. It’s highly encouraged to include motivating runnable [examples].

Do not use generics just because you are implementing an algorithm or data structure that does not care about the type of its member elements. If there is only one type being instantiated in practice, start by making your code work on that type without using generics at all. Adding polymorphism later will be straightforward compared to removing abstraction that is found to be unnecessary.

Do not use generics to invent domain-specific languages (DSLs). In particular, refrain from introducing error-handling frameworks that might put a significant burden on readers. Instead prefer established error handling practices. For testing, be especially wary of introducing assertion libraries or frameworks that result in less useful test failures.

In general:

  • Write code, don’t design types. From a GopherCon talk by Robert Griesemer and Ian Lance Taylor.
  • If you have several types that share a useful unifying interface, consider modeling the solution using that interface. Generics may not be needed.
  • Otherwise, instead of relying on the any type and excessive type switching, consider generics.

See also:

Pass values

Do not pass pointers as function arguments just to save a few bytes. If a function reads its argument x only as *x throughout, then the argument shouldn’t be a pointer. Common instances of this include passing a pointer to a string (*string) or a pointer to an interface value (*io.Reader). In both cases, the value itself is a fixed size and can be passed directly.

This advice does not apply to large structs, or even small structs that may increase in size. In particular, protocol buffer messages should generally be handled by pointer rather than by value. The pointer type satisfies the proto.Message interface (accepted by proto.Marshal, protocmp.Transform, etc.), and protocol buffer messages can be quite large and often grow larger over time.

Receiver type

A method receiver can be passed either as a value or a pointer, just as if it were a regular function parameter. The choice between the two is based on which method set(s) the method should be a part of.

Correctness wins over speed or simplicity. There are cases where you must use a pointer value. In other cases, pick pointers for large types or as future-proofing if you don’t have a good sense of how the code will grow, and use values for simple plain old data.

The list below spells out each case in further detail:

  • If the receiver is a slice and the method doesn’t reslice or reallocate the slice, use a value rather than a pointer.

    // Good:
    type Buffer []byte
    
    func (b Buffer) Len() int { return len(b) }
  • If the method needs to mutate the receiver, the receiver must be a pointer.

    // Good:
    type Counter int
    
    func (c *Counter) Inc() { *c++ }
    
    // See https://pkg.go.dev/container/heap.
    type Queue []Item
    
    func (q *Queue) Push(x Item) { *q = append([]Item{x}, *q...) }
  • If the receiver is a struct containing fields that cannot safely be copied, use a pointer receiver. Common examples are sync.Mutex and other synchronization types.

    // Good:
    type Counter struct {
        mu    sync.Mutex
        total int
    }
    
    func (c *Counter) Inc() {
        c.mu.Lock()
        defer c.mu.Unlock()
        c.total++
    }

    Tip: Check the type’s [Godoc] for information about whether it is safe or unsafe to copy.

  • If the receiver is a “large” struct or array, a pointer receiver may be more efficient. Passing a struct is equivalent to passing all of its fields or elements as arguments to the method. If that seems too large to pass by value, a pointer is a good choice.

  • For methods that will call or run concurrently with other functions that modify the receiver, use a value if those modifications should not be visible to your method; otherwise use a pointer.

  • If the receiver is a struct or array, any of whose elements is a pointer to something that may be mutated, prefer a pointer receiver to make the intention of mutability clear to the reader.

    // Good:
    type Counter struct {
        m *Metric
    }
    
    func (c *Counter) Inc() {
        c.m.Add(1)
    }
  • If the receiver is a built-in type, such as an integer or a string, that does not need to be modified, use a value.

    // Good:
    type User string
    
    func (u User) String() { return string(u) }
  • If the receiver is a map, function, or channel, use a value rather than a pointer.

    // Good:
    // See https://pkg.go.dev/net/http#Header.
    type Header map[string][]string
    
    func (h Header) Add(key, value string) { /* omitted */ }
  • If the receiver is a “small” array or struct that is naturally a value type with no mutable fields and no pointers, a value receiver is usually the right choice.

    // Good:
    // See https://pkg.go.dev/time#Time.
    type Time struct { /* omitted */ }
    
    func (t Time) Add(d Duration) Time { /* omitted */ }
  • When in doubt, use a pointer receiver.

As a general guideline, prefer to make the methods for a type either all pointer methods or all value methods.

Note: There is a lot of misinformation about whether passing a value or a pointer to a function can affect performance. The compiler can choose to pass pointers to values on the stack as well as copying values on the stack, but these considerations should not outweigh the readability and correctness of the code in most circumstances. When the performance does matter, it is important to profile both approaches with a realistic benchmark before deciding that one approach outperforms the other.

switch and break

Do not use break statements without target labels at the ends of switch clauses; they are redundant. Unlike in C and Java, switch clauses in Go automatically break, and a fallthrough statement is needed to achieve the C-style behavior. Use a comment rather than break if you want to clarify the purpose of an empty clause.

// Good:
switch x {
case "A", "B":
    buf.WriteString(x)
case "C":
    // handled outside of the switch statement
default:
    return fmt.Errorf("unknown value: %q", x)
}
// Bad:
switch x {
case "A", "B":
    buf.WriteString(x)
    break // this break is redundant
case "C":
    break // this break is redundant
default:
    return fmt.Errorf("unknown value: %q", x)
}

Note: If a switch clause is within a for loop, using break within switch does not exit the enclosing for loop.

for {
  switch x {
  case "A":
     break // exits the switch, not the loop
  }
}

To escape the enclosing loop, use a label on the for statement:

loop:
  for {
    switch x {
    case "A":
       break loop // exits the loop
    }
  }

Synchronous functions

Synchronous functions return their results directly and finish any callbacks or channel operations before returning. Prefer synchronous functions over asynchronous functions.

Synchronous functions keep goroutines localized within a call. This helps to reason about their lifetimes, and avoid leaks and data races. Synchronous functions are also easier to test, since the caller can pass an input and check the output without the need for polling or synchronization.

If necessary, the caller can add concurrency by calling the function in a separate goroutine. However, it is quite difficult (sometimes impossible) to remove unnecessary concurrency at the caller side.

See also:

  • “Rethinking Classical Concurrency Patterns”, talk by Bryan Mills: slides, video

Type aliases

Use a type definition, type T1 T2, to define a new type. Use a type alias, type T1 = T2, to refer to an existing type without defining a new type. Type aliases are rare; their primary use is to aid migrating packages to new source code locations. Don’t use type aliasing when it is not needed.

Use %q

Go’s format functions (fmt.Printf etc.) have a %q verb which prints strings inside double-quotation marks.

// Good:
fmt.Printf("value %q looks like English text", someText)

Prefer using %q over doing the equivalent manually, using %s:

// Bad:
fmt.Printf("value \"%s\" looks like English text", someText)
// Avoid manually wrapping strings with single-quotes too:
fmt.Printf("value '%s' looks like English text", someText)

Using %q is recommended in output intended for humans where the input value could possibly be empty or contain control characters. It can be very hard to notice a silent empty string, but "" stands out clearly as such.

Use any

Go 1.18 introduces an any type as an alias to interface{}. Because it is an alias, any is equivalent to interface{} in many situations and in others it is easily interchangeable via an explicit conversion. Prefer to use any in new code.