Lesson 16 of 18

Goroutines and Channels

Concurrency in Go

Concurrency is one of Go's defining features. Go makes it easy to run functions concurrently and communicate between them safely.

Goroutines

A goroutine is a lightweight thread managed by the Go runtime. You start one by putting the go keyword before a function call:

go doSomething()

That is it. The function runs concurrently with the rest of your program. Goroutines are extremely cheap: you can launch thousands without concern.

func printNumbers() {
    for i := 1; i <= 5; i++ {
        fmt.Println(i)
    }
}

func main() {
    go printNumbers() // runs concurrently
    fmt.Println("started")
    time.Sleep(time.Second) // wait for goroutine
}

"Hailing frequencies open, Captain." The Enterprise communicates with multiple ships at once, each on its own channel. Go's goroutines and channels work the same way --- parallel conversations, perfectly coordinated.

Channels

Channels are Go's way of communicating between goroutines. A channel is a typed conduit through which you send and receive values:

ch := make(chan int)  // create a channel of ints

go func() {
    ch <- 42  // send a value into the channel
}()

val := <-ch  // receive a value from the channel
fmt.Println(val) // 42

The <- operator is used for both sending and receiving. Sends and receives block until the other side is ready, which naturally synchronizes goroutines.

Channel Direction

Function signatures can restrict a channel to send-only or receive-only:

func producer(ch chan<- int) {  // can only send to ch
    ch <- 42
}

func consumer(ch <-chan int) {  // can only receive from ch
    val := <-ch
    fmt.Println(val)
}

This makes your intent clear and catches mistakes at compile time.

Closing Channels and Range

A sender can close a channel to signal that no more values will be sent. The receiver can detect this:

ch := make(chan int)

go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}()

for val := range ch {
    fmt.Println(val)
}

The range loop receives values from the channel until it is closed. This is the cleanest way to consume all values from a channel.

Buffered Channels

By default, channels are unbuffered: a send blocks until another goroutine receives. A buffered channel has a capacity, allowing sends to proceed without a receiver until the buffer is full:

ch := make(chan int, 3)  // buffer holds up to 3 values

ch <- 1  // does not block
ch <- 2  // does not block
ch <- 3  // does not block
// ch <- 4 would block here (buffer full)

fmt.Println(<-ch) // 1

Buffered channels are useful when the sender and receiver run at different speeds, or when you know the exact number of values that will be sent.

Synchronization with Channels

When you need to wait for a goroutine to finish, you can use a channel as a signal:

done := make(chan bool)

go func() {
    fmt.Println("working...")
    done <- true  // signal completion
}()

<-done  // wait for the goroutine to finish

For waiting on multiple goroutines, use sync.WaitGroup. Call Add before launching, Done when each goroutine finishes, and Wait to block until all are done:

var wg sync.WaitGroup
for i := 0; i < 3; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        fmt.Println(n)
    }(i)
}
wg.Wait() // blocks until all goroutines call Done

Your Task

Write a function squares(n int) <-chan int that returns a receive-only channel. It should start a goroutine that sends the squares of 1 through n into the channel, then closes it.

Go runtime loading...
Loading...
Click "Run" to execute your code.