Chapter 01 — Foundation

WHY GO?
THE CASE.

Go was designed at Google to solve the problems they had at scale: slow compile times, complex dependency trees, difficult concurrency, and too many languages in one codebase. It solves all four.

The Problem Go Solves

By 2007, Google had millions of lines of C++, Java, and Python. C++ compile times were measured in hours. Java's runtime overhead was significant. Python couldn't handle the concurrency they needed. Rob Pike, Ken Thompson, and Robert Griesemer designed Go to fix all three.

The Core Promise: Fast compilation (seconds not minutes), garbage collected (safe like Java), statically typed (errors caught early), built-in concurrency (goroutines and channels), single binary output (no runtime dependencies).

Speed Numbers That Matter

When to Use Go

When NOT to Use Go

Key Insight: Go is not trying to be everything. It does backend services, infrastructure, and data systems extremely well. In combination with Rust (for systems code) and Python (for ML), it covers 95% of what a modern tech company needs.

Hello, Production

package main

import (
    "fmt"
    "net/http"
    "log"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "alive")
    })
    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
    // Single binary. No Dockerfile needed to test. Compiles in <1s.
}

This is a production-ready HTTP server. No framework, no dependencies beyond stdlib. Build it: go build -o server . && ./server. That's Go's design philosophy — the standard library is enough for most things.

Chapter 02 — Foundation

TYPES,
STRUCTS &
INTERFACES

Go's type system is simple by design. No inheritance, no generics overload, no abstract classes. Composition over inheritance. Interfaces satisfied implicitly. It forces clean design.

Structs Are Your Data

type Order struct {
    ID       string
    Symbol   string
    Qty      int
    Price    float64
    Side     OrderSide  // custom type
}

type OrderSide string

const (
    Buy  OrderSide = "BUY"
    Sell OrderSide = "SELL"
)

// Methods attach behaviour to types
func (o Order) Total() float64 {
    return float64(o.Qty) * o.Price
}

Interfaces: Implicit Satisfaction

Go interfaces are the most important concept in the language. A type satisfies an interface simply by having the required methods — no declaration needed.

type Executor interface {
    Execute(o Order) error
    Cancel(id string) error
}

// DhanBroker satisfies Executor without saying so
type DhanBroker struct{ token string }

func (d *DhanBroker) Execute(o Order) error { /* ... */ return nil }
func (d *DhanBroker) Cancel(id string) error  { /* ... */ return nil }

// ZerodhaBroker also satisfies Executor — swap without changing calling code
type ZerodhaBroker struct{ apiKey string }
func (z *ZerodhaBroker) Execute(o Order) error { /* ... */ return nil }
func (z *ZerodhaBroker) Cancel(id string)  error  { /* ... */ return nil }

// This function works with ANY broker — decoupled from implementation
func PlaceOrder(broker Executor, order Order) error {
    return broker.Execute(order)
}
Design Rule: Accept interfaces, return concrete types. Functions that accept interfaces work with anything satisfying them. Functions returning concrete types give callers full access to the type's capabilities.

Embedding: Composition Over Inheritance

type BaseRepo struct{ db *sql.DB }
func (b *BaseRepo) Begin() (*sql.Tx, error) { return b.db.Begin() }

type OrderRepo struct {
    BaseRepo  // embedded — gets Begin() for free
}

// o.Begin() works — promoted from BaseRepo
Chapter 03 — Foundation

ERROR
HANDLING

Go has no exceptions. Errors are values. Every function that can fail returns an error as the last return value. This is verbose but honest — failure paths are explicit and visible.

The Basic Pattern

func ReadConfig(path string) (Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return Config{}, fmt.Errorf("ReadConfig: %w", err)
    }
    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return Config{}, fmt.Errorf("ReadConfig: parse: %w", err)
    }
    return cfg, nil
}

Wrapping Errors: %w

Use fmt.Errorf("context: %w", err) to wrap errors with context. Callers can use errors.Is() and errors.As() to inspect the chain.

Sentinel Errors and Custom Types

var ErrNotFound     = errors.New("not found")
var ErrUnauthorized = errors.New("unauthorized")

type ValidationError struct {
    Field   string
    Message string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation: %s: %s", e.Field, e.Message)
}

// Caller can detect specific types:
var ve *ValidationError
if errors.As(err, &ve) {
    // handle validation error specifically
}
Anti-pattern: Don't ignore errors with _. Don't use panic for normal error paths. Don't wrap errors more than 2–3 levels deep. Each wrap should add context, not noise.
Chapter 04 — Concurrency

GOROUTINES
& CHANNELS

Goroutines are Go's fundamental concurrency primitive. They're not threads — they're lightweight coroutines multiplexed by the Go runtime onto OS threads. You can have millions. Channels are how they communicate.

Goroutines: Start Cheap

