My raw notes on Go — Best practices, concurrency, memory and beyond

Go logo
Go logo

Over the last few months I’ve been doing some deeper research over some topics about Go that I found interesting and wanted to have a deeper knowledge about. While probably most people that have known the language for quite some time already know these topics, there are always newcomers who this might help. So instead of keeping my notes to myself I’m making them public.

Memory: Stack vs Heap

  • Go’s runtime creates 1 stack per goroutine.
  • Go’s runtime does not clean the stack every time a goroutine exits, it just marks it as invalid so it’s available for other programs or routine to claim it.
  • The Go runtime is observant enough to know that a reference to the salutation variable is still being held, and therefore will transfer the memory to the heap so that the goroutines can continue to access it. This in known as Escape Analysis.
  • The rule of thumb for memory allocation
Sharing down typically stays on the stackSharing up typically escapes to the heap

Only the compiler knows for sure when typically is not typically

To get accurate information build your program with gcflags

go build -gcflags=”-m -l” program.go
  • When does a variable escape to the heap?
- If it could possibly be referenced after the function returns- When a value is too big for the stack- When the compiler doesn’t know the size in compile time
  • Don’t do premature optimisations, relay on data and find problems before fixing them. Use profilers and analysis tools to find the root.

Goroutines

  • Every Go program has at least one goroutine: the main goroutine, which is automatically created and started when the process begins.
  • A goroutine is a function that is running concurrently. Notice the difference between concurrence and parallelism.
  • They’re not OS threads, they’re a higher level of abstraction known as coroutines. Coroutines are simply concurrent subroutines that are nonpreemptive (they cannot be interrupted).
  • Go follows a fork-join model for launching and waiting for goroutines.
  • Leak prevention: The way to successfully mitigate this is to establish a signal between the parent goroutine and its children that allows the parent to signal cancellation to its children. By convention, this signal is usually a read-only channel named done. The parent goroutine passes this channel to the child goroutine and then closes the channel when it wants to cancel the child goroutine.
  • If a goroutine is responsible for creating a goroutine, it is also responsible for ensuring it can stop the goroutine.
  • For better error handling inside goroutines create a struct that wraps both possible result and possible error. Return a channel of this struct.

Sync package

WaitGroup

Use it when you have to wait for a set of concurrent operations to complete when you either don’t care about the result of the concurrent operation, or you have other means of collecting their results.

Mutex and RWMutex

Locks a piece of code so only one goroutine can access it. The best practice is to lock and on the next line unlock with a defer statement.

A RWMutex gives you fine-grained control on when read or write privileges are given.

Channels

  • Channels serve as a conduit for a stream of information; values may be passed along the channel, and then read out downstream
  • Bidirectional or unidirectional
  • Channels in Go are blocking. This means that any goroutine that attempts to write to a channel that is full will wait until the channel has been emptied, and any goroutine that attempts to read from a channel that is empty will wait until at least one item is placed on it.
  • Reading sends two values, value and ok. If the channel is closed then value is the default value for the channel’s type and ok is false. Range over channel checks the ok value.
  • Creation of a channel should probably be tightly coupled to goroutines that will be performing writes on it so that we can reason about its behavior and performance more easily.
  • The goroutine that owns a channel should: Instantiate the channel, Perform writes, or pass ownership to another goroutine, close the channel, encapsulate the previous three things and expose them via a reader channel.

Select and for-select statements

  • In the select statement the evaluation of branches is done simultaneously, the first one that meets the condition is executed. If there are multiple options, go’s runtime does pseudorandom selection
  • Use <- time.after(n*time.Second) for timing out if none of the branches of the select statement become ready
  • Default clause in a select statement is executed if none is ready. Mixed with for loop to create fallthrough and check for other options every time
for {  select {    case <-done: 

return
default: // Do non-preemptable work }}

Pipelines

When constructing pipelines use a generator function to convert input to channel

func IntGenerator(done <-chan interface{}, integers ...int) <-chan int { 
intStream := make(chan int)
go func() {
defer close(intStream)
for _, i := range integers {
select {
case <-done:
return
case intStream <- i:
}
}
}()
return intStream
}

Fan-out, fan-in: reuse a single stage of our pipeline on multiple goroutines in an attempt to parallelise pulls from an upstream stage.

When to fan-out?

• It doesn’t rely on values that the stage had calculated before.

• It takes a long time to run.

Slices, declaration vs initialisation

var arr []string // declares slice, nil valuearr := make([]string, 3) //declares and initialises slice [“”,””,””]

Interfaces

Interface face implementation is done implicitly and checked in runtime. If you do not conform to an interface then an error will raise in production.

Add this line to do a compile time check of your interface implementation, will fail to compile if for example *Handler ever stops matching the http.Handler interface.

var _ http.Handler = (*Handler)(nil)
package mainimport (“fmt”)type Animal interface {  Speak() string}type Dog struct {}func (d Dog) Speak() string {  return “Woof!”}type Cat struct {}func (c *Cat) Speak() string {  return “Meow!”}func main() {  animals := []Animal{Dog{}, Cat{}}  for _, animal := range animals {    fmt.Println(animal.Speak())  }}// Output./prog.go:26:32: cannot use Cat literal (type Cat) as type Animal in slice literal:Cat does not implement Animal (Speak method has pointer receiver
A great rule of thumb for Go is accept interfaces, return structs.
–Jack Lindamood

Software engineer with 5+ years working for startups and some of LATAM’s unicorns. Passionate about high quality, resilient software with focus on product dev.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store