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

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.
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.


  • Every Go program has at least one goroutine: the main goroutine, which is automatically created and started when the process begins.

Sync package


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 serve as a conduit for a stream of information; values may be passed along the channel, and then read out downstream

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
for {  select {    case <-done: 

default: // Do non-preemptable work }}


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

func IntGenerator(done <-chan interface{}, integers <-chan int { 
intStream := make(chan int)
go func() {
defer close(intStream)
for _, i := range integers {
select {
case <-done:
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 [“”,””,””]


Compile time check

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)

Receivers matter

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

Interface segregation principle

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.