Go's concurrency model starts with two primitives that are easy to write and easy to misuse: goroutines and channels. A goroutine is a function running in a new concurrent execution unit; prefixing a call with go is enough. Channels connect those units. The smallest useful pattern is a goroutine sending a value and main receiving it.
package main
import (
"fmt"
)
func main() {
messages := make(chan string)
go func () { messages <- "ping" }()
msg := <-messages
fmt.Printf("%s\n", msg)
}
Here the go function sends ping into an unbuffered channel. main waits at the receive, so the program does not exit before the sender has completed its work. The runtime manages goroutines. They come from the same family of ideas as coroutines, but they are not identical: goroutines let a set of reusable functions run on a set of threads. Channels are the communication path between goroutines, and goroutines plus channels are how Go expresses concurrency.
Channels are created with make:
ch = make(chan int) // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // channel with capacity 3
Sending, receiving, and discarding a received value look like this:
ch <- x // send data to the channel
x = <-ch // receive data from the channel
<-ch // receive from the channel (ignore the value)
With an unbuffered channel, a send blocks the sending goroutine until another goroutine receives on the same channel. Once the value has crossed the channel, both goroutines can continue. If the receive happens first, the receiver blocks until a sender arrives on that channel. Send and receive on an unbuffered channel therefore perform a synchronization step, which is why unbuffered channels are sometimes called synchronous channels.
Go's documentation says that the receipt of the value happens before the reawakening of the sending goroutine. A practical reading is: the receiver gets the value before the sender is allowed to wake up and move on. The sender has already been blocked by the handoff; only after the receiver takes the value can the sender resume.
A three-stage pipeline shows the same idea with typed directional channels:
package main
import (
"fmt"
)
func counter(out chan<- int) {
for x := 0; x < 100; x++ {
out <- x
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
close(out)
}
func printer(in <-chan int) {
for v := range in {
fmt.Printf("==> %d\n", v)
}
}
func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}
The counter feeds integers into naturals; the squarer reads each one and writes its square into squares; the printer drains squares in the main goroutine. Each function closes its output channel when it is finished, so the range loops can terminate.
Buffered channels add a queue:
// create a channel queue that can hold three elements
ch := make(chan int, 3)
A goroutine can send three values into this channel without blocking. Once the buffer is full, the next send blocks until a receiver makes room. When the queue is empty, a receiver blocks until a value arrives. The buffer is a one-way FIFO queue.
One use of a buffered channel is to start several goroutines that request resources from multiple CDNs and send results back to one channel. The program can return as soon as the first result arrives. With an unbuffered channel, the goroutines that lose the race can remain stuck trying to send. That is a goroutine leak: the goroutines are not garbage collected, and the situation is a bug.
When launching work in a loop, a buffered channel can also serve as a completion counter. The main goroutine receives once for every launched goroutine and exits only after all have reported in.
package main
import (
"fmt"
)
func main() {
done := make(chan int, 10)
for i := 0; i < cap(done); i++ {
go func(i int) {
fmt.Printf("Hello World! %d\n", i)
done <- 1
}(i)
}
for i := 0; i < cap(done); i++ {
<-done
}
}
sync.WaitGroup expresses the same coordination more directly:
package main
import (
"sync"
"fmt"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
fmt.Printf("Hello World! %d\n", i)
wg.Done()
}(i)
}
wg.Wait()
}
wg.Add(1) increases the number of events being waited for. It needs to happen before the goroutine starts; placing it inside the goroutine does not guarantee it will run in time. After printing, each goroutine calls wg.Done() to mark one event complete. wg.Wait() in main blocks until all events are done.