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
235: Pipeline Pattern for Data Processing
Wait, isn't this just a bunch of functions? Why use channels?
You could definitely just pass a slice of data through a series of functions. But the pipeline pattern isn't about the transformation itself—it's about concurrency and streaming. If you have a 10GB log file, you can't load that into a slice. You want to start processing the first line while the disk is still reading the second.
In Go, a pipeline is a series of stages connected by channels. Each stage is a group of goroutines running the same function. This lets you utilize all your CPU cores; while stage three is aggregating data, stage one is still ingestion, and stage two is filtering. It's a conveyor belt for your data.
How do I actually structure a "stage" so it connects to the next one?
The pattern I always use is: a function that takes a receive-only channel as input and returns a receive-only channel as output. This makes the pipeline composable. You can plug in new stages or swap them out without breaking the rest of the chain.
Let's look at a real-world example: processing a stream of raw log strings. We want to convert them to structs and then filter out anything that isn't an "ERROR".
type LogEntry struct {
Level string
Message string
}
// Stage 1: Generator - converts a slice of strings into a channel
func generateLogs(logs []string) <-chan string {
out := make(chan string)
go func() {
for _, l := range logs {
out <- l
}
close(out)
}()
return out
}
// Stage 2: Parser - converts raw strings to LogEntry structs
func parseLogs(in <-chan string) <-chan LogEntry {
out := make(chan LogEntry)
go func() {
for s := range in {
// Simple split logic for example purposes
parts := strings.SplitN(s, ":", 2)
out <- LogEntry{Level: parts[0], Message: parts[1]}
}
close(out)
}()
return out
}
// Stage 3: Filter - only lets ERROR logs through
func filterErrors(in <-chan LogEntry) <-chan LogEntry {
out := make(chan LogEntry)
go func() {
for entry := range in {
if entry.Level == "ERROR" {
out <- entry
}
}
close(out)
}()
return out
}
How do I prevent goroutine leaks when the pipeline finishes?
This is where most developers trip up. If you stop reading from the final channel (the "sink") but the earlier stages are still trying to send data, those goroutines will hang forever. They're blocked on a channel send that will never be received. That's a memory leak.
I handle this in two ways. First, always ensure your stages close(out) when the input channel is exhausted. That's why I used for s := range in in the examples above—the loop terminates automatically when the previous stage closes its channel.
Second, if you need to be able to cancel the pipeline early (say, you found the first error and don't care about the rest), you must pass a context.Context through every stage. Inside your select block, you check for ctx.Done(). If the context is cancelled, you exit the goroutine immediately.
Here is how you'd wire it all together in your main function:
func main() {
rawLogs := []string{"INFO:System start", "ERROR:Disk full", "DEBUG:Checking port", "ERROR:Network timeout"}
// Pipeline wiring
source := generateLogs(rawLogs)
parsed := parseLogs(source)
errors := filterErrors(parsed)
// The "Sink" - this consumes the final output
for errLog := range errors {
fmt.Printf("Alert: %s\n", errLog.Message)
}
}📋 Practical Task
Exercise: Build a High-Value Transaction Monitor
You need to build a data pipeline that processes a stream of financial transactions. Your goal is to identify "High Value" transactions (those over $10,000) and format them for a reporting system.
Implement the following pipeline stages:
generateTransactions: Takes a slice of floats (transaction amounts) and sends them into a channel.filterHighValue: Receives amounts and only forwards those greater than 10,000.formatReport: Receives the high-value amounts and converts them into a string format:"HIGH VALUE: $X.XX".
In your main function, provide a slice of at least 5 different amounts (some above and some below 10,000), wire the pipeline together, and print the final results. Ensure that all channels are closed correctly to avoid goroutine leaks.
There are no comments for now.