Golang Coding Standard Basic Guidelines

Use defer to release resources

Use defer to release resources such as files and locks.

Not recommended:

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

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

return newCount

// It's easy to forget to unlock when there are multiple return branches

Recommended:

p.Lock()
defer p.Unlock()

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

p.count++
return p.count

// More readable

The overhead of defer is extremely low, so it should only be avoided when you can prove that the function execution time is in the nanosecond level. Using defer to improve readability is worthwhile because the cost of using them is negligible. This is especially applicable to larger methods that involve more than just simple memory access, where the resource consumption of other calculations far exceeds that of defer.

Channel size should be 1 or unbuffered

Channels should typically have a size of 1 or be unbuffered. By default, channels are unbuffered with a size of zero. Any other size must be strictly reviewed. We need to consider how to determine the size, consider what prevents the channel from writing under high loads and when blocked, and consider what changes occur in the system logic when this happens.

Not recommended:

// Should be enough to handle any situation!
c := make(chan int, 64)

Recommended:

// Size: 1
c := make(chan int, 1) // or
// Unbuffered channel, size is 0
c := make(chan int)

Enums start from 1

The standard method of introducing enums in Go is to declare a custom type and a const group that uses iota. Since the default value of variables is 0, enums should typically start with a non-zero value.

Not recommended:

type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)

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

Recommended:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

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

In some cases, using the zero value makes sense (enums starting from zero), for example, when the zero value is the ideal default behavior.

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

// LogToStdout=0, LogToFile=1, LogToRemote=2

Using atomic

Use atomic operations from the sync/atomic package to operate on primitive types (int32, int64, etc.) because it is easy to forget to use atomic operations to read from or modify variables.

go.uber.org/atomic adds type safety to these operations by hiding the underlying type. Additionally, it includes a convenient atomic.Bool type.

Not recommended approach:

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!
}

Recommended approach:

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 global variables

Use the dependency injection approach to avoid changing global variables. This is applicable for function pointers as well as other value types.

Not recommended approach 1:

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

Recommended approach 1:

// 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)
}

Not recommended approach 2:

// 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))
}

Recommended approach 2:

// 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 Using Pre-declared Identifiers

The Go Language Specification outlines several predeclared identifiers that should not be used in Go projects. These predeclared identifiers should not be reused as names in different contexts, as doing so will hide the original identifiers in the current scope (or any nested scope), potentially leading to code confusion. In the best case, the compiler will raise an error; in the worst case, such code may introduce potential, difficult-to-recover errors.

Unrecommended Practice 1:

var error string
// `error` implicitly shadows the built-in identifier

// or

func handleErrorMessage(error string) {
    // `error` implicitly shadows the built-in identifier
}

Recommended Practice 1:

var errorMessage string
// `error` now points to the non-shadowed built-in identifier

// or

func handleErrorMessage(msg string) {
    // `error` now points to the non-shadowed built-in identifier
}

Unrecommended Practice 2:

type Foo struct {
    // While these fields technically do not shadow, redefining `error` or `string` strings now becomes ambiguous.
    error  error
    string string
}

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

func (f Foo) String() string {
    // `string` and `f.string` visually appear similar
    return f.string
}

Recommended Practice 2:

type Foo struct {
    // `error` and `string` are now explicit.
    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 the predeclared identifiers, but tools like go vet will correctly point out these and other implicitly-related issues.

Avoid using init()

Try to avoid using init() as much as possible. When init() is unavoidable or preferred, the code should try to:

  1. Ensure completeness regardless of the program environment or call.
  2. Avoid relying on the order or side effects of other init() functions. Although the order of init() is explicit, the code can change, so the relationship between init() functions may make the code fragile and error-prone.
  3. Avoid accessing or manipulating global or environmental states, such as machine information, environment variables, working directories, program parameters/inputs, etc.
  4. Avoid I/O, including file systems, networking, and system calls.

Code that does not meet these requirements may belong as part of the main() call (or elsewhere in the program’s lifecycle) or be written as part of main() itself. In particular, libraries intended to be used by other programs should pay special attention to completeness rather than performing “init magic”.

Unrecommended approach 1:

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

Recommended approach 1:

var _defaultFoo = Foo{
    // ...
}
// or, for better testability:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
    return Foo{
        // ...
    }
}

Unrecommended approach 2:

type Config struct {
    // ...
}
var _config Config
func init() {
    // Bad: based on the current directory
    cwd, _ := os.Getwd()
    // Bad: I/O
    raw, _ := os.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    yaml.Unmarshal(raw, &_config)
}

Recommended approach 2:

type Config struct {
    // ...
}
func loadConfig() Config {
    cwd, err := os.Getwd()
    // handle error
    raw, err := os.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    // handle error
    var config Config
    yaml.Unmarshal(raw, &config)
    return config
}

Given the above considerations, in some cases, init() may be more preferred or necessary, including:

  • Cannot be represented as a single assignment of a complex expression.
  • Insertable hooks, such as database/sql, type registries, etc.

Prefer specifying slice capacity when appending

Always prioritize specifying a capacity value for make() when initializing a slice to be appended.

Unrecommended approach:

for n := 0; n < b.N; n++ {
  data := make([]int, 0)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}

Recommended approach:

for n := 0; n < b.N; n++ {
  data := make([]int, 0, size)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}

Using Field Tags in Struct Serialization

When serializing to JSON, YAML, or any other format that supports field naming based on tags, relevant tags should be used for annotation.

Not Recommended:

type Stock struct {
  Price int
  Name  string
}
bytes, err := json.Marshal(Stock{
  Price: 137,
  Name:  "UBER",
})

Recommended:

type Stock struct {
  Price int    `json:"price"`
  Name  string `json:"name"`
  // Safe to rename Name to Symbol.
}
bytes, err := json.Marshal(Stock{
  Price: 137,
  Name:  "UBER",
})

In theory, the serialization format of a structure is a contract between different systems. Making changes to the serialization form of the structure (including field names) will break this contract. Specifying field names in tags makes the contract explicit and also helps prevent accidental violations of the contract through refactoring or renaming of fields.