Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
30: Channels and Select
I want to start today with a piece of code that looks perfectly reasonable at first glance. You've probably written something like this if you've started playing with goroutines. We're trying to fetch a result from a worker, but we want to make sure the program doesn't hang forever if the worker gets stuck.
package main
import (
"fmt"
"time"
)
func fetchData(ch chan string) {
time.Sleep(2 * time.Second) // Simulate a slow API call
ch <- "Data received!"
}
func main() {
ch := make(chan string)
go fetchData(ch)
select {
case res := <-ch:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("Timeout reached!")
}
}
The Invisible Goroutine Leak
If you run this, you'll see "Timeout reached!" printed to the console. You might think, "Great, my timeout works." But there is a serious bug here that wouldn't show up in a small script, but would crash a production server over time: a goroutine leak.
Here is what's happening: fetchData is using an unbuffered channel. In Go, sending to an unbuffered channel blocks the sender until a receiver is ready to take the value. In our main function, the select statement waited for one second, hit the timeout case, and then the main function exited (or moved on).
When fetchData finally finishes its sleep after two seconds and tries to send "Data received!" into ch, there is no one left to receive it. That goroutine will stay blocked forever, clinging to its memory, waiting for a receiver that will never come. I've spent hours debugging "mysterious" memory growth in Go apps only to find thousands of these zombie goroutines.
Fixing the Leak with Buffered Channels
The simplest fix here is to give the channel a buffer. By changing the initialization to make(chan string, 1), we tell Go: "Allow one value to be sent into this channel even if no one is reading it yet."
// The fix: add a buffer of 1
ch := make(chan string, 1)
Now, when fetchData sends its result, it drops the value into the buffer and exits immediately. It doesn't matter if the main function has already timed out and stopped listening; the sender is no longer blocked. Always ask yourself: "If the receiver stops listening, will the sender be stuck forever?"
Multiplexing with Select
The select statement is essentially a switch for channels. It lets a goroutine wait on multiple communication operations. The first one that becomes "ready" wins, and its block is executed. If multiple are ready, Go picks one at random.
This is incredibly powerful for patterns like "first-response wins." Imagine you're querying three different DNS servers; you don't care which one answers first, you just want the fastest result. You can spin up three goroutines, have them all send to the same channel, and use select to grab the first one that arrives.
Handling Non-Blocking Operations
By default, select blocks until one of its cases can run. But sometimes, you don't want to wait. You want to check if a channel has a value, and if it doesn't, move on immediately. This is where the default case comes in.
select {
case msg := <-ch:
fmt.Println("Received:", msg)
case <-time.After(100 * time.Millisecond):
fmt.Println("Too slow")
default:
fmt.Println("No data available right now")
}
Adding a default case makes the select non-blocking. If no channels are ready to communicate at the exact microsecond the code hits the select, it executes the default block. Use this sparingly; putting a non-blocking select inside a for loop without a sleep or a throttle is a great way to accidentally pin your CPU to 100%.
📋 Practical Task
Exercise: Implementing a Concurrent API Race
You need to build a system that fetches a "Price Quote" from three different simulated providers. Since these providers can be flaky, your goal is to take the result from the first provider that responds, but stop waiting after a total of 500 milliseconds.
Requirements:
- Create a function
fetchQuote(name string, delay time.Duration, ch chan string)that sleeps for the given duration and then sends a string (e.g., "Price from Provider A: $100") into the channel. - In
main, initialize a buffered channel to prevent goroutine leaks. - Launch three goroutines calling
fetchQuotewith different delays:- Provider A: 200ms
- Provider B: 600ms
- Provider C: 100ms
- Use a
selectblock to print the first result that arrives. - Include a timeout case that prints "All providers timed out" if no one responds within 500ms.
Hint: Ensure your channel buffer is large enough to handle all providers sending their results, even if you only read one of them.
There are no comments for now.