Go Advanced: Concurrency, Interfaces & Performance
Go’s concurrency model — goroutines and channels — is the foundation of its success in cloud infrastructure. But building correct, performant concurrent programs requires understanding patterns beyond the basics.
In this tutorial, you’ll master goroutine lifecycle and scheduling, advanced channel patterns (fan-out, fan-in, pipeline, worker pool), the select statement for multiplexing, sync primitives (Mutex, RWMutex, WaitGroup, Once, Cond), idiomatic interface design, pprof profiling, and the Go memory model.
What You’ll Learn
- Goroutine scheduling by the Go runtime (GMP model)
- Channel patterns: fan-out, fan-in, pipeline, tee, drop
- Select multiplexing with timeouts and defaults
- Sync primitives: Mutex, RWMutex, WaitGroup, Once, Cond, Pool
- Interface design: empty interface, type assertions, type switches
- pprof profiling for CPU, memory, goroutines, and blocking
- Go memory model: happens-before, synchronization
Why Advanced Go Matters
Docker, Kubernetes, Prometheus, and Terraform are written in Go. Understanding goroutine scheduling and memory ordering helps you write correct, thread-safe systems. At DodaTech, Doda Browser’s networking layer uses these patterns for concurrent HTTP handling.
Learning Path
flowchart LR
A[Go Basics] --> B[Go Advanced<br/>You are here]
B --> C[Profiling & Optimization]
B --> D[Cloud Services]
style B fill:#f90,color:#fff
Goroutine Deep Dive
Behind the scenes, Go’s runtime implements the GMP model: Goroutines are multiplexed onto OS threads (M) via logical processors (P):
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// Number of logical processors (default: CPU cores)
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
// Goroutine stack starts at 2KB (grows as needed)
// OS thread stack: 1MB+
// You can run millions of goroutines
// Goroutine lifecycle
done := make(chan struct{})
go func() {
fmt.Println("Goroutine running on OS thread",
runtime.LockOSThread) // Pin to OS thread
close(done)
}()
<-done
// Gosched yields the processor
runtime.Gosched()
// NumGoroutine returns count
fmt.Println("Goroutines:", runtime.NumGoroutine())
// Enable preemption info in trace
// go tool trace trace.out
}Expected behavior: The goroutine runs on an OS thread managed by Go’s scheduler. GOMAXPROCS controls how many OS threads execute user-level Go code concurrently.
Advanced Channel Patterns
Fan-Out / Fan-In
Distribute work across multiple goroutines and collect results:
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
results <- job * 2
}
}
func main() {
const numJobs = 10
const numWorkers = 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
// Fan-out: start workers
for w := 1; w <= numWorkers; w++ {
go worker(w, jobs, results)
}
// Send jobs
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// Fan-in: collect results
for r := 1; r <= numJobs; r++ {
<-results
}
}Expected behavior: 3 workers consume jobs concurrently. Results are collected in order — but the actual computation order is interleaved. The fan-out/fan-in pattern is the foundation of Go concurrency.
Pipeline Pattern
Connect stages with channels:
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
// Pipeline: generate -> square -> print
for result := range square(generate(2, 3, 4)) {
fmt.Println(result) // 4, 9, 16
}
}Select Multiplexing
The select statement waits on multiple channel operations:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "one"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "two"
}()
select {
case msg := <-ch1:
fmt.Println("Received from ch1:", msg)
case msg := <-ch2:
fmt.Println("Received from ch2:", msg)
case <-time.After(150 * time.Millisecond):
fmt.Println("Timeout") // Ch1 wins (100ms < 150ms)
default:
fmt.Println("No channels ready") // Non-blocking
}
}Expected behavior: Select picks one ready case at random. If multiple are ready, one is chosen pseudorandomly. The timeout case fires if no channel is ready within 150ms.
Sync Primitives
| Primitive | Purpose | When to Use |
|---|---|---|
| Mutex | Mutual exclusion | Protect shared state from concurrent writes |
| RWMutex | Reader/writer lock | Many readers, few writers |
| WaitGroup | Wait for goroutines | Coordinate completion |
| Once | One-time execution | Singleton initialization |
| Cond | Condition variable | Signal/wait patterns |
| Pool | Object reuse | Reduce allocations |
// RWMutex — concurrent reads, exclusive writes
type SafeCounter struct {
mu sync.RWMutex
value int64
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *SafeCounter) Value() int64 {
c.mu.RLock()
defer c.mu.RUnlock()
return c.value
}Interface Design Patterns
Accept Interfaces, Return Structs
// Define interfaces where they're used, not where they're implemented
type Reader interface {
Read(p []byte) (n int, err error)
}
// Accept interface
func ProcessData(r Reader) error {
buf := make([]byte, 1024)
_, err := r.Read(buf)
return err
}
// Return concrete type
func NewFileReader(path string) *FileReader {
return &FileReader{path: path}
}Type Assertions and Switches
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %q (len=%d)\n", v, len(v))
case fmt.Stringer:
fmt.Printf("Stringer: %s\n", v.String())
default:
fmt.Printf("Unknown type: %T\n", v)
}
}pprof Profiling
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Profile endpoints:
// /debug/pprof/ — index
// /debug/pprof/profile — CPU (30s)
// /debug/pprof/heap — Heap
// /debug/pprof/goroutine — Goroutines
// /debug/pprof/block — Blocking
// Analyze with:
// go tool pprof http://localhost:6060/debug/pprof/heap
// (pprof) top10
// (pprof) web
}# CPU profile (30 seconds)
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile
# Compare heap profiles
go tool pprof -base base.pprof current.pprofCommon Mistakes
1. Goroutine Leaks
A goroutine blocked on channel send/receive that never completes never gets cleaned up. Always ensure channels are closed or have consumers.
2. Copying sync.Mutex
Mutexes must not be copied after first use. Pass by pointer, not by value.
3. Channel Direction Mismatch
A send-only channel can’t be received from. Declare channel directions in function signatures to enforce usage.
4. Accessing Map Without Synchronization
Go maps are not safe for concurrent access. Use sync.Map or protect with a mutex.
5. Ignoring the Zero Value
var mu sync.Mutex is ready to use — no constructor needed. Slices, maps, and channels have useful zero values.
6. Not Using -race Flag
Always test with go run -race or go test -race. The race detector catches data races reliably.
Practice Questions
1. What is the GMP model?
Goroutines (G) are multiplexed onto OS threads (M) via logical processors (P). The scheduler handles distribution.
2. What does select do when multiple cases are ready?
It picks one case pseudorandomly. This prevents starvation.
3. When should you use RWMutex over Mutex?
RWMutex allows concurrent reads while blocking writes. Use when reads significantly outnumber writes.
4. What’s the difference between interface{} and any?
They’re identical. any is an alias for interface{} introduced in Go 1.18.
5. Challenge: Build a rate-limited worker pool.
Create a worker pool where each goroutine is limited to 5 operations per second. Use a ticker channel with select.
Mini Project: Concurrent Web Crawler
Build a concurrent web crawler that respects robots.txt and limits concurrent requests:
type Crawler struct {
client *http.Client
semaphore chan struct{}
visited sync.Map
}
func (c *Crawler) Crawl(url string) {
c.semaphore <- struct{}{}
defer func() { <-c.semaphore }()
if _, loaded := c.visited.LoadOrStore(url, true); loaded {
return
}
resp, err := c.client.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
// Parse links and crawl recursively
}FAQ
What’s Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro