Avoiding Overly Long Lines

Avoid using code lines that require readers to horizontally scroll or excessively rotate the document.

We recommend limiting line length to 99 characters. Authors should break the line before this limit, but it's not a hard rule. It's permissible for code to exceed this limit.

Consistency

Some of the standards outlined in this document are based on subjective judgments, scenarios, or contexts. However, the most crucial aspect is to maintain consistency.

Consistent code is easier to maintain, more rational, demands less learning cost, and is easier to migrate, update, and fix errors when new conventions emerge or errors occur.

Conversely, including multiple completely different or conflicting code styles in a codebase leads to increased maintenance costs, uncertainty, and cognitive biases. All these directly result in slower speed, painful code reviews, and an increased number of bugs.

When applying these standards to a codebase, it's recommended to make changes at the package (or larger) level. Applying multiple styles at the subpackage level violates the above concerns.

Group Similar Declarations

Go language supports grouping similar declarations.

Not Recommended:

import "a"
import "b"

Recommended:

import (
  "a"
  "b"
)

This also applies to constant, variable, and type declarations:

Not Recommended:

const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64

Recommended:

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

Only group related declarations together and avoid grouping unrelated declarations.

Not Recommended:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  EnvVar = "MY_ENV"
)

Recommended:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const EnvVar = "MY_ENV"

There are no restrictions on where to use grouping. For example, you can use them within a function:

Not Recommended:

func f() string {
  red := color.New(0xff0000)
  green := color.New(0x00ff00)
  blue := color.New(0x0000ff)

  ...
}

Recommended:

func f() string {
  var (
    red   = color.New(0xff0000)
    green = color.New(0x00ff00)
    blue  = color.New(0x0000ff)
  )

  ...
}

Exception: If variable declarations are adjacent to other variables, especially within function-local declarations, they should be grouped together. Perform this even for unrelated variables declared together.

Not Recommended:

func (c *client) request() {
  caller := c.name
  format := "json"
  timeout := 5*time.Second
  var err error
  // ...
}

Recommended:

func (c *client) request() {
  var (
    caller  = c.name
    format  = "json"
    timeout = 5*time.Second
    err error
  )
  // ...
}

Import Grouping

Imports should be grouped into two categories:

  • Standard library
  • Other libraries

By default, this is the grouping applied by goimports. Not Recommended:

