# Go Best Practices This document contains my personal list of idioms, patterns, and best practices for Go. Some of these are my own, many are not, but this is an inherently subjective list. ## Error wrapping ... ## Prefer `%q` over `%s` If you're interpolating text that my include whitespace, prefer `%q` over `%s` because it removes ambiguity for the reader. Bad: ```go filename := "the door" fmt.Printf("couldn't open %s", filename) // couldn't open the door ``` Good: ```go fmt.Printf("couldn't open %q", filename) // couldn't open "the door" ``` ## Prefer channels over mutexes Coming from other languages where mutexes are the most common form of synchronisation, it's tempting to use mutexes everywhere. But in Go, an alternative pattern is to have a background coroutine that synchronises mutations and data-access via channels. Mutexes: ```go type state struct { state map[string]Msg mu sync.Mutex } func newState() func (s *state) add(msg Msg) { s.mu.Lock() defer s.mu.Unlock() s.state[msg.Key] = msg } func (s *state) remove(msg Msg) { // Whoops I forgot to acquire the lock! s.state[msg.Key] = msg } ``` Coordinating goroutine: ```go type state struct { addCh chan Msg removeCh chan Msg } func newState() *state { s := &state{ addCh: make(chan Msg), removeCh: make(chan Msg), } go s.worker() return s } func (s *state) add(msg Msg) { s.addCh <- msg } func (s *state) remove(msg Msg) { s.removeCh <- msg } func (s *state) worker() { state := map[string]Msg{} for { // Do something with state // Delay or act on messages. select { case <-time.After(time.Second): case msg := <-s.addCh: case msg := <-s.removeCh: } } } ``` ## Returning a result from a channel send By including a return channel _with_ the message payload, we can achieve synchronous sending with acknowledgement: ```go type acked struct { msg []byte ack chan error } ch := make(chan acked) func sender(msg []byte) error { ack := make(chan error) ch <- acked{msg: msg, ack: result} return <-ack } func receiver() { for { envelope := <-ch err := doSomething(envelope.msg) envelope.ack <- err } } ``` ## Don't check if injected dependencies are valid There can be a tendency to want to validate injected dependencies, checking they're not nil, and so on. This is usually unnecessary because Go will panic by itself if you access a field or method on a nil struct. By omitting this validation noise we improve clarity. Bad: ```go func MyFunc(db *DB, id int) error { if db == nil { panic("DB not provided") } return db.Load(id) } ``` Good: ```go func MyFunc(db *DB, id int) error { return db.Load(id) // This will panic automatically. } ``` ## Know when to panic Use panics very sparingly. Panic only if the state of your code is somehow in an inconsistent state, otherwise error. Bad: ```go func MyFunc(input string) (Result, error) { if input != "expected" { panic("input was not as expected") } // ... } ``` Good (but [don't do this](#Don't-check-if-injected-dependencies-are-valid)): ```go func MyFunc(db *DB, input string) (Result, error) { if db == nil { panic("DB not provided") } // ... } ``` # Concurrency ## Use errgroup It's rare in my experience to # Testing ## Assertion libraries Use one. I like [my own](https://github.com/alecthomas/assert) because it's tiny but empirically sufficient, but use whatever you prefer. It saves time, duplication, and lines of code, and it improves readability. Bad: ```go func TestOptionMarshalJSON(t *testing.T) { o := Some(1) b, err := o.MarshalJSON() if err != nil { t.Errorf("failed to marshal Some: %s", err) } if string(b) != "1" { t.Errorf("%q != 1", string(b)) } o = None[int]() b, err = o.MarshalJSON() if err != nil { t.Errorf("failed to marshal None: %s", err) } if string(b) != "null" { t.Errorf("%s != null", string(b)) } } ``` Good: ```go func TestOptionMarshalJSON(t *testing.T) { o := Some(1) b, err := o.MarshalJSON() assert.NoError(t, err) assert.Equal(t, "1", string(b)) o = None[int]() b, err = o.MarshalJSON() assert.NoError(t, err) assert.Equal(t, "null", string(b)) } ``` ## Don't be afraid to write small helper functions for tests Sometimes your assertion library won't be sufficient to assert your intent. In those cases, if you keep repeating the same assertion logic (rule of three), consider pulling the logic out into a small helper function. ## Use table-driven tests They're great. I'll typically use the [rule of three](https://en.wikipedia.org/wiki/Rule_of_three_(computer_programming)) for table-driven tests as I do elsewhere. That is, if I have three sets of assertions or tests that have similar logic except for the values, I'll convert them to table-driven tests. Bad(ish): ```go func TestOptionMarshalJSON(t *testing.T) { o := Some(1) b, err := o.MarshalJSON() assert.NoError(t, err) assert.Equal(t, "1", string(b)) o = None[int]() b, err = o.MarshalJSON() assert.NoError(t, err) assert.Equal(t, "null", string(b)) value := "hello" p := Ptr(&value) b, err = o.MarshalJSON() assert.NoError(t, err) assert.Equal(t, `"hello"`, string(b)) } ``` Good: ```go func TestOptionMarshalJSON(t *testing.T) { ptr := "hello" tests := []struct{ name string input any expected string }{ {"some", Some(1), "1"}, {"none", None[int](), "null"}, {"ptr", Ptr(&ptr), `"hello"`} } for _, test := range tests { t.Run(test.name, func(t *testing.T) { b, err := o.MarshalJSON() assert.NoError(t, err) assert.Equal(t, t.expected, string(b)) }) } } ```