Concurrency in Go is the practice of structuring a program so multiple tasks can make progress independently, often using goroutines and channels.

Go is known for making concurrency relatively simple to write. The main building blocks are:

  • Goroutines, which are lightweight concurrent functions
  • Channels, which pass values between goroutines
  • select, which waits on multiple channel operations
  • sync, which provides coordination primitives such as mutexes and wait groups
flowchart LR
    Main["main goroutine"] --> Worker["worker goroutine"]
    Main --> Select["select"]
    Worker --> Chan["channel"]
    Chan --> Main
    Chan --> Select

Goroutines

A goroutine is started by putting go in front of a function call.

package main
 
import (
	"fmt"
	"time"
)
 
func worker(done chan<- string) {
	time.Sleep(100 * time.Millisecond)
	done <- "work finished"
}
 
func main() {
	done := make(chan string)
	go worker(done)
 
	fmt.Println(<-done)
}

Goroutines are useful when you want to run tasks concurrently without managing threads directly.

Channels

Channels let goroutines communicate by sending and receiving values.

package main
 
import "fmt"
 
func main() {
	ch := make(chan int)
 
	go func() {
		ch <- 42
	}()
 
	value := <-ch
	fmt.Println(value)
}

Channels are useful for:

  • Passing work between stages
  • Signaling completion
  • Coordinating pipelines
  • Avoiding shared mutable state when possible

select

select waits on multiple channel operations and chooses one that is ready.

flowchart LR
    Ch1["ch1"] --> Select["select"]
    Ch2["ch2"] --> Select
    Select --> Case1["case 1"]
    Select --> Case2["case 2"]
    Select --> Default["default"]
select {
case msg := <-ch1:
	fmt.Println(msg)
case ch2 <- value:
	fmt.Println("sent value")
default:
	fmt.Println("nothing ready")
}

sync

The sync package is used when channels are not the right tool. Common types include:

  • sync.Mutex
  • sync.RWMutex
  • sync.WaitGroup

Use a mutex when multiple goroutines need safe access to shared data. Use a wait group when you want to wait for several goroutines to finish.

Tradeoffs

Go makes concurrency ergonomic, but concurrency is still easy to get wrong.

Common problems include:

  • Race conditions
  • Deadlocks
  • Goroutine leaks
  • Blocking on channels that nobody reads from

Rule of thumb:

  • Use goroutines to do work concurrently
  • Use channels to communicate
  • Use mutexes when shared state is unavoidable