Every function spawned with go
keyword creates new goroutine, e.g.:
go fn(a, b, c)
-
Avoid Concurrency.
Concurrency in Go is not a silver bullet, it wouldn't make code faster in all cases.
Async code harder to understand and debug is. Hrrmmm.
Master Yoda
e.g. there is no speed benefit in this example for one CPU, and async solution consumes 50%–100% more memory.
Another example:
var wg sync.WaitGroup wg.Add(1) go serve(&wg) wg.Wait()
vs.:
serve()
-
Make sure, that there would be a limited amount of running goroutines. Use one of next:
- use errgroup with SetLimit()
- use semaphore or just some
make(chan struct{}, N)
before spawning a new goroutine. - use pool from conc with
WithMaxGoroutines
- create worker pool. Shouldn't be used in most cases since there would be hanging workers, that are doing nothing, and spawning a new goroutine is quite cheap.
- etc., e.g. HTTP server has connections limit.
-
It should be possible to stop running goroutines, e.g. on service shutdown or HTTP timeout. Use one of next for cancellation:
context.Context
(Context is cancelled or the deadline is exceeded. Preferable.)- stop channel(kinda legacy solution and hard to use with other APIs, e.g. with some DB ORM or HTTP clients)
Also write context aware code.
HTTP timeout example:
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // http.Server should have timeouts var entity Entity _ = json.NewDecoder(r.Body).Decode(&entity) // Don’t forget to check the error and close the body group, _ := errgroup.WithContext(ctx) group.Go(func() (err error) { return h.db1.CreateEntity(ctx, entity) // a lot of methods takes context, not stop chan }) group.Go(func() (err error) { return h.db2.TransactionLog(ctx, entity) }) err := group.Wait() // blocking if err != nil { http.Error(w, "db error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) }
-
Never start a goroutine without knowing when it will stop.
e.g. on shutdown app should wait for all goroutines to stop.
Use one of next:
sync.WaitGroup
context.Context
- returned by goroutine spawner, e.g. errgroup.WithContext- done channel - example
See also: when you spawn goroutines, make it clear when or whether they exit.
-
Panics. Recover could catch panic only in current goroutine, so make sure, that panic is handled in goroutine.
func f1() { if true { panic("f1") } fmt.Println("f1 done") } func main() { defer func() { if r := recover(); r != nil { fmt.Println("recovered panic:", r) } }() go f1() for i := 0; i < 10; i++ { fmt.Println("tick", i) time.Sleep(time.Second) } }
Also, panic could block goroutine forever, so call
mu.Unlock
,wg.Wait
orclose(ch)
in defers. -
wg.Add(...)
should be called before running goroutine.
-
or use sourcegraph/conc.WaitGroup with semaphore.
- Add profiler labels, this would help to debug and read stacktrace.
- Use the race detector (
-race
flag) andt.Parallel()
in unit tests and subtests. - Use uber-go/goleak to detect goroutine leaks in your tests.
- You can update pre-allocated slice concurrently.
Channels are a typed conduit through which you can send and receive messages.
-
Consider using shared memory instead of message passing:
Our study found that message passing does not necessarily make multithreaded programs less error-prone than shared memory. In fact, message passing is the main cause of blocking bugs. To make it worse, when combined with traditional synchronization primitives or with other new language features and libraries, message passing can cause blocking bugs that are very hard to detect. Message passing causes less nonblocking bugs than shared memory synchronization and surprisingly, was even used to fix bugs that are caused by wrong shared memory synchronization. We believe that message passing offers a clean form of inter-thread communication and can be useful in passing data and signals. But they are only useful if used correctly, which requires programmers to not only understand message passing mechanisms well but also other synchronization mechanisms of Go.
-
Do not send into closed channel. However, reading from closed channel is OK.
-
Do not send to or receive from a nil channel it will block forever.
-
Don't make huge buffered channels. Channel is just a data buffer, don't try to feet all results there(you would either make it too small and block on writing, or make it too big and use redundant memory). Prefer channels with a size of zero or one. Most other sizes are guesses.
-
Channel consumer should write values into DB/cache/file/socket/map/slice/other data structures.
-
If channel producer(writer) is not running in goroutine the channel consumer should be spawned before it.
-
Channel should be closed once either by the producer(if it's one) or with the help of
sync.WaitGroup
/sync.Once
(if there are many producers).