The Complete Go Mastery Guide

This guide covers everything a senior Go developer should know - from the basics of the type system to production observability. Each section builds on the previous, creating a complete mental model of the Go language and ecosystem.

Part 1: Language Fundamentals

Chapter 1: The Type System

Basic Types

Go has a deliberately small set of built-in types. Understanding them deeply is essential because Go doesn't hide complexity behind abstractions - what you see is what you get.
Numeric Types
Go provides explicit sizing for integers, which matters for memory layout, serialization, and interoperability with external systems.
go
// Signed integers - can hold negative values var i8 int8 // -128 to 127 var i16 int16 // -32768 to 32767 var i32 int32 // -2147483648 to 2147483647 var i64 int64 // -9223372036854775808 to 9223372036854775807 // Unsigned integers - only positive values var u8 uint8 // 0 to 255 (also called byte) var u16 uint16 // 0 to 65535 var u32 uint32 // 0 to 4294967295 var u64 uint64 // 0 to 18446744073709551615 // Platform-dependent sizes var i int // 32 or 64 bits depending on platform var u uint // 32 or 64 bits depending on platform var ptr uintptr // Large enough to hold a pointer // Floating point var f32 float32 // IEEE-754 32-bit var f64 float64 // IEEE-754 64-bit (preferred for most uses) // Complex numbers var c64 complex64 // float32 real and imaginary parts var c128 complex128 // float64 real and imaginary parts
When should you use which? Use int for general integer work - loop counters, slice indices, general arithmetic. Use sized integers when you need exact sizes for binary protocols, memory-mapped structures, or when memory is constrained. Use float64 for floating-point work unless you have a specific reason for float32 (like interfacing with graphics APIs or saving memory in large arrays).
Strings
Strings in Go are immutable sequences of bytes, not characters. This is a crucial distinction that affects how you work with text.
go
s := "Hello, 世界" // Length returns bytes, not characters len(s) // Returns 13, not 9 // Indexing returns bytes, not characters s[0] // Returns 72 (ASCII for 'H') s[7] // Returns 228, first byte of '世', NOT the character // To work with characters (runes), convert or use range for i, r := range s { fmt.Printf("Position %d: %c (Unicode: %U)\n", i, r, r) } // Explicit conversion to runes runes := []rune(s) len(runes) // Returns 9 // Strings are immutable - this creates a new string s2 := s + "!" // New string allocated
The immutability of strings has performance implications. Concatenating strings in a loop creates many intermediate allocations. Use strings.Builder for building strings incrementally.
go
// Bad - O(n²) allocations result := "" for i := 0; i < 1000; i++ { result += strconv.Itoa(i) } // Good - O(n) allocations var builder strings.Builder for i := 0; i < 1000; i++ { builder.WriteString(strconv.Itoa(i)) } result := builder.String()
Runes
A rune is Go's type for a Unicode code point. It's an alias for int32.
go
var r rune = '世' // Single quotes for rune literals fmt.Printf("%T %v %c\n", r, r, r) // int32 19990 世 // Rune literals can use escape sequences newline := '\n' tab := '\t' unicode := '\u4e16' // Unicode code point

Composite Types

