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
37: Channel Direction and Closing Channels
Why should I bother with channel directions if bidirectional channels just work?
I've seen this a lot. When you're first starting out, adding <- to your channel types feels like extra typing for no reason. After all, a bidirectional channel can do everything. But here's the thing: in a professional codebase, we use channel directions to communicate intent and to let the compiler catch our mistakes.
Imagine you have a function that's supposed to just log data coming out of a channel. If you pass it a bidirectional channel, that function could accidentally send data back into the channel, potentially causing a deadlock that's a nightmare to debug. By specifying <-chan, you're telling the compiler, "This function is a consumer. If I try to send something, blow up right now during compilation."
type LogMessage string
// This function only receives. It cannot accidentally send.
func logger(logs <-chan LogMessage) {
for msg := range logs {
fmt.Println("LOG:", msg)
}
}
// This function only sends. It cannot accidentally read.
func producer(logs chan<- LogMessage) {
logs <- "System started"
logs <- "Connecting to DB..."
}
It's essentially like marking a variable as const or readonly. It makes your API honest.
How do I actually tell if a channel was closed or if it's just empty?
This is a classic Go "gotcha." If you read from a closed channel, Go doesn't panic. Instead, it immediately returns the zero value for the channel's type. If you're expecting an integer, you'll get a 0. The problem is, was that 0 actually sent by the producer, or is the channel just closed?
To solve this, Go gives us the "comma-ok" idiom. The second return value is a boolean that tells you if the value was delivered by a successful send operation or if it's a zero value because the channel is closed.
val, ok := <-ch
if !ok {
fmt.Println("Channel is closed and empty!")
}
Now, if you're just draining a channel until it's empty, the for range loop is much cleaner because it handles this check for you under the hood. It will keep looping until the channel is closed and all remaining buffered values have been read.
Who is actually supposed to close the channel?
The rule of thumb I follow—and the one you should too—is that the sender should close the channel. Never close a channel from the receiver side, and never close it from a third party that doesn't "own" the sending logic.
Why? Because sending to a closed channel causes a panic. If the receiver closes the channel while the sender is still trying to push data, your whole program crashes. The sender, however, knows exactly when there is no more data to provide. Once the sender is done, they call close(ch), and the receiver sees the ok == false signal and shuts down gracefully.
I'll give you a quick example of this flow in a real-world scenario: a simple task distributor.
func worker(tasks <-chan int) {
for t := range tasks {
fmt.Printf("Processing task %d\n", t)
}
fmt.Println("Worker: No more tasks, shutting down.")
}
func main() {
tasks := make(chan int, 10)
go worker(tasks)
for i := 1; i<= 3; i++ {
tasks <- i
}
// The sender closes the channel
close(tasks)
// We need a way to wait here, otherwise main exits immediately
time.Sleep(time.Second)
}📋 Practical Task
Implement a Concurrent Order Pipeline
You are building a simplified order processing system. Your goal is to ensure the system shuts down cleanly without panicking or leaking goroutines.
- Create a function
produceOrders(orders chan<- int)that sends 5 order IDs (1 through 5) into the channel and then closes it. - Create a function
processOrders(orders <-chan int)that reads from the channel using afor rangeloop and prints "Processing order X" for each. - In your
mainfunction, initialize the channel, start the processor in a goroutine, and call the producer. - Challenge: Use a
sync.WaitGroupinmainto ensure the program doesn't exit until theprocessOrdersfunction has finished draining the channel and exited its loop.
There are no comments for now.