import (
  "fmt"
  "os"
  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

Recommended:

import (
  "fmt"
  "os"

  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

Package Name

When naming a package, please follow these rules:

  • All lowercase, no capital letters or underscores.
  • In most cases, no need for renaming when importing.
  • Short and concise. Remember that the name is fully qualified everywhere it's used.
  • Avoid plurals. For example, use net/url instead of net/urls.
  • Avoid using "common," "util," "shared," or "lib." These are not informative enough.

Function Naming

We adhere to the Go community convention of using MixedCaps for function names. An exception is made for grouping related test cases, where the function name may contain underscores, such as: TestMyFunction_WhatIsBeingTested.

Import Aliases

If the package name does not match the last element of the import path, an import alias must be used.

import (
  "net/http"

  client "example.com/client-go"
  trace "example.com/trace/v2"
)

In all other cases, import aliases should be avoided unless there is a direct conflict between imports. Not Recommended:

import (
  "fmt"
  "os"

  nettrace "golang.net/x/trace"
)

Recommended:

import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

Function Grouping and Order

  • Functions should be sorted roughly in the order they are called.
  • Functions within the same file should be grouped by receiver.

Therefore, exported functions should appear first in the file, placed after the struct, const, and var definitions.

A newXYZ()/NewXYZ() may appear after type definitions but before the remaining methods of the receiver.

As functions are grouped by receiver, general utility functions should appear at the end of the file. Not Recommended:

func (s *something) Cost() {
  return calcCost(s.weights)
}

type something struct{ ... }

func calcCost(n []int) int {...}

func (s *something) Stop() {...}

func newSomething() *something {
    return &something{}
}

Recommended:

type something struct{ ... }

func newSomething() *something {
    return &something{}
}

func (s *something) Cost() {
  return calcCost(s.weights)
}

func (s *something) Stop() {...}

func calcCost(n []int) int {...}

Reduce Nesting

Code should reduce nesting by handling error/special cases as early as possible and either returning or continuing the loop. Reducing nesting reduces the amount of code at multiple levels.

Not Recommended:

for _, v := range data {
  if v.F1 == 1 {
    v = process(v)
    if err := v.Call(); err == nil {
      v.Send()
    } else {
      return err
    }
  } else {
    log.Printf("Invalid v: %v", v)
  }
}

Recommended:

for _, v := range data {
  if v.F1 != 1 {
    log.Printf("Invalid v: %v", v)
    continue
  }

  v = process(v)
  if err := v.Call(); err != nil {
    return err
  }
  v.Send()
}

Unnecessary else

If a variable is being set in both branches of an if, it can be replaced with a single if statement.

Not Recommended:

var a int
if b {
  a = 100
} else {
  a = 10
}

Recommended:

a := 10
if b {
  a = 100
}

Top-level Variable Declaration

At the top level, use the standard var keyword. Do not specify the type unless it differs from the type of the expression.

Not Recommended:

var _s string = F()

func F() string { return "A" }

Recommended:

var _s = F()
// Since F explicitly returns a string type, we do not need to explicitly specify the type for _s

func F() string { return "A" }

Specify the type if it does not exactly match the type needed for the expression.

type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var _e error = F()
// F returns an instance of type myError, but we need error type

Use '_' as a Prefix for Unexported Top-level Constants and Variables

For unexported top-level vars and consts, prefix them with an underscore _ to explicitly indicate their global nature when used.

Basic rationale: Top-level variables and constants have package-level scope. Using generic names can easily lead to accidentally using the wrong value in other files.

Not Recommended:

// foo.go

const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

func Bar() {
  defaultPort := 9090
  ...
  fmt.Println("Default port", defaultPort)

  // We will not see a compile error if the first line of
  // Bar() is deleted.
}

Recommended:

// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

Exception: Unexported error values can use the err prefix without an underscore. See error naming.

Embedding in Structs

Embedded types (such as mutex) should be placed at the top of the field list within the struct and must have an empty line separating the embedded fields from the regular fields.

Not recommended:

type Client struct {
  version int
  http.Client
}

Recommended:

type Client struct {
  http.Client

  version int
}

Embedding should provide tangible benefits, such as adding or enhancing functionality in a semantically appropriate way. It should be used without any adverse impact on the user. (Also see: Avoid embedding types in public structs)

Exceptions: Even in unexported types, Mutex should not be used as an embedded field. Also see: Zero value Mutex is valid.

Embedding should not:

  • Solely exist for aesthetics or convenience.
  • Make it more difficult to construct or use the outer type.
  • Affect the zero value of the outer type. If the outer type has a useful zero value, there should still be a useful zero value after embedding the inner type.
  • Have the side effect of exposing unrelated functions or fields from the embedded inner type.
  • Expose unexported types.
  • Affect the cloning form of the outer type.
  • Change the API or type semantics of the outer type.
  • Embed the inner type in a non-standard form.
  • Expose implementation details of the outer type.
  • Allow users to observe or control the internal type.
  • Change the general behavior of internal functions in a way that may surprise users.

In short, embed consciously and purposefully. A good litmus test is, "Will all these exported methods/fields from the inner type be directly added to the outer type?" If the answer is some or no, do not embed the inner type - use fields instead.

Not recommended:

type A struct {
    // Bad: A.Lock() and A.Unlock() are now available
    // Provides no functional benefit and allows the user to control internal details of A.
    sync.Mutex
}

Recommended:

type countingWriteCloser struct {
    // Good: Write() is provided at the outer level for a specific purpose, 
    // and delegates the work to the Write() of the inner type.
    io.WriteCloser
    count int
}
func (w *countingWriteCloser) Write(bs []byte) (int, error) {
    w.count += len(bs)
    return w.WriteCloser.Write(bs)
}

Local Variable Declarations

If a variable is explicitly set to a value, the short variable declaration form (:=) should be used.

Not recommended:

var s = "foo"

Recommended:

s := "foo"

However, in some cases, using the var keyword for default values can be clearer.

Not recommended:

func f(list []int) {
  filtered := []int{}
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

Recommended:

func f(list []int) {
  var filtered []int
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

nil is a valid slice

nil is a valid slice with a length of 0, which means:

  • You should not explicitly return a slice with a length of zero. Instead, return nil.

Not recommended:

if x == "" {
  return []int{}
}

Recommended:

if x == "" {
  return nil
}
  • To check if a slice is empty, always use len(s) == 0 instead of nil.

Not recommended:

func isEmpty(s []string) bool {
  return s == nil
}

Recommended:

func isEmpty(s []string) bool {
  return len(s) == 0
}
  • Zero value slices (slices declared with var) can be used immediately without calling make().

Not recommended:

nums := []int{}
// or, nums := make([]int)

if add1 {
  nums = append(nums, 1)
}

if add2 {
  nums = append(nums, 2)
}

Recommended:

var nums []int

if add1 {
  nums = append(nums, 1)
}

if add2 {
  nums = append(nums, 2)
}

Remember, although a nil slice is a valid slice, it is not equal to a slice with a length of 0 (one is nil and the other is not), and they may be treated differently in different situations (e.g., serialization).

Narrow Variable Scope

If possible, try to narrow the scope of variables, unless it conflicts with the rule of reducing nesting.

Not recommended:

err := os.WriteFile(name, data, 0644)
if err != nil {
 return err
}

Recommended:

if err := os.WriteFile(name, data, 0644); err != nil {
 return err
}

If the result of a function call outside the if statement needs to be used, do not attempt to narrow the scope.

Not recommended:

if data, err := os.ReadFile(name); err == nil {
  err = cfg.Decode(data)
  if err != nil {
    return err
  }

  fmt.Println(cfg)
  return nil
} else {
  return err
}

Recommended:

data, err := os.ReadFile(name)
if err != nil {
   return err
}

if err := cfg.Decode(data); err != nil {
  return err
}

fmt.Println(cfg)
return nil

Avoid Naked Parameters

Unclear parameters in function calls may harm readability. When the meaning of parameter names is not obvious, add C-style comments (/* ... */) to the parameters.

Not recommended:

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true, true)

Recommended:

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)

For the example above, a better approach could be to replace the bool types with custom types. This way, the parameter can potentially support more than just two states (true/false) in the future.

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

const (
  StatusReady Status= iota + 1
  StatusDone
  // Maybe we will have a StatusInProgress in the future.
)

func printInfo(name string, region Region, status Status)

Use raw string literals to avoid escaping

Go supports the use of raw string literals, which is indicated by " ` " to represent raw strings. In scenarios where escaping is required, we should use this approach to replace the more difficult-to-read manually escaped strings.

It can span multiple lines and include quotes. Using these strings can avoid more difficult-to-read manually escaped strings.

Not recommended:

wantError := "unknown name:\"test\""

Recommended:

wantError := `unknown error:"test"`

Initialize structs

Initialize structures using field names

When initializing a structure, field names should almost always be specified. This is currently enforced by go vet.

Not recommended:

k := User{"John", "Doe", true}

Recommended:

k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

Exception: When there are 3 or fewer fields, field names in test tables may be omitted.

tests := []struct{
  op Operation
  want string
}{
  {Add, "add"},
  {Subtract, "subtract"},
}

Omit zero-value fields in structures

When initializing a structure with named fields, unless meaningful context is provided, ignore fields with a zero value. That is, let us automatically set these to zero values.

Not recommended:

user := User{
  FirstName: "John",
  LastName: "Doe",
  MiddleName: "",
  Admin: false,
}

Recommended:

user := User{
  FirstName: "John",
  LastName: "Doe",
}

This helps reduce reading barriers by omitting default values in the context. Only specify meaningful values.

Include zero value where field names provide meaningful context. For example, test cases in a table-driven test can benefit from naming the fields, even if they are zero values.

tests := []struct{
  give string
  want int
}{
  {give: "0", want: 0},
  // ...
}

Use var for zero-value structs

If all fields of a struct are omitted in the declaration, use var to declare the struct.

Not recommended:

user := User{}

Recommended:

var user User

This distinguishes zero-value structs from those with non-zero value fields, similar to what we prefer when declaring an empty slice.

Initialize struct references

When initializing structure references, use &T{} instead of new(T) to make it consistent with struct initialization.

Not recommended:

sval := T{Name: "foo"}

// inconsistent
sptr := new(T)
sptr.Name = "bar"

Recommended:

sval := T{Name: "foo"}

sptr := &T{Name: "bar"}

Initialize Maps

For an empty map, use make(..) to initialize it, and the map is filled programmatically. This makes the map initialization different from declaration in appearance, and it also conveniently allows for adding size hints after make.

Not Recommended:

var (
  // m1 is read-write safe;
  // m2 panics when writing
  m1 = map[T1]T2{}
  m2 map[T1]T2
)

Recommended:

var (
  // m1 is read-write safe;
  // m2 panics when writing
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

| Declaration and initialization look very similar. | Declaration and initialization look very different. |

Where possible, provide the map capacity size during initialization, see Specifying Map Capacity for details.

Additionally, if the map contains a fixed list of elements, use map literals to initialize the map.

Not Recommended:

m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3

Recommended:

m := map[T1]T2{
  k1: v1,
  k2: v2,
  k3: v3,
}

The basic guideline is to use map literals for adding a fixed set of elements during initialization. Otherwise, use make (and if possible, specify the map capacity).

String Format for Printf-style Functions

If you declare a Printf-style function's format string outside a function, set it as a const constant.

This helps go vet to perform static analysis on the format string.

Not Recommended:

msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

Recommended:

const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

Naming Printf-style Functions

When declaring Printf-style functions, ensure that go vet can detect and check the format string.

This means you should use predefined Printf-style function names as much as possible. go vet will check these by default. For more information, see Printf Family.

If predefined names cannot be used, end the selected name with f: Wrapf instead of Wrap. go vet can request specific Printf-style names to be checked, but the name must end with f.

go vet -printfuncs=wrapf,statusf