This content originally appeared on Level Up Coding - Medium and was authored by Martin Stancanelli
My raw notes on Go — Best practices, concurrency, memory and beyond
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 is known as Escape Analysis.
- The rule of thumb for memory allocation
Sharing down typically stays on the stack
Sharing 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 value
arr := make([]string, 3) //declares and initialises slice [“”,””,””]
Interfaces
Compile time check
Interface 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 main
import (
“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
Resources
- Memory management in Go
- Understanding Allocations: the Stack and the Heap — GopherCon SG 2019 (Jacob Walker)
- Golang UK Conference 2016 — Dave Cheney — SOLID Go Design
- MapReduce in Go
- Concurrency in Go, Katherine Cox-Buday
- https://github.com/uber-go/guide/blob/master/style.md
- https://github.com/golang-standards/project-layout
- https://github.com/mastanca/go-concurrent-utils
My raw notes on Go — Best practices, concurrency, memory and beyond was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Martin Stancanelli
Martin Stancanelli | Sciencx (2022-02-27T18:56:03+00:00) My raw notes on Go — Best practices, concurrency, memory and beyond. Retrieved from https://www.scien.cx/2022/02/27/my-raw-notes-on-go-best-practices-concurrency-memory-and-beyond/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.