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
62: Go Memory Model Basics
A few years ago, I was reviewing code for a colleague who had built a high-performance telemetry aggregator. He had a worker goroutine that processed a queue of events in a loop, and to shut it down gracefully, he used a simple boolean flag: stopWorker := false. When the main thread wanted the worker to quit, it just set stopWorker = true. On his local machine, it worked perfectly every time. But once we deployed it to a massive 64-core production server, we started seeing "zombie" workers—goroutines that simply refused to die, continuing to process data long after the shutdown signal was sent. He was baffled because the code was logically sound; the flag was being set, yet the worker loop never saw it.
What he ran into is the "invisible" part of Go: the Go Memory Model. Most of us assume that if Goroutine A writes a value to a variable, Goroutine B will see that value immediately. In reality, that is not guaranteed. Between your code and the hardware, there are compilers and CPUs that love to reorder operations or cache values in registers to make things faster. Without explicit synchronization, Go doesn't guarantee that a write in one goroutine is ever observed by another.
Understanding the "Happens-Before" Relationship
The entire Go Memory Model is built around a concept called the "happens-before" relationship. If event A happens-before event B, then the memory effects of A are guaranteed to be visible to the observer of B. If there is no happens-before relationship, you have a data race, and the behavior is undefined. You might see the correct value, you might see a stale value, or in rare cases, you might see a partially written value.
For example, a channel send happens-before the corresponding channel receive completes. This is why channels are the gold standard for communication in Go; they don't just move data, they create a synchronization barrier that forces the memory to sync across CPU cores. Similarly, a sync.Mutex.Unlock() happens-before a subsequent sync.Mutex.Lock() on the same mutex. When you use these tools, you aren't just preventing concurrent access; you're telling the compiler and the CPU, "Do not reorder these operations, and make sure the other thread sees what I just did."
The Cost of Naked Shared Variables
When you use a "naked" variable—like my colleague's boolean flag—without a mutex, an atomic operation, or a channel, you're gambling. The Go compiler might decide that since the worker loop doesn't modify the stopWorker variable itself, it can just load that value into a CPU register once and reuse it forever. To the CPU, it looks like an optimization. To you, it looks like a bug that only happens on certain hardware architectures.
// DANGEROUS: This may never terminate on some architectures
func worker(stop *bool) {
for !*stop {
// do work
}
}
I've seen people try to "fix" this by using time.Sleep inside the loop, thinking it forces a refresh. Don't do that. It's a band-aid on a bullet wound. The only correct way to handle this is to establish a formal happens-before relationship. Either use a channel to signal the shutdown, protect the flag with a sync.RWMutex, or use the sync/atomic package to ensure the load and store operations are visible across all cores.
📋 Practical Task
Fixing the Ghost-Loop Shutdown Signal
You've been handed a piece of code for a background logger. The developer used a shared boolean to signal when the logger should stop, but it's failing the race detector and occasionally hanging during shutdown in the CI environment.
Your task is to refactor the Logger struct and its Stop() method to ensure the running flag is handled according to the Go Memory Model. You may use sync/atomic or a sync.Mutex, but the goal is to eliminate the data race and guarantee that the run() loop terminates immediately when Stop() is called.
package main
import (
"fmt"
"time"
)
type Logger struct {
running bool // This is the problematic field
}
func (l *Logger) run() {
fmt.Println("Logger started...")
for l.running {
fmt.Println("Logging data...")
time.Sleep(100 * time.Millisecond)
}
fmt.Println("Logger stopped.")
}
func (l *Logger) Stop() {
l.running = false
}
func main() {
l := &Logger{running: true}
go l.run()
time.Sleep(500 * time.Millisecond)
l.Stop()
// Wait a moment to see if it actually stops
time.Sleep(200 * time.Millisecond)
fmt.Println("Main exiting.")
}
There are no comments for now.