func fetchPrices(symbols []string) []float64 {
    results := make([]float64, len(symbols))
    var wg sync.WaitGroup

    for i, sym := range symbols {
        wg.Add(1)
        go func(i int, sym string) {  // capture i and sym by value
            defer wg.Done()
            results[i] = getPrice(sym)  // safe: each i is unique
        }(i, sym)
    }

    wg.Wait()
    return results
}

Channels: Typed Pipes

func pipeline() {
    jobs  := make(chan string, 100)
    done  := make(chan struct{})

    // producer
    go func() {
        for _, sym := range symbols { jobs <- sym }
        close(jobs)
    }()

    // 5 workers
    for i := 0; i < 5; i++ {
        go func() {
            for job := range jobs {
                process(job)
            }
        }()
    }
}
Rule: Don't communicate by sharing memory — share memory by communicating. Channels enforce ownership transfer. Mutexes protect shared state. Use channels for pipelines and coordination, mutexes for caches and counters.
Chapter 05 — Concurrency

SYNC,
CONTEXT &
PATTERNS

sync.Mutex, sync.RWMutex, sync.Once, sync.Pool — the tools for shared-state concurrency. context — the standard way to cancel work and pass deadlines across goroutine trees.

Context: Cancellation Tree

func handleRequest(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()  // always defer cancel to avoid leak

    result, err := fetchData(ctx)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, "timeout", 504)
        }
        return
    }
    json.NewEncoder(w).Encode(result)
}

sync.Once: Singleton Init

var (
    instance *DB
    once     sync.Once
)

func GetDB() *DB {
    once.Do(func() {
        instance = connect()  // runs exactly once
    })
    return instance
}
Production Pattern: Always pass context as the first argument to functions that do I/O. Propagate it down the call tree. Check ctx.Err() in long loops. This is what makes Go services resilient under load.
Chapter 06 — Build It

BUILD:
HTTP SERVER
FROM SCRATCH

Inspired by CodeCrafters. Build an HTTP/1.1 server from a raw TCP socket — no net/http. Understand what the standard library does for you by doing it yourself first.
PROJECT: HTTP/1.1 Server
Estimated time: 3–4 hours · Skills: TCP, parsing, concurrency
Build an HTTP/1.1 compliant server that handles GET/POST, parses headers, serves static files, and handles concurrent connections — using only net.Listener.
Step 1: Open a TCP listener on port 4221
Step 2: Accept connections in a loop, handle each in a goroutine
Step 3: Parse the HTTP request line: METHOD /path HTTP/1.1
Step 4: Parse headers (key: value\r\n pairs)
Step 5: Route requests: GET / → 200, GET /echo/{str} → echo body
Step 6: Handle POST /files/{name} → write body to disk
Step 7: Support gzip Content-Encoding if Accept-Encoding: gzip
Step 8: Add persistent connections (Connection: keep-alive)

Start Here: The Listener

package main

import "net"

func main() {
    l, _ := net.Listen("tcp", ":4221")
    defer l.Close()

    for {
        conn, _ := l.Accept()
        go handle(conn)  // each connection gets its own goroutine
    }
}

func handle(conn net.Conn) {
    defer conn.Close()
    buf := make([]byte, 4096)
    n, _ := conn.Read(buf)
    req := parseRequest(buf[:n])
    respond(conn, req)
}
What you'll learn: HTTP wire format, TCP connection lifecycle, goroutine-per-connection pattern, buffered I/O, CRLF parsing, response formatting. After this, net/http is no longer magic.
Chapter 07 — Build It

BUILD:
TCP ECHO
SERVER

A TCP server is the foundation of everything — databases, message queues, HTTP servers, Redis. Build one that echoes messages back, then extend it to broadcast to all connected clients.
PROJECT: TCP Chat Server
Estimated time: 2 hours · Skills: TCP, channels, broadcast pattern
Multi-client TCP chat: clients connect, send messages, all others receive them. Uses a hub goroutine with channels — the canonical Go broadcast pattern.
Accept connections, assign each a goroutine
Each connection sends to a shared broadcast channel
Hub goroutine: receives from broadcast, fans out to all clients
Handle disconnect: remove client from hub's registry
Add /nick command: client sets display name
type Hub struct {
    clients   map[net.Conn]struct{}
    broadcast chan string
    join      chan net.Conn
    leave     chan net.Conn
}

func (h *Hub) Run() {
    for {
        select {
        case c := <-h.join:
            h.clients[c] = struct{}{}
        case c := <-h.leave:
            delete(h.clients, c)
        case msg := <-h.broadcast:
            for c := range h.clients {
                fmt.Fprintln(c, msg)
            }
        }
    }
}
Chapter 08 — Build It

BUILD:
REDIS CLONE

