Teaching Concurrency with Go: A Non-Conventional Approach to Advanced Operating Systems
Teaching Concurrency with Go: A Non-Conventional Approach to Advanced Operating Systems
Why I Ditched Java and C++ for Goroutines
As I prepared to teach Advanced Operating Systems to 4th-year undergraduate students this semester, I faced a familiar dilemma: stick with the traditional approach (Java or C++) or try something different. After years of watching students struggle with segmentation faults when they should be thinking about semaphores, I decided to take a risk.
I chose Go. And it changed everything.
The Legacy: From LOTOS to Practice
Here’s some context: our program has a history of teaching process algebra, specifically LOTOS (Language Of Temporal Ordering Specification). Students learn formal specification and reasoning about concurrent systems using process algebra notation.
When it came time to choose the implementation language for this OS course, I realized Go’s CSP-based model could create some nice connections between the formal theory students had seen and practical implementation. An added bonus rather than a complete redesign.
The Problem with Traditional Approaches
Don’t get me wrong—teaching concurrency with Java or C++ has merit. But here’s what I observed over the years:
- Students spent 60% of their time debugging memory issues instead of understanding synchronization concepts
- Java’s
synchronizedkeyword is too abstract – it hides what’s actually happening under the hood (monitor entry/exit, wait sets, etc.) - Threading models felt disconnected from modern cloud-native systems
- Boilerplate code obscured the core concepts
When a student’s critical section implementation crashes because of a dangling pointer rather than a logical flaw in their mutex algorithm, something is wrong with our pedagogy.
When students use synchronized without understanding monitors, wait queues, or what the JVM is actually doing, they’re memorizing patterns rather than learning concepts.
Why Go? Three Core Reasons
1. CSP: A Different Lens on Concurrency
Here’s where it got interesting: Go’s concurrency model is based on Communicating Sequential Processes (CSP), formalized by Tony Hoare in 1978—the same theoretical foundation underlying LOTOS and other process algebras.
Instead of teaching students process algebra theory in one course and then using a completely different mental model for practical implementation, Go allowed for some nice pedagogical connections:
- LOTOS taught them: Process composition, synchronization through communication, formal reasoning
- Go let them see: Similar concepts in production code
Go’s implementation of CSP through channels introduced my students to message-passing concurrency alongside traditional shared-memory approaches. Students who had studied LOTOS could see some familiar patterns.
Consider the classic producer-consumer problem:
LOTOS specification (conceptual):
process Producer [send] : exit :=
send !item; Producer [send]
endproc
process Consumer [receive] : exit :=
receive ?item; Consumer [receive]
endproc
Go implementation:
func producer(ch chan<- Item, item Item) {
ch <- item // Send action - similar idea to LOTOS
}
func consumer(ch <-chan Item) Item {
return <-ch // Receive action
}
For comparison, the traditional approach with locks is a different paradigm entirely:
var mu sync.Mutex
var buffer []Item
func produce(item Item) {
mu.Lock()
buffer = append(buffer, item)
mu.Unlock()
}
The connection wasn’t perfect or necessary, but it was a nice bonus. One student mentioned: “This reminds me of what we did in LOTOS specs.”
More importantly, students learned both paradigms (shared-memory AND message-passing), which is valuable regardless of the LOTOS connection.
2. Diverse Synchronization Primitives
Go offered the perfect sandbox for comparing synchronization approaches:
- Low-level:
sync.Mutex,sync.RWMutex(classical shared-memory) - Mid-level:
sync.Condfor monitor patterns - High-level: Channels with
selectfor message passing - Atomic operations:
sync/atomicfor lock-free programming - Specialized:
sync.WaitGroup,sync.Once,sync.Map
In one language, students could explore the entire spectrum of synchronization strategies and compare their trade-offs empirically.
3. The Race Detector: A Teaching Superpower
This deserves its own section. Go’s built-in race detector (go run -race) became my most powerful teaching tool.
Before:
- Student: “My program works most of the time, so it’s correct, right?”
- Me: “Well, actually…” launches into explanation of non-deterministic behavior
- Student: confused stare
After:
- Student: “My program works!”
- Me: “Run it with -race flag”
- Student: sees 47 race condition warnings
- Me: “Still works?”
- Student: enlightenment achieved
The immediate, visual feedback transformed abstract concepts into concrete problems they could see and fix.
The Course Structure
I designed the course as a progression through synchronization complexity:
TP-1: Go Fundamentals & First Goroutines
Students get comfortable with Go syntax and spawn their first goroutine. Simple, friendly introduction.
TP-2: Classical Mutual Exclusion Algorithms
Peterson’s algorithm, Bakery algorithm—implementing the classics. This grounds them in theory before introducing high-level primitives.
TP-3-5: Synchronization Primitives Deep Dive
- Channels vs. locks for producer-consumer
- Reader-writer locks for database-like scenarios
- Monitor patterns with condition variables
TP-6: Distributed Scenarios
Client-server architectures where synchronization crosses process boundaries.
Mini-Project: Database Synchronization
The capstone: students receive an intentionally unsynchronized database implementation. Their task:
- Identify race conditions (using the race detector)
- Implement four different synchronization approaches:
- Coarse-grained mutex
- Fine-grained monitor pattern
- Reader-writer locks
- Channel-based architecture
- Benchmark and compare the performance trade-offs
- Write a technical report analyzing their results
This project became the heart of the course—students told me it was the first time they truly understood why different synchronization primitives exist.
What Worked Exceptionally Well
1. Lower Cognitive Load
Without manual memory management, students focused on concurrency concepts rather than pointer arithmetic. A student once told me: “I finally understand what a critical section IS instead of just where to put the lock.”
2. Fast Feedback Loops
- Compilation: ~1 second (vs. 30+ seconds for large C++ projects)
- Race detection: built-in and fast
- Testing: first-class citizen with
go test
Students could iterate rapidly, trying different approaches without the friction of slow compile times.
3. Real-World Relevance
When I mentioned that Docker, Kubernetes, and Prometheus are all written in Go, students perked up. This wasn’t theoretical computer science—these were skills they’d use in industry.
4. Visual Understanding Through Tooling
go test -race: Detect race conditionsgo test -bench: Performance comparisongo tool trace: Visualize goroutine executiongo tool pprof: Lock contention analysis
These tools made invisible concurrency bugs visible.
The Challenges (And How We Addressed Them)
Challenge 1: “Why Not Just Use Channels Everywhere?”
Students initially gravitated toward channels for everything—they’re elegant and feel “Go-ish.” But this missed the point.
Solution: I made them implement the same problem four different ways and benchmark each. When they saw that a simple mutex was 3x faster for their counter increment scenario, the “right tool for the job” lesson clicked.
Challenge 2: Goroutines Hide Too Much
Goroutines are too lightweight. Students struggled to appreciate the cost of thread creation because go func() made it seem free.
Solution: We analyzed the mini-project with increasingly ridiculous numbers of goroutines (1, 10, 100, 10,000, 100,000). Watching performance degrade taught them that abstractions have costs.
Challenge 3: Less Classical OS Theory
Go abstracts away OS threads, which meant less exposure to classical OS constructs like futexes and kernel-level synchronization.
Solution: I paired the practicals with theory lectures that mapped Go constructs to underlying OS primitives. For example:
- “A goroutine blocked on a channel? That’s essentially a futex wait.”
- “RWMutex? Linux has
pthread_rwlock_t, Go just makes it nicer.”
Unexpected Benefits
1. Students Taught Me
One student implemented a lock-free algorithm using sync/atomic that I hadn’t considered for the mini-project. Another used context.Context for graceful cancellation in their distributed scenario. Go’s modern features invited creative exploration.
2. Better Reports
Because testing and benchmarking were frictionless, students produced data-driven reports with graphs comparing their implementations. Their analysis went deeper than previous years.
3. Cross-Course Synergy
Students taking distributed systems or cloud computing courses simultaneously found immediate connections. One student said: “Wait, this goroutine pattern is exactly how Kubernetes controllers work!”
Lessons Learned for Next Time
What I’ll Add:
- Classic Problems Worksheet: Dining Philosophers, Sleeping Barber, Cigarette Smokers—all implemented in Go
- Progressive Difficulty: TP-3 felt thin; I’ll expand it with channel-based problems
- Theory Mapping Document: Explicit connections between classical OS concepts and Go constructs
- Failure Scenarios: Dedicated exercises on common deadlocks and how to debug them
What I’ll Keep:
- The race detector demonstrations (absolute gold)
- The multi-approach mini-project
- Benchmarking requirements
- The “intentionally broken” starter code approach
Advice for Other Educators
If you’re considering Go for teaching concurrency:
Do this if:
- Your focus is synchronization concepts rather than low-level OS implementation
- You want students to explore multiple paradigms (shared-memory AND message-passing)
- You value rapid iteration and modern tooling
- Your students will work with cloud-native systems
- You teach process algebra or formal methods – Go provides a practical implementation of CSP theory
Especially do this if:
- You have a LOTOS or CSP background in your curriculum – the pedagogical continuity is invaluable
Stick with C++/Java if:
- You’re teaching OS internals (schedulers, memory management)
- You need direct exposure to POSIX threads
- Your curriculum is deeply tied to classical textbook examples in those languages
- You don’t teach process algebra (the CSP connection won’t resonate)
Hybrid approach: Use Go for concurrency/synchronization modules and C for kernel-level topics. Best of both worlds.
For process algebra instructors specifically: If your students learn LOTOS, CSP, CCS, or similar formal specification languages, Go is a natural bridge from theory to implementation. The mental models align, the syntax mirrors the semantics, and students see their abstractions become concrete.
The Verdict
After one semester, I’m convinced this was the right call. Student engagement was higher, conceptual understanding was deeper, and practical skills were more relevant to modern systems.
One student’s final reflection captured it perfectly:
“I took this course expecting to endure it. Instead, I found myself excited about concurrency—something I didn’t think was possible. Go made it feel like building things instead of fighting the language.”
That’s the metric that matters.
Will I use Go again next year? Absolutely. Will I keep refining the approach? Also absolutely. Teaching is itself an iterative process—much like goroutines converging on a correct synchronization solution.
Resources
If you’d like to try this approach, I’ve open-sourced all course materials:
- Workshop problems (TP-1 through TP-7)
- Mini-project with starter code and reference solutions
- Comprehensive test suites
- Instructor guides with grading rubrics
The repository includes intentionally broken code, race detector demonstrations, and multiple synchronization approaches for each problem—everything you need to run a similar course.
Essential Go Resources
For students new to Go, these resources were invaluable:
- Official Go Documentation – Comprehensive documentation including the language specification, effective Go guide, and standard library reference
- A Tour of Go – Interactive introduction to Go’s basics
- Introduction to Go Programming Language – Excellent textbook covering fundamentals through advanced concurrency patterns
- Go Concurrency Patterns – Official blog posts on concurrency patterns and best practices
- The Go Memory Model – Essential reading for understanding synchronization guarantees
Closing Thoughts
Programming languages are tools, but they’re also lenses through which students see concepts. By changing the lens from Java/C++ to Go, I didn’t just change syntax—I changed how students think about concurrency.
And that’s worth breaking with tradition.
Have you tried teaching OS concepts with non-traditional languages? What worked for you? I’d love to hear about your experiences in the comments below.