Goroutines and Channels
A goroutine is a function running concurrently, managed by Go's runtime rather than the operating system. Starting one costs a couple of kilobytes, so a program can have hundreds of thousands - which is why Go code blocks freely instead of contorting itself around callbacks.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
start := time.Now()
// A WaitGroup counts outstanding work. Add before starting, Done when
// finished, Wait until the count reaches zero.
var wg sync.WaitGroup
for i := 1; i <= 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(100 * time.Millisecond) // pretend this is I/O
fmt.Printf(" worker %d finished\n", i)
}()
}
wg.Wait()
fmt.Printf("four 100ms jobs in about 100ms: %t\n", time.Since(start) < 350*time.Millisecond)
}
Without the Wait, main returns and the program exits - taking every
goroutine with it, finished or not. Nothing waits for a goroutine automatically.
Channels: typed pipes between goroutines
package main
import "fmt"
func main() {
// Unbuffered: a send blocks until a receive is ready, and vice versa.
// That is a synchronisation point, not just a queue.
messages := make(chan string)
go func() { messages <- "from the goroutine" }()
fmt.Println(<-messages)
// Buffered: a send blocks only when the buffer is full.
buffered := make(chan int, 3)
buffered <- 1
buffered <- 2
fmt.Println(len(buffered), cap(buffered))
fmt.Println(<-buffered, <-buffered)
// Closing says "no more values". Ranging over a channel ends when it closes.
work := make(chan int, 5)
go func() {
defer close(work) // the sender closes, never the receiver
for i := 1; i <= 5; i++ {
work <- i * i
}
}()
total := 0
for value := range work {
total += value
}
fmt.Println("total", total)
// Receiving from a closed channel gives the zero value immediately; the
// comma-ok form tells you which.
if v, ok := <-work; !ok {
fmt.Printf("closed, got the zero value %d\n", v)
}
}
The deadlocks you will actually hit
package main
import (
"fmt"
"sync"
"time"
)
func main() {
// 1. Sending on an unbuffered channel with no receiver ready.
// ch := make(chan int); ch <- 1 // fatal error: all goroutines are asleep
// The fix is a receiver, or a buffer.
ch := make(chan int, 1)
ch <- 1
fmt.Println("buffered send did not block:", <-ch)
// 2. Ranging over a channel nobody closes - the loop waits forever.
// The fix is close() in the sender, usually with defer.
done := make(chan int, 3)
go func() {
defer close(done)
for i := 0; i < 3; i++ {
done <- i
}
}()
for range done {
}
fmt.Println("range ended because the sender closed")
// 3. Forgetting wg.Done, so Wait never returns. defer is the fix.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done() // even if this panics
time.Sleep(10 * time.Millisecond)
}()
wg.Wait()
fmt.Println("Wait returned because Done was deferred")
// 4. Sending to a channel whose reader has gone away. Select with a
// default, or a done channel, is the escape.
orphan := make(chan int)
select {
case orphan <- 1:
fmt.Println("sent")
default:
fmt.Println("nobody listening, and we did not block")
}
}
select: waiting on several channels
package main
import (
"fmt"
"time"
)
func main() {
fast := make(chan string)
slow := make(chan string)
go func() { time.Sleep(20 * time.Millisecond); fast <- "fast" }()
go func() { time.Sleep(80 * time.Millisecond); slow <- "slow" }()
// select takes whichever is ready first.
for received := 0; received < 2; received++ {
select {
case msg := <-fast:
fmt.Println("got", msg)
case msg := <-slow:
fmt.Println("got", msg)
}
}
// A timeout is just another case.
quiet := make(chan string)
select {
case msg := <-quiet:
fmt.Println(msg)
case <-time.After(50 * time.Millisecond):
fmt.Println("gave up after 50ms")
}
// A default makes the whole select non-blocking.
select {
case msg := <-quiet:
fmt.Println(msg)
default:
fmt.Println("nothing ready, carrying on")
}
}
Channels are for handing over, mutexes are for protecting
Both are correct tools. The rule of thumb: use a channel when a value moves from
one goroutine to another; use a sync.Mutex when several goroutines share one
piece of state.
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // unlocked even if the body panics
c.n++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
func main() {
counter := &Counter{}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for n := 0; n < 100; n++ {
counter.Increment()
}
}()
}
wg.Wait()
fmt.Println("counter:", counter.Value(), "- exactly 10000, every time")
// Without the mutex this is a data race: run it under `go run -race` and
// the detector will say so. Use -race in CI; it catches what tests miss.
}
What to take away
- A goroutine is cheap; nothing waits for one unless you make it.
sync.WaitGroup:Addbefore starting,defer Done,Waitat the end.- An unbuffered channel is a synchronisation point, not merely a queue.
- The sender closes a channel, never the receiver;
rangeends when it closes. selectwaits on several channels;time.Afteris a timeout,defaultis non-blocking.- Channels hand values over, mutexes protect shared state - and
-racefinds the rest.