Redis is a TCP server speaking the RESP protocol. Building a minimal Redis clone teaches you: protocol parsing, in-memory data structures, TTL/expiry, and concurrent access patterns.
PROJECT: MiniRedis
Estimated time: 6–8 hours · Skills: RESP protocol, TTL, concurrency, persistence
Implement SET, GET, DEL, EXPIRE, TTL, LPUSH, LRANGE, HSET, HGET over the RESP wire protocol. Clients connect with redis-cli.
Parse RESP: *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue
Implement SET / GET with sync.RWMutex protected map
Add TTL: store expiry time, background goroutine sweeps expired keys
Implement LPUSH / LRANGE using []string per key
Persistence: append commands to AOF log, replay on startup
type Store struct {
    mu      sync.RWMutex
    strings map[string]string
    expiry  map[string]time.Time
}

func (s *Store) Get(key string) (string, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    if exp, ok := s.expiry[key]; ok && time.Now().After(exp) {
        return "", false  // expired
    }
    v, ok := s.strings[key]
    return v, ok
}
Chapter 09 — Build It

BUILD:
gRPC SERVICE

gRPC is HTTP/2 + Protocol Buffers. It's faster, stricter, and more type-safe than REST/JSON. Used by Google, Netflix, Cloudflare for internal services. Essential for any Go microservices architecture.
PROJECT: Trading Signal Service
Estimated time: 4 hours · Skills: protobuf, gRPC streaming, interceptors
gRPC service that streams live trading signals to multiple subscribers. Server-side streaming + unary RPC. Auth interceptor for JWT validation.
Define .proto: SignalService with GetSignal (unary) and StreamSignals (server-stream)
Generate Go code: protoc --go_out=. --go-grpc_out=.
Implement server: embed UnimplementedSignalServiceServer
StreamSignals: send signal to stream every 100ms until context done
Add auth interceptor: validate JWT from metadata
// signal.proto
service SignalService {
  rpc StreamSignals(SignalRequest) returns (stream Signal);
}

// Go implementation
func (s *Server) StreamSignals(req *pb.SignalRequest, stream pb.SignalService_StreamSignalsServer) error {
    for {
        select {
        case <-stream.Context().Done():
            return nil
        case sig := <-s.signals:
            stream.Send(sig)
        }
    }
}
Chapter 10 — Build It

BUILD:
SQL QUERY
ENGINE

Build a minimal SQL engine: tokenizer → parser → AST → executor. Supports SELECT, WHERE, ORDER BY on in-memory tables. Understanding query execution makes you 10× better at using databases.
PROJECT: MiniSQL
Estimated time: 8–12 hours · Skills: parsing, AST, evaluation, indexes
Tokenizer: break SQL string into tokens (keyword, ident, operator, literal)
Parser: recursive descent → AST nodes (SelectStmt, WhereClause, etc.)
Executor: scan in-memory rows, apply WHERE predicate, project columns
ORDER BY: sort rows by column(s) using sort.Slice
Index: map[value][]rowID for fast WHERE equality scans
Why build this? Database internals interviewers love this problem. Engineers who've implemented a query engine understand indexes, execution plans, and query costs intuitively — not just from reading docs.
Chapter 11 — Production

PERFORMANCE
& PROFILING

Go ships with world-class profiling tools. pprof, trace, benchmarks — use them before optimizing. Never guess where the bottleneck is.

Benchmarks First

func BenchmarkJSON(b *testing.B) {
    order := Order{ID: "123", Symbol: "NIFTY", Qty: 50}
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        json.Marshal(order)
    }
}
// go test -bench=. -benchmem -cpuprofile=cpu.out
// go tool pprof cpu.out

Common Optimizations

Chapter 12 — Production

DEPLOY:
DOCKER
& KUBERNETES

A Go binary is the perfect container workload. Static binary, no runtime, tiny base image. Multi-stage Docker build → distroless image → Kubernetes deployment.

Multi-Stage Dockerfile

# Stage 1: build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o server .

# Stage 2: minimal runtime image (~5MB total)
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
Result: ~5MB Docker image vs 200MB+ for JVM. Starts in milliseconds. No shell, no package manager, minimal attack surface. Go's static binary makes distroless images trivial.

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: api
        image: ghcr.io/servloci/api:latest
        resources:
          requests: { cpu: "100m", memory: "32Mi" }
          limits:   { cpu: "500m", memory: "128Mi" }
        livenessProbe:
          httpGet: { path: /health, port: 8080 }
Cloud Cost Impact: 32Mi memory request per pod. 10 replicas = 320MB. Same Java service: 512Mi per pod × 10 = 5GB. At $0.01/GB/hour on AWS, that's $43/month vs $730/month. Go pays for itself.
TEST YOUR KNOWLEDGE
Take the MCQ → Read Rust Book →