Hambz

A Blog to explain some computer science theory

31 Jan 2026

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 synchronized keyword 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.Cond for monitor patterns
  • High-level: Channels with select for message passing
  • Atomic operations: sync/atomic for 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:

  1. Identify race conditions (using the race detector)
  2. Implement four different synchronization approaches:
    • Coarse-grained mutex
    • Fine-grained monitor pattern
    • Reader-writer locks
    • Channel-based architecture
  3. Benchmark and compare the performance trade-offs
  4. 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 conditions
  • go test -bench: Performance comparison
  • go tool trace: Visualize goroutine execution
  • go 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:

  1. Classic Problems Worksheet: Dining Philosophers, Sleeping Barber, Cigarette Smokers—all implemented in Go
  2. Progressive Difficulty: TP-3 felt thin; I’ll expand it with channel-based problems
  3. Theory Mapping Document: Explicit connections between classical OS concepts and Go constructs
  4. 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.

comments powered by Disqus