01GMP Scheduler Architecture
M Machine (OS Thread) โ executes G, needs P to run
P Processor โ logical CPU, local run queue (256 G max), GC state
M needs P to run G: M + P โ executes G1 G2 G3...
GOMAXPROCS = number of P's (default: number of CPU cores)
Global queue: Gs not assigned to any P yet
Work-stealing: idle M steals half of busy P's local queue
Scheduler Events โ When Goroutines Yield
Goroutines yield cooperatively at: runtime.Gosched(), channel ops, syscalls, function calls (preemption points added in Go 1.14+). Before 1.14: tight CPU loops could starve other goroutines โ preemptive scheduling solved this via signal-based preemption (SIGURG).
Syscall handling: On blocking syscall, M releases P โ P can be taken by another M โ goroutine blocked on syscall gets its own M. When syscall returns: try to re-acquire P, otherwise park M, goroutine goes to global queue. This is how Go handles 10K+ blocking DB calls.
02Key Concepts
Tricolor GC โ How It Works
- White: not yet visited (candidates for collection)
- Gray: visited, references not yet scanned
- Black: visited, all references scanned (safe)
- Start: all white. Mark roots gray. Process gray โ black.
- Sweep: all remaining white are garbage
- Concurrent: runs alongside user goroutines (write barrier)
GC Tuning โ GOGC
GOGC=100(default): GC when heap doubles- Lower GOGC: more frequent GC, less memory, more CPU
- Higher GOGC: rarer GC, more memory, less CPU overhead
- Go 1.19+:
GOMEMLIMITโ soft memory limit (better than GOGC) runtime.GC(): force collection (testing only)
Channel Internals
- hchan struct: circular buffer + mutex + send/recv queues
- Buffered: copy into buffer if space, else park sender goroutine
- Unbuffered: sender parks until receiver ready (rendezvous)
- Closed channel: receive returns zero value + false
- select: pseudo-random choice among ready cases
Interface Internals
- Interface = two pointers: (type, data) = (itab, value)
- itab: interface type + concrete type + method pointer table
- Type assertion: check itab.type matches expected
- nil interface โ interface with nil value inside
- Dynamic dispatch: one pointer dereference + function call
Memory Model
- Happens-before: goroutine creation, channel ops, sync.Mutex
- No synchronization = data race = undefined behavior
- Race detector:
go run -race(30% overhead, catch races) - sync/atomic: lock-free operations at L1 cache speed
- sync.Map: RWMutex optimized for mostly-reads + many keys
Goroutine Stack
- Initial: 2KB (Go 1.4+, was 8KB before)
- Grows by copying to new location (no stack pointer tricks)
- Shrinks at GC if usage much less than allocated
- Max default: 1GB (can set with SetMaxStack)
- Stack scan at GC: roots for tricolor marking
03Must-Know Deep Dives
๐ฅ Memory Management: Go vs Rust
To master Go, you must understand how its memory model differs from systems languages like Rust. While both are memory-safe, they achieve it through polar opposite strategies.
| Feature | ๐น Go (Runtime-Driven) | ๐ฆ Rust (Compile-Driven) |
|---|---|---|
| Deallocation | Non-deterministic GC (Tricolor Mark & Sweep) | Deterministic RAII (Ownership & Scope) |
| Strategy | Escape Analysis (decides Stack vs Heap) | Explicit control (Stack by default, Heap via Box) |
| Concurrency | Memory safe, but vulnerable to Data Races | Compile-time guarantee against Data Races |
| Overhead | Metadata pointers in every object, GC metadata | Zero-cost abstractions (no runtime metadata) |
๐ฅ Heap Escape Analysis โ The Compiler's Secret
Go tries to keep variables on the Stack because it's nearly free (no GC). If the compiler determines a variable must outlive its function, it "escapes" to the Heap.
- Escapes if: The function returns a pointer to a local variable.
- Escapes if: The variable is too large for the stack.
- Escapes if: The variable is passed to a function taking an
interface{}(likefmt.Println).
๐ฅ pprof โ Profiling Your Go Service
For your notification service (10K+ alerts/sec): start with CPU profile โ find the hot functions. If latency spikes: use trace to see GC pause impact. Goroutine count growing? Check goroutine dump for leak pattern.
๐ฅ Common Performance Pitfalls
๐ฅ Go Generics โ Patterns for Expert Use
๐ฅ nil Interface Trap โ Classic Bug
This bit every Go developer once. Especially dangerous in middleware/wrapper functions. Lesson: always return bare nil for error, never return a typed nil pointer as error interface.
04Go Design โ OOP, Interfaces, Composition
How Go Does OOP
- No classes, inheritance hierarchy, or method overloading
- State lives in
struct, behavior in methods - Polymorphism comes from small interfaces
- Reuse comes from composition, not inheritance
- Encapsulation is package-level: exported vs unexported names
Interfaces โ Idiomatic Rule
- Define interfaces where they are consumed, not where they are implemented
- Prefer tiny interfaces:
io.Reader,io.Writer - Accept interfaces, return concrete types
- Do not create an interface just because one implementation exists today
- Use interface to model behavior boundary: storage, clock, notifier, queue
Composition Over Inheritance
- Embed a struct to reuse fields/methods
- Think in capabilities, not parent-child class trees
- Decorator-style wrapping is natural in Go middleware
- Explicit dependencies are easier to test and reason about
- Best fit for handlers, repositories, service objects, adapters
Struct + Methods + Interfaces = Go's OOP Model
This is the standard Go answer to "how do we do OOP?" Data in structs. Behavior in methods. Polymorphism through interfaces. Dependency injection through constructors. No base class needed.
Method Sets + Pointer vs Value Receivers
Interview rule: if the method mutates state, contains a mutex, or the struct is large, use a pointer receiver. If the type is small and immutable-like, value receiver is fine. What matters is consistency across the type.
Struct Embedding and Composition
Embedding promotes methods, but don't treat it like Java inheritance. Use it sparingly for shared capability. For most business code, explicit named fields are clearer than deep embedding chains.
What Senior Engineers Watch For In Go Design
Interface Size
Large "god interfaces" usually mean weak boundaries. Split by use case, not by entity.
Error Design
Prefer sentinel errors or wrapped errors with errors.Is/errors.As, not giant exception-like hierarchies.
Package Design
Encapsulation in Go is package-first. Keep invariants behind unexported fields and constructors.
Testing
Mock behavior boundaries, not everything. Small interfaces make fake implementations trivial.
Interview Language โ How To Explain OOP in Go
If someone asks "does Go support OOP?", the strongest answer is: Go supports object-oriented design, but not classical inheritance-based OOP. It gives you encapsulation via packages, polymorphism via interfaces, and reuse via composition. In practice this produces flatter, more explicit designs that are easier to test and refactor.
That is the staff-level framing: explain the trade-off, not just the syntax.
05Resources
The Go team explains GC design goals. How they reduced STW from seconds to sub-millisecond. Essential context.
Bill Kennedy's deep dive on GMP. Best external scheduler resource. Read all 3 parts.
25-min deep dive on hchan internals. Watch this if you use channels heavily (your notification service).
Complete profiling API. Integrate into your services โ not just for debugging, for performance SLOs.
Official explanation of the nil interface trap. Short, authoritative.
The official reference for idiomatic interface design, receivers, embedding, and method sets.
Reflection is where interface values, concrete types, and method sets really click together.
Read the schedule() and findRunnable() functions. 1000 lines. Best GMP doc.