root@coding-prodigies:~# โ–Š
// lesson 2 of 12 ยท 18 min

Goroutines and channels

โš‘ Report an issue with this lesson
func fetchPrice(course string, results chan<- string) {
    // pretend this takes some time
    results <- course + ": $29.99"
}

func main() {
    results := make(chan string)
    go fetchPrice("HTML Fundamentals", results)
    go fetchPrice("Python Basics", results)

    fmt.Println(<-results)
    fmt.Println(<-results)
}

go starts a function running concurrently as a goroutine -- cheap enough that Go programs commonly run thousands of them. Channels are the safe way for goroutines to communicate, instead of sharing memory directly.

Goroutines are cheap because they aren't OS threads. Each one starts with a small (a few KB), growable stack managed by the Go runtime, which multiplexes many goroutines onto a much smaller number of actual OS threads. Spawning a goroutine costs closer to allocating a small object than to creating a thread, which is why "just start a goroutine for it" is a normal, everyday tool in Go rather than something you reach for cautiously.

By default, channels are unbuffered: a send blocks until another goroutine is ready to receive, and a receive blocks until a value arrives. This is a real synchronization point, not just a queue -- results <- value completing tells you the receiver has already taken the value, not merely that it was queued somewhere. A buffered channel relaxes that: make(chan string, 3) lets up to 3 sends complete without a waiting receiver, only blocking once the buffer fills:

results := make(chan string, 2)
results <- "a" // doesn't block -- buffer has room
results <- "b" // doesn't block -- buffer now full
// results <- "c" // would block until something reads from results

A closed channel is how a sender tells receivers "no more values are coming." Reading from a closed channel never blocks -- it immediately returns the zero value, plus a second boolean that's false once the channel is drained and closed:

results := make(chan int)
go func() {
    for i := 0; i < 3; i++ {
        results <- i
    }
    close(results) // signal: no more values
}()

for v := range results { // range exits automatically when the channel closes
    fmt.Println(v)
}

Only the sender should ever close a channel -- closing a channel you receive from (or closing an already-closed channel) panics. A common gotcha: forgetting to close a channel a range loop is reading from causes that loop to block forever once all values have been consumed, since range has no other way to know the sequence is finished.

When you need to wait on multiple channels at once -- a result channel and a cancellation signal, say -- select picks whichever case is ready first, and a default case makes it non-blocking:

select {
case v := <-results:
    fmt.Println("got:", v)
case <-time.After(2 * time.Second):
    fmt.Println("timed out")
default:
    fmt.Println("nothing ready yet")
}

Try it yourself

Exercise: Complete fetchPrice so it sends course + ": $29.99" on the results channel. In main, launch two goroutines for "HTML Fundamentals" and "Python Basics", receive both results into a slice, sort the slice alphabetically (goroutines can finish in either order, so sort first), and print each line.
Expected output:
HTML Fundamentals: $29.99
Python Basics: $29.99
go
Output

      
    

Run your code and get it working before marking this lesson complete.

// free preview

9 more lessons โ€” including Project: build and deploy a courses API โ€” and a final exam plus a certificate are waiting.

Unlock the full course โ€” $99.99