Welcome to the Ultimate Go Interview Preparation Guide & Cheat Sheet. This reference has been meticulously designed and engineered to serve as a high-density, easily skimmable, and visually rich reference during live technical screens, system design rounds, or rapid pre-interview revision.
It preserves deep technical accuracy while presenting complex topics in a "scan-and-speak" layout.
Important
When hit with a tough Go question, follow this three-step blueprint:
-
Deliver the 5-Second Answer: State the core mechanism instantly using exact technical keywords (e.g., "A Go channel is a pointer to an
hchanstruct protected by a mutex..."). This proves immediate confidence. -
Draw the Architecture: Reference scheduler entities (
$G, M, P$ ), memory layouts, or tri-color stages. - Reveal the Runtime "Why": Explain the design trade-off (e.g., "Go chose an M:N user-space scheduler to avoid expensive OS kernel context-switches...").
- β‘ One: Core Concurrency & Runtime (The Engine)
- π§Ή Two: Memory Management & GC Internals
- π§© Three: Go Language Deep-Dives
- π Four: Networking & APIs (HTTP vs gRPC)
- ποΈ Five: System Design & Infrastructure
- π₯ Six: The Master Cheat Sheets
- π» Seven: Production-Ready Code Exercises
- π Eight: High-Quality Curated Resources
- Concurrency vs. Parallelism: Concurrency is about structure (handling multiple tasks at once). Parallelism is about execution (simultaneous work on multiple CPU cores).
-
M:N Scheduler: Maps
$M$ Goroutines ($G$ ) onto$N$ OS Threads ($M$ ) using$P$ Logical Contexts. -
Work Stealing: Idle
$P$ steals half of another$P$ 's Local Run Queue (LRQ), checks the Global Run Queue (GRQ) every 61 ticks, or checks network pollers. -
Syscall Handoff: Network I/O detaches
$G$ to the Network Poller (non-blocking). Blocking syscalls detach the OS Thread ($M$ ) from its Processor ($P$ ), allowing$P$ to run other$G$ 's on a different$M$ . -
Preemption: Asynchronous since Go 1.14 using Unix OS signals (
SIGURG) sent every 10ms to interrupt tight, function-less loops. - Goroutine vs Thread: Goroutine starts with a 2 KB dynamic stack (vs. 1-8 MB fixed thread stack), executes in user-space, and has a sub-100ns context-switch time.
-
Channels: Managed by the
hchanstruct. Contains a circular queue buffer, a lock, and waiting sender/receiver linked lists.
Go uses an M:N user-space scheduler to multiplex Goroutines across physical kernel threads without OS overhead. The scheduler maps P) which maintains a local run queue (LRQ). If a local queue is empty, the scheduler will attempt work stealing from other local queues. If local queues are full, it will stack (overflow) goroutines onto the global run queue (GRQ).
graph TD
subgraph GRS ["Go Runtime Scheduler"]
G1[Goroutine G1] -->|scheduled on| P1[Processor P1]
G2[Goroutine G2] -->|waiting in LRQ| P1
P1 -->|bound to| M1[OS Thread M1]
G3[Goroutine G3] -->|scheduled on| P2[Processor P2]
P2 -->|bound to| M2[OS Thread M2]
GQ[Global Run Queue] -->|steal fallback| P1
GQ -->|steal fallback| P2
end
style G1 fill:#00ADD8,stroke:#005F73,stroke-width:2px,color:#fff
style G2 fill:#E0F7FA,stroke:#00ADD8,stroke-width:1px,color:#000
style G3 fill:#00ADD8,stroke:#005F73,stroke-width:2px,color:#fff
style P1 fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff
style P2 fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff
style M1 fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#fff
style M2 fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#fff
style GQ fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff
- G (Goroutine): User-space green thread. Holds execution stack (starts at 2 KB, grows dynamically up to 1 GB), program counter, and scheduling metadata.
- M (OS Thread / Machine): A real OS thread managed by the host OS kernel scheduler.
- P (Processor / Logical Context): Represents resources required to execute Go code. The number of Ps is governed by
$GOMAXPROCS(defaults to CPU cores). Each P maintains a Local Run Queue (LRQ) of up to 256 runnable Goroutines.
- Work Stealing: When a P finishes its LRQ:
- It checks the Global Run Queue (GRQ) (polled 1 out of every 61 scheduler ticks to prevent starvation).
- It attempts to steal half of another P's local run queue.
- If still empty, it checks network pollers.
- Syscall Handoff (Network Poller vs. Syscall):
- Blocking Syscall: When G blocks on a file I/O syscall, the scheduler detaches the running OS thread (M) from its P. The P continues executing other Gs by acquiring or creating a new OS thread (M).
- Network I/O: Handled out-of-band by the Network Poller (using OS abstractions like
epollorkqueue). The G registers its interest, detaches from P, and goes to sleep, freeing the P and M to run other Gs immediately.
- Preemption:
- Cooperative (Pre-Go 1.14): Goroutines could only be preempted at function call boundaries where compiler-injected stack checks (
morestack) occurred. A tight loopfor {}without function calls could freeze a thread. - Asynchronous (Go 1.14+): Uses OS signals (
SIGURGon Unix systems) to interrupt and preempt running goroutines every 10ms, preventing long-running goroutines from hogging CPUs.
- Cooperative (Pre-Go 1.14): Goroutines could only be preempted at function call boundaries where compiler-injected stack checks (
| Feature | Goroutine (Go Green Thread) | OS Thread (Kernel Thread) |
|---|---|---|
| Memory Footprint | Dynamic stack starts at 2 KB (grows/shrinks up to 1GB). | Fixed stack size set by OS (typically 1 MB to 8 MB). |
| Creation Cost | Extremely low (~nanoseconds). Allocated purely in user-space heap. | High (~microseconds). Demands a system call to the OS kernel. |
| Context Switch Cost | Very fast (~10ns - 100ns). Saves ~14 registers. | Slow (~1Β΅s - 2Β΅s). Causes CPU cache line misses, TLB flushes. |
| Scheduling Model | User-space M:N scheduler (cooperative & async signal). | Kernel-space 1:1 scheduler (preemptive). |
A channel is a typed conduit in Go that enables concurrent goroutines to communicate by sending and receiving values, while automatically synchronizing their execution. Channels are the concrete implementation of Communicating Sequential Processes (CSP), aligning with Go's core concurrency philosophy:
"Do not communicate by sharing memory; instead, share memory by communicating."
- Type Safety: Channels are strongly typed (e.g.,
chan int,chan string). A channel can only transmit values of its declared element type. - Synchronization: Beyond data transmission, channels act as synchronization barriers, allowing goroutines to coordinate their execution states without explicit mutexes or condition variables.
- Reference Type: Channels are reference types. When you call
make(chan T), you allocate memory on the heap and receive a pointer to the underlying runtime structure.
Under the hood, a Go channel is not a magical pipe; it is a pointer to an hchan struct (defined in runtime/chan.go):
type hchan struct {
qcount uint // Total data in the circular queue
dataqsiz uint // Capacity of the circular queue (buffer size)
buf unsafe.Pointer // Pointer to an underlying circular array
elemsize uint16
closed uint32 // 1 if closed, 0 otherwise
elemtype *_type // Element type
sendx uint // Send index in the circular buffer
recvx uint // Receive index in the circular buffer
recvq waitq // Linked list of blocked receivers (waitq of sudog)
sendq waitq // Linked list of blocked senders (waitq of sudog)
lock mutex // Protects all fields in hchan
}Warning
This table is the single most tested aspect of Go concurrency in coding screens!
| Channel State | Send (ch <- x) |
Receive (<-ch) |
Close (close(ch)) |
|---|---|---|---|
Nil (var ch chan T) |
Blocks forever | Blocks forever | Panics (panic: close of nil channel) |
| Open & Empty | Succeeds (blocks if unbuffered) | Blocks | Succeeds (receivers get zero-value, ok == false) |
| Open & Full | Blocks | Succeeds | Succeeds (receivers get zero-value, ok == false) |
| Closed | Panics (panic: send on closed channel) |
Succeeds immediately (returns zero-value, ok == false) |
Panics (panic: close of closed channel) |
A buffered channel allows sending values without an immediate receiver until the buffer is full.
- Sender blocks only when the buffer is full
- Receiver blocks only when the buffer is empty
ch := make(chan int, 3) // buffer holds up to 3 valuesAn unbuffered channel has no storage. A send and receive must happen at the same time.
- Sender blocks until a receiver is ready
- Receiver blocks until a sender is ready
ch := make(chan int) // no bufferA race condition (specifically, a data race in Go) occurs when two or more goroutines concurrently access the same memory location, at least one of these accesses is a write, and there is no synchronization (e.g., mutexes, atomic operations, channels) to order the accesses.
sequenceDiagram
autonumber
actor G1 as Goroutine 1
actor G2 as Goroutine 2
participant Mem as Shared Memory (Counter = 42)
G1->>Mem: Read Counter (42)
G2->>Mem: Read Counter (42)
G1->>Mem: Increment & Write Counter (43)
G2->>Mem: Increment & Write Counter (43)
Note over Mem: Lost Update! Expected: 44, Actual: 43.
Here is a classic interview question displaying a severe data race. Run it with multiple workers, and the final count will be less than the target because updates are silently lost!
package main
import (
"fmt"
"sync"
)
func main() {
var count = 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
count++ // β DATA RACE! Read-modify-write is not atomic
}()
}
wg.Wait()
fmt.Printf("Final count: %d (Expected: 1000)\n", count)
}Go provides a built-in ThreadSanitizer-backed race detector.
- Command:
go test -race ./...orgo run -race main.go - Under the Hood: The compiler injects instrumentation at every read and write operation. It records the logical thread clock of accesses. If it detects overlapping unsynchronized access boundaries, it prints a detailed stack trace showing both memory operations.
- Performance Cost: Enabling
-raceincreases CPU overhead by 2x to 20x and memory usage by 5x to 10x. Do not use it in performance-critical production builds, but always use it in local testing and CI/CD pipelines!
sync.Mutex prevents race conditionsβsituations where multiple goroutines try to access a critical section of the code at the same timeβby ensuring that only one Goroutine can enter a critical section at any given time.
Under the hood (in sync/mutex.go), a Mutex is an extremely lightweight struct occupying just 8 bytes:
type Mutex struct {
state int32 // 32-bit state containing lock flags & waiter count
sema uint32 // Semaphore for sleeping/waking blocked waiters
}The 32-bit state integer is bit-packed to avoid alignment overhead:
- Bit 0 (
mutexLocked): 1 if locked, 0 if free. - Bit 1 (
mutexWoken): 1 if a sleeping waiter has been woken and is trying to acquire the lock. - Bit 2 (
mutexStarving): 1 if the Mutex is operating in Starvation Mode. - Bits 3-31 (
waitersCount): Count of goroutines currently queued up and blocked on the semaphore.
+--------------------------------------+----+----+----+
| waitersCount | star|woke|lock|
| (29 bits) | (1) | (1) | (1)|
+--------------------------------------+----+----+----+
31 3 2 1 0 (Bit indices)
- Fast Path (CAS Lock):
If the mutex is completely free (state is 0), the calling goroutine attempts to flip the
mutexLockedbit to 1 using a single atomic Compare-And-Swap (atomic.CompareAndSwapInt32). If it succeeds, it returns immediately. This executes in a single CPU instruction (< 1ns). - Slow Path (Spinning & Enqueueing):
If CAS fails (already locked), the goroutine enters the slow path:
- Active Spinning: If the goroutine is on a multi-core machine and the scheduler predicts the current owner will release the lock soon, it spins (executes a tight loop of
PAUSEinstructions) up to a limit, attempting to acquire the lock when released without leaving the CPU. - Parking (Blocking): If spinning is not allowed or fails, the goroutine increments
waitersCount, registers itself on the lock'ssemaqueue, and calls the runtime functiongopark()to put itself to sleep, yielding its OS thread.
- Active Spinning: If the goroutine is on a multi-core machine and the scheduler predicts the current owner will release the lock soon, it spins (executes a tight loop of
Go's Mutex is highly optimized to balance average throughput and worst-case latency.
| Attribute | Normal Mode | Starvation Mode |
|---|---|---|
| Priority | Favors high overall throughput. Spawns CPU-level competition. | Protects tail-latency. Favors fairness. |
| New Goroutines | Newly active CPU goroutines can steal the lock from queued waiters. | Blocked from stealing the lock. Must enqueue at the tail. |
| Hand-Off | Woken waiter competes with incoming goroutines. Often loses. | Lock ownership is transferred directly to the first waiter. |
| Trigger | Default mode of operation. | A waiter has failed to acquire the lock for |
graph TD
subgraph NM ["Normal Mode (High Throughput)"]
NewG[New Goroutine on CPU] -->|Fights for lock| LockN{Lock}
WaiterG[Woken Waiter from Queue] -->|Fights for lock| LockN
LockN -->|New G usually wins| NewG
end
subgraph SM ["Starvation Mode (Fairness / Low Tail Latency)"]
NewG2[New Goroutine on CPU] -->|Directly Enqueued| Queue[Waiter Queue]
LockS{Lock} -->|Transferred Directly| FrontG[Front Waiter in Queue]
end
style LockN fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#fff
style LockS fill:#FF5722,stroke:#d03b0d,stroke-width:2px,color:#fff
style NewG fill:#00ADD8,stroke:#005F73,stroke-width:2px,color:#fff
style WaiterG fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff
style NewG2 fill:#00ADD8,stroke:#005F73,stroke-width:2px,color:#fff
style FrontG fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff
Using sync.Mutex to protect the critical read-modify-write block ensures data safety:
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // Always unlock inside a defer to prevent deadlocks on panics
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
counter := &SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Printf("Final count: %d\n", counter.Value()) // Guaranteed: 1000
}Warning
Pro-Level Gotcha: Copying Mutexes is Catastrophic! A Mutex contains an internal state. If you pass a struct containing a Mutex by value (copying it), you copy the Mutex's state. If a locked Mutex is copied, the copy is also locked, leading to instant deadlocks.
- Standard Practice: Always pass structs containing Mutexes by pointer or make the receiver a pointer (e.g.
func (c *SafeCounter) ...).
sync.WaitGroup synchronizes the work of multiple concurrent goroutines. It blocks execution and waits for the completion of each of them before proceeding further.
A WaitGroup is defined as:
type WaitGroup struct {
noCopy noCopy // Prevents copying by static checkers (go vet)
state align64State // Bit-packed state representing wait count, waiter count, and sema
}- Wait Counter (32 bits): The number of active goroutines registered with
Add(). - Waiter Count (32 bits): The number of goroutines blocked on
Wait(). - Semaphore (32 bits): Used to wake up the goroutines blocked on
Wait()when the wait counter drops to zero.
wg.Add(delta int): Adds the delta (can be positive or negative) to the wait counter. If the wait counter becomes 0, all blocked goroutines waiting onWait()are woken up via the internal semaphore. If the counter goes negative, the runtime triggers a panic.wg.Done(): Syntactic sugar forwg.Add(-1).wg.Wait(): Blocks the calling goroutine. It checks if the wait counter is 0; if yes, it returns immediately. Otherwise, it increments the waiter count and sleeps on the semaphore.
stateDiagram-v2
[*] --> CounterZero : wg := sync.WaitGroup{}
CounterZero --> CounterPositive : wg.Add(3)
CounterPositive --> CounterPositive : wg.Done() (Counter = 2)
CounterPositive --> CounterPositive : wg.Done() (Counter = 1)
CounterPositive --> CounterZero : wg.Done() (Counter = 0)
CounterZero --> [*] : Wakes up all Waiters!
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
func fetchStatus(url string, wg *sync.WaitGroup) {
defer wg.Done() // Guaranteed to decrease counter on return
client := http.Client{Timeout: 2 * time.Second}
resp, err := client.Get(url)
if err != nil {
fmt.Printf("β %s is down: %v\n", url, err)
return
}
defer resp.Body.Close()
fmt.Printf("β
%s returned HTTP %d\n", url, resp.StatusCode)
}
func main() {
urls := []string{
"https://golang.org",
"https://go.dev",
"https://github.com",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1) // Increment counter BEFORE spawning
go fetchStatus(url, &wg) // Pass WaitGroup as a pointer!
}
fmt.Println("Waiting for all lookups to complete...")
wg.Wait() // Blocks until counter returns to 0
fmt.Println("All lookups finished!")
}Caution
Junior developers break these constantly. Senior candidates are expected to spot them in 2 seconds:
- β Spawning before Adding: Calling
wg.Add(1)inside the spawned goroutine rather than before it.- Why: The parent goroutine can reach
wg.Wait()before the spawned goroutines are scheduled and callAdd(1). The parent will read a counter of 0, assume work is done, and exit immediately! - Fix: Always call
Add()before executinggo worker().
- Why: The parent goroutine can reach
- β Copying by Value: Passing
wgby value into functions (e.g.,func worker(wg sync.WaitGroup)).- Why: It duplicates the internal state. The spawned goroutine calls
Done()on the copy, which has no effect on the parent'swg. The parent blocks forever (deadlock). - Fix: Always pass the WaitGroup by pointer (
*sync.WaitGroup).
- Why: It duplicates the internal state. The spawned goroutine calls
- β Negative Counter Panic: Calling
wg.Done()more times thanwg.Add(1)was called.- Why: Triggers an instant, non-recoverable runtime panic:
panic: sync: negative WaitGroup counter. - Fix: Ensure
wg.Done()matcheswg.Add()exactly, often usingdefer wg.Done().
- Why: Triggers an instant, non-recoverable runtime panic:
- β Concurrent Re-use: Calling
wg.Add()concurrently whilewg.Wait()is already blocking.- Why: Causes a race condition and potential panic or deadlock. A WaitGroup can only be reused once all previous waiters have exited
Wait().
- Why: Causes a race condition and potential panic or deadlock. A WaitGroup can only be reused once all previous waiters have exited
sync.RWMutex: A reader-writer lock. Multiple readers can hold the read lock (RLock), but the write lock (Lock) is completely exclusive. To prevent writer starvation, new readers are blocked if a writer is already waiting.sync.Map: Specialized concurrent map optimized for two cases:- Read-heavy workloads where keys don't change frequently.
- Disjoint concurrent writes (different keys written by different goroutines).
- How it works: Uses two maps: a lockless
readmap (updated atomically) and a lockeddirtymap. Lookups checkreadfirst. If it misses repeatedly (above a threshold), thedirtymap is promoted to thereadmap under a lock.
- Tri-Color GC: Concurrent, low-latency mark-and-sweep. Divides pointers into White (unreachable candidates), Gray (visited, children unscanned), and Black (visited and fully scanned).
- Write Barrier: Intercepts runtime pointers to enforce the GC invariant: ensures a running application doesn't hide a white object behind a black one.
- Stack vs. Heap: Stack is LIFO, managed by threads, zero-GC overhead. Heap is globally shared, TCMalloc-designed, managed by the GC.
- Escape Analysis: Compile-time analysis to decide if memory goes to the stack or escapes to the heap.
- Heap Escape Triggers: Returning local pointers, interface values (dynamic dispatch like
fmt.Println), dynamic/large size slice allocations, and sending pointers over channels.
Go uses a concurrent, tri-color mark-and-sweep garbage collector designed for low latency. The garbage collector starts with all objects initially marked as white. It then scans the roots, marking them gray. Since objects contain references, the GC traverses through those lists to mark the referenced target objects as gray, and once all references from an object are scanned, that parent object becomes black. This traversal continues until there are no more gray objects. Finally, all remaining white objects are garbage collected.
graph LR
subgraph TCGC ["Tri-Color GC State Machine"]
White[White Set<br>Unvisited / Sweep Candidates]
Gray[Gray Set<br>Visited, Unexplored]
Black[Black Set<br>Reachable & Fully Explored]
end
Roots((Roots<br>Stacks, Globals)) -->|1. Mark gray| Gray
Gray -->|2. Scan fields| Black
Gray -.->|3. Discover pointers| White
style Roots fill:#FF5722,stroke:#d03b0d,stroke-width:2px,color:#fff
style White fill:#ECEFF1,stroke:#90A4AE,stroke-width:2px,color:#37474F
style Gray fill:#CFD8DC,stroke:#607D8B,stroke-width:2px,color:#263238
style Black fill:#263238,stroke:#212121,stroke-width:2px,color:#fff
- White Set (Unvisited): Garbage collection candidates. At the start of a GC cycle, all objects are White.
- Gray Set (Visited, Unexplored): Reachable objects to be processed in the future.
- Black Set (Visited & Explored): Reachable, fully explored objects. Black objects contain no pointers directly to White objects.
- Phase 1: Sweep Termination (STW - Stop The World): Prepares for marking, activates write barriers.
- Phase 2: Concurrent Mark: Scans root pointers (stacks, globals) and pushes them onto the gray queue. Goroutines traverse gray objects, mark children gray, and mark parents black.
- Phase 3: Mark Termination (STW): Flushes local cache buffers, completes the marking phase.
- Phase 4: Concurrent Sweep: Reclaims memory occupied by remaining White objects and returns it to the allocator.
Since GC runs concurrently with application threads (mutators), a mutator could hide a white object by assigning it to a black object and breaking the pointer chain from gray objects. The Write Barrier intercepts write operations at runtime: if a pointer to a white object is written, it is forced into the gray set, preserving the GC invariants.
- Stack: Very fast, local allocation. Follows LIFO structure, managed at thread-level, does not require garbage collection.
- Heap: Shared memory pool. Slower allocation, managed via a TCMalloc-inspired allocator (
mcache->mcentral->mheap), reclaimed by the Garbage Collector.
The Go compiler uses Escape Analysis at compile-time to decide if a variable can reside on the stack or must escape to the heap.
Important
Common Causes of Heap Escape:
- Returning a Pointer: A pointer to a local variable is returned from a function. The variable's lifetime exceeds the function's stack frame.
- Interface Dynamic Dispatch: Storing a concrete type in an interface (e.g., calling
fmt.Println(x)orjson.Marshal(x)). - Dynamic / Large Allocations: Creating a slice with a size only known at runtime (e.g.,
make([]byte, size)) or exceeding the compiler's size threshold (~64KB). - Channel Transmission: Sending a pointer to a channel (the compiler cannot guarantee which goroutine receives it or when).
go build -gcflags="-m -l" main.goNote: The -l flag disables function inlining, making escape analysis decisions easier to trace.
- Pass-by-Value: Go strictly passes everything by value (copies the variable's value or header metadata).
- Pointers: Points to a specific location in memory. Passing a pointer copies the 8-byte memory address, still referencing the original data. No pointer arithmetic is allowed for safety.
-
Slices: 24-byte header referencing a backing array. Contains
Datapointer,Len, andCap. -
Slice Growth: Doubles if
$< 256$ elements. For larger sizes, grows by a scaling factor transitioning smoothly toward$1.25\times$ . -
Maps: Pointer to an
hmapstruct holding a collection of 8-item buckets (bmap). Concurrent read/write throws a non-recoverable runtime panic. -
Interface Nil Trap: Interfaces hold
typeanddatafields. An interface is onlynilif both fields arenil. -
Defer: Postpones function execution until the surrounding function returns. Executed in LIFO (Last-In, First-Out) order. Arguments are evaluated immediately when the
deferline is encountered, not when executing.
A pointer is a variable that points to a specific location in memory (storing the memory address of another value).
graph LR
subgraph Mem ["Memory Layout"]
VarX["Variable 'x'<br>Address: 0xc0000140b8<br>Value: 42"]
PtrP["Pointer 'p'<br>Address: 0xc0000140c0<br>Value: 0xc0000140b8"]
end
PtrP -->|points to| VarX
style VarX fill:#BBDEFB,stroke:#1976D2,stroke-width:2px,color:#000
style PtrP fill:#FFE0B2,stroke:#F57C00,stroke-width:2px,color:#000
&(Address-of): Generates a pointer to a variable. E.g.,p := &xstores the address ofxinp.*(Dereference): Accesses or modifies the value at the address stored in a pointer. E.g.,*p = 100modifies the original variablex.
package main
import "fmt"
func main() {
x := 42
p := &x // Type of p is *int (pointer to int)
fmt.Printf("Value of x: %d\n", x) // 42
fmt.Printf("Memory address of x: %p\n", p) // e.g. 0xc0000140b8
fmt.Printf("Value via pointer: %d\n", *p) // 42
*p = 100 // Dereferencing to modify x
fmt.Printf("New value of x: %d\n", x) // 100
}-
Strict Pass-by-Value Semantics: Go is strictly pass-by-value. When a pointer is passed to a function, Go copies the pointer value (the 8-byte memory address). The caller and callee have different pointer variables, but because they hold the same memory address, mutating the dereferenced value inside the function alters the original caller's data.
-
Zero Pointer Arithmetic (Safety): Unlike C/C++, Go does not allow pointer arithmetic (e.g.,
p++to jump to the next memory address is a compile-time error). This prevents buffer overflows, segment faults, and arbitrary memory corruption. For low-level runtime implementation, Go providesunsafe.Pointeranduintptr, but these bypass compile-time safety and garbage collection tracking. -
The
nilPointer Trap: The zero-value of a pointer isnil. Attempting to dereference anilpointer (e.g.,var p *int; *p = 1) triggers an immediate, unrecoverable runtime panic:panic: runtime error: invalid memory address or nil pointer dereference. -
Escape Analysis Decisions: Declaring a pointer or taking a variable's address often triggers Escape Analysis to move the allocation from the stack to the heap. For example, returning a pointer to a local variable from a function causes that variable to escape because its lifetime exceeds the function's stack frame.
-
Value vs. Pointer Receivers:
- Pointer Receiver (
(t *T)): Must be used if the method mutates the receiver's state, or to avoid copying large structs (since copying a pointer is only 8 bytes, whereas copying a large struct degrades performance). - Value Receiver (
(t T)): The method operates on a copy of the receiver. Safe for read-only concurrency, but modifications do not propagate back to the caller.
- Pointer Receiver (
A slice is not an array; it is a descriptor header containing metadata that references a backing array. It is defined in reflect.SliceHeader:
graph TD
subgraph SH ["Slice Header Struct (24 Bytes)"]
Data["Data (uintptr Pointer to Backing Array)"]
Len["Len (int = 3)"]
Cap["Cap (int = 5)"]
end
subgraph BAM ["Backing Array in Memory"]
A0["Array[0] (Reachable)"]
A1["Array[1] (Reachable)"]
A2["Array[2] (Reachable)"]
A3["Array[3] (Capacity Margin)"]
A4["Array[4] (Capacity Margin)"]
end
Data --> A0
style Data fill:#BBDEFB,stroke:#1976D2,stroke-width:2px
style Len fill:#C8E6C9,stroke:#388E3C,stroke-width:2px
style Cap fill:#FFE0B2,stroke:#F57C00,stroke-width:2px
style Slice_Header fill:#F5F5F5,stroke:#9E9E9E
type SliceHeader struct {
Data uintptr // Pointer to the underlying array element
Len int // Length: number of elements in the slice
Cap int // Capacity: maximum elements the backing array can hold
}- Pass-By-Value: Go passes everything by value. Passing a slice into a function copies the 24-byte
SliceHeader. Modifying elements inside the function alters the shared backing array, but appending to the slice within the function may reallocate a new backing array, leaving the caller's slice header unchanged.
- If the new capacity is greater than double the old capacity, the new capacity is set to the requested capacity.
- Otherwise, if the old capacity is less than 256, it doubles.
- For larger capacities, it transitions to a scale factor:
$$\text{newcap} = \text{oldcap} + \frac{\text{oldcap} + 3 \times 256}{4}$$ This ensures a smooth transition from $2\times$ growth to $1.25\times$ growth.
A Go map is a pointer to an hmap struct. Maps are implemented as a collection of buckets:
- Structure: Each bucket (
bmapstruct) holds up to 8 key-value pairs. - Accessing a Key: Go hashes the key. The low-order bits determine which bucket contains the key, and the high-order bits (
tophash) identify the specific key within the bucket. - Why maps are NOT thread-safe: Maps are optimized for speed. Reading/writing maps concurrently sets a
flagsbit inhmap. If the runtime detects a write while another operation is in progress, it triggers a non-recoverable runtime crash:fatal error: concurrent map writes. - How to make them safe: Wrap the map in a struct with a
sync.RWMutex, or usesync.Map.
| Feature | Map (map[K]V) |
Array ([N]T) |
|---|---|---|
| Sizing | Dynamic. Can grow and shrink dynamically as keys are added/removed. | Fixed size. Size is part of the type (e.g. [5]int vs [10]int). |
| Lookup Time |
|
|
| Memory Layout | Non-contiguous bucket structure (hmap pointing to multiple bmap buckets). |
Contiguous block of memory. Highly cache-friendly. |
| Thread Safety | Not thread-safe. Concurrent writes trigger a fatal runtime panic. | Safe for concurrent reads/writes to disjoint indices. |
| Key Types | Any comparable type can be a key. | Indices must be non-negative integers. |
This is one of the most common senior-level Go trick questions.
var p *int = nil
var i any = p
fmt.Println(i == nil) // Outputs: FALSE!An interface variable contains two fields internally:
type(dynamic type information, e.g.,*int).data(pointer to the concrete value, e.g.,nil).
An interface value is only considered nil if both the type and data fields are nil. In the example above, i has a valid type pointer (*int), so i == nil evaluates to false.
CPUs do not read memory one byte at a time; instead, they read in word sizes (typically 8 bytes on a 64-bit architecture). To optimize CPU memory access, compilers use memory alignment, inserting "padding bytes" to ensure fields align to boundaries of their type's size.
Consider the following two structs containing identical fields, but arranged in different orders:
type BadStruct struct {
A bool // 1 byte
B int64 // 8 bytes
C bool // 1 byte
} // Size: 24 bytes! (83% wasted space)
type GoodStruct struct {
B int64 // 8 bytes
A bool // 1 byte
C bool // 1 byte
} // Size: 16 bytes! (25% wasted space)| Byte Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| Row 1 (0-7) | A (bool) |
π« Pad | π« Pad | π« Pad | π« Pad | π« Pad | π« Pad | π« Pad |
| Row 2 (8-15) | B (int64) |
B (int64) |
B (int64) |
B (int64) |
B (int64) |
B (int64) |
B (int64) |
B (int64) |
| Row 3 (16-23) | C (bool) |
π« Pad | π« Pad | π« Pad | π« Pad | π« Pad | π« Pad | π« Pad |
Reason: An 8-byte field must start on an offset that is a multiple of 8. Therefore, 7 bytes of padding are added after A. Another 7 bytes of padding are added at the end of C to ensure the next struct in an array aligns correctly.
| Byte Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| Row 1 (0-7) | B (int64) |
B (int64) |
B (int64) |
B (int64) |
B (int64) |
B (int64) |
B (int64) |
B (int64) |
| Row 2 (8-15) | A (bool) |
C (bool) |
π« Pad | π« Pad | π« Pad | π« Pad | π« Pad | π« Pad |
Reason: Grouping the smaller bool fields together allows them to fit within the same 8-byte word, saving an entire 8-byte block!
Tip
Interview Pro-Tip: To instantly optimize memory usage of a struct, always order fields from largest to smallest.
Go does not support class inheritance. Instead, it embraces composition. Composition means embedding one struct inside another so that the parent struct can use the fields and methods of the embedded struct.
When you define a struct field without an explicit field name, it is an embedded (or anonymous) field. The outer struct automatically gains access to all the fields and methods of the embedded struct. This mechanism is known as field and method promotion.
type Engine struct {
HP int
}
func (e *Engine) Start() {
fmt.Println("Engine starting...")
}
type Car struct {
Engine // Embedded field (no name, only type)
Brand string
}- Composition, Not Subtyping: A
Carcontains anEngine, but aCaris not anEngine. You cannot pass aCarvalue to a function expecting anEngine. There is no polymorphism via struct hierarchy in Go. - Method & Field Promotion: Since
Engineis embedded, we can callcar.Start()and accesscar.HPdirectly, instead of writingcar.Engine.Start()orcar.Engine.HP. - Explicit Access (No Shadowing Lockout): If the outer struct defines a field or method with the same name as an embedded one, it "shadows" (overrides) the embedded one. However, the embedded one is not lost; it remains accessible via its explicit type name:
type Car struct { Engine HP int // Shadows Engine.HP } myCar := Car{} myCar.HP = 200 // Outer HP myCar.Engine.HP = 150 // Inner HP (still fully accessible)
- Interface Satisfaction: If the embedded struct implements an interface, the outer struct automatically implements that interface too via method promotion.
Important
Why Go Rejects Inheritance: Inheritance creates tight coupling between parent and child classes (the fragile base class problem). Struct embedding maintains complete independence between the components while providing the syntactic convenience of field/method delegation.
context.Context is the standardized mechanism in Go for carrying deadlines, cancellation signals, and request-scoped values across API boundaries.
Under the hood, a Context is defined by a simple interface containing four methods:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}emptyCtx: An empty, non-cancelable context with no values (returned bycontext.Background()orcontext.TODO()). It is internally represented as a simple custom integer (type emptyCtx int).cancelCtx: Formed usingWithCancelorWithDeadline. It contains a mutex, a channel closed on cancellation, and a map of children:type cancelCtx struct { Context mu sync.Mutex done atomic.Value // of chan struct{} children map[canceler]struct{} // tracks child cancelCtxs to propagate cancel err error // returns non-nil after cancellation }
valueCtx: Formed usingWithValue. It bundles a single key-value pair and embeds a parent context.
graph TD
Parent[context.Background] -->|WithValue| ValCtx1[valueCtx: userID=42]
ValCtx1 -->|WithCancel| CanCtx[cancelCtx: WithCancel]
CanCtx -->|WithValue| ValCtx2[valueCtx: reqID=abc]
style Parent fill:#CFD8DC,stroke:#607D8B,stroke-width:2px,color:#000
style ValCtx1 fill:#BBDEFB,stroke:#1976D2,stroke-width:2px,color:#000
style CanCtx fill:#FFCDD2,stroke:#D32F2F,stroke-width:2px,color:#000
style ValCtx2 fill:#BBDEFB,stroke:#1976D2,stroke-width:2px,color:#000
Warning
The .Value(key) traverse the context chain upwards recursively. Searching for a key requires
- Rules of Thumb: Never use context values for optional function parameters, configuration objects, or high-throughput transaction stores. Use them exclusively for request-scoped metadata (e.g., Auth tokens, Request IDs, or trace IDs).
Introduced in Go 1.18, generics enable compile-time type safety without sacrificing performance. Different languages implement generics differently:
- C++ (Monomorphization): Generates dedicated compiled code for every unique type argument combination. Results in fast runtime but massive binary bloat.
- Java (Type Erasure): Replaces type parameters with
Objectand inserts dynamic runtime casts. Zero binary bloat but causes heap allocation/boxing overhead.
Go implements a hybrid approach to maintain fast compilation and small binary sizes while keeping execution swift:
- GC-Shape (Gshape) Grouping: The compiler groups type parameters by their garbage collection properties.
- All pointer types (e.g.,
*User,*Item,*string) share the same Gshape because they are represented as 8-byte addresses. - Integral types of the same size share Gshapes.
- All pointer types (e.g.,
- Dictionary Passing: The compiler generates a single, shared binary function for each Gshape. To handle type-specific behaviors (like invoking methods or comparing values), it implicitly passes a type dictionary to the compiled function at runtime containing sizes, hashes, and method pointers.
Go does not use exceptions; errors are values. Go 1.13+ added standardized error wrapping, and Go 1.20 extended this to support multi-error wrapping.
Any error that implements the Unwrap() error method is considered a wrapped error. Since Go 1.20, errors can also implement Unwrap() []error to support multiple parents (e.g., errors.Join()).
errors.Is(err, target): Recursively walks the error wrapper tree usingUnwrap(). Returnstrueif any error in the chain matches thetargetvalue. Useful for checking sentinel errors (e.g.,errors.Is(err, sql.ErrNoRows)).errors.As(err, target): Recursively walks the error chain. If an error matches the type oftarget(which must be a pointer to an error type), it sets the target variable and returnstrue. Useful for extracting custom error structs:
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("Failed path:", pathErr.Path)
}- Idempotency:
GET,PUT,DELETEare idempotent (repeated calls yield identical server states).POST,PATCHare non-idempotent. - HTTP/2 Benefits: Fully binary transport, multiplexes multiple streams over one TCP connection, uses HPACK header compression, and supports Server Push.
- gRPC core: Operates over HTTP/2, uses binary Protobuf serialization, and facilitates four streaming modes: Unary, Client-Streaming, Server-Streaming, and Bidirectional-Streaming.
| Method | Safe | Idempotent | Request Body | Response Body |
|---|---|---|---|---|
| GET | β Yes | β Yes | β No | β Yes |
| POST | β No | β No | β Yes | β Yes |
| PUT | β No | β Yes | β Yes | β Yes/No |
| PATCH | β No | β No | β Yes | β Yes |
| DELETE | β No | β Yes | β No | β Yes/No |
- Safe: Does not modify resource state on the server.
- Idempotent: Making multiple identical requests yields the same server state as a single request.
| Feature | HTTP/1.1 | HTTP/2 |
|---|---|---|
| Transport Format | Plain Text | Binary Framing (Stream/Frame structure) |
| Multiplexing | β No (Requires multiple TCP connections) | β Yes (Concurrent streams over a single TCP connection) |
| Header Compression | β No (Text headers sent repeatedly) | β Yes (HPACK compression) |
| Server Push | β No | β Yes (Server can preemptively push resources) |
- Autoscaling: HPA scales pod counts horizontally (via CPU/metrics). VPA scales pod resources vertically (CPU/memory limits). Do not run both on the same metric!
- Rate Limiting: Token Bucket (allows bursts up to capacity), Leaky Bucket (smooths output to a constant rate), Sliding Window (strict time window accuracy).
- Resiliency: Circuit Breakers fail-fast immediately on open states to protect downstream systems. Backoffs use dynamic exponential intervals with random jitter to prevent thundering herd spikes.
- Horizontal Pod Autoscaler (HPA): Scales the number of pods in response to metrics like CPU usage, Memory limits, or custom Prometheus metrics (e.g., HTTP request rate).
- Vertical Pod Autoscaler (VPA): Adjusts the CPU and Memory limits of existing pods. Useful for stateful services.
- Production Warning: Do not run HPA and VPA concurrently on the exact same resource metrics (like CPU/Memory) to avoid race conditions.
- Token Bucket: A bucket is filled with tokens at a constant rate up to a max capacity. Every request consumes a token. Allows handling bursts up to the bucket capacity.
- Leaky Bucket: Requests enter a queue and are processed at a constant, fixed rate. Smoothes out traffic spikes but adds latency to bursts.
- Sliding Window Counter: Divides time into windows and keeps track of requests. Prevents sudden traffic bursts at window boundaries.
- Circuit Breaker: Prevents cascading failures.
- Closed: Normal traffic flows.
- Open: Fails fast immediately without calling the struggling downstream service.
- Half-Open: Periodically sends a small fraction of traffic to test if the downstream service has recovered.
- Exponential Backoff and Jitter: When retrying failed requests, double the wait time with each retry (exponential) and add a random variance (jitter) to prevent the "thundering herd" problem from overwhelming downstream databases.
This section is engineered to be your primary companion during a live interview. The middle column offers immediate, high-yield answers you can deliver within the first 5 seconds.
| π― The Trick Question | β‘ 5-Second Answer (Immediate Impact) | π Detailed Technical Deep-Dive |
|---|---|---|
| When should you use a pointer receiver? | Use them to modify receiver state or to avoid copying massive structs on method execution. | 1. If the method mutates the struct's internal fields. 2. When passing a large struct by value degrades memory and CPU performance due to deep copy allocations. 3. When maintaining consistent method sets across interfaces. |
| What happens if you write to a nil map? | It triggers an immediate, unrecoverable runtime panic. | A nil map pointer does not reference an initialized hmap struct. To avoid panic: assignment to entry in nil map, you must allocate bucket memory first via make(map[K]V). |
| How do you avoid goroutine leaks? | Ensure every goroutine has a guaranteed exit condition using contexts or closed channels. | 1. Never launch a goroutine without knowing how and when it terminates. 2. Pass a cancelable context.Context to abort waiting workers.3. Avoid blocking sends on unbuffered channels where no receiver is active. |
Why does Go use make vs. new? |
new allocates zeroed memory returning *T; make initializes internal headers for slices, maps, and channels. |
- new(T) returns a pointer (*T) to a zero-filled type T (works on all types).- make(T, args) is restricted exclusively to slices, maps, and channels; it sets up complex internal runtime headers (like backing arrays, hmap bucket pointers, or hchan circular buffers). |
| How does Go 1.22 fix the loop variable bug? | Loop variables are now freshly allocated per iteration, resolving closures capturing the same address. | Prior to 1.22, a loop variable shared a single memory address across all iterations. Goroutines launched inside the loop captured that same pointer, leading to race conditions. Since 1.22, the compiler generates a new scope and allocation per iteration. |
| How do you safely detect race conditions? | Execute your test suite or runtime binary with the -race compiler flag. |
Enabling the race detector (go test -race or go run -race) injects thread instrumentation that tracks memory access boundaries. It raises warnings if two concurrent goroutines access the same memory location, where at least one access is a write, without synchronization. |
How does errors.Is differ from standard == error checks? |
errors.Is traverses the entire wrapped error chain, whereas == only checks direct pointer equality. |
- Standard == fails if an error has been wrapped using %w (e.g. fmt.Errorf("wrapped: %w", err)).- errors.Is recursively unwraps the error to inspect if the target sentinel error exists anywhere in its tree structure. |
| π― The Trick Question | β‘ 5-Second Answer (Immediate Impact) | π Detailed Technical Deep-Dive |
|---|---|---|
| How do you tune Go's GC? | Optimize memory cycles using GOGC for target heap ratios and GOMEMLIMIT to prevent OOMs. |
1. GOGC (default 100): Governs target heap growth ratio. A value of 100 means GC runs when live heap size doubles.2. GOMEMLIMIT (Go 1.19+): Defines a hard memory limit. Prevents OOM kills in containerized environments by triggering aggressive GC sweeps as memory usage approaches the limit. |
| Explain Mutex Starvation mode. | It prevents waiter starvation by transferring the lock directly to the first queue waiter if wait time exceeds 1ms. | - Normal Mode: Waiters queue, but newly spawned active goroutines on the CPU often steal the lock because they are already scheduled. - Starvation Mode: Triggered if a waiter has waited |
| How do you profile a Go service in production? | Integrate net/http/pprof to extract and analyze low-overhead flamegraphs. |
Pull interactive, low-overhead performance charts directly using go tool pprof. You can collect CPU, memory, thread creation, and blocking profiles with negligible impact on live requests, helping to identify lock contention or excessive allocations. |
| What is Profile-Guided Optimization (PGO)? | PGO lets the compiler optimize code generation using real performance profiles collected from production. | Introduced in Go 1.20, PGO allows the compiler to optimize code generation (e.g., devirtualizing interface calls, aggressive function inlining) using real performance profiles collected from production, boosting CPU efficiency by 2% to 14%. |
| What are contiguous stacks in Go? | Go uses dynamic contiguous stacks that automatically double in size and copy values when exhausted. | If a goroutine requires more stack space than its current frame provides, Go allocates a new, double-sized contiguous memory block, copies the old stack, updates all pointers, and frees the old block. |
| Why are maps not concurrent safe? | To maximize raw speed; Go chooses immediate crashes over silent data corruption on concurrent writes. | To maximize execution speed. Concurrent map writes set a flag; if Go detects simultaneous read/write or write/write operations, it calls throw() to trigger an immediate, non-recoverable runtime crash. |
Why is uintptr unsafe to store pointers? |
Because the GC does not track uintptr, so the referenced memory can be garbage-collected at any time. |
- unsafe.Pointer is a real pointer tracked by the garbage collector.- uintptr is a plain integer. If a heap-allocated object is only referenced via a uintptr (after conversion), the GC treats it as unreachable and may sweep it, leading to dangling pointers/corruption. |
| Why does context lookup take |
Each WithValue call creates a new node in a singly-linked list traversing upwards, rather than using a hash map. |
- Context is designed to be immutable and concurrent-safe without complex locking/synchronization. - Storing values creates a parent-child chain. Resolving .Value(key) recursively bubbles up to the root. Thus, depth |
| Does Go support class inheritance? | No; Go uses composition via struct embedding, which promotes fields and methods but does not establish a subtype relation. | 1. No subclassing: Embedding anonymous structs promotes fields/methods to the outer type, but doesn't allow subtyping (you cannot pass the outer struct where the inner type is expected). 2. Delegation, not inheritance: The outer struct delegates calls to the inner struct. Fields can be shadowed (overridden), but the inner fields remain explicitly accessible via their type name. |
These templates demonstrate clean coding styles, precise concurrency control, and standard idioms expected in senior interviews.
This is a standard senior-level interview task requiring generics, concurrent read-write synchronization, and a background janitor to prune expired entries.
π οΈ Click to view Cache Implementation
package cache
import (
"sync"
"time"
)
// Option configures the Cache at construction time.
type Option func(*cacheConfig)
type cacheConfig struct {
defaultTTL time.Duration // 0 means no default expiration
cleanupEvery time.Duration // 0 means background cleanup is disabled
}
// WithDefaultTTL configures the default lifetime of items in the cache.
func WithDefaultTTL(ttl time.Duration) Option {
return func(c *cacheConfig) { c.defaultTTL = ttl }
}
// WithCleanupInterval configures how often the background cleanup janitor runs.
func WithCleanupInterval(every time.Duration) Option {
return func(c *cacheConfig) { c.cleanupEvery = every }
}
type entry[V any] struct {
value V
expireAt time.Time // Zero value means no expiration
}
// Cache is a high-performance, concurrent-safe in-memory key-value store.
type Cache[K comparable, V any] struct {
mu sync.RWMutex
items map[K]entry[V]
defaultTTL time.Duration
cleanupEvery time.Duration
stopCh chan struct{}
doneCh chan struct{}
}
// New creates a new, optimized Cache instance.
func New[K comparable, V any](opts ...Option) *Cache[K, V] {
cfg := cacheConfig{}
for _, o := range opts {
o(&cfg)
}
c := &Cache[K, V]{
items: make(map[K]entry[V]),
defaultTTL: cfg.defaultTTL,
cleanupEvery: cfg.cleanupEvery,
}
if c.cleanupEvery > 0 {
c.startJanitor()
}
return c
}
// Close stops the background janitor cleanly, blocking until it exits.
func (c *Cache[K, V]) Close() {
if c.stopCh == nil {
return
}
close(c.stopCh)
<-c.doneCh
}
// Set inserts or updates a key-value pair using the default TTL.
func (c *Cache[K, V]) Set(key K, value V) {
c.SetWithTTL(key, value, c.defaultTTL)
}
// SetWithTTL inserts or updates a key-value pair with a specific custom TTL.
func (c *Cache[K, V]) SetWithTTL(key K, value V, ttl time.Duration) {
var exp time.Time
if ttl > 0 {
exp = time.Now().Add(ttl)
}
c.mu.Lock()
c.items[key] = entry[V]{value: value, expireAt: exp}
c.mu.Unlock()
}
// Get retrieves a value by key. Handles lazy deletion on cache misses.
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
e, ok := c.items[key]
if !ok {
c.mu.RUnlock()
var zero V
return zero, false
}
expired := !e.expireAt.IsZero() && time.Now().After(e.expireAt)
value := e.value
c.mu.RUnlock()
if !expired {
return value, true
}
// Double-check expiration under a write lock to perform lazy deletion safely
c.mu.Lock()
if e2, ok2 := c.items[key]; ok2 && !e2.expireAt.IsZero() && time.Now().After(e2.expireAt) {
delete(c.items, key)
}
c.mu.Unlock()
var zero V
return zero, false
}
// Pop removes and returns an item from the cache.
func (c *Cache[K, V]) Pop(key K) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
e, ok := c.items[key]
if !ok {
var zero V
return zero, false
}
if !e.expireAt.IsZero() && time.Now().After(e.expireAt) {
delete(c.items, key)
var zero V
return zero, false
}
delete(c.items, key)
return e.value, true
}
func (c *Cache[K, V]) startJanitor() {
c.stopCh = make(chan struct{})
c.doneCh = make(chan struct{})
go func() {
defer close(c.doneCh)
ticker := time.NewTicker(c.cleanupEvery)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.removeExpired()
case <-c.stopCh:
return
}
}
}()
}
func (c *Cache[K, V]) removeExpired() {
now := time.Now()
c.mu.Lock()
for k, e := range c.items {
if !e.expireAt.IsZero() && now.After(e.expireAt) {
delete(c.items, k)
}
}
c.mu.Unlock()
}This design demonstrates standard channel orchestration, clean synchronization, and immediate shutdown on parent contexts aborting.
π οΈ Click to view Worker Pool Implementation
package pool
import (
"context"
"fmt"
"sync"
"time"
)
// Task represents a unit of concurrent work.
type Task struct {
ID int
Data string
}
// Result represents the outcome of a processed Task.
type Result struct {
TaskID int
Output string
Err error
}
// WorkerPool manages the concurrent processing of tasks.
type WorkerPool struct {
numWorkers int
tasksChan chan Task
resultsChan chan Result
wg sync.WaitGroup
}
// NewWorkerPool initializes a new WorkerPool.
func NewWorkerPool(numWorkers int) *WorkerPool {
return &WorkerPool{
numWorkers: numWorkers,
tasksChan: make(chan Task),
resultsChan: make(chan Result),
}
}
// Start spawns the workers and prepares them to listen for tasks.
func (wp *WorkerPool) Start(ctx context.Context) {
for i := 1; i <= wp.numWorkers; i++ {
wp.wg.Add(1)
go wp.worker(ctx, i)
}
}
func (wp *WorkerPool) worker(ctx context.Context, workerID int) {
defer wp.wg.Done()
for {
select {
case <-ctx.Done():
// Exit worker immediately on context cancellation
return
case task, ok := <-wp.tasksChan:
if !ok {
// Tasks channel closed, exit gracefully
return
}
output, err := wp.process(ctx, task)
select {
case <-ctx.Done():
return
case wp.resultsChan <- Result{TaskID: task.ID, Output: output, Err: err}:
}
}
}
}
func (wp *WorkerPool) process(ctx context.Context, t Task) (string, error) {
// Simulate work or network request
select {
case <-time.After(50 * time.Millisecond):
case <-ctx.Done():
return "", ctx.Err()
}
if t.ID%5 == 0 {
return "", fmt.Errorf("simulated error for task %d", t.ID)
}
return fmt.Sprintf("Success processing data: %s", t.Data), nil
}
// Submit sends a task into the queue.
func (wp *WorkerPool) Submit(task Task) {
wp.tasksChan <- task
}
// Results returns a read-only channel for collecting outputs.
func (wp *WorkerPool) Results() <-chan Result {
return wp.resultsChan
}
// Stop cleanly terminates all workers and closes open channels.
func (wp *WorkerPool) Stop() {
close(wp.tasksChan)
wp.wg.Wait()
close(wp.resultsChan)
}This design implements a thread-safe Token Bucket rate limiter using lock-based synchronization and time math rather than high-overhead background tickers, ensuring sub-10ns evaluations.
π οΈ Click to view Rate Limiter Implementation
package rate
import (
"sync"
"time"
)
// Limiter implements a high-performance thread-safe rate limiter.
type Limiter struct {
mu sync.Mutex
capacity float64
tokens float64
ratePerSec float64
lastRefilled time.Time
}
// NewLimiter creates a new Token Bucket Limiter.
func NewLimiter(capacity, ratePerSec float64) *Limiter {
return &Limiter{
capacity: capacity,
tokens: capacity,
ratePerSec: ratePerSec,
lastRefilled: time.Now(),
}
}
// Allow is shorthand for AllowN(now, 1).
func (l *Limiter) Allow() bool {
return l.AllowN(time.Now(), 1)
}
// AllowN checks if n tokens can be consumed. Refills dynamically.
func (l *Limiter) AllowN(now time.Time, n float64) bool {
l.mu.Lock()
defer l.mu.Unlock()
// Dynamic lazy refill instead of resource-intensive active tickers
elapsed := now.Sub(l.lastRefilled).Seconds()
l.lastRefilled = now
l.tokens += elapsed * l.ratePerSec
if l.tokens > l.capacity {
l.tokens = l.capacity
}
if l.tokens >= n {
l.tokens -= n
return true
}
return false
}- Official Documentation:
- Effective Go β The definitive style guide.
- Go Memory Model β In-depth details on execution ordering and synchronization invariants.
- Deep-Dive Reading:
- Go 101 β Structural mechanics, semantic rules, and internals.
- High Performance Go (Dave Cheney) β Crucial guidelines for profiling, heap layout, and optimization.
- Tools & Profiling:
go tool pprofβ Native CPU and memory profiling system.go tool traceβ Advanced tracer for viewing execution bottlenecks.
Maintained with β€οΈ for the Go engineering community.