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
29: Goroutines Explained
When I first started with Go, I fell into a trap that almost every developer does: I assumed a goroutine was just a fancy name for a thread. It sounds like a pedantic distinction, but thinking of them as 1:1 mappings to OS threads will lead you to write inefficient code and misunderstand how Go actually scales.
"Goroutines are just lightweight OS threads"
If goroutines were simply OS threads, your computer would buckle under the pressure long before you hit a million of them. Standard OS threads are expensive; they have a fixed-size stack (often 1MB or 2MB) and the context switch—the process of the CPU swapping from one thread to another—requires a trip to the kernel, which is slow.
Try to imagine creating 100,000 OS threads on your laptop. You'd likely run out of memory or crash the system because you'd be asking for 100GB+ of RAM just for the stacks. But in Go, I can spin up 100,000 goroutines on a cheap VPS and it won't even make the fan spin up. Here is why that's possible:
package main
import (
"fmt"
"runtime"
)
func main() {
// I'm launching 100,000 goroutines here.
// If these were OS threads, this program would likely crash.
for i := 0; i < 100000; i++ {
go func(id int) {
_ = id // simulate some work
}(i)
}
fmt.Printf("Running on %d OS threads\n", runtime.GOMAXPROCS(0))
}
Multiplexing via the Go Runtime
The reality is that Go uses an M:N scheduler. This means it multiplexes M goroutines onto N OS threads. The Go runtime handles the heavy lifting; it manages a pool of OS threads and schedules goroutines to run on them. When a goroutine blocks—say, it's waiting for a network response—the scheduler simply moves it off the OS thread and puts another runnable goroutine in its place. The OS thread never actually sleeps; it just switches tasks.
Another huge win is the stack. While an OS thread has a rigid size, a goroutine starts with a tiny 2KB stack that grows and shrinks dynamically. I've always found this to be the "secret sauce" of Go's concurrency: you don't have to budget your memory for every single concurrent task you launch.
Triggering Execution with the go Keyword
Actually using them is deceptively simple. You just put the word go before a function call. But there is a catch: the main function doesn't care about your goroutines. If main reaches its closing brace, the program terminates immediately, killing every background goroutine regardless of whether they finished their work.
Let's look at a real-world scenario: checking the status of multiple websites. If we did this sequentially, we'd wait for Site A to respond before even asking Site B. With goroutines, we fire off all the requests at once.
package main
import (
"fmt"
"net/http"
"time"
)
func checkSite(url string) {
resp, err := http.Get(url)
if err != nil {
fmt.Printf("[!] %s is down\n", url)
return
}
fmt.Printf("[✓] %s returned %d\n", url, resp.StatusCode)
}
func main() {
sites := []string{
"https://google.com",
"https://golang.org",
"https://github.com",
"https://nonexistent-site-123.com",
}
for _, site := range sites {
// We launch each check in its own goroutine
go checkSite(site)
}
// I'm adding a sleep here just so the program doesn't exit
// before the goroutines finish. In a real app,
// you'd use a WaitGroup or Channels (which we'll cover soon).
time.Sleep(2 * time.Second)
fmt.Println("Finished checking sites.")
}
Notice that the order of the output will be random. Since the goroutines are scheduled independently, the fastest website to respond will likely print its result first, regardless of its position in the slice. This non-deterministic behavior is the price we pay for the massive performance gain.
📋 Practical Task
Exercise: Concurrent Image Processing Simulator
You are building a system that simulates processing high-resolution images. Each "process" takes a different amount of time based on the image size.
Your Goal: Modify the provided code so that the image processing happens concurrently. Currently, the program processes images one by one, which is too slow. You must use the go keyword to ensure all images start processing at the same time.
package main
import (
"fmt"
"time"
)
func processImage(name string, duration time.Duration) {
fmt.Printf("Starting %s...\n", name)
time.Sleep(duration) // Simulate heavy processing
fmt.Printf("Finished %s after %v\n", name, duration)
}
func main() {
images := map[string]time.Duration{
"vacation.jpg": 500 * time.Millisecond,
"portrait.png": 200 * time.Millisecond,
"landscape.webp": 800 * time.Millisecond,
"thumbnail.jpg": 100 * time.Millisecond,
}
for name, duration := range images {
// TODO: Make this call concurrent
processImage(name, duration)
}
// Keep the main function alive long enough to see the results
time.Sleep(1 * time.Second)
fmt.Println("All images processed.")
}
Success Criteria:
- The program should output "Starting..." for all images before it starts outputting "Finished..." for the longer tasks.
- The total execution time should be roughly equal to the longest single task (800ms) rather than the sum of all tasks (1.6s).
There are no comments for now.