Go Style Guide

Mainly notes from the book 100 Go Mistakes and How to Avoid Them

Happy path on the left, errors and edge cases on the right

// Bad
if val == 42 {
  doStuff()
} else {
  return errors.New("val is not 42")
}

// Good
if val != 42 {
  return errors.New("val is not 42")
} 
doStuff()

If an if block returns, don’t add else (see above)

Avoid shadowing variables

Leads to confusion, nasty bugs.

//Bad
var f *File
if (customFileName != nil) {
    r, err := os.Open(customFileName)
    if err != nil {
        // handle error
    }
} else  {
    f, err := os.Open(defaultFileName)
    if err != nil {
        // handle error
    }
}

//Good
var f *File
var err error
if (customFileName != nil) {
    f, err = os.Open(customFileName)
} else  {
    f, err = os.Open(defaultFileName)
}
if err != nil {
    // handle error
}

Avoid using init functions

They make testing and code understanding more difficult (harder to reason about state). They can potentially depend on the filename ordering if a package has multiple init functions (so subject to renaming files).

Interfaces on the Consumer Side

Go satisfies interfaces implicitly (there’s no implements keyword or anything like that).

This allows for the consumer to define its own interface, exactly as it needs it. The producer can satisfy that interface implicitly, without needing to depend on the consumer’s package.

This leads to better abstractions and interface segregation.

Use the functional options pattern

In languages like java, it’s very common to use chained setters (variations of the builder pattern) to configure objects. That could work in go, but it limits us if we want to do any sort of validation in those setters. Returning an error from a setter would mean that they can no longer be chained.

Other languages have optional parameters (go doesn’t have them).

A workaround would be to require a config struct (an empty one would mean that defaults are applied), but that is still not ideal, as it requires client changes every time we add a new field to the config struct (even if it’s unused by the client).

The functional options pattern allows us to have optional parameters, and also to validate them, while maintaining a convenient api


type options struct {
   config1 *int 
   config2 int  
}

type Option func(options *options) error

func WithConfig1(config1 int) Option {
   return func(options *options) error {
      options.config1 = &config1
      return nil
   }
}

func WithConfig2(config2 int) Option {
   return func(options *options) error {
      if config2 < 0 {
         return errors.New("config2 must be positive")
      }
      options.config2 = config2
      return nil
   }
}

func NewThing(options ...Option) (*Thing, error) {
   opts := &options{}
   for _, opt := range options {
      if err := opt(opts); err != nil {
         return nil, err
      }
   }
   var config1 int
   if opts.config1 == nil {
      config1 = someDefaultValue()
   }
   return &Thing{config1, opts.config2}, nil
}

Updated:

Comments