Arrays
Arrays in Go have a fixed size that's part of their type. [3]int and [4]int are completely different types.
go
var arr [5]int // Zero-valued array arr2 := [5]int{1, 2, 3, 4, 5} // Initialized array arr3 := [...]int{1, 2, 3} // Size inferred from elements // Arrays are values, not references a := [3]int{1, 2, 3} b := a // b is a COPY of a b[0] = 999 // Does not affect a fmt.Println(a[0]) // Still 1 // This has implications for function calls func modify(arr [3]int) { arr[0] = 999 // Modifies the copy, not the original }
Arrays are rarely used directly in Go code. Their main use is as the underlying storage for slices and in cases where you need a fixed-size collection with value semantics.
Slices
Slices are Go's workhorse collection type. A slice is a descriptor containing a pointer to an array, a length, and a capacity.
go
// Slice header (conceptually) type SliceHeader struct { Data uintptr // Pointer to underlying array Len int // Number of elements Cap int // Capacity of underlying array }
Understanding slice internals prevents bugs:
go
// Creating slices s1 := []int{1, 2, 3} // Slice literal s2 := make([]int, 5) // Length 5, capacity 5, zero-valued s3 := make([]int, 0, 10) // Length 0, capacity 10 // Slicing creates a new header pointing to same array original := []int{1, 2, 3, 4, 5} slice := original[1:3] // [2, 3] slice[0] = 999 // Modifies original too! fmt.Println(original) // [1, 999, 3, 4, 5] // Append may or may not create a new array s := make([]int, 3, 5) // len=3, cap=5 s = append(s, 4) // len=4, cap=5, same underlying array s = append(s, 5) // len=5, cap=5, same underlying array s = append(s, 6) // len=6, cap=10, NEW underlying array (doubled)
The slice gotcha that catches everyone:
go
func getFirstThree(data []int) []int { return data[:3] // Returns slice sharing memory with data } // Later, if data's underlying array is modified or garbage collected, // the returned slice may see unexpected changes or become invalid. // Safer version: func getFirstThreeSafe(data []int) []int { result := make([]int, 3) copy(result, data[:3]) return result }
Maps
Maps are Go's hash table implementation. They're reference types - passing a map to a function gives that function access to the same underlying data.
go
// Creating maps m1 := make(map[string]int) m2 := map[string]int{"one": 1, "two": 2} // Zero value of a map is nil var m3 map[string]int m3["key"] = 1 // PANIC! Can't assign to nil map // Reading from nil map is safe (returns zero value) v := m3["key"] // v = 0, no panic // Check if key exists value, exists := m2["three"] if exists { fmt.Println(value) } else { fmt.Println("Key not found") } // Delete keys delete(m2, "one") // Maps are not safe for concurrent use // Use sync.Map or protect with mutex for concurrent access
Map iteration order is intentionally randomized. Never rely on iteration order.
go
m := map[string]int{"a": 1, "b": 2, "c": 3} for k, v := range m { fmt.Println(k, v) // Order will vary between runs } // If you need ordered iteration, sort the keys first keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { fmt.Println(k, m[k]) }
Structs
Structs are Go's way of creating composite types. They're value types - assigning a struct copies all its fields.
go
type Person struct { FirstName string LastName string Age int private string // lowercase = unexported } // Creating structs p1 := Person{FirstName: "John", LastName: "Doe", Age: 30} p2 := Person{"John", "Doe", 30, ""} // Positional (fragile, avoid) // Zero values var p3 Person // All fields are zero-valued // Anonymous structs (useful for one-off structures) config := struct { Host string Port int }{ Host: "localhost", Port: 8080, } // Struct embedding (composition) type Employee struct { Person // Embedded - fields promoted EmployeeID string } emp := Employee{ Person: Person{FirstName: "Jane"}, EmployeeID: "E123", } fmt.Println(emp.FirstName) // Promoted field access
Struct Tags
Struct tags provide metadata that can be read at runtime via reflection. They're heavily used for serialization.
go
type User struct { ID int `json:"id" db:"user_id"` Email string `json:"email" validate:"required,email"` Password string `json:"-"` // Excluded from JSON CreatedAt time.Time `json:"created_at,omitempty"` } // Tags are accessed via reflection t := reflect.TypeOf(User{}) field, _ := t.FieldByName("Email") jsonTag := field.Tag.Get("json") // "email" validateTag := field.Tag.Get("validate") // "required,email"

Pointers

Go has pointers but no pointer arithmetic (except in unsafe package). Pointers are used for sharing, not for performance tricks.
go
x := 42 p := &x // p is *int, points to x *p = 100 // x is now 100 // Pointer zero value is nil var ptr *int *ptr = 1 // PANIC! nil pointer dereference // new() allocates and returns pointer ptr2 := new(int) // ptr2 is *int pointing to zero-valued int *ptr2 = 42 // When to use pointers: // 1. When you need to modify the original value // 2. To avoid copying large structs // 3. To represent optional values (nil = absent) // 4. When implementing methods that modify receiver type Counter struct { value int } func (c *Counter) Increment() { c.value++ // Modifies the actual Counter } func (c Counter) Value() int { return c.value // Just reading, no need for pointer }

Type Definitions and Aliases

go
// Type definition - creates new distinct type type UserID int64 type Temperature float64 var id UserID = 123 var temp Temperature = 98.6 // These are different types - this fails: // var x int64 = id // Compile error // Type alias - creates alternate name for same type type Celsius = float64 type Fahrenheit = float64 var c Celsius = 100 var f Fahrenheit = c // Works - same underlying type // Custom types can have methods func (t Temperature) ToFahrenheit() float64 { return float64(t)*9/5 + 32 }

Chapter 2: Control Flow

If Statements

Go's if statements don't require parentheses but always require braces.
go
if x > 0 { fmt.Println("Positive") } else if x < 0 { fmt.Println("Negative") } else { fmt.Println("Zero") } // If with initialization statement if err := doSomething(); err != nil { return err } // err is not accessible here // The initialization pattern is idiomatic if user, err := getUser(id); err != nil { return err } else { // Both user and err are accessible fmt.Println(user.Name) }

Switch Statements

Go's switch is more powerful than in many languages. Cases don't fall through by default.
go
// Basic switch switch day { case "Monday": fmt.Println("Start of week") case "Friday": fmt.Println("Almost weekend") case "Saturday", "Sunday": // Multiple values fmt.Println("Weekend!") default: fmt.Println("Midweek") } // Switch with no expression (cleaner than if-else chains) switch { case score >= 90: grade = "A" case score >= 80: grade = "B" case score >= 70: grade = "C" default: grade = "F" } // Switch with initialization switch os := runtime.GOOS; os { case "darwin": fmt.Println("macOS") case "linux": fmt.Println("Linux") default: fmt.Printf("%s\n", os) } // Type switch func describe(i interface{}) { switch v := i.(type) { case int: fmt.Printf("Integer: %d\n", v) case string: fmt.Printf("String: %s\n", v) case bool: fmt.Printf("Boolean: %t\n", v) default: fmt.Printf("Unknown type: %T\n", v) } } // Fallthrough (rarely needed) switch n { case 1: fmt.Println("One") fallthrough // Explicitly fall through case 2: fmt.Println("One or Two") }

Loops

Go has only one looping construct: for. It handles all looping scenarios.
go
// Traditional for loop for i := 0; i < 10; i++ { fmt.Println(i) } // While-style loop for condition { // Do something } // Infinite loop for { // Break when done if shouldStop() { break } } // Range over slice numbers := []int{1, 2, 3, 4, 5} for index, value := range numbers { fmt.Printf("Index: %d, Value: %d\n", index, value) } // Ignore index for _, value := range numbers { fmt.Println(value) } // Only index for index := range numbers { fmt.Println(index) } // Range over map for key, value := range myMap { fmt.Printf("%s: %v\n", key, value) } // Range over string (iterates over runes, not bytes) for index, runeValue := range "Hello, 世界" { fmt.Printf("%d: %c\n", index, runeValue) } // Range over channel for msg := range messageChannel { process(msg) }

Defer, Panic, and Recover

Defer
Defer schedules a function call to run when the surrounding function returns. Deferred calls are executed in LIFO order.
go
func readFile(filename string) ([]byte, error) { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() // Will execute when function returns return io.ReadAll(f) } // Deferred functions see final values of variables func example() { i := 0 defer fmt.Println(i) // Prints 0 - value captured at defer time i++ return } // Use closure to capture current value func example2() { for i := 0; i < 3; i++ { defer func(n int) { fmt.Println(n) // Prints 2, 1, 0 }(i) } } // Common pattern: measure function execution func measureTime() { start := time.Now() defer func() { fmt.Printf("Execution took %v\n", time.Since(start)) }() // Do work... }
Panic and Recover
Panic is for unrecoverable errors. It unwinds the stack, running deferred functions. Recover can stop the unwinding.
go
// Panic stops normal execution func mustPositive(n int) { if n < 0 { panic("negative number not allowed") } } // Recover catches panics func safeCall() (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("recovered from panic: %v", r) } }() riskyOperation() return nil } // When to use panic: // 1. Unrecoverable errors during initialization // 2. Programmer errors (invalid arguments to internal functions) // 3. When continuing would cause data corruption // When NOT to use panic: // 1. For expected errors (file not found, network timeout) // 2. In library code (return errors instead) // 3. For flow control

Chapter 3: Functions

Function Basics

go
// Basic function func add(a, b int) int { return a + b } // Multiple return values func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } // Named return values func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return // Naked return - returns x, y } // Variadic functions func sum(numbers ...int) int { total := 0 for _, n := range numbers { total += n } return total } // Calling variadic functions sum(1, 2, 3) nums := []int{1, 2, 3, 4} sum(nums...) // Spread slice into arguments

Functions as Values

Functions are first-class values in Go.
go
// Function type type Operation func(int, int) int // Function as variable var op Operation = func(a, b int) int { return a + b } // Function as parameter func apply(a, b int, op func(int, int) int) int { return op(a, b) } result := apply(3, 4, func(a, b int) int { return a * b }) // 12 // Function returning function (closure) func multiplier(factor int) func(int) int { return func(n int) int { return n * factor } } double := multiplier(2) triple := multiplier(3) fmt.Println(double(5)) // 10 fmt.Println(triple(5)) // 15

Closures

Closures capture variables from their surrounding scope.
go
func counter() func() int { count := 0 return func() int { count++ return count } } c := counter() fmt.Println(c()) // 1 fmt.Println(c()) // 2 fmt.Println(c()) // 3 // Each call to counter creates new count variable c2 := counter() fmt.Println(c2()) // 1 (independent counter) // Closure gotcha in loops funcs := make([]func(), 3) for i := 0; i < 3; i++ { funcs[i] = func() { fmt.Println(i) // Captures variable i, not its value } } for _, f := range funcs { f() // All print 3! } // Fix: create new variable in each iteration for i := 0; i < 3; i++ { i := i // Shadow with new variable funcs[i] = func() { fmt.Println(i) } }

Methods

Methods are functions with a receiver argument.
go
type Rectangle struct { Width, Height float64 } // Value receiver - operates on copy func (r Rectangle) Area() float64 { return r.Width * r.Height } // Pointer receiver - can modify original func (r *Rectangle) Scale(factor float64) { r.Width *= factor r.Height *= factor } rect := Rectangle{Width: 10, Height: 5} fmt.Println(rect.Area()) // 50 rect.Scale(2) fmt.Println(rect.Area()) // 200 // When to use pointer receivers: // 1. When the method needs to modify the receiver // 2. When the receiver is a large struct // 3. For consistency (if any method needs pointer, use pointer for all) // Methods on any type (not just structs) type MyInt int func (n MyInt) Double() MyInt { return n * 2 }

Chapter 4: Interfaces

Interface Basics

Interfaces define behavior. Any type that implements all methods of an interface implicitly satisfies it.
go
// Interface definition type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } // Interface composition type ReadWriter interface { Reader Writer } // Implementing an interface (implicit) type MyBuffer struct { data []byte } func (b *MyBuffer) Read(p []byte) (int, error) { n := copy(p, b.data) b.data = b.data[n:] return n, nil } func (b *MyBuffer) Write(p []byte) (int, error) { b.data = append(b.data, p...) return len(p), nil } // MyBuffer now implements ReadWriter var rw ReadWriter = &MyBuffer{}

Empty Interface

The empty interface interface{} (or any in Go 1.18+) can hold any value.
go
func printAnything(v interface{}) { fmt.Printf("Type: %T, Value: %v\n", v, v) } printAnything(42) printAnything("hello") printAnything([]int{1, 2, 3}) // Type assertion var i interface{} = "hello" s := i.(string) // s = "hello" s, ok := i.(string) // s = "hello", ok = true n, ok := i.(int) // n = 0, ok = false // Type assertion without ok panics if wrong type n = i.(int) // PANIC!

Interface Design Principles

Keep interfaces small
go
// Bad: Large interface type DataStore interface { Get(id string) (interface{}, error) Set(id string, value interface{}) error Delete(id string) error List() ([]string, error) Clear() error Backup() error Restore() error Migrate() error } // Good: Small, focused interfaces type Getter interface { Get(id string) (interface{}, error) } type Setter interface { Set(id string, value interface{}) error } type GetterSetter interface { Getter Setter }
Define interfaces at point of use
go
// In package that USES the interface, not the one that implements it package handler type UserFinder interface { FindByID(id string) (*User, error) } type Handler struct { users UserFinder // Depends on interface, not concrete type }
Accept interfaces, return structs
go
// Good: Accept interface func ProcessReader(r io.Reader) error { // Works with any Reader } // Good: Return concrete type func NewBuffer() *Buffer { return &Buffer{} }

Interface Internals

An interface value consists of two words: a type and a value.
go
var w io.Writer // w is (nil, nil) - both type and value are nil var buf *bytes.Buffer w = buf // w is (*bytes.Buffer, nil) - type is set, value is nil // w != nil, but w holds a nil pointer! if w != nil { w.Write([]byte("hello")) // PANIC! nil pointer } // This is a common gotcha with interfaces func returnsNil() io.Writer { var buf *bytes.Buffer = nil return buf // Returns non-nil interface with nil value! } w = returnsNil() if w != nil { // TRUE! // Danger zone }

Chapter 5: Error Handling

Error Basics

Errors in Go are values. The error interface is simple:
go
type error interface { Error() string }
Creating errors
go
import "errors" // Simple error err := errors.New("something went wrong") // Formatted error err = fmt.Errorf("failed to process %s: invalid format", filename) // Custom error type type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation failed for %s: %s", e.Field, e.Message) }

Error Wrapping

Go 1.13+ introduced error wrapping for preserving error chains.
go
// Wrap error with context originalErr := errors.New("connection refused") wrappedErr := fmt.Errorf("failed to connect to database: %w", originalErr) // Unwrap to get original unwrapped := errors.Unwrap(wrappedErr) // Check if error chain contains specific error if errors.Is(wrappedErr, originalErr) { fmt.Println("Connection was refused") } // Check if error chain contains specific type var validationErr *ValidationError if errors.As(err, &validationErr) { fmt.Printf("Validation failed for field: %s\n", validationErr.Field) }

Sentinel Errors

Sentinel errors are predefined error values used for comparison.
go
var ( ErrNotFound = errors.New("not found") ErrUnauthorized = errors.New("unauthorized") ErrInvalidInput = errors.New("invalid input") ) func GetUser(id string) (*User, error) { user, exists := users[id] if !exists { return nil, ErrNotFound } return user, nil } // Using sentinel errors user, err := GetUser("123") if errors.Is(err, ErrNotFound) { // Handle not found case }

Error Handling Patterns

go
// Pattern 1: Handle and return func processFile(path string) error { f, err := os.Open(path) if err != nil { return fmt.Errorf("opening file: %w", err) } defer f.Close() data, err := io.ReadAll(f) if err != nil { return fmt.Errorf("reading file: %w", err) } return process(data) } // Pattern 2: Handle inline for cleanup func writeFile(path string, data []byte) (err error) { f, err := os.Create(path) if err != nil { return err } defer func() { closeErr := f.Close() if err == nil { err = closeErr } }() _, err = f.Write(data) return err } // Pattern 3: Error accumulation type MultiError struct { Errors []error } func (m *MultiError) Error() string { var msgs []string for _, err := range m.Errors { msgs = append(msgs, err.Error()) } return strings.Join(msgs, "; ") } func (m *MultiError) Add(err error) { if err != nil { m.Errors = append(m.Errors, err) } } func (m *MultiError) ErrorOrNil() error { if len(m.Errors) == 0 { return nil } return m }

When to Panic vs Return Error

go
// Return error for: // - File not found // - Network timeouts // - Invalid user input // - Any expected failure condition // Panic for: // - Nil pointer where nil is never valid // - Index out of bounds in internal code // - Invalid state that indicates a bug // - Initialization failures that make the program unusable // Example: panic for programmer errors func MustCompile(pattern string) *regexp.Regexp { re, err := regexp.Compile(pattern) if err != nil { panic(err) // Programmer should have tested the pattern } return re } // Usage at package init (panics are acceptable here) var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) --- # Part 2: Concurrency ## Chapter 6: Goroutines ### Goroutine Basics Goroutines are lightweight threads managed by the Go runtime. They're cheap to create (a few KB of stack) and the runtime multiplexes many goroutines onto fewer OS threads. ```go // Start a goroutine go func() { fmt.Println("Hello from goroutine") }() // Goroutine with named function go processItem(item) // Main goroutine doesn't wait for others func main() { go fmt.Println("Hello") // Program may exit before goroutine runs }

Goroutine Lifecycle

go
// Goroutines run until: // 1. The function returns // 2. The program exits // 3. They're blocked forever (goroutine leak) // Common pattern: WaitGroup for synchronization var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(n int) { defer wg.Done() process(n) }(i) } wg.Wait() // Block until all goroutines complete

Goroutine Leaks

Goroutine leaks occur when goroutines are blocked forever, consuming resources.
go
// LEAK: Channel never receives func leaky() { ch := make(chan int) go func() { val := <-ch // Blocked forever if nothing sends fmt.Println(val) }() // Function returns, goroutine still waiting } // FIX: Use context for cancellation func notLeaky(ctx context.Context) { ch := make(chan int) go func() { select { case val := <-ch: fmt.Println(val) case <-ctx.Done(): return // Clean exit on cancellation } }() } // LEAK: Infinite loop without exit func leakyLoop() { go func() { for { doWork() // Never exits! } }() } // FIX: Check for done signal func notLeakyLoop(done <-chan struct{}) { go func() { for { select { case <-done: return default: doWork() } } }() }

Chapter 7: Channels

Channel Basics

Channels are Go's primary synchronization mechanism. They're typed conduits for sending and receiving values.
go
// Create channel ch := make(chan int) // Unbuffered channel buffered := make(chan int, 10) // Buffered channel with capacity 10 // Send and receive ch <- 42 // Send (blocks until receiver ready) value := <-ch // Receive (blocks until sender ready) // Close channel (only sender should close) close(ch) // Receive from closed channel returns zero value value, ok := <-ch if !ok { fmt.Println("Channel closed") }

Unbuffered vs Buffered Channels

go
// Unbuffered: synchronous communication // Send blocks until receive happens ch := make(chan int) go func() { ch <- 1 // Blocks until main receives }() <-ch // Unblocks the sender // Buffered: asynchronous up to capacity // Send blocks only when buffer is full buffered := make(chan int, 3) buffered <- 1 // Doesn't block buffered <- 2 // Doesn't block buffered <- 3 // Doesn't block buffered <- 4 // Blocks! Buffer full

Channel Direction

go
// Send-only channel func producer(ch chan<- int) { ch <- 42 // <-ch // Compile error: receive from send-only channel } // Receive-only channel func consumer(ch <-chan int) { value := <-ch // ch <- 1 // Compile error: send to receive-only channel } // Bidirectional channel converts to directional ch := make(chan int) go producer(ch) // Converts to chan<- int go consumer(ch) // Converts to <-chan int

Select Statement

Select lets you wait on multiple channel operations.
go
select { case msg := <-ch1: fmt.Println("Received from ch1:", msg) case msg := <-ch2: fmt.Println("Received from ch2:", msg) case ch3 <- 42: fmt.Println("Sent to ch3") default: fmt.Println("No channel ready") } // Select with timeout select { case result := <-ch: process(result) case <-time.After(5 * time.Second): fmt.Println("Timeout!") } // Non-blocking receive select { case msg := <-ch: fmt.Println(msg) default: fmt.Println("No message available") }

Channel Patterns

Fan-out: One producer, multiple consumers
go
func fanOut(input <-chan int, workers int) []<-chan int { outputs := make([]<-chan int, workers) for i := 0; i < workers; i++ { outputs[i] = worker(input) } return outputs } func worker(input <-chan int) <-chan int { output := make(chan int) go func() { defer close(output) for n := range input { output <- process(n) } }() return output }
Fan-in: Multiple producers, one consumer
go
func fanIn(inputs ...<-chan int) <-chan int { output := make(chan int) var wg sync.WaitGroup for _, input := range inputs { wg.Add(1) go func(ch <-chan int) { defer wg.Done() for v := range ch { output <- v } }(input) } go func() { wg.Wait() close(output) }() return output }
Pipeline: Chain of processing stages
go
func generate(nums ...int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { out <- n } }() return out } func square(in <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range in { out <- n * n } }() return out } func double(in <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range in { out <- n * 2 } }() return out } // Usage nums := generate(1, 2, 3, 4) squared := square(nums) doubled := double(squared) for result := range doubled { fmt.Println(result) // 2, 8, 18, 32 }
Worker Pool
go
func workerPool(jobs <-chan Job, results chan<- Result, numWorkers int) { var wg sync.WaitGroup for i := 0; i < numWorkers; i++ { wg.Add(1) go func(workerID int) { defer wg.Done() for job := range jobs { result := process(job) results <- result } }(i) } wg.Wait() close(results) }

Chapter 8: Synchronization Primitives

sync.Mutex

Mutex provides mutual exclusion for protecting shared state.
go
type SafeCounter struct { mu sync.Mutex value int } func (c *SafeCounter) Increment() { c.mu.Lock() defer c.mu.Unlock() c.value++ } func (c *SafeCounter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.value } // Common mistake: copying mutex counter := SafeCounter{} counter2 := counter // WRONG! Copies the mutex // Use pointer or embed carefully

sync.RWMutex

RWMutex allows multiple readers or one writer.
go
type SafeMap struct { mu sync.RWMutex data map[string]string } func (m *SafeMap) Get(key string) (string, bool) { m.mu.RLock() // Multiple goroutines can read simultaneously defer m.mu.RUnlock() val, ok := m.data[key] return val, ok } func (m *SafeMap) Set(key, value string) { m.mu.Lock() // Exclusive access for writing defer m.mu.Unlock() m.data[key] = value }

sync.WaitGroup

WaitGroup waits for a collection of goroutines to finish.
go
var wg sync.WaitGroup urls := []string{"url1", "url2", "url3"} for _, url := range urls { wg.Add(1) go func(u string) { defer wg.Done() fetch(u) }(url) } wg.Wait() // Block until all fetches complete

sync.Once

Once ensures a function runs exactly once.
go
var ( instance *Database once sync.Once ) func GetDatabase() *Database { once.Do(func() { instance = &Database{} instance.Connect() }) return instance }

sync.Cond

Cond provides a way to wait for/announce conditions.
go
type Queue struct { items []int cond *sync.Cond } func NewQueue() *Queue { return &Queue{ cond: sync.NewCond(&sync.Mutex{}), } } func (q *Queue) Put(item int) { q.cond.L.Lock() defer q.cond.L.Unlock() q.items = append(q.items, item) q.cond.Signal() // Wake one waiting goroutine } func (q *Queue) Get() int { q.cond.L.Lock() defer q.cond.L.Unlock() for len(q.items) == 0 { q.cond.Wait() // Release lock and wait } item := q.items[0] q.items = q.items[1:] return item }

sync.Pool

Pool provides a set of temporary objects to reuse.
go
var bufferPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) }, } func processRequest(data []byte) { buf := bufferPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufferPool.Put(buf) }() buf.Write(data) // Process buffer... }

sync.Map

Map is safe for concurrent use without additional locking.
go
var cache sync.Map // Store cache.Store("key", "value") // Load value, ok := cache.Load("key") if ok { fmt.Println(value.(string)) } // LoadOrStore actual, loaded := cache.LoadOrStore("key", "default") // Delete cache.Delete("key") // Range cache.Range(func(key, value interface{}) bool { fmt.Println(key, value) return true // Continue iteration })

Atomic Operations

Package sync/atomic provides low-level atomic operations.
go
var counter int64 // Atomic increment atomic.AddInt64(&counter, 1) // Atomic load val := atomic.LoadInt64(&counter) // Atomic store atomic.StoreInt64(&counter, 100) // Compare and swap old := int64(100) new := int64(200) swapped := atomic.CompareAndSwapInt64(&counter, old, new) // Atomic value for any type var config atomic.Value config.Store(Config{Debug: true}) cfg := config.Load().(Config)

Chapter 9: Context

Context Basics

Context carries deadlines, cancellation signals, and request-scoped values.
go
// Create contexts ctx := context.Background() // Root context ctx := context.TODO() // Placeholder when unsure // Context with cancellation ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Always call cancel to release resources // Context with timeout ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // Context with deadline deadline := time.Now().Add(30 * time.Second) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() // Context with value ctx = context.WithValue(ctx, "requestID", "12345")

Using Context

go
func longRunningTask(ctx context.Context) error { for { select { case <-ctx.Done(): return ctx.Err() // Returns context.Canceled or context.DeadlineExceeded default: // Do work if err := doSomeWork(); err != nil { return err } } } } // HTTP handler with context func handler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() result, err := processWithContext(ctx) if err != nil { if errors.Is(err, context.Canceled) { return // Client disconnected } http.Error(w, err.Error(), 500) return } json.NewEncoder(w).Encode(result) }

Context Best Practices

go
// 1. Pass context as first parameter func ProcessRequest(ctx context.Context, req *Request) (*Response, error) // 2. Don't store context in structs // Bad type Service struct { ctx context.Context } // Good func (s *Service) Process(ctx context.Context) error // 3. Use context values sparingly (for request-scoped data only) type contextKey string const requestIDKey contextKey = "requestID" func WithRequestID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, requestIDKey, id) } func RequestIDFromContext(ctx context.Context) string { id, _ := ctx.Value(requestIDKey).(string) return id } // 4. Always check ctx.Done() in long operations func fetchAll(ctx context.Context, urls []string) ([]Result, error) { results := make([]Result, len(urls)) for i, url := range urls { select { case <-ctx.Done(): return nil, ctx.Err() default: } result, err := fetch(ctx, url) if err != nil { return nil, err } results[i] = result } return results, nil }

Part 3: Advanced Language Features

Chapter 10: Generics (Go 1.18+)

Type Parameters

go
// Generic function func Min[T constraints.Ordered](a, b T) T { if a < b { return a } return b } // Usage Min(3, 5) // int Min(3.14, 2.71) // float64 Min("a", "b") // string // Generic type type Stack[T any] struct { items []T } func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) } func (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T return zero, false } item := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return item, true } // Usage intStack := &Stack[int]{} intStack.Push(1) intStack.Push(2) val, _ := intStack.Pop() // 2

Constraints

go
// Built-in constraints from constraints package import "golang.org/x/exp/constraints" func Sum[T constraints.Integer | constraints.Float](nums []T) T { var sum T for _, n := range nums { sum += n } return sum } // Custom constraints type Number interface { int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64 } // Constraint with method type Stringer interface { String() string } func PrintAll[T Stringer](items []T) { for _, item := range items { fmt.Println(item.String()) } } // Constraint with underlying type type Integer interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } type MyInt int // Works with Integer constraint due to ~ // Comparable constraint func Contains[T comparable](slice []T, target T) bool { for _, v := range slice { if v == target { return true } } return false }

Generic Patterns

go
// Map function func Map[T, U any](slice []T, f func(T) U) []U { result := make([]U, len(slice)) for i, v := range slice { result[i] = f(v) } return result } // Filter function func Filter[T any](slice []T, predicate func(T) bool) []T { result := make([]T, 0) for _, v := range slice { if predicate(v) { result = append(result, v) } } return result } // Reduce function func Reduce[T, U any](slice []T, initial U, f func(U, T) U) U { result := initial for _, v := range slice { result = f(result, v) } return result } // Usage nums := []int{1, 2, 3, 4, 5} squared := Map(nums, func(n int) int { return n * n }) evens := Filter(nums, func(n int) bool { return n%2 == 0 }) sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })

Chapter 11: Reflection

Reflection Basics

Reflection lets you inspect types and values at runtime.
go
import "reflect" // Get type information t := reflect.TypeOf(42) fmt.Println(t.Name()) // int fmt.Println(t.Kind()) // int // Get value information v := reflect.ValueOf(42) fmt.Println(v.Int()) // 42 // Examine struct type Person struct { Name string `json:"name"` Age int `json:"age"` } p := Person{"Alice", 30} t = reflect.TypeOf(p) v = reflect.ValueOf(p) for i := 0; i < t.NumField(); i++ { field := t.Field(i) value := v.Field(i) tag := field.Tag.Get("json") fmt.Printf("%s: %v (tag: %s)\n", field.Name, value, tag) }

Modifying Values with Reflection

go
// Must use pointer to modify x := 42 v := reflect.ValueOf(&x).Elem() // Elem() dereferences pointer v.SetInt(100) fmt.Println(x) // 100 // Modify struct field p := &Person{Name: "Alice", Age: 30} v = reflect.ValueOf(p).Elem() nameField := v.FieldByName("Name") if nameField.CanSet() { nameField.SetString("Bob") }

Calling Functions with Reflection

go
func add(a, b int) int { return a + b } v := reflect.ValueOf(add) args := []reflect.Value{reflect.ValueOf(3), reflect.ValueOf(4)} results := v.Call(args) fmt.Println(results[0].Int()) // 7

When to Use Reflection

go
// Good uses: // 1. Serialization/deserialization (JSON, XML) // 2. ORM mapping // 3. Dependency injection frameworks // 4. Testing utilities // Avoid when: // 1. Generics can solve the problem // 2. Performance is critical // 3. Compile-time type safety is preferred

Chapter 12: Unsafe Package

The unsafe package provides operations that bypass Go's type safety.
go
import "unsafe" // Sizeof returns size in bytes fmt.Println(unsafe.Sizeof(int64(0))) // 8 // Alignof returns alignment requirement fmt.Println(unsafe.Alignof(struct{ x int32; y int64 }{})) // Offsetof returns byte offset of field type Example struct { a bool b int64 c bool } fmt.Println(unsafe.Offsetof(Example{}.b)) // Usually 8 due to alignment // Pointer conversion var x int64 = 42 ptr := unsafe.Pointer(&x) intPtr := (*int64)(ptr) *intPtr = 100 fmt.Println(x) // 100 // String to bytes without copy (dangerous!) func stringToBytes(s string) []byte { return *(*[]byte)(unsafe.Pointer(&s)) }
Warning: unsafe code can crash your program or cause data corruption. Only use when absolutely necessary and you understand the implications.

Part 4: Standard Library Deep Dive

Chapter 13: I/O and Files

io Package

go
// Fundamental interfaces type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type Closer interface { Close() error } // Composed interfaces type ReadWriter interface { Reader Writer } type ReadWriteCloser interface { Reader Writer Closer } // Utility functions // Copy from reader to writer n, err := io.Copy(dst, src) // Read all bytes data, err := io.ReadAll(reader) // Read exactly n bytes buf := make([]byte, 100) _, err := io.ReadFull(reader, buf) // Limit reader to n bytes limited := io.LimitReader(reader, 1024) // Multi-reader (concatenates readers) multi := io.MultiReader(reader1, reader2) // Tee reader (writes to writer while reading) tee := io.TeeReader(reader, writer)

File Operations

go
// Open for reading f, err := os.Open("file.txt") if err != nil { return err } defer f.Close() // Create/truncate for writing f, err := os.Create("file.txt") // Open with specific flags and permissions f, err := os.OpenFile("file.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) // Read entire file data, err := os.ReadFile("file.txt") // Write entire file err := os.WriteFile("file.txt", data, 0644) // File info info, err := os.Stat("file.txt") fmt.Println(info.Name(), info.Size(), info.Mode(), info.ModTime()) // Check if file exists if _, err := os.Stat("file.txt"); os.IsNotExist(err) { fmt.Println("File does not exist") } // Directory operations err := os.Mkdir("dir", 0755) err := os.MkdirAll("path/to/dir", 0755) entries, err := os.ReadDir("dir") err := os.Remove("file.txt") err := os.RemoveAll("dir")

Buffered I/O

go
// Buffered reader reader := bufio.NewReader(file) line, err := reader.ReadString('\n') bytes, err := reader.ReadBytes('\n') // Scanner for line-by-line reading scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() process(line) } if err := scanner.Err(); err != nil { return err } // Custom split function scanner.Split(bufio.ScanWords) // Split by words // Buffered writer writer := bufio.NewWriter(file) writer.WriteString("Hello") writer.Flush() // Don't forget to flush!

Chapter 14: Encoding and Serialization

JSON

go
import "encoding/json" type Person struct { Name string `json:"name"` Age int `json:"age"` Email string `json:"email,omitempty"` // Omit if empty Secret string `json:"-"` // Never serialize Tags []string `json:"tags,omitempty"` } // Encoding p := Person{Name: "Alice", Age: 30} data, err := json.Marshal(p) // Pretty print data, err := json.MarshalIndent(p, "", " ") // Decoding var p Person err := json.Unmarshal(data, &p) // Streaming with encoder/decoder encoder := json.NewEncoder(writer) encoder.Encode(p) decoder := json.NewDecoder(reader) decoder.Decode(&p) // Decode to map (unknown structure) var result map[string]interface{} json.Unmarshal(data, &result) // json.RawMessage for delayed parsing type Request struct { Type string `json:"type"` Payload json.RawMessage `json:"payload"` } // Custom marshaling func (p Person) MarshalJSON() ([]byte, error) { type Alias Person return json.Marshal(&struct { Alias FullName string `json:"fullName"` }{ Alias: Alias(p), FullName: p.Name, }) } // Custom unmarshaling func (p *Person) UnmarshalJSON(data []byte) error { type Alias Person aux := &struct { *Alias Age string `json:"age"` // Age comes as string }{ Alias: (*Alias)(p), } if err := json.Unmarshal(data, aux); err != nil { return err } // Convert string age to int age, _ := strconv.Atoi(aux.Age) p.Age = age return nil }

XML

go
import "encoding/xml" type Person struct { XMLName xml.Name `xml:"person"` Name string `xml:"name"` Age int `xml:"age,attr"` // As attribute Email string `xml:"contact>email"` // Nested element } data, err := xml.Marshal(p) data, err := xml.MarshalIndent(p, "", " ") err := xml.Unmarshal(data, &p)

Binary Encoding

go
import "encoding/binary" // Write binary data var num uint32 = 42 err := binary.Write(writer, binary.LittleEndian, num) // Read binary data var num uint32 err := binary.Read(reader, binary.LittleEndian, &num) // Encoding/gob for Go-specific encoding import "encoding/gob" encoder := gob.NewEncoder(writer) encoder.Encode(value) decoder := gob.NewDecoder(reader) decoder.Decode(&value)

Chapter 15: Networking

TCP

go
import "net" // TCP Server listener, err := net.Listen("tcp", ":8080") if err != nil { log.Fatal(err) } defer listener.Close() for { conn, err := listener.Accept() if err != nil { log.Println(err) continue } go handleConnection(conn) } func handleConnection(conn net.Conn) { defer conn.Close() // Set timeouts conn.SetDeadline(time.Now().Add(30 * time.Second)) buf := make([]byte, 1024) for { n, err := conn.Read(buf) if err != nil { return } conn.Write(buf[:n]) // Echo back } } // TCP Client conn, err := net.Dial("tcp", "localhost:8080") if err != nil { log.Fatal(err) } defer conn.Close() conn.Write([]byte("Hello")) buf := make([]byte, 1024) n, _ := conn.Read(buf) fmt.Println(string(buf[:n]))

HTTP Client

go
import "net/http" // Simple GET resp, err := http.Get("https://api.example.com/data") if err != nil { return err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) // Custom request req, err := http.NewRequest("POST", "https://api.example.com/data", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer token") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) // Custom transport transport := &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, IdleConnTimeout: 90 * time.Second, } client := &http.Client{ Transport: transport, Timeout: 30 * time.Second, } // Request with context ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := client.Do(req)

HTTP Server

go
// Simple handler http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, World!") }) // Handler type type apiHandler struct { db *Database } func (h *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Handle request } http.Handle("/api", &apiHandler{db: db}) // Server with configuration server := &http.Server{ Addr: ":8080", Handler: mux, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } // Graceful shutdown go func() { if err := server.ListenAndServe(); err != http.ErrServerClosed { log.Fatal(err) } }() // Wait for interrupt signal quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt) <-quit ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() server.Shutdown(ctx)

Chapter 16: Time and Duration

go
import "time" // Current time now := time.Now() utc := time.Now().UTC() // Creating time t := time.Date(2024, time.January, 15, 10, 30, 0, 0, time.UTC) // Parsing time t, err := time.Parse("2006-01-02", "2024-01-15") t, err := time.Parse(time.RFC3339, "2024-01-15T10:30:00Z") // Format time s := t.Format("2006-01-02 15:04:05") s := t.Format(time.RFC3339) // Duration d := 5 * time.Second d := time.Duration(500) * time.Millisecond d := time.Hour + 30*time.Minute // Time arithmetic future := now.Add(24 * time.Hour) past := now.Add(-1 * time.Hour) duration := future.Sub(now) // Comparison if t1.Before(t2) { } if t1.After(t2) { } if t1.Equal(t2) { } // Sleep time.Sleep(100 * time.Millisecond) // Ticker (repeated events) ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for range ticker.C { fmt.Println("Tick") } // Timer (one-time event) timer := time.NewTimer(5 * time.Second) <-timer.C fmt.Println("Timer expired") // Timeout pattern select { case result := <-ch: process(result) case <-time.After(5 * time.Second): fmt.Println("Timeout") } // Measure execution time start := time.Now() doWork() elapsed := time.Since(start) fmt.Printf("Took %v\n", elapsed)

Chapter 17: Regular Expressions

go
import "regexp" // Compile pattern (panics on error) re := regexp.MustCompile(`\d+`) // Compile pattern (returns error) re, err := regexp.Compile(`\d+`) // Match check matched := re.MatchString("abc123def") // Find first match match := re.FindString("abc123def456") // "123" // Find all matches matches := re.FindAllString("abc123def456", -1) // ["123", "456"] // Find with position loc := re.FindStringIndex("abc123def") // [3, 6] // Capture groups re := regexp.MustCompile(`(\w+)@(\w+)\.(\w+)`) matches := re.FindStringSubmatch("user@example.com") // ["user@example.com", "user", "example", "com"] // Named capture groups re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`) match := re.FindStringSubmatch("user@example.com") for i, name := range re.SubexpNames() { if i > 0 && name != "" { fmt.Printf("%s: %s\n", name, match[i]) } } // Replace result := re.ReplaceAllString("abc123def456", "X") // Replace with function result := re.ReplaceAllStringFunc("abc123def456", func(s string) string { n, _ := strconv.Atoi(s) return strconv.Itoa(n * 2) }) // Split parts := re.Split("abc123def456ghi", -1) // ["abc", "def", "ghi"]

Part 5: Testing

Chapter 18: Unit Testing

Test Basics

go
// math_test.go package math import "testing" // Test function must start with Test func TestAdd(t *testing.T) { result := Add(2, 3) if result != 5 { t.Errorf("Add(2, 3) = %d; want 5", result) } } // Table-driven tests func TestAddTableDriven(t *testing.T) { tests := []struct { name string a, b int expected int }{ {"positive numbers", 2, 3, 5}, {"negative numbers", -2, -3, -5}, {"mixed numbers", -2, 3, 1}, {"zeros", 0, 0, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Add(tt.a, tt.b) if result != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) } }) } } // Parallel tests func TestParallel(t *testing.T) { tests := []struct { name string input int }{ {"test1", 1}, {"test2", 2}, {"test3", 3}, } for _, tt := range tests { tt := tt // Capture range variable t.Run(tt.name, func(t *testing.T) { t.Parallel() // Mark as parallel result := slowFunction(tt.input) if result != expected { t.Error("Failed") } }) } }

Test Helpers

go
// Helper function func assertEqual(t *testing.T, got, want interface{}) { t.Helper() // Marks this as helper (error line points to caller) if got != want { t.Errorf("got %v, want %v", got, want) } } // Setup and teardown func TestWithSetup(t *testing.T) { // Setup db := setupTestDatabase() defer db.Close() // Teardown // Test code result := db.Query("SELECT 1") assertEqual(t, result, 1) } // TestMain for package-level setup/teardown func TestMain(m *testing.M) { // Setup setup() // Run tests code := m.Run() // Teardown teardown() os.Exit(code) } // Cleanup function (Go 1.14+) func TestWithCleanup(t *testing.T) { file := createTempFile(t) t.Cleanup(func() { os.Remove(file.Name()) }) // Test code }

Testify Library

go
import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestWithTestify(t *testing.T) { // assert continues on failure assert.Equal(t, 5, Add(2, 3)) assert.NotNil(t, obj) assert.True(t, condition) assert.Contains(t, "hello world", "world") assert.Len(t, slice, 3) // require stops on failure require.NoError(t, err) require.NotNil(t, obj) // Test stops here if nil obj.DoSomething() // Safe to call } // Mock with testify import "github.com/stretchr/testify/mock" type MockDatabase struct { mock.Mock } func (m *MockDatabase) GetUser(id string) (*User, error) { args := m.Called(id) return args.Get(0).(*User), args.Error(1) } func TestWithMock(t *testing.T) { mockDB := new(MockDatabase) mockDB.On("GetUser", "123").Return(&User{Name: "Alice"}, nil) service := NewService(mockDB) user, err := service.GetUser("123") assert.NoError(t, err) assert.Equal(t, "Alice", user.Name) mockDB.AssertExpectations(t) }

Chapter 19: Benchmarking

go
// Benchmark function must start with Benchmark func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { Add(2, 3) } } // Reset timer after setup func BenchmarkWithSetup(b *testing.B) { data := expensiveSetup() b.ResetTimer() // Don't include setup time for i := 0; i < b.N; i++ { process(data) } } // Report allocations func BenchmarkAllocations(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { _ = make([]byte, 1024) } } // Parallel benchmark func BenchmarkParallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { doWork() } }) } // Sub-benchmarks func BenchmarkSizes(b *testing.B) { sizes := []int{100, 1000, 10000} for _, size := range sizes { b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) { data := make([]int, size) b.ResetTimer() for i := 0; i < b.N; i++ { process(data) } }) } }
Running benchmarks:
bash
go test -bench=. # Run all benchmarks go test -bench=BenchmarkAdd # Run specific benchmark go test -bench=. -benchmem # Include memory stats go test -bench=. -benchtime=5s # Run for 5 seconds go test -bench=. -count=5 # Run 5 times

Chapter 20: Mocking and Test Doubles

Interface-Based Mocking

go
// Define interface for dependency type UserRepository interface { FindByID(id string) (*User, error) Save(user *User) error } // Real implementation type PostgresUserRepo struct { db *sql.DB } func (r *PostgresUserRepo) FindByID(id string) (*User, error) { // Actual database query } // Mock implementation type MockUserRepo struct { Users map[string]*User SavedUsers []*User FindError error SaveError error } func (m *MockUserRepo) FindByID(id string) (*User, error) { if m.FindError != nil { return nil, m.FindError } return m.Users[id], nil } func (m *MockUserRepo) Save(user *User) error { if m.SaveError != nil { return m.SaveError } m.SavedUsers = append(m.SavedUsers, user) return nil } // Test using mock func TestUserService(t *testing.T) { mock := &MockUserRepo{ Users: map[string]*User{ "123": {ID: "123", Name: "Alice"}, }, } service := NewUserService(mock) user, err := service.GetUser("123") assert.NoError(t, err) assert.Equal(t, "Alice", user.Name) } // Test error case func TestUserService_NotFound(t *testing.T) { mock := &MockUserRepo{ FindError: ErrNotFound, } service := NewUserService(mock) _, err := service.GetUser("999") assert.ErrorIs(t, err, ErrNotFound) }

HTTP Testing

go
import ( "net/http" "net/http/httptest" "testing" ) func TestHandler(t *testing.T) { // Create request req := httptest.NewRequest("GET", "/users/123", nil) req.Header.Set("Content-Type", "application/json") // Create response recorder rec := httptest.NewRecorder() // Call handler handler := NewUserHandler(mockRepo) handler.ServeHTTP(rec, req) // Assert response assert.Equal(t, http.StatusOK, rec.Code) var user User json.Unmarshal(rec.Body.Bytes(), &user) assert.Equal(t, "123", user.ID) } // Test server func TestAPI(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"id": "123", "name": "Alice"}`)) })) defer server.Close() // Use server.URL as base URL client := NewAPIClient(server.URL) user, err := client.GetUser("123") assert.NoError(t, err) assert.Equal(t, "Alice", user.Name) }

Chapter 21: Integration and E2E Testing

Database Testing

go
import ( "database/sql" "testing" _ "github.com/lib/pq" ) func setupTestDB(t *testing.T) *sql.DB { db, err := sql.Open("postgres", "postgres://test:test@localhost/testdb?sslmode=disable") require.NoError(t, err) t.Cleanup(func() { db.Close() }) // Run migrations _, err = db.Exec(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL ) `) require.NoError(t, err) return db } func TestUserRepository(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test") } db := setupTestDB(t) repo := NewPostgresUserRepo(db) // Test insert user := &User{Name: "Alice"} err := repo.Save(user) require.NoError(t, err) // Test find found, err := repo.FindByID(user.ID) require.NoError(t, err) assert.Equal(t, "Alice", found.Name) }

Testcontainers

go
import ( "context" "testing" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) func TestWithContainer(t *testing.T) { ctx := context.Background() // Start PostgreSQL container container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ Image: "postgres:15", ExposedPorts: []string{"5432/tcp"}, Env: map[string]string{ "POSTGRES_USER": "test", "POSTGRES_PASSWORD": "test", "POSTGRES_DB": "testdb", }, WaitingFor: wait.ForListeningPort("5432/tcp"), }, Started: true, }) require.NoError(t, err) defer container.Terminate(ctx) // Get connection string host, _ := container.Host(ctx) port, _ := container.MappedPort(ctx, "5432") dsn := fmt.Sprintf("postgres://test:test@%s:%s/testdb?sslmode=disable", host, port.Port()) // Run tests with real database db, _ := sql.Open("postgres", dsn) repo := NewPostgresUserRepo(db) // ... run tests }

Part 6: Project Structure and Architecture

Chapter 22: Package Organization

Standard Project Layout

myproject/ ├── cmd/ # Application entry points │ ├── api/ │ │ └── main.go │ └── worker/ │ └── main.go ├── internal/ # Private packages │ ├── config/ # Configuration │ ├── domain/ # Core business logic │ │ ├── user/ │ │ │ ├── entity.go │ │ │ ├── repository.go │ │ │ └── service.go │ │ └── order/ │ ├── handler/ # HTTP handlers │ ├── repository/ # Data access │ │ ├── postgres/ │ │ └── redis/ │ └── middleware/ # HTTP middleware ├── pkg/ # Public packages │ └── validator/ ├── api/ # API specifications │ └── openapi.yaml ├── migrations/ # Database migrations ├── scripts/ # Build/deploy scripts ├── test/ # Additional test files │ ├── integration/ │ └── fixtures/ ├── go.mod ├── go.sum ├── Makefile ├── Dockerfile └── README.md

Package Design Principles

go
// 1. Package names should be short, lowercase, singular package user // Good package users // Avoid plural package userService // Avoid camelCase // 2. Avoid package names like "util", "common", "misc" package util // Bad - unclear purpose package stringutil // Better - specific purpose // 3. Package should provide a focused API // Bad: package does too many things package helper func FormatDate() func ValidateEmail() func CalculateTax() // Good: focused packages package dateutil func Format() package emailvalidator func Validate() // 4. Avoid circular imports by designing proper dependency flow // Low-level packages should not import high-level packages // 5. Use internal/ to hide implementation details internal/database/ // Only importable by this module pkg/database/ // Importable by external modules

Dependency Injection

go
// Constructor injection type UserService struct { repo UserRepository cache Cache logger Logger } func NewUserService(repo UserRepository, cache Cache, logger Logger) *UserService { return &UserService{ repo: repo, cache: cache, logger: logger, } } // Wire all dependencies in main func main() { // Create dependencies db := setupDatabase() cache := setupRedis() logger := setupLogger() // Create repositories userRepo := postgres.NewUserRepository(db) // Create services userService := service.NewUserService(userRepo, cache, logger) // Create handlers userHandler := handler.NewUserHandler(userService) // Setup routes router := setupRouter(userHandler) // Start server http.ListenAndServe(":8080", router) }

Chapter 23: Clean Architecture in Go

Layer Separation

// Domain Layer - Core business logic (no external dependencies) // internal/domain/user/entity.go package user type User struct { ID string Email string Name string CreatedAt time.Time } func NewUser(email, name string) (*User, error) { if email == "" { return nil, errors.New("email is required") } return &User{ ID: uuid.New().String(), Email: email, Name: name, CreatedAt: time.Now(), }, nil } // Repository interface (defined in domain, implemented elsewhere) // internal/domain/user/repository.go package user type Repository interface { FindByID(ctx context.Context, id string) (*User, error) FindByEmail(ctx context.Context, email string) (*User, error) Save(ctx context.Context, user *User) error Delete(ctx context.Context, id string) error } // Service (business logic) // internal/domain/user/service.go package user type Service struct { repo Repository hasher PasswordHasher } func NewService(repo Repository, hasher PasswordHasher) *Service { return &Service{repo: repo, hasher: hasher} } func (s *Service) Register(ctx context.Context, email, name, password string) (*User, error) { // Check if user exists existing, _ := s.repo.FindByEmail(ctx, email) if existing != nil { return nil, ErrUserAlreadyExists } // Create user user, err := NewUser(email, name) if err != nil { return nil, err } // Save user if err := s.repo.Save(ctx, user); err != nil { return nil, err } return user, nil }

Part 7: Logging, Metrics, and Observability

Chapter 24: Logging

Standard Library Logging

go
import "log" // Basic logging log.Println("Server started") log.Printf("Listening on port %d", port) log.Fatal("Failed to start server") // Logs and calls os.Exit(1) log.Panic("Critical error") // Logs and panics // Custom logger logger := log.New(os.Stdout, "APP: ", log.Ldate|log.Ltime|log.Lshortfile) logger.Println("Custom log message") // Log flags log.Ldate // Date: 2009/01/23 log.Ltime // Time: 01:23:23 log.Lmicroseconds // Microsecond time: 01:23:23.123123 log.Llongfile // Full file path: /a/b/c/d.go:23 log.Lshortfile // File name: d.go:23 log.LUTC // Use UTC time log.Lmsgprefix // Prefix at start of line log.LstdFlags // Ldate | Ltime

Structured Logging with Zerolog

go
import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) // Setup zerolog.TimeFieldFormat = zerolog.TimeFormatUnix log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) // Basic logging log.Info().Msg("Server started") log.Error().Err(err).Msg("Failed to connect") // With fields log.Info(). Str("service", "api"). Int("port", 8080). Msg("Server started") // With context logger := log.With(). Str("request_id", requestID). Str("user_id", userID). Logger() logger.Info().Msg("Processing request") // Log levels log.Trace().Msg("Trace message") log.Debug().Msg("Debug message") log.Info().Msg("Info message") log.Warn().Msg("Warning message") log.Error().Msg("Error message") log.Fatal().Msg("Fatal message") // Exits log.Panic().Msg("Panic message") // Panics

Structured Logging with Zap

go
import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // Development logger logger, _ := zap.NewDevelopment() defer logger.Sync() // Production logger logger, _ := zap.NewProduction() // Sugared logger (slower but more convenient) sugar := logger.Sugar() sugar.Infow("Server started", "port", 8080, "env", "production", ) // Structured logger (faster) logger.Info("Server started", zap.Int("port", 8080), zap.String("env", "production"), ) // With context contextLogger := logger.With( zap.String("request_id", requestID), ) contextLogger.Info("Processing request") // Custom configuration config := zap.Config{ Level: zap.NewAtomicLevelAt(zap.InfoLevel), Development: false, Encoding: "json", EncoderConfig: zapcore.EncoderConfig{ TimeKey: "timestamp", LevelKey: "level", NameKey: "logger", CallerKey: "caller", MessageKey: "message", StacktraceKey: "stacktrace", LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.LowercaseLevelEncoder, EncodeTime: zapcore.ISO8601TimeEncoder, EncodeDuration: zapcore.SecondsDurationEncoder, EncodeCaller: zapcore.ShortCallerEncoder, }, OutputPaths: []string{"stdout"}, ErrorOutputPaths: []string{"stderr"}, } logger, _ := config.Build()

Chapter 25: Metrics and Monitoring

Prometheus Metrics

go
import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" ) // Counter - monotonically increasing var requestsTotal = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "http_requests_total", Help: "Total number of HTTP requests", }, []string{"method", "path", "status"}, ) // Gauge - value that can go up or down var activeConnections = promauto.NewGauge( prometheus.GaugeOpts{ Name: "active_connections", Help: "Number of active connections", }, ) // Histogram - distribution of values var requestDuration = promauto.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "HTTP request duration in seconds", Buckets: prometheus.DefBuckets, // .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10 }, []string{"method", "path"}, ) // Summary - similar to histogram but calculates quantiles var responseSize = promauto.NewSummaryVec( prometheus.SummaryOpts{ Name: "http_response_size_bytes", Help: "HTTP response size in bytes", Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }, []string{"method", "path"}, ) // Usage func handler(w http.ResponseWriter, r *http.Request) { timer := prometheus.NewTimer(requestDuration.WithLabelValues(r.Method, r.URL.Path)) defer timer.ObserveDuration() requestsTotal.WithLabelValues(r.Method, r.URL.Path, "200").Inc() // Handle request... } // Expose metrics endpoint http.Handle("/metrics", promhttp.Handler())

Middleware for Metrics

go
func metricsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() // Wrap response writer to capture status code wrapped := &responseWriter{ResponseWriter: w, statusCode: 200} next.ServeHTTP(wrapped, r) duration := time.Since(start).Seconds() requestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration) requestsTotal.WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(wrapped.statusCode)).Inc() }) } type responseWriter struct { http.ResponseWriter statusCode int } func (w *responseWriter) WriteHeader(code int) { w.statusCode = code w.ResponseWriter.WriteHeader(code) }

Chapter 26: Distributed Tracing

OpenTelemetry

go
import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/jaeger" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" ) // Setup tracer func initTracer() (*trace.TracerProvider, error) { exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/traces"))) if err != nil { return nil, err } tp := trace.NewTracerProvider( trace.WithBatcher(exporter), trace.WithResource(resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String("my-service"), attribute.String("environment", "production"), )), ) otel.SetTracerProvider(tp) return tp, nil } // Create spans func handleRequest(ctx context.Context) error { tracer := otel.Tracer("my-service") ctx, span := tracer.Start(ctx, "handleRequest") defer span.End() // Add attributes span.SetAttributes( attribute.String("user.id", userID), attribute.Int("items.count", len(items)), ) // Create child span ctx, childSpan := tracer.Start(ctx, "processItems") err := processItems(ctx, items) childSpan.End() if err != nil { span.RecordError(err) return err } return nil } // HTTP middleware for tracing func tracingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tracer := otel.Tracer("http-server") ctx, span := tracer.Start(r.Context(), r.URL.Path) defer span.End() span.SetAttributes( attribute.String("http.method", r.Method), attribute.String("http.url", r.URL.String()), ) next.ServeHTTP(w, r.WithContext(ctx)) }) }

Part 8: Performance and Optimization

Chapter 27: Profiling

CPU Profiling

go
import ( "os" "runtime/pprof" ) func main() { // Start CPU profile f, _ := os.Create("cpu.prof") pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() // Run your code doWork() } // Or via HTTP import _ "net/http/pprof" func main() { go func() { http.ListenAndServe("localhost:6060", nil) }() // Your application }
Analyze with:
bash
go tool pprof cpu.prof go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

Memory Profiling

go
// Write heap profile f, _ := os.Create("mem.prof") pprof.WriteHeapProfile(f) f.Close() // Or via HTTP go tool pprof http://localhost:6060/debug/pprof/heap

Trace

go
import "runtime/trace" func main() { f, _ := os.Create("trace.out") trace.Start(f) defer trace.Stop() // Your code }
Analyze with:
go tool trace trace.out

Benchmarking Memory

go
func BenchmarkAllocations(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { _ = make([]byte, 1024) } }

Chapter 28: Memory Management

Understanding Allocations

go
// Stack allocation (cheap) func stackAlloc() int { x := 42 // Stays on stack return x } // Heap allocation (expensive) func heapAlloc() *int { x := 42 return &x // Escapes to heap } // Check escape analysis // go build -gcflags="-m" main.go // Reduce allocations // 1. Reuse objects var bufPool = sync.Pool{ New: func() interface{} { return make([]byte, 1024) }, } func process() { buf := bufPool.Get().([]byte) defer bufPool.Put(buf) // Use buf } // 2. Preallocate slices func processItems(items []Item) { results := make([]Result, 0, len(items)) // Preallocate for _, item := range items { results = append(results, process(item)) } } // 3. Use value types for small data type Point struct { X, Y float64 } // 16 bytes - good for value type LargeStruct struct { /* many fields */ } // Use pointer

Garbage Collection

go
import "runtime" // Force garbage collection runtime.GC() // Get memory stats var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Printf("Alloc = %v MiB\n", m.Alloc / 1024 / 1024) fmt.Printf("TotalAlloc = %v MiB\n", m.TotalAlloc / 1024 / 1024) fmt.Printf("Sys = %v MiB\n", m.Sys / 1024 / 1024) fmt.Printf("NumGC = %v\n", m.NumGC) // Set GC target percentage (default 100) debug.SetGCPercent(50) // More aggressive GC debug.SetGCPercent(200) // Less aggressive GC // Environment variable // GOGC=50 ./myprogram

Chapter 29: Performance Patterns

Avoiding Common Pitfalls

go
// 1. String concatenation // Bad s := "" for i := 0; i < 1000; i++ { s += strconv.Itoa(i) } // Good var b strings.Builder for i := 0; i < 1000; i++ { b.WriteString(strconv.Itoa(i)) } s := b.String() // 2. Unnecessary conversions // Bad for _, b := range []byte(s) { } // Good (when possible) for i := 0; i < len(s); i++ { b := s[i] } // 3. Defer in tight loops // Bad for _, item := range items { f, _ := os.Open(item) defer f.Close() // Defers pile up process(f) } // Good for _, item := range items { func() { f, _ := os.Open(item) defer f.Close() process(f) }() } // 4. Interface boxing // Bad func sum(nums []interface{}) int { total := 0 for _, n := range nums { total += n.(int) // Type assertion on each iteration } return total } // Good func sum(nums []int) int { total := 0 for _, n := range nums { total += n } return total } // 5. Map pre-sizing // Bad m := make(map[string]int) for i := 0; i < 10000; i++ { m[strconv.Itoa(i)] = i // Many resizes } // Good m := make(map[string]int, 10000) // Pre-sized for i := 0; i < 10000; i++ { m[strconv.Itoa(i)] = i }

Part 9: Database and Persistence

Chapter 30: Database/SQL

Connection Management

go
import ( "database/sql" _ "github.com/lib/pq" ) func initDB() (*sql.DB, error) { db, err := sql.Open("postgres", "postgres://user:pass@localhost/dbname?sslmode=disable") if err != nil { return nil, err } // Connection pool settings db.SetMaxOpenConns(25) // Max open connections db.SetMaxIdleConns(5) // Max idle connections db.SetConnMaxLifetime(5 * time.Minute) // Max connection lifetime db.SetConnMaxIdleTime(1 * time.Minute) // Max idle time // Verify connection if err := db.Ping(); err != nil { return nil, err } return db, nil }

Queries

go
// Query single row var name string var age int err := db.QueryRow("SELECT name, age FROM users WHERE id = $1", id).Scan(&name, &age) if err == sql.ErrNoRows { return nil, ErrNotFound } if err != nil { return nil, err } // Query multiple rows rows, err := db.Query("SELECT id, name FROM users WHERE active = $1", true) if err != nil { return nil, err } defer rows.Close() var users []User for rows.Next() { var u User if err := rows.Scan(&u.ID, &u.Name); err != nil { return nil, err } users = append(users, u) } if err := rows.Err(); err != nil { return nil, err } // Execute (INSERT, UPDATE, DELETE) result, err := db.Exec("INSERT INTO users (name, email) VALUES ($1, $2)", name, email) if err != nil { return err } id, _ := result.LastInsertId() affected, _ := result.RowsAffected() // With context ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", id)

Transactions

go
func transferMoney(db *sql.DB, from, to string, amount int) error { tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() // No-op if committed // Deduct from sender result, err := tx.Exec("UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1", amount, from) if err != nil { return err } if rows, _ := result.RowsAffected(); rows == 0 { return errors.New("insufficient funds") } // Add to receiver _, err = tx.Exec("UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, to) if err != nil { return err } return tx.Commit() } // With options tx, err := db.BeginTx(ctx, &sql.TxOptions{ Isolation: sql.LevelSerializable, ReadOnly: false, })

Prepared Statements

go
// Prepare once, use many times stmt, err := db.Prepare("SELECT name FROM users WHERE id = $1") if err != nil { return err } defer stmt.Close() for _, id := range ids { var name string err := stmt.QueryRow(id).Scan(&name) if err != nil { return err } fmt.Println(name) }

Chapter 31: GORM and ORMs

go
import "gorm.io/gorm" // Define model type User struct { ID uint `gorm:"primaryKey"` Name string `gorm:"size:100;not null"` Email string `gorm:"uniqueIndex"` Age int CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` // Soft delete } // Connect db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) // Auto migrate db.AutoMigrate(&User{}) // Create user := User{Name: "Alice", Email: "alice@example.com"} result := db.Create(&user) // user.ID is set after insert // Read var user User db.First(&user, 1) // By primary key db.First(&user, "email = ?", "alice@example.com") db.Find(&users) // All users // Update db.Model(&user).Update("Name", "Bob") db.Model(&user).Updates(User{Name: "Bob", Age: 30}) db.Model(&user).Updates(map[string]interface{}{"name": "Bob", "age": 30}) // Delete db.Delete(&user, 1) // Associations type User struct { ID uint Name string Articles []Article `gorm:"foreignKey:AuthorID"` } type Article struct { ID uint Title string AuthorID uint Author User } // Preload associations db.Preload("Articles").Find(&users) // Transactions db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&user).Error; err != nil { return err // Rollback } return nil // Commit })

Chapter 32: Redis

go
import "github.com/redis/go-redis/v9" // Connect rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", DB: 0, }) ctx := context.Background() // String operations err := rdb.Set(ctx, "key", "value", time.Hour).Err() val, err := rdb.Get(ctx, "key").Result() if err == redis.Nil { fmt.Println("Key does not exist") } // Hash operations rdb.HSet(ctx, "user:1", "name", "Alice", "age", 30) name, _ := rdb.HGet(ctx, "user:1", "name").Result() user, _ := rdb.HGetAll(ctx, "user:1").Result() // List operations rdb.LPush(ctx, "queue", "item1", "item2") item, _ := rdb.RPop(ctx, "queue").Result() // Set operations rdb.SAdd(ctx, "tags", "go", "programming") tags, _ := rdb.SMembers(ctx, "tags").Result() isMember, _ := rdb.SIsMember(ctx, "tags", "go").Result() // Sorted set rdb.ZAdd(ctx, "leaderboard", redis.Z{Score: 100, Member: "alice"}) rdb.ZAdd(ctx, "leaderboard", redis.Z{Score: 200, Member: "bob"}) top, _ := rdb.ZRevRangeWithScores(ctx, "leaderboard", 0, 9).Result() // Pub/Sub pubsub := rdb.Subscribe(ctx, "channel") defer pubsub.Close() for msg := range pubsub.Channel() { fmt.Println(msg.Channel, msg.Payload) } // Publish rdb.Publish(ctx, "channel", "message") // Transactions (Pipeline) pipe := rdb.Pipeline() pipe.Set(ctx, "key1", "value1", 0) pipe.Set(ctx, "key2", "value2", 0) pipe.Exec(ctx) // Distributed lock lock := rdb.SetNX(ctx, "lock:resource", "owner", 30*time.Second) if lock.Val() { defer rdb.Del(ctx, "lock:resource") // Do work }

Part 10: Deployment and Operations

Chapter 33: Docker

Dockerfile for Go

dockerfile
# Build stage FROM golang:1.21-alpine AS builder WORKDIR /app # Copy go mod files COPY go.mod go.sum ./ RUN go mod download # Copy source COPY . . # Build binary RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./cmd/api # Final stage FROM alpine:3.18 # Add ca-certificates for HTTPS RUN apk --no-cache add ca-certificates tzdata WORKDIR /app # Copy binary from builder COPY --from=builder /app/server . # Create non-root user RUN adduser -D -g '' appuser USER appuser EXPOSE 8080 ENTRYPOINT ["./server"]

Docker Compose

yaml
version: '3.8' services: api: build: . ports: - "8080:8080" environment: - DB_HOST=postgres - REDIS_HOST=redis depends_on: - postgres - redis postgres: image: postgres:15 environment: POSTGRES_USER: app POSTGRES_PASSWORD: secret POSTGRES_DB: appdb volumes: - postgres_data:/var/lib/postgresql/data redis: image: redis:7 volumes: - redis_data:/data volumes: postgres_data: redis_data:

Chapter 34: Kubernetes Basics

Deployment

yaml
apiVersion: apps/v1 kind: Deployment metadata: name: api-server spec: replicas: 3 selector: matchLabels: app: api-server template: metadata: labels: app: api-server spec: containers: - name: api image: myapp:latest ports: - containerPort: 8080 env: - name: DB_HOST valueFrom: configMapKeyRef: name: app-config key: db_host - name: DB_PASSWORD valueFrom: secretKeyRef: name: app-secrets key: db_password resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "256Mi" cpu: "500m" livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5

Service

yaml
apiVersion: v1 kind: Service metadata: name: api-service spec: selector: app: api-server ports: - port: 80 targetPort: 8080 type: ClusterIP

Chapter 35: CI/CD with Go

GitHub Actions

yaml
name: CI on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.21' - name: Install dependencies run: go mod download - name: Run tests run: go test -v -race -coverprofile=coverage.out ./... - name: Upload coverage uses: codecov/codecov-action@v3 with: file: ./coverage.out lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: version: latest build: runs-on: ubuntu-latest needs: [test, lint] steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.21' - name: Build run: go build -v ./... - name: Build Docker image run: docker build -t myapp:${{ github.sha }} . - name: Push to registry if: github.ref == 'refs/heads/main' run: | docker tag myapp:${{ github.sha }} registry.example.com/myapp:latest docker push registry.example.com/myapp:latest

Part 11: Security

Chapter 36: Security Best Practices

Input Validation

go
import "github.com/go-playground/validator/v10" type CreateUserRequest struct { Email string `json:"email" validate:"required,email"` Password string `json:"password" validate:"required,min=8,max=72"` Age int `json:"age" validate:"gte=0,lte=150"` } var validate = validator.New() func validateRequest(req interface{}) error { return validate.Struct(req) }

SQL Injection Prevention

go
// Always use parameterized queries // BAD - SQL injection vulnerable query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", name) db.Query(query) // GOOD - parameterized db.Query("SELECT * FROM users WHERE name = $1", name)

Password Hashing

go
import "golang.org/x/crypto/bcrypt" func hashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(bytes), err } func checkPassword(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil }

JWT Authentication

go
import "github.com/golang-jwt/jwt/v5" type Claims struct { UserID string `json:"user_id"` jwt.RegisteredClaims } func generateToken(userID string, secret []byte) (string, error) { claims := Claims{ UserID: userID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(secret) } func validateToken(tokenString string, secret []byte) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return secret, nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(*Claims); ok && token.Valid { return claims, nil } return nil, errors.New("invalid token") }

HTTPS and TLS

go
// Generate self-signed certificate for development // openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes // HTTPS server server := &http.Server{ Addr: ":443", Handler: handler, TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS12, }, } server.ListenAndServeTLS("cert.pem", "key.pem") // HTTPS client client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS12, }, }, }

Part 12: Tools and Ecosystem

Chapter 37: Essential Go Tools

Build and Run

bash
go build # Build current package go build -o app ./cmd/api # Build with output name go run main.go # Compile and run go install # Build and install to $GOPATH/bin # Build flags go build -ldflags="-w -s" # Strip debug info (smaller binary) go build -race # Enable race detector go build -tags=integration # Build with tag # Cross-compilation GOOS=linux GOARCH=amd64 go build GOOS=windows GOARCH=amd64 go build GOOS=darwin GOARCH=arm64 go build

Testing

bash
go test # Run tests in current package go test ./... # Run tests in all packages go test -v # Verbose output go test -run TestName # Run specific test go test -cover # Show coverage go test -coverprofile=cover.out # Coverage profile go tool cover -html=cover.out # View coverage in browser go test -race # Enable race detector go test -short # Skip long tests go test -bench=. # Run benchmarks go test -benchmem # Include memory stats in benchmarks

Code Quality

bash
go fmt ./... # Format code go vet ./... # Report suspicious constructs go mod tidy # Remove unused dependencies # golangci-lint (install separately) golangci-lint run # Run multiple linters # Static analysis go install golang.org/x/tools/go/analysis/passes/...

Documentation

bash
go doc fmt # View package documentation go doc fmt.Println # View function documentation godoc -http=:6060 # Start documentation server

Module Management

bash
go mod init mymodule # Initialize new module go mod tidy # Add missing, remove unused go mod download # Download dependencies go mod verify # Verify dependencies go mod vendor # Create vendor directory go mod graph # Print module dependency graph go list -m all # List all dependencies go get -u ./... # Update all dependencies go get package@v1.2.3 # Get specific version

Chapter 38: Code Generation

go generate

go
//go:generate stringer -type=Status type Status int const ( Pending Status = iota Active Completed ) // Run: go generate ./... // Creates status_string.go with String() method

Wire (Dependency Injection)

go
// wire.go //go:build wireinject package main import "github.com/google/wire" func InitializeApp() (*App, error) { wire.Build( NewConfig, NewDatabase, NewUserRepository, NewUserService, NewApp, ) return nil, nil } // Run: wire ./... // Generates wire_gen.go with actual initialization code

Mockgen

go
//go:generate mockgen -source=repository.go -destination=mocks/repository_mock.go -package=mocks type UserRepository interface { FindByID(id string) (*User, error) Save(user *User) error } // Run: go generate ./... // Generates mock implementation

Web Frameworks

  • Echo: High performance, minimalist
  • Gin: Fast, middleware support
  • Fiber: Express-inspired, fasthttp
  • Chi: Lightweight, composable router

Database

  • GORM: Full-featured ORM
  • sqlx: Extensions to database/sql
  • pgx: PostgreSQL driver
  • go-redis: Redis client

Validation

  • validator: Struct and field validation
  • ozzo-validation: Validation rules as code

Configuration

  • Viper: Configuration management
  • envconfig: Environment variables to struct

Logging

  • Zerolog: Zero allocation JSON logger
  • Zap: Blazing fast, structured logging
  • Logrus: Structured, pluggable logging

Testing

  • Testify: Assertions and mocking
  • gomock: Mock generation
  • testcontainers: Docker containers for tests

HTTP Clients

  • resty: Simple HTTP client
  • req: Simple and powerful

CLI

  • Cobra: CLI framework
  • urfave/cli: Simple CLI applications

Conclusion

This guide covered the complete landscape of Go development, from the fundamentals of the type system to production deployment. Key takeaways:
  1. Simplicity is a feature - Go's simplicity is intentional. Embrace it.
  2. Explicit is better than implicit - Error handling, type conversions, and dependencies should be visible.
  3. Composition over inheritance - Use interfaces and embedding.
  4. Concurrency is not parallelism - Goroutines and channels are about structure, not just speed.
  5. Write tests - Table-driven tests, benchmarks, and integration tests are first-class citizens.
  6. Profile before optimizing - Use pprof and benchmarks to find real bottlenecks.
  7. Keep dependencies minimal - The standard library is powerful. Use external libraries judiciously.
  8. Structure matters - cmd/, internal/, pkg/ conventions exist for good reasons.
  9. Security is not optional - Validate input, use parameterized queries, hash passwords properly.
  10. Observability is crucial - Logging, metrics, and tracing make production systems manageable.
Master these concepts, and you'll be well-equipped to build reliable, efficient, and maintainable Go applications.

"Don't communicate by sharing memory; share memory by communicating." - Go Proverb
undefined