Skip to content

Go Cheatsheet

Fast, statically typed language built for simplicity and concurrency.

01

Getting Started

Hello World

Every Go program starts in package main's main() function. Use 'go run' to execute, 'go build' to compile. gofmt automatically formats code (tabs, spacing). The import statement pulls in packages — fmt handles formatted I/O.

go
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
    fmt.Printf("Name: %s, Age: %d\n", "Alice", 30)
}

// Run: go run main.go
// Build: go build -o app main.go
// Format: gofmt -w main.go

Go Modules (go.mod)

Go modules (go.mod) manage dependencies since Go 1.11. 'go mod init' creates the module file. 'go get' adds dependencies. 'go mod tidy' removes unused imports and adds missing ones. The module path is the import path for your package.

go
// Initialize a new module
// $ go mod init github.com/user/project

// go.mod file:
module github.com/user/project

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    golang.org/x/sync v0.3.0
)

// Add dependencies:
// $ go get github.com/gin-gonic/gin
// $ go mod tidy  // clean up unused deps

Package Structure

Go organizes code in packages — one package per directory. Capitalized names (Println, Add) are exported (public); lowercase names (private) are package-private. The 'internal' directory restricts imports to the parent module. The package name should match the directory name.

go
// project structure:
// myproject/
//   go.mod
//   main.go          // package main
//   utils/
//     helper.go      // package utils
//     math.go
//   internal/
//     api/           // private (only importable within parent)

// main.go
package main

import (
    "fmt"
    "github.com/user/project/utils"
)

func main() {
    fmt.Println(utils.Add(1, 2))
}

// Exported names start with uppercase letter
// unexported names start with lowercase letter

Variables & Constants

Use 'var name type = value' for explicit declarations, 'name := value' for short declarations (functions only). Go has zero values: 0 for numbers, '' for strings, false for booleans, nil for pointers/slices/maps. Constants (const) are compile-time and can't use :=.

go
package main

import "fmt"

// Package-level declarations
var version = "1.0.0" // inferred type
const Pi = 3.14159

func main() {
    // var with type
    var name string = "Alice"
    var age int = 30

    // Short declaration (only inside functions)
    city := "NYC"

    // Multiple assignment
    a, b, c := 1, 2, 3

    // Zero values (default)
    var x int      // 0
    var s string   // ""
    var ok bool    // false
    var p *int     // nil

    fmt.Println(name, age, city, a, b, c, x, s, ok, p)
}

Basic Types & Conversion

Go requires explicit type conversion — there's no implicit conversion like in C. rune is an alias for int32 (represents a Unicode code point). byte is an alias for uint8. Converting between numeric types may lose precision (float to int truncates). string(65) converts a code point to its character.

go
// Numeric types
var i int = 42
var i8 int8 = 127
var u uint = 42
var f32 float32 = 3.14
var f64 float64 = 3.141592653589793

// Other types
var b bool = true
var s string = "hello"
var r rune = 'A' // int32 alias for Unicode code point
var by byte = 255 // uint8 alias

// Type conversion (explicit, no implicit conversion)
var n int = 42
var f float64 = float64(n)  // int -> float64
var s string = string(65)   // 65 -> "A" (rune to string)
var i2 int = int(f)         // float64 -> int (truncates)

// Type inference
var x = 42        // int
var y = 3.14      // float64
var z = len("hi") // int
02

Strings & Formatting

String Basics

Go strings are immutable byte sequences. len() returns byte count, not character count — use utf8.RuneCountInString() for Unicode. Strings are compared lexicographically with ==, <, >. Range over a string iterates by rune (Unicode code point), handling multi-byte characters correctly.

go
package main

import (
    "fmt"
    "strings"
)

func main() {
    s := "Hello, World"

    // Length (bytes, not runes!)
    fmt.Println(len(s)) // 12

    // Concatenation
    s1 := "Hello" + " " + "World"
    s2 := fmt.Sprintf("%s = %d", "age", 30)

    // Comparison
    fmt.Println("abc" == "abc") // true
    fmt.Println("a" < "b")      // true (lexicographic)

    // Iteration (by rune)
    for i, r := range "Go语" {
        fmt.Printf("%d: %c\n", i, r)
    }
}

strings Package

The strings package provides common string operations. Contains/HasPrefix/HasSuffix check for substrings. Split breaks by delimiter; Join combines. Replace takes a count (-1 for all). TrimSpace removes leading/trailing whitespace. All functions return new strings (strings are immutable).

go
import (
    "fmt"
    "strings"
)

s := "Hello, World"

// Case
strings.ToUpper(s)         // "HELLO, WORLD"
strings.ToLower(s)         // "hello, world"
strings.Title("hello world") // "Hello World"

// Search
strings.Contains(s, "World")  // true
strings.HasPrefix(s, "Hello") // true
strings.HasSuffix(s, "World") // true
strings.Index(s, "World")     // 7 (-1 if not found)
strings.Count(s, "l")         // 3

// Split & Join
parts := strings.Split("a,b,c", ",")  // ["a", "b", "c"]
joined := strings.Join(parts, "-")    // "a-b-c"

// Replace
strings.Replace(s, "l", "L", 1)  // "HeLlo, World" (1 occurrence)
strings.ReplaceAll(s, "l", "L")  // "HeLLo, WorLd"
strings.TrimSpace("  hi  ")      // "hi"

strconv & Formatting

strconv converts between strings and numbers — always check the error return. fmt.Printf formats output: %d (int), %f (float), %s (string), %t (bool), %T (type), %x (hex), %q (quoted). Use %.2f for 2 decimal places, %05d for zero-padding. Sprintf returns the string instead of printing.

go
import (
    "fmt"
    "strconv"
)

// String to number
n, err := strconv.Atoi("42")     // string -> int
f, err := strconv.ParseFloat("3.14", 64)

// Number to string
s1 := strconv.Itoa(42)            // int -> string
s2 := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"

// Printf formatting
fmt.Printf("Int: %d\n", 42)
fmt.Printf("Float: %.2f\n", 3.14159)
fmt.Printf("String: %s\n", "hi")
fmt.Printf("Bool: %t\n", true)
fmt.Printf("Type: %T\n", 42)     // int
fmt.Printf("Hex: %x\n", 255)     // ff
fmt.Printf("Pad: %05d\n", 42)    // 00042
fmt.Printf("Quote: %q\n", "hi")  // "hi"

// Sprintf returns formatted string
result := fmt.Sprintf("Name: %s, Age: %d", "Alice", 30)

Raw Strings & Multiline

Raw strings (backticks) preserve everything literally — no escape sequences, can span multiple lines. Use for regex, SQL, HTML templates. Interpreted strings (double quotes) process \n, \t, etc. For efficient string concatenation in loops, use strings.Builder (avoids O(n²) allocation).

go
// Raw string literal (backticks) - no escape processing
raw := `This is a
multiline string
with \n (literal backslash-n)`

// Interpreted string (double quotes) - processes escapes
interpreted := "Line1\nLine2\tTabbed"

// Raw strings are useful for:
// - Regex patterns
// - HTML/SQL templates
// - File paths on Windows
regex := `^d{4}-d{2}-d{2}$`

// String builder (efficient concatenation)
var sb strings.Builder
for i := 0; i < 1000; i++ {
    sb.WriteString("line\n")
}
result := sb.String()

Unicode & Runes

Go strings are UTF-8 encoded byte sequences. len() gives bytes; utf8.RuneCountInString() gives characters. Range over a string decodes UTF-8 automatically. To modify a string, convert to []rune, change, and convert back. This is essential for internationalized text processing.

go
import (
    "fmt"
    "unicode/utf8"
)

s := "Hello, 世界"

// Byte length vs rune length
fmt.Println(len(s))                    // 13 (bytes)
fmt.Println(utf8.RuneCountInString(s)) // 9 (runes/characters)

// Decode runes manually
for i := 0; i < len(s); {
    r, size := utf8.DecodeRuneInString(s[i:])
    fmt.Printf("%c ", r)
    i += size
}

// Range automatically decodes runes
for i, r := range s {
    fmt.Printf("%d:%c ", i, r)
}

// Rune slice (mutable)
runes := []rune("Hello")
runes[0] = 'J'
s2 := string(runes) // "Jello"
03

Control Flow

If / Else

Go's if doesn't need parentheses around conditions, but braces are mandatory (even for single-line bodies). The init statement (if x := f(); x > 0) is scoped to the if/else block — common for error checking. This pattern keeps variable scope tight.

go
// Basic if/else
score := 85
if score >= 90 {
    fmt.Println("A")
} else if score >= 80 {
    fmt.Println("B")
} else {
    fmt.Println("C")
}

// If with initialization statement
if n := computeValue(); n > 100 {
    fmt.Println("big:", n)
} else {
    fmt.Println("small:", n)
}
// n is not accessible here (scoped to if)

// No parentheses needed, but braces are required

For Loops

Go has only one loop keyword: 'for'. It handles C-style, while-style (for cond), infinite (for), and iteration (for range). Range works with slices, maps, strings, and channels. Use _ to skip the index or value. Map iteration order is random by design.

go
// C-style for
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

// While-style (condition only)
n := 10
for n > 0 {
    n--
}

// Infinite loop
for {
    break
}

// Range (iterate slices, maps, strings)
nums := []int{10, 20, 30}
for index, value := range nums {
    fmt.Printf("%d: %d\n", index, value)
}

// Skip index or value with _
for _, value := range nums {
    fmt.Println(value)
}

// Range over map (unordered!)
m := map[string]int{"a": 1, "b": 2}
for key, val := range m {
    fmt.Println(key, val)
}

Switch

Go's switch doesn't fall through by default (unlike C/Java) — each case is a separate branch. Use fallthrough to force it. Multiple values per case use commas. Switch with no expression acts as a cleaner if/else chain. Switch with init statement scopes the variable to the switch.

go
// Basic switch
day := 3
switch day {
case 1:
    fmt.Println("Mon")
case 2, 3, 4, 5:
    fmt.Println("Weekday")
case 6, 7:
    fmt.Println("Weekend")
default:
    fmt.Println("Invalid")
}

// Switch with no expression (like if/else chain)
switch {
case score >= 90:
    grade = "A"
case score >= 80:
    grade = "B"
default:
    grade = "C"
}

// Switch with init
switch os := runtime.GOOS; os {
case "linux":
    fmt.Println("Linux")
case "darwin":
    fmt.Println("macOS")
}

// Fallthrough (rare, goes to next case unconditionally)
switch 1 {
case 1:
    fmt.Println("one")
    fallthrough
case 2:
    fmt.Println("two") // executes even though value is 1
}

Defer

defer schedules a function call to run when the surrounding function returns — last-in-first-out (LIFO). Use it for cleanup (closing files, releasing locks, closing connections). Deferred calls' arguments are evaluated immediately, but the call executes at return. Defers run even if the function panics.

go
// Defer runs when function returns (LIFO order)
func main() {
    defer fmt.Println("third")
    defer fmt.Println("second")
    fmt.Println("first")
}
// Output: first, second, third

// Common: resource cleanup
func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // runs when readFile returns

    // ... use f ...
    return nil
}

// Defer evaluates arguments immediately
i := 1
defer fmt.Println(i) // prints 1 (not 2)
i = 2

Goto, Break, Continue

break exits the innermost loop; continue skips to the next iteration. Labels (break outer) exit nested loops. goto exists but is rarely used — prefer structured control flow. Labels can also be used with continue to skip to the next iteration of an outer loop.

go
// Break and continue
for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue // skip even numbers
    }
    if i > 7 {
        break // exit loop
    }
    fmt.Println(i)
}

// Break with label (for nested loops)
outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            break outer // exits both loops
        }
    }
}

// Goto (rarely used, avoid)
i := 0
loop:
    if i < 5 {
        fmt.Println(i)
        i++
        goto loop
    }

Select (Channel Operations)

select is like switch for channels — it waits on multiple channel operations and picks the first one ready. The default case makes it non-blocking. Use select in a loop for event-driven patterns. time.After() creates a timeout channel. select picks randomly if multiple cases are ready, preventing starvation.

go
// Select waits on multiple channel operations
select {
case msg := <-messages:
    fmt.Println("received:", msg)
case <-timeout:
    fmt.Println("timed out")
default:
    fmt.Println("no activity") // non-blocking
}

// Select with send
ch1 := make(chan int)
ch2 := make(chan int)
go func() { ch1 <- 1 }()
go func() { ch2 <- 2 }()

select {
case v := <-ch1:
    fmt.Println("ch1:", v)
case v := <-ch2:
    fmt.Println("ch2:", v)
}

// Select in a loop (event loop pattern)
for {
    select {
    case msg := <-ch:
        fmt.Println(msg)
    case <-time.After(5 * time.Second):
        return // timeout after 5s of inactivity
    }
}
04

Functions

Define & Multiple Returns

Go functions can return multiple values — the canonical pattern is (result, error). Named returns act as declared variables initialized to zero values; a 'naked' return uses them. Named returns improve readability for complex functions but can be confusing if overused. Always check errors immediately.

go
// Function with multiple return values
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("divide 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 (uses named values)
}

// Usage
result, err := divide(10, 2)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result) // 5

x, y := split(100)
fmt.Println(x, y)

Variadic & Closures

Variadic functions (...T) accept any number of arguments, received as a slice. Spread a slice with .... Closures capture variables by reference from their enclosing scope — the counter() function returns a closure that remembers 'count'. Closures are useful for callbacks, iterators, and stateful functions.

go
// Variadic function (variable arguments)
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}
fmt.Println(sum(1, 2, 3))       // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15

// Spread a slice
nums := []int{1, 2, 3}
fmt.Println(sum(nums...)) // 6

// Closure (function that captures variables)
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

next := counter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3

Functions as Values

Functions in Go are first-class values — they can be assigned to variables, passed as arguments, and stored in data structures. Define function types with 'type Name func(params) returns'. Maps of functions are useful for dispatch tables, command handlers, and strategy patterns.

go
// Functions are first-class values
func apply(f func(int) int, x int) int {
    return f(x)
}

func double(x int) int { return x * 2 }
func square(x int) int { return x * x }

fmt.Println(apply(double, 5)) // 10
fmt.Println(apply(square, 5)) // 25

// Function type
type MathFunc func(int) int

var fn MathFunc = func(x int) int { return x + 1 }
fmt.Println(fn(10)) // 11

// Map of functions
ops := map[string]func(int, int) int{
    "add": func(a, b int) int { return a + b },
    "sub": func(a, b int) int { return a - b },
    "mul": func(a, b int) int { return a * b },
}
fmt.Println(ops["add"](3, 4)) // 7

init() & Anonymous Functions

init() functions run automatically before main(), in the order they're declared. Use for setup (config loading, validation, registration). Multiple init() functions per file are allowed. Anonymous functions can be immediately invoked (IIFE) or assigned to variables. They're also used for goroutines.

go
// init() runs before main(), once per package
// Multiple init() functions allowed, run in order declared
package main

import "fmt"

var config string

func init() {
    // Setup code: config, connections, validation
    config = "production"
    fmt.Println("init 1")
}

func init() {
    fmt.Println("init 2, config:", config)
}

func main() {
    fmt.Println("main")
}
// Output: init 1, init 2, main

// Anonymous function (IIFE)
result := func(x int) int {
    return x * 2
}(5) // immediately invoked, result = 10

// Goroutine with anonymous function
go func(msg string) {
    fmt.Println(msg)
}("async")

Generics (Go 1.18+)

Go 1.18+ supports generics with type parameters [T any]. Type constraints (interfaces) restrict allowed types — use 'any' for no constraint, 'comparable' for == / != operators. Union constraints (int | float64) allow specific types. Generics enable type-safe reusable data structures and algorithms without code duplication.

go
// Generic function with type parameter
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
}

nums := []int{1, 2, 3}
doubled := Map(nums, func(n int) int { return n * 2 })
// [2, 4, 6]

strs := Map(nums, func(n int) string {
    return fmt.Sprintf("num%d", n)
})
// ["num1", "num2", "num3"]

// Type constraints
type Number interface {
    int | int64 | float64
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// comparable constraint (for maps/slices keys)
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

Methods

Methods are functions with a receiver argument. Value receivers (r Rectangle) work on a copy — can't modify the original. Pointer receivers (r *Rectangle) can modify and avoid copying large structs. Consistency matters: if one method uses a pointer receiver, all should. Methods can be defined on any type in the same package.

go
type Rectangle struct {
    Width, Height float64
}

// Method with value receiver
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Method with pointer receiver (can modify)
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

// Method on non-struct types
type MyString string

func (s MyString) Shout() string {
    return strings.ToUpper(string(s)) + "!"
}

// Usage
r := Rectangle{Width: 10, Height: 5}
fmt.Println(r.Area()) // 50
r.Scale(2)
fmt.Println(r.Width)  // 20

s := MyString("hello")
fmt.Println(s.Shout()) // HELLO!
05

Data Structures

Arrays & Slices

Arrays have fixed length; slices are dynamic (backed by arrays). append() adds elements, growing capacity as needed. make([]T, len, cap) pre-allocates for efficiency. Slicing creates a view (shares underlying array) — use copy() for independent data. Always check len/cap when optimizing.

go
// Array (fixed length, rarely used directly)
var arr [3]int = [3]int{1, 2, 3}
arr2 := [...]int{4, 5, 6} // size inferred

// Slice (dynamic array, most common)
nums := []int{1, 2, 3}
nums = append(nums, 4)        // [1, 2, 3, 4]
nums = append(nums, 5, 6, 7) // [1, 2, 3, 4, 5, 6, 7]

// Make a slice with capacity
s := make([]int, 3, 10) // len=3, cap=10

// Slicing
sub := nums[1:4]  // [2, 3, 4]
first := nums[:2] // [1, 2]
last := nums[3:]  // [4, 5, 6, 7]

// Copy
dst := make([]int, len(nums))
copy(dst, nums)

// Length and capacity
fmt.Println(len(nums), cap(nums))

Maps

Maps are Go's hash tables — unordered key/value pairs. The comma-ok pattern (val, ok := m[key]) checks if a key exists. delete() removes a key. Map iteration order is random by design. Maps are reference types — passing them to functions shares the underlying data. nil maps can't be written to (use make()).

go
// Create a map
m := map[string]int{
    "Alice": 30,
    "Bob":   25,
}

// Using make
ages := make(map[string]int)
ages["Charlie"] = 35

// Access
fmt.Println(m["Alice"]) // 30

// Check existence (comma-ok pattern)
age, ok := m["David"]
if !ok {
    fmt.Println("not found")
}

// Delete
delete(m, "Bob")

// Iterate (random order)
for name, age := range m {
    fmt.Printf("%s: %d\n", name, age)
}

// Length
fmt.Println(len(m))

// Nested maps
matrix := map[string]map[string]int{}
matrix["row1"] = map[string]int{"col1": 1}

Structs

Structs group related fields. Use named initialization (Person{Name: ...}) for clarity. new() returns a pointer with zero values. Anonymous structs are useful for one-off data shapes. Struct embedding (no field name) promotes the embedded struct's fields and methods — Go's alternative to inheritance.

go
// Define a struct
type Person struct {
    Name string
    Age  int
    Address string // field names are exported (capitalized)
}

// Create instances
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{"Bob", 25, "NYC"} // positional (not recommended)
p3 := new(Person)              // returns *Person, zero values
p3.Name = "Charlie"

// Anonymous struct (one-off)
config := struct {
    Port int
    Host string
}{
    Port: 8080,
    Host: "localhost",
}

// Struct embedding (composition)
type Employee struct {
    Person          // embedded (promotes fields)
    Salary   float64
}

emp := Employee{
    Person: Person{Name: "Dave", Age: 40},
    Salary: 50000,
}
fmt.Println(emp.Name) // "Dave" (promoted from Person)

Pointers

Go pointers (*T) hold memory addresses. & takes address, * dereferences. Unlike C, Go has no pointer arithmetic (safer). Struct pointers allow shorthand (u.Name instead of (*u).Name). nil pointers cause panics on dereference. Go has garbage collection — no manual free() needed.

go
// Pointer basics
x := 42
p := &x          // p is *int, points to x
fmt.Println(*p)  // 42 (dereference)
*p = 100         // modify x through pointer
fmt.Println(x)   // 100

// new() allocates and returns pointer
p2 := new(int)   // *int, value 0
*p2 = 42

// Pointers to structs
type User struct{ Name string }
u := &User{Name: "Alice"} // *User
u.Name = "Bob"            // (*u).Name shorthand
fmt.Println(u.Name)       // Bob

// nil pointer
var p3 *int // nil
// *p3 // panic: nil pointer dereference

// No pointer arithmetic (unlike C)
// p++ is illegal

Slice Operations

Go doesn't have built-in filter/map/reduce — write them or use slices package (Go 1.21+). Insert/remove require shifting elements with append+copy. Be careful with slice aliasing: s[:i] and s[i+1:] share the underlying array. For 2D slices, allocate each row separately.

go
// Filter
func filter(nums []int, pred func(int) bool) []int {
    result := []int{}
    for _, n := range nums {
        if pred(n) {
            result = append(result, n)
        }
    }
    return result
}

evens := filter([]int{1,2,3,4,5}, func(n int) bool {
    return n%2 == 0
}) // [2, 4]

// Insert at index
func insert(s []int, i, v int) []int {
    s = append(s, 0)
    copy(s[i+1:], s[i:])
    s[i] = v
    return s
}

// Remove at index
func remove(s []int, i int) []int {
    return append(s[:i], s[i+1:]...)
}

// 2D slice
matrix := make([][]int, 3)
for i := range matrix {
    matrix[i] = make([]int, 4)
}

Sorting & Searching

sort.Ints/Strings/Float64s sort in place. sort.Slice with a comparator handles custom types. sort.Search* does binary search on sorted slices. Go 1.21+ adds the slices package with generic sort functions. All sorting is in-place — make a copy first if you need the original order.

go
import "sort"

// Sort a slice
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
sort.Ints(nums) // [1 1 2 3 4 5 6 9]
fmt.Println(nums)

// Sort strings
strs := []string{"banana", "apple", "cherry"}
sort.Strings(strs) // [apple banana cherry]

// Sort with custom comparator
type Person struct {
    Name string
    Age  int
}

people := []Person{
    {"Alice", 30},
    {"Bob", 25},
    {"Charlie", 35},
}

sort.Slice(people, func(i, j int) bool {
    return people[i].Age < people[j].Age // by age ascending
})

// Binary search (sorted slice)
idx := sort.SearchInts(nums, 4) // index of 4

// Sort package (Go 1.21+): slices.Sort
import "slices"
slices.Sort(people) // needs Less method or cmp func
06

Methods & Interfaces

Defining Interfaces

Interfaces define method signatures. Go uses structural typing — a type implements an interface automatically if it has all the required methods (no explicit 'implements' declaration). This enables decoupled design: define interfaces where you use them, not where you implement them.

go
// Interface: a set of method signatures
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Implement implicitly (no 'implements' keyword)
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Use the interface
func describe(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n",
        s.Area(), s.Perimeter())
}

c := Circle{Radius: 5}
describe(c) // works! Circle implements Shape

Empty Interface & Type Assertions

interface{} (or 'any' in Go 1.18+) holds any value. Type assertion (v.(T)) extracts the concrete type — panics if wrong type, so use the comma-ok pattern. Type switch (switch v.(type)) handles multiple types cleanly. Empty interface is useful for generic containers but loses type safety — prefer generics.

go
// Empty interface (any type)
// Go 1.18+: 'any' is an alias for interface{}
func printAny(v any) {
    fmt.Println(v)
}

printAny(42)
printAny("hello")
printAny([]int{1, 2, 3})

// Type assertion
var i any = "hello"
s := i.(string)    // panics if not string
fmt.Println(s)

// Safe type assertion (comma-ok)
s, ok := i.(string)
if ok {
    fmt.Println("string:", s)
}

// Type switch
func describe(v any) {
    switch x := v.(type) {
    case int:
        fmt.Printf("int: %d\n", x)
    case string:
        fmt.Printf("string: %s\n", x)
    case []int:
        fmt.Printf("int slice: %v\n", x)
    default:
        fmt.Printf("unknown type: %T\n", x)
    }
}

Interface Composition

Interfaces can embed other interfaces (composition). io.Reader and io.Writer are Go's most important interfaces — implemented by files, network connections, buffers, etc. This enables powerful abstractions: functions taking io.Reader work with any readable source. Small, focused interfaces (1-3 methods) are idiomatic.

go
// Compose interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// ReadWriter combines Reader and Writer
type ReadWriter interface {
    Reader
    Writer
}

// io.Reader and io.Writer are built-in interfaces
// Many types implement them: *os.File, *bytes.Buffer, net.Conn

func copyData(r io.Reader, w io.Writer) error {
    buf := make([]byte, 1024)
    for {
        n, err := r.Read(buf)
        if n > 0 {
            if _, err := w.Write(buf[:n]); err != nil {
                return err
            }
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
    }
    return nil
}

Pointer vs Value Receivers

Pointer receivers can modify the struct and avoid copying large values. Value receivers are safe (can't modify) and allow the method on both values and pointers. If any method has a pointer receiver, all should (for consistency). Pointer receiver methods satisfy interfaces for both T and *T; value receiver methods only for T.

go
type Counter struct {
    count int
}

// Value receiver: works on a copy, can't modify
func (c Counter) Get() int {
    return c.count
}

// Pointer receiver: can modify the original
func (c *Counter) Increment() {
    c.count++
}

c := Counter{}
c.Increment() // Go auto-takes address (&c).Increment()
c.Increment()
fmt.Println(c.Get()) // 2

// Interface implementation note:
// *Counter implements both Get() and Increment()
// Counter (value) implements only Get()
// So *Counter satisfies an interface requiring both

Stringer Interface

The Stringer interface (String() string) controls how a type appears in Print/Printf — like toString() in Java/JS. %v uses String(); %+v shows field names; %#v shows Go syntax. The error interface works the same way: implement Error() string to make any type an error. These are Go's most common built-in interfaces.

go
// Stringer interface (like toString() in other languages)
type Stringer interface {
    String() string
}

type Person struct {
    Name string
    Age  int
}

// Implement Stringer
func (p Person) String() string {
    return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}

p := Person{Name: "Alice", Age: 30}
fmt.Println(p)       // Alice (30 years) — uses String()
fmt.Printf("%v\n", p) // Alice (30 years)
fmt.Printf("%+v\n", p) // Name:Alice Age:30
fmt.Printf("%#v\n", p) // main.Person{Name:"Alice", Age:30}

// Error interface is similar:
// type error interface { Error() string }
// Implement Error() to make a type usable as an error

Type Embedding & Composition

Go uses composition instead of inheritance. Embedding a struct promotes its fields and methods to the outer struct. You can override promoted methods by defining a method with the same name on the outer type. Embedding an interface allows decorating/delegating — the outer type satisfies the interface and can forward calls.

go
// Embed a struct (composition over inheritance)
type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return a.Name + " makes a sound"
}

type Dog struct {
    Animal // embedded — promotes Name and Speak()
    Breed  string
}

d := Dog{
    Animal: Animal{Name: "Rex"},
    Breed:  "Labrador",
}

fmt.Println(d.Name)      // "Rex" (promoted)
fmt.Println(d.Speak())   // "Rex makes a sound" (promoted)
fmt.Println(d.Animal.Name) // explicit access also works

// Override a promoted method
func (d Dog) Speak() string {
    return d.Name + " barks!"
}
fmt.Println(d.Speak()) // "Rex barks!"

// Embed an interface
type Logger struct {
    io.Writer // embed interface
}
// Logger now has Write() method, delegating to the embedded Writer
07

Error Handling

Error Basics

Go handles errors as values, not exceptions. Functions return (result, error) — always check err != nil immediately. errors.New() creates simple errors; fmt.Errorf() adds formatting. Sentinel errors (var ErrX = errors.New()) enable comparison with ==. Never ignore errors (use _ only when intentional).

go
// Functions return errors as last return value
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

// Always check errors immediately
result, err := divide(10, 0)
if err != nil {
    log.Fatal(err) // or handle gracefully
}
fmt.Println(result)

// fmt.Errorf for formatted errors
func validate(age int) error {
    if age < 0 {
        return fmt.Errorf("invalid age: %d (must be positive)", age)
    }
    return nil
}

// Sentinel errors
var ErrNotFound = errors.New("not found")
if err := lookup("key"); err == ErrNotFound {
    // handle not found
}

Custom Error Types

Custom error types implement the error interface (Error() string). They carry structured data (fields, codes, context) beyond a simple message. Use type assertion (*ValidationError) to access the data. This is essential for domain-specific error handling — e.g., HTTP status codes, validation details, retry logic.

go
// Custom error type (implements error interface)
type ValidationError struct {
    Field   string
    Message string
}

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

// Use the custom error
func validateUser(name string) error {
    if name == "" {
        return &ValidationError{
            Field:   "name",
            Message: "cannot be empty",
        }
    }
    return nil
}

// Type-assert to access fields
err := validateUser("")
if ve, ok := err.(*ValidationError); ok {
    fmt.Println("Field:", ve.Field)   // "name"
    fmt.Println("Message:", ve.Message) // "cannot be empty"
}

errors.Is & errors.As (Go 1.13+)

Go 1.13+ added error wrapping with %w (fmt.Errorf). errors.Is() checks if an error matches a sentinel (unwrapping the chain). errors.As() extracts a specific error type from the chain. This enables layered error handling: low-level errors wrapped with context, checked at high levels. Always wrap with %w (not %v) to preserve the chain.

go
import "errors"

// Wrapped errors with %w
var ErrNotFound = errors.New("not found")

func getUser(id int) error {
    if id == 0 {
        return fmt.Errorf("getUser(%d): %w", id, ErrNotFound)
    }
    return nil
}

// errors.Is: check if error matches (unwraps the chain)
err := getUser(0)
if errors.Is(err, ErrNotFound) {
    fmt.Println("user not found")
}

// errors.As: extract a specific error type
var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Println("field:", ve.Field)
}

// errors.Unwrap: get the wrapped error
inner := errors.Unwrap(err)

Panic & Recover

panic() is for unrecoverable errors (bugs, invariant violations) — not for normal error handling. recover() catches panics but only works in deferred functions. Use panic/recover for: programming errors (index out of range), package initialization failures, and protecting goroutines from crashing the program. Prefer returning errors for expected failures.

go
// Panic: unrecoverable error (like throw)
func mustParse(s string) int {
    n, err := strconv.Atoi(s)
    if err != nil {
        panic(fmt.Sprintf("invalid number: %s", s))
    }
    return n
}

// Recover: catch a panic (only in deferred functions)
func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    panic("something went wrong")
}

// Practical: safe goroutine
func safeGo(fn func()) {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                log.Println("goroutine panic:", r)
            }
        }()
        fn()
    }()
}

Error Wrapping Patterns

Wrap errors with context at each layer using fmt.Errorf with %w. This creates an error chain: top-level sees the full path (getUserProfile → fetchUser → sql error). The context (function name, parameters) helps debugging. Use errors.Is/As to check for specific errors in the chain. Avoid wrapping the same error multiple times with the same context.

go
// Wrap errors with context as they propagate up
func fetchUser(id int) (*User, error) {
    row := db.QueryRow("SELECT ... WHERE id = ?", id)
    var u User
    if err := row.Scan(&u.Name, &u.Age); err != nil {
        return nil, fmt.Errorf("fetchUser(%d): %w", id, err)
    }
    return &u, nil
}

func getUserProfile(id int) (*Profile, error) {
    user, err := fetchUser(id)
    if err != nil {
        return nil, fmt.Errorf("getUserProfile(%d): %w", id, err)
    }
    // ...
}

// At the top level, log the full chain
profile, err := getUserProfile(42)
if err != nil {
    log.Printf("error: %v", err)
    // Output: getUserProfile(42): fetchUser(42): sql: no rows
}

// golang.org/x/xerrors or pkg/errors for stack traces

Defer, Panic, Recover Together

Combine defer (cleanup), panic (fatal errors), and recover (catch panics) for robust resource management. Defers run in LIFO order, even during panics. The named return value (err error) can be set inside a deferred recover. This pattern ensures files/connections are closed and transactions are rolled back, even if code panics.

go
// Complete pattern: cleanup + panic recovery
func processFile(path string) (err error) {
    file, e := os.Open(path)
    if e != nil {
        return e
    }
    defer func() {
        // Close file regardless of panic
        file.Close()

        // Recover from panic and convert to error
        if r := recover(); r != nil {
            err = fmt.Errorf("panic in processFile: %v", r)
        }
    }()

    // Do work that might panic
    data := parseFile(file) // might panic
    return saveData(data)
}

// Defer for resource cleanup (always runs)
func withDatabase(db *sql.DB, fn func(*sql.DB) error) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer func() {
        if err != nil {
            tx.Rollback()
        } else {
            tx.Commit()
        }
    }()
    return fn(tx)
}
08

Concurrency

Goroutines

Goroutines are Go's lightweight threads — start with 'go'. They're cheap (~2KB stack) and managed by Go's runtime scheduler (M:N scheduling). The main function doesn't wait for goroutines — use sync.WaitGroup or channels for synchronization. Never use time.Sleep for synchronization in production (use WaitGroup).

go
package main

import (
    "fmt"
    "time"
)

func sayHello(name string) {
    for i := 0; i < 3; i++ {
        fmt.Println(name, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // Launch goroutine with 'go' keyword
    go sayHello("Alice")
    go sayHello("Bob")

    // Anonymous goroutine
    go func() {
        fmt.Println("anonymous goroutine")
    }()

    // Wait for goroutines (simple approach)
    time.Sleep(1 * time.Second)
    fmt.Println("done")
}

// Goroutines are lightweight (~2KB stack, grows as needed)
// Millions of goroutines can run concurrently

Channels

Channels are typed conduits for goroutine communication. Unbuffered (make(chan T)) blocks until both sender and receiver are ready (synchronous). Buffered (make(chan T, n)) blocks only when full (asynchronous). The sender should close channels, never the receiver. Range over a channel until it's closed. Channels enable 'share memory by communicating'.

go
// Unbuffered channel (synchronous)
ch := make(chan string)

// Send and receive
go func() {
    ch <- "hello" // blocks until received
}()
msg := <-ch // blocks until sent
fmt.Println(msg)

// Buffered channel (asynchronous)
buf := make(chan int, 3)
buf <- 1 // doesn't block (buffer has space)
buf <- 2
buf <- 3
// buf <- 4 // would block (buffer full)
fmt.Println(<-buf) // 1

// Close a channel (sender closes, never receiver)
close(buf)

// Range over channel (until closed)
for v := range buf {
    fmt.Println(v)
}

// Check if closed
v, ok := <-buf
if !ok {
    fmt.Println("channel closed")
}

Select Statement

select lets a goroutine wait on multiple channel operations — it picks the first ready one (random if multiple). The default case makes it non-blocking. time.After() creates timeout channels. Select is the heart of concurrent Go: event loops, fan-in/fan-out, timeouts. Always include timeouts to avoid deadlocks.

go
// Select: multiplex channel operations
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
    time.Sleep(1 * time.Second)
    ch1 <- "one"
}()
go func() {
    time.Sleep(2 * time.Second)
    ch2 <- "two"
}()

// Wait for first to arrive
for i := 0; i < 2; i++ {
    select {
    case msg1 := <-ch1:
        fmt.Println("received:", msg1)
    case msg2 := <-ch2:
        fmt.Println("received:", msg2)
    }
}

// Timeout with select
select {
case result := <-slowOperation():
    fmt.Println(result)
case <-time.After(3 * time.Second):
    fmt.Println("timeout!")
}

// Non-blocking receive (default case)
select {
case msg := <-ch:
    fmt.Println(msg)
default:
    fmt.Println("no message") // runs if no data
}

sync.WaitGroup

sync.WaitGroup waits for a group of goroutines to finish. Add(n) increments the counter, Done() decrements it (use defer), Wait() blocks until zero. Always pass loop variables as parameters to goroutines to avoid closure capture bugs (fixed in Go 1.22 but still recommended). WaitGroup is simpler than channels for fire-and-forget concurrency.

go
import "sync"

func main() {
    var wg sync.WaitGroup

    // Launch 5 goroutines
    for i := 0; i < 5; i++ {
        wg.Add(1) // increment counter
        go func(id int) {
            defer wg.Done() // decrement when done
            fmt.Printf("Worker %d started\n", id)
            time.Sleep(time.Duration(id) * 100 * time.Millisecond)
            fmt.Printf("Worker %d done\n", id)
        }(i)
    }

    wg.Wait() // block until all Done() called
    fmt.Println("all workers finished")
}

// Common mistake: passing loop variable to goroutine
// Always pass as parameter: go func(i int) { ... }(i)
// (Go 1.22+ fixes loop variable scoping, but still good practice)

Mutex & Sync

sync.Mutex protects shared state from concurrent access — Lock/Unlock with defer. RWMutex allows multiple readers or one writer (better for read-heavy workloads). sync.Once ensures initialization happens exactly once (singleton pattern). Prefer channels for communication, mutexes for protecting shared state. 'Share memory by communicating; don't communicate by sharing memory.'

go
import "sync"

// Mutex: mutual exclusion lock
type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock() // always unlock with defer
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

// RWMutex: multiple readers, one writer
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()         // read lock (multiple allowed)
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *Cache) Set(key, val string) {
    c.mu.Lock()          // write lock (exclusive)
    defer c.mu.Unlock()
    c.data[key] = val
}

// sync.Once: run exactly once
var (
    once sync.Once
    instance *Database
)

func GetDB() *Database {
    once.Do(func() {
        instance = &Database{}
    })
    return instance
}

Concurrency Patterns

Worker pool: fixed number of goroutines process jobs from a channel, sending results to another. This limits concurrency and prevents resource exhaustion. Fan-out/fan-in: distribute work across goroutines, then merge results. The key insight: close channels when no more data will be sent, so range loops terminate. These patterns are the foundation of concurrent Go.

go
// Worker pool
func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        time.Sleep(time.Second) // simulate work
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // Start 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Send 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    // Collect results
    for a := 1; a <= 5; a++ {
        fmt.Println(<-results)
    }
}

// Fan-out/Fan-in
func fanOut(input <-chan int, workers int) <-chan int {
    output := make(chan int)
    var wg sync.WaitGroup
    wg.Add(workers)
    for i := 0; i < workers; i++ {
        go func() {
            defer wg.Done()
            for v := range input {
                output <- v * 2
            }
        }()
    }
    go func() { wg.Wait(); close(output) }()
    return output
}
09

File I/O & OS

Reading Files

os.ReadFile() reads an entire file into memory (simple but not for large files). For large files, open with os.Open(), read in chunks, and always defer Close(). bufio.Scanner reads line by line — ideal for text processing. Always check errors, especially io.EOF to know when reading is done.

go
import (
    "io"
    "os"
)

// Read entire file (small files)
data, err := os.ReadFile("input.txt")
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))

// Open and read (large files)
file, err := os.Open("large.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

// Read in chunks
buf := make([]byte, 1024)
for {
    n, err := file.Read(buf)
    if n > 0 {
        // process buf[:n]
    }
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatal(err)
    }
}

// Read line by line
scanner := bufio.NewScanner(file)
for scanner.Scan() {
    line := scanner.Text()
    fmt.Println(line)
}

Writing Files

os.WriteFile() creates/truncates and writes atomically (simple). os.OpenFile() with flags gives control: O_APPEND (add to end), O_CREATE (create if missing), O_TRUNC (truncate). Use bufio.Writer for many small writes (buffers in memory, flush at end). File mode 0644: owner can read/write, others can read.

go
// Write entire file (creates or truncates)
err := os.WriteFile("output.txt", []byte("Hello, World!"), 0644)
if err != nil {
    log.Fatal(err)
}

// Open for writing (with options)
file, err := os.OpenFile("log.txt",
    os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
    log.Fatal(err)
}
defer file.Close()

// Write
if _, err := file.WriteString("log entry\n"); err != nil {
    log.Fatal(err)
}

// Buffered writer (efficient for many small writes)
writer := bufio.NewWriter(file)
for i := 0; i < 1000; i++ {
    writer.WriteString(fmt.Sprintf("line %d\n", i))
}
writer.Flush() // don't forget to flush!

// File modes: 0644 = owner read/write, others read
// os.O_APPEND, O_CREATE, O_WRONLY, O_RDONLY, O_RDWR, O_TRUNC

File Operations

os.Mkdir/MkdirAll create directories. os.ReadDir lists entries efficiently. filepath.WalkDir recursively traverses directories. os.Stat gives file info (size, mod time, permissions). os.IsNotExist checks if a file is missing. os.Remove deletes one file; RemoveAll deletes directories recursively. Always check errors.

go
// Create directory
os.Mkdir("mydir", 0755)       // one level
os.MkdirAll("a/b/c", 0755)    // nested directories

// List directory
entries, err := os.ReadDir(".")
if err != nil {
    log.Fatal(err)
}
for _, entry := range entries {
    fmt.Println(entry.Name(), entry.IsDir())
}

// Walk directory tree
filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
    if err != nil {
        return err
    }
    fmt.Println(path)
    return nil
})

// File info
info, _ := os.Stat("file.txt")
fmt.Println(info.Size())      // bytes
fmt.Println(info.ModTime())   // last modified
fmt.Println(info.IsDir())

// Check existence
if _, err := os.Stat("file.txt"); os.IsNotExist(err) {
    fmt.Println("file does not exist")
}

// Remove
os.Remove("file.txt")
os.RemoveAll("directory") // recursive

Environment & Command Line

os.Getenv/LookupEnv/Setenv manage environment variables. os.Args gives raw command-line arguments. The flag package provides parsed flags with defaults and help text. LookupEnv distinguishes between unset and empty values. Environment variables are the standard way to configure 12-factor apps (API keys, database URLs).

go
// Environment variables
os.Setenv("API_KEY", "secret")
key := os.Getenv("API_KEY") // "" if not set

// With existence check
if val, ok := os.LookupEnv("HOME"); ok {
    fmt.Println("HOME:", val)
}

// All environment variables
for _, e := range os.Environ() {
    fmt.Println(e)
}

// Command line arguments
// args := os.Args // []string, args[0] is program name

// Using flag package
var port int
var host string
flag.IntVar(&port, "port", 8080, "server port")
flag.StringVar(&host, "host", "localhost", "server host")
flag.Parse()
fmt.Printf("Running on %s:%d\n", host, port)

// Run: go run main.go -port 3000 -host 0.0.0.0

Executing Commands

os/exec runs external commands. Command() creates the command; Output() captures stdout; Run() executes with custom Stdout/Stderr. Use CommandContext for timeouts (kills the process). Always check errors — exec.ExitError indicates non-zero exit codes. Be careful with user input to prevent command injection.

go
import "os/exec"

// Run a command and get output
cmd := exec.Command("ls", "-la", "/tmp")
output, err := cmd.Output()
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(output))

// Capture stdout and stderr separately
cmd = exec.Command("git", "status")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
fmt.Println("stdout:", stdout.String())
fmt.Println("stderr:", stderr.String())

// Pipe input to command
cmd = exec.Command("grep", "error")
cmd.Stdin = strings.NewReader("info\nerror\nwarn\n")
result, _ := cmd.Output()
fmt.Println(string(result)) // "error"

// Run with context (timeout)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd = exec.CommandContext(ctx, "sleep", "10")
err = cmd.Run() // killed after 5s
10

JSON & Encoding

JSON Marshal & Unmarshal

encoding/json converts between Go structs and JSON. Marshal (struct → JSON), Unmarshal (JSON → struct). Struct tags (json:"name") control field naming and visibility. omitempty skips zero/empty values. json:"-" excludes a field entirely. This is the standard way to handle API requests/responses in Go.

go
import "encoding/json"

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email,omitempty"` // omit if empty
    Password string `json:"-"`               // never serialize
    Age      int    `json:"age,omitempty"`
}

// Struct to JSON (marshal)
user := User{ID: 1, Name: "Alice", Age: 30}
data, err := json.Marshal(user)
// {"id":1,"name":"Alice","age":30}

// Pretty print
pretty, _ := json.MarshalIndent(user, "", "  ")

// JSON to struct (unmarshal)
jsonStr := `{"id":2,"name":"Bob","email":"[email protected]"}`
var u User
err := json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u.Name) // "Bob"

// Struct tags control field names:
// json:"name"        -> rename
// json:"name,omitempty" -> skip if zero value
// json:"-"           -> skip entirely
// json:",omitempty"  -> keep name, skip if empty

JSON with Maps & Slices

For dynamic JSON (unknown structure), unmarshal into map[string]any (or interface{}). JSON numbers become float64 — type-assert to access. json.Decoder/Encoder work with streams (files, HTTP bodies) efficiently. Use struct unmarshaling when you know the schema; use maps for flexible/dynamic data.

go
// Parse arbitrary JSON into map[string]interface{}
jsonStr := `{"name":"Alice","scores":[90,85,92],"active":true}`

var result map[string]any
json.Unmarshal([]byte(jsonStr), &result)

// Type-assert to access values
name := result["name"].(string)
scores := result["scores"].([]any)
for _, s := range scores {
    fmt.Println(s.(float64)) // JSON numbers are float64
}

// Encode a map
m := map[string]any{
    "count": 42,
    "items": []string{"a", "b", "c"},
    "meta":  map[string]any{"page": 1},
}
data, _ := json.Marshal(m)

// Decode JSON stream
dec := json.NewDecoder(strings.NewReader(jsonStr))
var v map[string]any
dec.Decode(&v)

// Encode to stream
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "  ")
enc.Encode(user)

Custom JSON Marshaling

Implement MarshalJSON/UnmarshalJSON for custom serialization. This is useful for: computed fields, alternative formats (Money as '99.99 USD'), sensitive data handling, and time formatting. The method receiver for UnmarshalJSON must be a pointer to modify the struct. time.Time automatically serializes as RFC 3339 strings.

go
// Implement MarshalJSON/UnmarshalJSON
type Money struct {
    Amount   float64
    Currency string
}

func (m Money) MarshalJSON() ([]byte, error) {
    return json.Marshal(map[string]any{
        "amount":   m.Amount,
        "currency": m.Currency,
        "display":  fmt.Sprintf("%.2f %s", m.Amount, m.Currency),
    })
}

func (m *Money) UnmarshalJSON(data []byte) error {
    var v struct {
        Amount   float64
        Currency string
    }
    if err := json.Unmarshal(data, &v); err != nil {
        return err
    }
    m.Amount = v.Amount
    m.Currency = v.Currency
    return nil
}

money := Money{Amount: 99.99, Currency: "USD"}
data, _ := json.Marshal(money)
// {"amount":99.99,"currency":"USD","display":"99.99 USD"}

// Time formatting
type Event struct {
    Time time.Time `json:"time"`
}
// time.Time marshals as RFC 3339 by default

HTTP Server

The net/http package builds HTTP servers. http.HandleFunc registers handlers. http.ResponseWriter writes the response; *http.Request reads the request. For JSON APIs, set Content-Type and use json.NewEncoder(w).Encode(data). The standard library is production-ready — no framework needed for simple APIs. Use mux routers (gorilla/mux, chi) for complex routing.

go
import (
    "encoding/json"
    "net/http"
)

type Response struct {
    Status  string `json:"status"`
    Message string `json:"message"`
}

func main() {
    // Simple handler
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
    })

    // JSON API endpoint
    http.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
        user := User{ID: 1, Name: "Alice"}
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(user)
    })

    // Start server
    fmt.Println("Server running on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

// Request methods:
// r.Method  -> "GET", "POST", etc.
// r.URL.Query().Get("key") -> query params
// r.Body    -> request body (io.ReadCloser)

HTTP Client

http.Get/Post are convenient shortcuts. For custom headers, methods, or bodies, use http.NewRequest + client.Do(). Always defer resp.Body.Close() to avoid connection leaks. Set a timeout on the client (default is no timeout — dangerous). For production, reuse http.Client (it connection-pools) and use context for cancellation.

go
// Simple GET
resp, err := http.Get("https://api.example.com/users")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
fmt.Println(string(body))

// Custom request with headers
req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
req.Header.Set("Authorization", "Bearer token123")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
defer resp.Body.Close()

// POST with JSON body
user := User{Name: "Alice"}
body, _ := json.Marshal(user)
resp, err := http.Post(
    "https://api.example.com/users",
    "application/json",
    bytes.NewBuffer(body),
)
defer resp.Body.Close()

// With timeout
client := &http.Client{Timeout: 10 * time.Second}
resp, err = client.Get("https://slow-api.example.com")
11

Testing & Benchmarking

Unit Tests

Test files end with _test.go, test functions start with Test. Table-driven tests are the idiomatic pattern: define test cases in a slice, loop with t.Run for subtests (named, individually runnable). Use t.Errorf for failures (continues), t.Fatalf for fatal failures (stops). Run with 'go test -v' for verbose output.

go
// math_test.go (file must end with _test.go)
package math

import "testing"

// Test function: func TestXxx(t *testing.T)
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

// Table-driven tests (idiomatic Go)
func TestAddTable(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -1, -2},
        {"zero", 0, 0, 0},
        {"mixed", -5, 10, 5},
    }

    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)
            }
        })
    }
}

// Run: go test -v
// Run specific: go test -run TestAddTable/positive

Benchmarks

Benchmark functions start with Benchmark and use *testing.B. The b.N loop is adjusted by the runtime to get stable measurements. Run with 'go test -bench=.'. b.ResetTimer() excludes setup time. b.ReportAllocs() shows memory allocations. Compare implementations with 'benchstat' to verify improvements are significant.

go
// Benchmark function: func BenchmarkXxx(b *testing.B)
func BenchmarkFibonacci(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Fibonacci(20)
    }
}

// Benchmark with allocations
func BenchmarkStringConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := ""
        for j := 0; j < 100; j++ {
            s += "a"
        }
    }
}

// Run: go test -bench=.
// Output:
// BenchmarkFibonacci-8        300     4234567 ns/op
// BenchmarkStringConcat-8     200     6789012 ns/op   100 B/op   1 allocs/op

// Benchmark with setup
func BenchmarkProcess(b *testing.B) {
    data := setup() // setup not measured
    b.ResetTimer()  // start timing here
    for i := 0; i < b.N; i++ {
        process(data)
    }
}

// Memory allocations
func BenchmarkAllocs(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        make([]int, 100)
    }
}

Test Helpers & Mocking

t.Helper() improves error messages by skipping helper functions in stack traces. Mock by implementing interfaces (Go's approach to mocking — no mock framework needed). t.Cleanup() registers cleanup functions (like defer, but for test scope). For complex mocking, use testify/assert and mockery or gomock for generating mocks from interfaces.

go
// t.Helper() marks helper functions (better error locations)
func assertEqual(t *testing.T, got, want int) {
    t.Helper()
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

// Interfaces for mocking
type DataStore interface {
    Get(key string) (string, error)
}

type MockStore struct {
    data map[string]string
}

func (m *MockStore) Get(key string) (string, error) {
    if v, ok := m.data[key]; ok {
        return v, nil
    }
    return "", errors.New("not found")
}

func TestService(t *testing.T) {
    store := &MockStore{data: map[string]string{"key": "value"}}
    svc := NewService(store)
    result := svc.GetValue("key")
    assertEqual(t, result, "value")
}

// Subtests with t.Run for setup/teardown
func TestWithCleanup(t *testing.T) {
    t.Cleanup(func() {
        // runs after test (LIFO order)
        os.Remove("tempfile")
    })
    // test code
}

Standard Library Highlights

Go's standard library is comprehensive. time handles dates/times (note: format uses reference time 2006-01-02). regexp for pattern matching. context for cancellation/timeouts across goroutines. sync.Pool for object reuse (reduces GC pressure). Go 1.21+ adds the slices and maps packages with generic utilities.

go
// time package
now := time.Now()
future := now.Add(24 * time.Hour)
formatted := now.Format("2006-01-02 15:04:05")
parsed, _ := time.Parse("2006-01-02", "2024-01-15")

// regexp
re := regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
matches := re.FindString("date: 2024-01-15")
all := re.FindAllString("2024-01-15 and 2024-02-20", -1)

// context (cancellation, timeouts)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go doWork(ctx)
// cancel() or timeout stops doWork

// sync.Pool (reuse objects)
var bufPool = sync.Pool{
    New: func() any { return new(bytes.Buffer) },
}
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)

// sort (Go 1.21+ slices package)
slices.Sort(nums)
slices.Contains(nums, 42)
slices.Reverse(nums)
12

Goroutines Deep Dive

Starting Goroutines & WaitGroup

Goroutines are lightweight threads managed by the Go runtime (not OS threads) — you can spawn hundreds of thousands. sync.WaitGroup coordinates goroutine completion: Add(1) before starting, Done() when finished (use defer), and Wait() to block until the counter reaches zero. Always pass a pointer to the WaitGroup so all goroutines share the same counter.

go
package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // signal completion when function returns
    fmt.Printf("Worker %d started\n", id)
    // ... do work ...
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1) // increment counter before starting goroutine
        go worker(i, &wg)
    }

    wg.Wait() // block until all goroutines call Done()
    fmt.Println("All workers finished")
}

GOMAXPROCS & Scheduling

GOMAXPROCS controls how many OS threads run goroutines simultaneously — it defaults to the number of CPU cores and is almost always optimal. Go uses M:N scheduling: many goroutines on few OS threads. Blocking I/O or channel operations cause the scheduler to run other goroutines on the same thread. runtime.Gosched() explicitly yields. You rarely need to tune GOMAXPROCS.

go
package main

import (
    "fmt"
    "runtime"
)

func main() {
    // GOMAXPROCS: number of OS threads available to run goroutines
    fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // defaults to CPU cores

    // Set it manually (rarely needed — default is optimal)
    runtime.GOMAXPROCS(2)

    // Goroutines are multiplexed onto OS threads (M:N scheduling)
    // - Blocking syscalls (I/O) don't block other goroutines
    // - The scheduler steals work across threads
    // - A goroutine blocked on channel/IO yields its thread

    // Number of goroutines currently running
    go func() { runtime.Gosched() }() // yield to other goroutines
    fmt.Println("Goroutines:", runtime.NumGoroutine())
}

Goroutine Leaks & Prevention

A goroutine leak occurs when a goroutine blocks forever (e.g., sending on an unbuffered channel nobody reads) — it's never garbage collected. Prevent leaks by: buffering channels, using select with a cancellation channel/context, and always providing an exit path. Leaked goroutines accumulate memory and CPU. Use runtime.NumGoroutine() and pprof to detect leaks in production.

go
package main

// BAD: goroutine leaks — this goroutine never exits
func leaky() <-chan int {
    ch := make(chan int)
    go func() {
        ch <- 42 // blocks forever if nobody reads from ch
    }()
    return ch
}

// GOOD: use context for cancellation
func safe(ctx context.Context) <-chan int {
    ch := make(chan int, 1) // buffered — sender never blocks
    go func() {
        select {
        case ch <- 42:
        case <-ctx.Done(): // exit when context is cancelled
            return
        }
    }()
    return ch
}

// GOOD: always ensure senders can exit
// 1. Buffer the channel so send doesn't block
// 2. Use select with a done/cancel channel
// 3. Use context.WithCancel/WithTimeout for lifecycle control

sync.Once (One-Time Initialization)

sync.Once ensures a function executes exactly once across all goroutines — the standard way to implement thread-safe singletons and lazy initialization. It's more efficient than mutex-protected flag checking. The Do method blocks concurrent callers until the first invocation completes. sync.Once is also used internally in many standard library patterns like sync.OnceValue (Go 1.21+).

go
package main

import (
    "fmt"
    "sync"
)

var (
    instance *Database
    once     sync.Once
)

type Database struct{ name string }

func GetDB() *Database {
    // sync.Once guarantees the function runs exactly once,
    // even if called from many goroutines simultaneously
    once.Do(func() {
        instance = &Database{name: "production"}
        fmt.Println("Database initialized")
    })
    return instance
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            GetDB() // "Database initialized" prints only once
        }()
    }
    wg.Wait()
}

Worker Pool with Goroutines

The worker pool pattern limits concurrency to a fixed number of goroutines processing jobs from a channel. This prevents resource exhaustion from spawning unbounded goroutines. Workers use 'for job := range jobs' which exits when the channel is closed. close(jobs) signals all workers to stop. This is the canonical Go concurrency pattern for bounded parallelism.

go
package main

import (
    "fmt"
    "sync"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs { // exits when jobs channel is closed
        results <- job * job // process and send result
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    var wg sync.WaitGroup

    // Start 3 workers
    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // Send 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs) // signal workers to stop (range loop exits)

    wg.Wait()
    close(results)

    for r := range results {
        fmt.Println("Result:", r)
    }
}
13

Channels & Select

Channel Basics (Unbuffered & Buffered)

Unbuffered channels (make(chan T)) synchronize sender and receiver — the send blocks until a receiver is ready. Buffered channels (make(chan T, n)) allow n sends without a receiver, decoupling sender/receiver timing. Only the sender should close a channel (to signal 'no more values'). Receiving from a closed channel returns the zero value with ok=false. Range loops exit when the channel is closed.

go
package main

import "fmt"

func main() {
    // Unbuffered: send blocks until someone receives (synchronous)
    ch := make(chan int)
    go func() {
        ch <- 42 // blocks until main reads
    }()
    fmt.Println(<-ch) // 42

    // Buffered: send blocks only when buffer is full
    buf := make(chan int, 3)
    buf <- 1 // doesn't block (buffer has space)
    buf <- 2
    buf <- 3
    // buf <- 4 // would block — buffer full
    fmt.Println(<-buf) // 1 (FIFO)

    // Close a channel (sender closes, never the receiver)
    close(buf)

    // Check if closed (ok is false when closed and empty)
    val, ok := <-buf
    fmt.Println(val, ok) // 2 true

    // Range over channel until closed
    nums := make(chan int, 3)
    nums <- 10; nums <- 20; nums <- 30
    close(nums)
    for n := range nums {
        fmt.Println(n) // 10, 20, 30
    }
}

Select Statement

select lets a goroutine wait on multiple channel operations simultaneously — it chooses the first ready case (randomly if multiple are ready). The default case makes select non-blocking. time.After creates a timeout channel. select is the heart of Go's concurrency coordination: multiplexing, timeouts, cancellation, and fan-in/fan-out patterns all build on it.

go
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "from ch1"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "from ch2"
    }()

    // select waits on multiple channel operations
    // picks the first one that's ready (random if multiple ready)
    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println(msg)
        case msg := <-ch2:
            fmt.Println(msg)
        }
    }

    // Non-blocking receive with default
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    default:
        fmt.Println("no message ready") // runs immediately
    }

    // Timeout with time.After
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    case <-time.After(500 * time.Millisecond):
        fmt.Println("timed out")
    }
}

Fan-In & Fan-Out Patterns

Fan-out distributes work across multiple goroutines for parallelism; fan-in merges multiple channels into one. Together they form a parallel pipeline: fan-out workers process independently, then fan-in collects results. The fan-in uses a WaitGroup to close the merged channel only after all input channels are exhausted. These patterns are fundamental to Go's concurrent data processing.

go
package main

import (
    "fmt"
    "sync"
)

// Fan-Out: distribute work across multiple goroutines
func fanOut(input <-chan int, n int) []<-chan int {
    outputs := make([]<-chan int, n)
    for i := 0; i < n; i++ {
        outputs[i] = process(input) // each worker reads from same input
    }
    return outputs
}

func process(input <-chan int) <-chan int {
    output := make(chan int)
    go func() {
        defer close(output)
        for v := range input {
            output <- v * v // transform
        }
    }()
    return output
}

// Fan-In: merge multiple channels into one
func fanIn(channels ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    merged := make(chan int)

    output := func(c <-chan int) {
        defer wg.Done()
        for v := range c {
            merged <- v
        }
    }

    wg.Add(len(channels))
    for _, c := range channels {
        go output(c)
    }

    go func() {
        wg.Wait()
        close(merged)
    }()
    return merged
}

Directional Channels (Send/Receive Only)

Directional channel types (<-chan T for receive-only, chan<- T for send-only) enforce channel usage at compile time. A bidirectional channel implicitly converts to a directional type when passed to a function. This documents intent and prevents bugs — a producer function literally cannot receive from its own output channel. Use directional types in function signatures to make contracts clear.

go
package main

import "fmt"

// <-chan int: receive-only (can read, cannot send)
// chan<- int: send-only (can send, cannot read)
func producer(out chan<- int) { // can only send
    for i := 0; i < 3; i++ {
        out <- i
    }
    close(out)
}

func consumer(in <-chan int) { // can only receive
    for v := range in {
        fmt.Println("consumed:", v)
    }
}

func main() {
    ch := make(chan int)

    // Directional restrictions are enforced at compile time
    go producer(ch) // bidirectional chan converts to send-only
    consumer(ch)    // bidirectional chan converts to receive-only

    // This prevents bugs: producer can't accidentally read,
    // consumer can't accidentally send or close
}

Ticker & Timer Channels

time.Ticker fires repeatedly at intervals — use ticker.C as a channel in select for periodic tasks. time.Timer fires once after a duration. Always call Stop() on tickers and timers to release resources and avoid leaks. time.After is a convenient one-shot timer that returns a channel (but can't be cancelled, so prefer NewTimer in select loops to avoid accumulation). Reset lets you reschedule a timer.

go
package main

import (
    "fmt"
    "time"
)

func main() {
    // Ticker: fires repeatedly at intervals
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop() // always stop to release resources

    go func() {
        for t := range ticker.C { // ticker.C is a channel
            fmt.Println("Tick at", t)
        }
    }()

    // Timer: fires once after a duration
    timer := time.NewTimer(2 * time.Second)
    <-timer.C // blocks until timer fires
    fmt.Println("Timer fired!")

    // time.After: one-shot timer as a channel (no Stop needed)
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("1 second elapsed")
    }

    // Reset a timer (cancel and reschedule)
    timer2 := time.NewTimer(5 * time.Second)
    timer2.Reset(100 * time.Millisecond)
    <-timer2.C
}
14

Context Package

context.WithCancel & WithTimeout

context.Context carries cancellation, timeouts, and request-scoped values across goroutine boundaries. WithCancel returns a context and a cancel function; WithTimeout auto-cancels after a duration. Goroutines check ctx.Done() (a channel) in a select to know when to stop. ALWAYS call the cancel function (use defer) to release resources, even if the timeout fires — otherwise the context leaks.

go
package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done(): // cancelled or timed out
            fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    // WithCancel: manual cancellation
    ctx, cancel := context.WithCancel(context.Background())
    go worker(ctx, 1)
    time.Sleep(2 * time.Second)
    cancel() // stop the worker
    time.Sleep(500 * time.Millisecond)

    // WithTimeout: auto-cancel after duration
    ctx2, cancel2 := context.WithTimeout(context.Background(), 1500*time.Millisecond)
    defer cancel2() // always call cancel to release resources
    go worker(ctx2, 2)
    time.Sleep(2 * time.Second) // worker stops after 1.5s
}

Propagating Context Through Calls

Context should be the first parameter of every function that does I/O, and it should be propagated through the entire call chain. http.Request.Context() is automatically cancelled when the client disconnects. Passing ctx to database/HTTP operations (QueryRowContext, NewRequestWithContext) ensures they abort when the context is cancelled — preventing wasted work and resource leaks. Never store contexts in structs.

go
package main

import (
    "context"
    "database/sql"
    "net/http"
)

// Context should be the FIRST parameter, named ctx
func GetUser(ctx context.Context, db *sql.DB, id int) (string, error) {
    // Pass ctx to all blocking operations so they cancel together
    var name string
    err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id=?", id).Scan(&name)
    return name, err
}

func handler(w http.ResponseWriter, r *http.Request) {
    // r.Context() is cancelled when the client disconnects
    ctx := r.Context()

    name, err := GetUser(ctx, db, 42)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    w.Write([]byte(name))
}

// BEST PRACTICE: pass context through every function in the call chain
// that might do I/O. This ensures a client disconnect or timeout
// cancels ALL in-flight work (DB queries, HTTP calls, etc.)

Context Values (Request-Scoped Data)

context.WithValue stores request-scoped data (like user IDs, trace IDs, auth tokens) that flows through the call chain. Use a custom key type (not a string) to avoid key collisions. Values should be data the request needs, not function parameters — the Go team recommends using it sparingly, mainly for cross-cutting concerns like tracing/auth. Always type-assert when retrieving values.

go
package main

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

// Define a custom key type to avoid collisions
type contextKey string

const userIDKey contextKey = "userID"

// Set a value in context
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userID := 42 // authenticate...
        // WithValue creates a new context with the value
        ctx := context.WithValue(r.Context(), userIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Get a value from context (must type-assert)
func getUserID(ctx context.Context) (int, bool) {
    if v, ok := ctx.Value(userIDKey).(int); ok {
        return v, true
    }
    return 0, false
}

func handler(w http.ResponseWriter, r *http.Request) {
    if uid, ok := getUserID(r.Context()); ok {
        fmt.Fprintf(w, "User ID: %d", uid)
    }
}

Graceful Shutdown with Context

Graceful shutdown lets in-flight requests complete before the server exits. signal.Notify captures OS signals (Ctrl+C, SIGTERM from container orchestrators). server.Shutdown(ctx) stops accepting new connections and waits for active ones to finish (up to the context timeout). This is essential for production servers — without it, active requests are abruptly terminated, causing errors and data corruption.

go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    server := &http.Server{Addr: ":8080", Handler: mux}

    // Listen for OS interrupt signals (Ctrl+C, SIGTERM)
    stop := make(chan os.Signal, 1)
    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        log.Println("Server starting on :8080")
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    <-stop // block until signal received
    log.Println("Shutting down...")

    // Give in-flight requests 10 seconds to finish
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Printf("Forced shutdown: %v", err)
    }
    log.Println("Server stopped gracefully")
}

Context Deadline & Error Handling

context.WithDeadline cancels at an absolute time; WithTimeout cancels after a relative duration (WithTimeout is just WithDeadline(now+timeout)). ctx.Err() returns context.DeadlineExceeded or context.Canceled so you can distinguish why it stopped. Use errors.Is() to check context errors. Always check ctx.Err() at the start of long operations and use select with ctx.Done() during blocking waits.

go
package main

import (
    "context"
    "errors"
    "fmt"
    "time"
)

func slowOperation(ctx context.Context) (string, error) {
    // Check if already cancelled before starting
    if err := ctx.Err(); err != nil {
        return "", err
    }

    select {
    case <-time.After(3 * time.Second): // simulate slow work
        return "result", nil
    case <-ctx.Done(): // cancelled or deadline exceeded
        return "", ctx.Err()
    }
}

func main() {
    // Deadline: cancel at a specific time
    deadline := time.Now().Add(1 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    result, err := slowOperation(ctx)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            fmt.Println("Operation timed out")
        } else if errors.Is(err, context.Canceled) {
            fmt.Println("Operation was cancelled")
        }
        return
    }
    fmt.Println(result)
}
15

HTTP Server & Client

HTTP Server (net/http)

net/http provides a production-ready HTTP server. http.HandleFunc registers handlers by path. Use json.NewEncoder(w).Encode() to write JSON responses and json.NewDecoder(r.Body).Decode() to parse request bodies. Always set Content-Type headers and check r.Method. http.Error sends an error status. ListenAndServe starts the server; wrap with log.Fatal to catch errors.

go
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func getUser(w http.ResponseWriter, r *http.Request) {
    user := User{ID: 1, Name: "Alice"}

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    user.ID = 99
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func main() {
    http.HandleFunc("/users/1", getUser)
    http.HandleFunc("/users", createUser)

    log.Println("Server on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

HTTP Client & Requests

The default http.Client has NO timeout — always set one to avoid hanging forever on unresponsive servers. Use http.NewRequestWithContext to attach a context for cancellation/timeouts. ALWAYS defer resp.Body.Close() to avoid connection leaks. For production, reuse a single http.Client (it manages connection pooling). http.Get is a shortcut but lacks timeout and customization.

go
package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

func main() {
    // Simple GET
    resp, err := http.Get("https://api.example.com/users")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close() // ALWAYS close the body

    var users []map[string]any
    json.NewDecoder(resp.Body).Decode(&users)
    fmt.Println(users)

    // Custom client with timeout (default has NO timeout!)
    client := &http.Client{Timeout: 30 * time.Second}

    // POST with JSON body and context
    body, _ := json.Marshal(map[string]string{"name": "Bob"})
    req, _ := http.NewRequestWithContext(context.Background(),
        "POST", "https://api.example.com/users", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")

    resp, err = client.Do(req)
    defer resp.Body.Close()
    fmt.Println("Status:", resp.Status)
}

Middleware Pattern

Middleware wraps handlers to add cross-cutting concerns (logging, auth, CORS, rate limiting) without modifying the handler itself. The signature func(http.Handler) http.Handler is the standard middleware type. Chain applies them in order (outermost first). This pattern is the basis of frameworks like Chi, Echo, and Gin. The ResponseWriter can be wrapped to capture status codes for logging.

go
package main

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

// Middleware wraps an http.Handler to add cross-cutting concerns
type Middleware func(http.Handler) http.Handler

func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r) // call the wrapped handler
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func Auth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("Authorization") == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Chain multiple middlewares
func Chain(h http.Handler, mws ...Middleware) http.Handler {
    for i := len(mws) - 1; i >= 0; i-- {
        h = mws[i](h)
    }
    return h
}

func main() {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello!"))
    })
    // Apply middlewares: Auth -> Logging -> handler
    http.Handle("/", Chain(handler, Logging, Auth))
    http.ListenAndServe(":8080", nil)
}

Serve Static Files & Templates

http.FileServer serves static files from a directory; http.StripPrefix removes the path prefix so file paths resolve correctly. html/template renders HTML safely (auto-escapes to prevent XSS). template.Must panics on parse errors (fine for startup). Templates use {{.Field}} for data and {{range}} for iteration. For production, consider embedding files with go:embed instead of reading from disk.

go
package main

import (
    "html/template"
    "net/http"
)

func main() {
    // Serve static files (CSS, JS, images)
    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))

    // HTML templates
    tmpl := template.Must(template.ParseFiles("templates/index.html"))

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        data := struct {
            Title string
            Items []string
        }{
            Title: "My Page",
            Items: []string{"Apple", "Banana", "Cherry"},
        }
        tmpl.Execute(w, data)
    })

    http.ListenAndServe(":8080", nil)
}

// templates/index.html:
// <h1>{{.Title}}</h1>
// <ul>{{range .Items}}<li>{{.}}</li>{{end}}</ul>

Graceful Shutdown & go:embed

go:embed bundles files into the compiled binary at build time — enabling true single-binary deployments with no external file dependencies. Use //go:embed directives above a var declaration. embed.FS is a read-only virtual filesystem. fs.Sub creates a sub-filesystem (strips the directory prefix). This works for static assets, HTML templates, SQL migrations, config files, and more.

go
package main

import (
    "embed"
    "io/fs"
    "net/http"
)

//go:embed static/*
var staticFiles embed.FS

func main() {
    // Embed static files into the binary (no external files needed)
    // Access the embedded filesystem
    sub, _ := fs.Sub(staticFiles, "static")
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))

    // go:embed benefits:
    // - Single binary deployment (no external files)
    // - Files are compiled into the executable
    // - Works with templates, configs, migrations, etc.

    // Embed a single file
    //go:embed config.json
    // var configData []byte

    // Embed with pattern
    //go:embed templates/*.html
    // var templates embed.FS

    http.ListenAndServe(":8080", nil)
}
16

Sync Package

sync.Mutex & sync.RWMutex

sync.Mutex provides exclusive locking — only one goroutine can hold it at a time. sync.RWMutex allows multiple concurrent readers OR one exclusive writer — use it when reads far outnumber writes. Always pair Lock with defer Unlock to prevent deadlocks if the function panics. Embed the mutex in the struct (lowercase mu) to keep it private. Never copy a mutex (always use pointers).

go
package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock() // always unlock with defer
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

// RWMutex: allows multiple readers OR one writer
type SafeCache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *SafeCache) Get(key string) (string, bool) {
    c.mu.RLock()         // read lock (multiple readers OK)
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *SafeCache) Set(key, val string) {
    c.mu.Lock()          // write lock (exclusive)
    defer c.mu.Unlock()
    c.data[key] = val
}

sync.Map (Concurrent Map)

sync.Map is a concurrent-safe map optimized for specific scenarios: write-once-read-many (caches) or disjoint-key access across goroutines. It avoids locks on reads via atomic operations. However, for general-purpose concurrent maps, a regular map protected by sync.RWMutex is often faster and more ergonomic. sync.Map's API uses any (interface{}) for keys and values, losing type safety.

go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map

    // Store and Load (thread-safe, no locks needed)
    m.Store("name", "Alice")
    m.Store("age", 30)

    val, ok := m.Load("name")
    fmt.Println(val, ok) // Alice true

    // LoadOrStore: atomic get-or-set
    actual, loaded := m.LoadOrStore("name", "Bob")
    fmt.Println(actual, loaded) // Alice true (already existed)

    // Delete
    m.Delete("age")

    // Range over all entries
    m.Range(func(key, value any) bool {
        fmt.Printf("%v = %v\n", key, value)
        return true // continue; return false to stop
    })

    // Use sync.Map when:
    // 1. Keys are written once, read many times (caches)
    // 2. Multiple goroutines read/write disjoint keys
    // For most cases, a regular map + Mutex is simpler and faster
}

sync.Cond (Condition Variables)

sync.Cond implements condition variables — goroutines wait for a condition to become true. Wait() atomically releases the lock and sleeps; Signal() wakes one waiter, Broadcast() wakes all. Always use a for loop around Wait() (not if) to handle spurious wakeups. Cond is useful for producer-consumer queues and waiting for state changes, though channels often provide a simpler alternative.

go
package main

import (
    "fmt"
    "sync"
    "time"
)

type Queue struct {
    items []int
    mu    sync.Mutex
    cond  *sync.Cond
}

func NewQueue() *Queue {
    q := &Queue{}
    q.cond = sync.NewCond(&q.mu) // cond is tied to the mutex
    return q
}

func (q *Queue) Put(item int) {
    q.mu.Lock()
    defer q.mu.Unlock()
    q.items = append(q.items, item)
    q.cond.Signal() // wake one waiting goroutine
}

func (q *Queue) Get() int {
    q.mu.Lock()
    defer q.mu.Unlock()
    for len(q.items) == 0 {
        q.cond.Wait() // atomically unlocks mu, sleeps, re-locks on wake
    }
    item := q.items[0]
    q.items = q.items[1:]
    return item
}

func main() {
    q := NewQueue()
    go func() {
        time.Sleep(1 * time.Second)
        q.Put(42)
    }()
    fmt.Println("Waiting for item...")
    fmt.Println("Got:", q.Get()) // blocks until item available
}

sync.Pool (Object Reuse)

sync.Pool reuses objects to reduce allocations and GC pressure — ideal for frequently allocated short-lived objects like bytes.Buffer. Get() returns a pooled object (calling New if empty); Put() returns it. Pools are cleared during GC, so don't rely on them for persistence. Always reset objects before reuse. The standard library uses sync.Pool extensively (http, json, fmt).

go
package main

import (
    "bytes"
    "sync"
)

var bufPool = sync.Pool{
    New: func() any {
        return new(bytes.Buffer) // create when pool is empty
    },
}

func Process(data []byte) string {
    // Get a buffer from the pool (or create via New)
    buf := bufPool.Get().(*bytes.Buffer)
    defer bufPool.Put(buf) // return it when done

    buf.Reset() // clear before reuse
    buf.Write(data)
    buf.WriteString("-processed")
    return buf.String()
}

// Benefits:
// - Reduces GC pressure by reusing objects
// - Avoids allocation overhead for short-lived objects
// - Pool entries can be garbage collected between GC cycles
//
// Use for: bytes.Buffer, json encoders, temporary slices
// NOT for: long-lived objects or when size is unpredictable

errgroup (Group with Error Propagation)

errgroup (from golang.org/x/sync) is a WaitGroup that returns the first error and can cancel remaining goroutines via context. g.Go() starts a goroutine; g.Wait() blocks and returns the first non-nil error. WithContext creates a context that's cancelled when any goroutine returns an error — so other goroutines stop early. This is the idiomatic way to run parallel operations that should all succeed or fail together.

go
package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/errgroup"
    "net/http"
)

// errgroup is like WaitGroup but propagates the first error
// and can cancel remaining goroutines on failure
func fetchAll(urls []string) ([]*http.Response, error) {
    g, ctx := errgroup.WithContext(context.Background())
    results := make([]*http.Response, len(urls))

    for i, url := range urls {
        i, url := i, url // capture loop variables
        g.Go(func() error {
            req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                return err // cancels ctx, stopping other goroutines
            }
            results[i] = resp
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        return nil, err // first error returned
    }
    return results, nil
}

func main() {
    urls := []string{
        "https://example.com",
        "https://golang.org",
    }
    resps, err := fetchAll(urls)
    fmt.Println(len(resps), err)
}
17

Generics (Go 1.18+)

Generic Functions & Type Parameters

Go generics (1.18+) use type parameters in square brackets: [T any] declares T as any type. The compiler infers types from arguments, so you rarely specify them explicitly. Generics enable type-safe reusable functions like Map/Filter/Reduce without interface{} and type assertions. 'any' is an alias for interface{} introduced with generics.

go
package main

import "fmt"

// Type parameters in brackets: [T any] means T can be any type
func Map[T, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

func Filter[T any](slice []T, predicate func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
    result := initial
    for _, v := range slice {
        result = fn(result, v)
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    squared := Map(nums, func(n int) int { return n * n })
    fmt.Println(squared) // [1 4 9 16 25]

    evens := Filter(nums, func(n int) bool { return n%2 == 0 })
    fmt.Println(evens) // [2 4]

    sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
    fmt.Println(sum) // 15

    // Works with any type
    words := []string{"go", "rust", "python"}
    lengths := Map(words, func(s string) int { return len(s) })
    fmt.Println(lengths) // [2 4 6]
}

Type Constraints & comparable

Type constraints limit which types a generic accepts. 'comparable' is a built-in constraint for types supporting == (needed for map keys and comparisons). Custom constraints use type unions (int | float64) to allow specific types. The golang.org/x/exp/constraints package provides Ordered (for <, >, etc.). Constraints can combine type sets and methods.

go
package main

import "fmt"

// comparable: types that support == and != (built-in constraint)
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

// Custom constraint with a type set
type Number interface {
    int | int64 | float64 | float32
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// Multiple type parameters
func Pair[T, U any](first T, second U) struct{ First T; Second U } {
    return struct{ First T; Second U }{first, second}
}

func main() {
    fmt.Println(Contains([]string{"a", "b", "c"}, "b")) // true
    fmt.Println(Contains([]int{1, 2, 3}, 5))             // false

    fmt.Println(Sum([]int{1, 2, 3}))           // 6
    fmt.Println(Sum([]float64{1.5, 2.5}))      // 4

    p := Pair("Alice", 30)
    fmt.Println(p) // {Alice 30}
}

Generic Data Structures

Generic types (Stack[T any]) create type-safe data structures that work with any type without interface{} boxing or type assertions. The type parameter T is part of the type, so Stack[int] and Stack[string] are distinct, compile-time-checked types. This eliminates an entire class of runtime type errors. Generic structs, methods, and interfaces all support type parameters.

go
package main

import "fmt"

// Generic Stack — works with any type, type-safe
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
    }
    index := len(s.items) - 1
    item := s.items[index]
    s.items = s.items[:index]
    return item, true
}

func (s *Stack[T]) Len() int {
    return len(s.items)
}

func main() {
    // Type-safe stack of ints
    intStack := &Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    val, _ := intStack.Pop()
    fmt.Println(val) // 2

    // Type-safe stack of strings
    strStack := &Stack[string]{}
    strStack.Push("hello")
    s, _ := strStack.Pop()
    fmt.Println(s) // hello

    // intStack.Push("oops") // compile error! type-safe
}

Generic Constraints with ~ (Underlying Type)

The ~ prefix in a constraint matches any type whose underlying type is the named type — so ~string matches both string and type MyString string. Without ~, a constraint only matches the exact named type, which is rarely useful for custom types. Use ~ when you want generics to work with type aliases and named types derived from primitives (common in domain modeling).

go
package main

import "fmt"

// ~ allows any type whose UNDERLYING type matches
// (e.g., type MyString string would match ~string)
type StringLike interface {
    ~string
}

func Join[S StringLike](items []S, sep string) string {
    result := ""
    for i, item := range items {
        if i > 0 {
            result += sep
        }
        result += string(item) // convert underlying type to string
    }
    return result
}

// Numeric constraint with ~
type Integer interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

func Double[T Integer](v T) T {
    return v * 2
}

// Custom type based on int
type Score int

func main() {
    type MyString string
    items := []MyString{"a", "b", "c"}
    fmt.Println(Join(items, "-")) // a-b-c

    s := Double(Score(21))
    fmt.Println(s) // 42
}

Generics vs Interfaces (When to Use)

Choose generics when the algorithm is identical across types (collections, math, transforms) — they provide compile-time type safety with zero runtime overhead. Choose interfaces when different types need different implementations (polymorphism) or when you need runtime dispatch (dependency injection, mocking). They're complementary: generics can be constrained by interfaces (T Stringer) for the best of both worlds.

go
package main

import "fmt"

// GENERICS: best for algorithms/data structures that work
// the same way regardless of type (collections, sorting, mapping)
func Max[T int | float64](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// INTERFACES: best when different types need DIFFERENT implementations
// (polymorphism, dependency injection, mocking)
type Stringer interface {
    String() string
}

type Dog struct{ Name string }
func (d Dog) String() string { return "Dog: " + d.Name }

type Cat struct{ Name string }
func (c Cat) String() string { return "Cat: " + c.Name }

func Print[T Stringer](v T) {
    fmt.Println(v.String())
}

func main() {
    fmt.Println(Max(3, 7))       // 7
    fmt.Println(Max(3.14, 2.71)) // 3.14

    Print(Dog{"Rex"}) // Dog: Rex
    Print(Cat{"Whiskers"}) // Cat: Whiskers
}

// RULE OF THUMB:
// - Same logic, different types -> Generics
// - Different logic, shared contract -> Interfaces
// - Need runtime polymorphism -> Interfaces
// - Need compile-time type safety -> Generics
18

Reflection

reflect.Type & reflect.Value

reflect.TypeOf returns the runtime type; reflect.ValueOf returns the runtime value. From a Type you can inspect struct fields, methods, and tags. From a Value you can read and (with CanSet) modify fields. Reflection is powerful but slow (10-100x slower than direct access) and bypasses compile-time type safety — use it sparingly, mainly for serialization, ORMs, and frameworks.

go
package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    u := User{Name: "Alice", Age: 30}

    // Get type and value at runtime
    t := reflect.TypeOf(u)
    v := reflect.ValueOf(u)

    fmt.Println("Type:", t.Name())        // User
    fmt.Println("Kind:", t.Kind())         // struct
    fmt.Println("NumFields:", t.NumField())

    // Iterate struct fields
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        tag := field.Tag.Get("json")
        fmt.Printf("%s (%s) = %v [json:%s]\n",
            field.Name, field.Type, value, tag)
    }
    // Name (string) = Alice [json:name]
    // Age (int) = 30 [json:age]
}

Modifying Values with Reflection

To modify a value via reflection, you must pass a pointer and call .Elem() to dereference it. CanSet() reports whether a field is assignable (exported fields of an addressable value). Unexported (lowercase) fields can be read but not set. SetInt/SetString/Set are type-checked at runtime and panic on mismatch. Reflection-based mutation is the basis of config parsers and ORMs.

go
package main

import (
    "fmt"
    "reflect"
)

func main() {
    type Config struct {
        Port int
        Host string
    }

    cfg := Config{Port: 8080, Host: "localhost"}

    // MUST pass a pointer to modify (ValueOf of a struct is a copy)
    v := reflect.ValueOf(&cfg).Elem()

    // Check if a field is settable
    portField := v.FieldByName("Port")
    fmt.Println("Settable:", portField.CanSet()) // true

    // Modify fields by name
    portField.SetInt(9090)
    v.FieldByName("Host").SetString("0.0.0.0")

    fmt.Println(cfg) // {9090 0.0.0.0}

    // Set via interface (type-checked at runtime)
    v.FieldByName("Port").Set(reflect.ValueOf(7070))
    fmt.Println(cfg) // {7070 0.0.0.0}

    // Unexported fields are NOT settable (CanSet returns false)
}

Calling Methods Dynamically

Reflection can call methods dynamically by name — useful for RPC frameworks, plugin systems, and routing. MethodByName returns a Value; Call() invokes it with a slice of reflect.Value arguments and returns a slice of results. NumMethod/Method enumerate methods. Method calls via reflection are slow and bypass type safety, so use them only when the method name isn't known at compile time.

go
package main

import (
    "fmt"
    "reflect"
)

type Calculator struct{}

func (c Calculator) Add(a, b int) int { return a + b }
func (c Calculator) Mul(a, b int) int { return a * b }

func main() {
    calc := Calculator{}
    v := reflect.ValueOf(calc)

    // Find a method by name
    method := v.MethodByName("Add")
    fmt.Println("Found:", method.IsValid()) // true

    // Call with arguments (each arg must be a reflect.Value)
    args := []reflect.Value{
        reflect.ValueOf(3),
        reflect.ValueOf(4),
    }
    results := method.Call(args)
    fmt.Println("3 + 4 =", results[0].Int()) // 7

    // List all methods
    t := reflect.TypeOf(calc)
    for i := 0; i < t.NumMethod(); i++ {
        m := t.Method(i)
        fmt.Printf("Method: %s, Type: %v\n", m.Name, m.Type)
    }
    // Method: Add, Type: func(main.Calculator, int, int) int
    // Method: Mul, Type: func(main.Calculator, int, int) int
}

Practical Use: Struct to Map (Tag-based)

This pattern — iterating struct fields and reading tags — is how encoding/json, YAML parsers, ORMs, and validation libraries work under the hood. reflect makes it possible to write generic code that processes any struct based on its tags. The json:'-' tag convention (skip this field) is standard. This is one of the most legitimate uses of reflection in Go.

go
package main

import (
    "fmt"
    "reflect"
)

// Convert any struct to a map using struct tags as keys
func StructToMap(obj any, tag string) map[string]any {
    result := make(map[string]any)
    v := reflect.ValueOf(obj)
    t := reflect.TypeOf(obj)

    // Dereference pointers
    if v.Kind() == reflect.Ptr {
        v = v.Elem()
        t = t.Elem()
    }

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        // Use the tag value if present, else the field name
        key := field.Tag.Get(tag)
        if key == "" {
            key = field.Name
        }
        // Skip fields tagged with "-"
        if key == "-" {
            continue
        }
        result[key] = v.Field(i).Interface()
    }
    return result
}

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    Pass string `json:"-"` // excluded
}

func main() {
    u := User{Name: "Alice", Age: 30, Pass: "secret"}
    m := StructToMap(u, "json")
    fmt.Println(m) // map[age:30 name:Alice]
}
19

Build & Tooling

go build, go run & go install

go build compiles to an executable; go run compiles to a temp file and runs it (great for development); go install puts the binary in $GOPATH/bin for global access. Go's standout feature is easy cross-compilation via GOOS/GOARCH — no toolchain needed. -ldflags='-s -w' strips debug info for ~30% smaller binaries. -X injects values (like version strings) at build time for CI/CD.

go
# Build an executable (outputs to current directory)
go build -o myapp              # custom output name
go build -o bin/myapp ./cmd    # build a specific package

# Build for different OS/arch (cross-compilation)
GOOS=linux GOARCH=amd64 go build -o myapp-linux
GOOS=windows GOARCH=amd64 go build -o myapp.exe
GOOS=darwin GOARCH=arm64 go build -o myapp-mac  # Apple Silicon

# Run without building an executable (compiles to temp, runs, deletes)
go run main.go                 # run single file
go run ./cmd/server            # run a package

# Install: builds and puts binary in $GOPATH/bin
go install ./cmd/myapp         # now 'myapp' is on your PATH

# Build flags
go build -v                    # verbose: print packages being compiled
go build -race                 # enable race detector (for testing)
go build -ldflags="-s -w"      # strip debug info (smaller binary)
go build -ldflags="-X main.Version=1.0.0"  # inject version at build time

go mod (Module Management)

go mod manages dependencies via go.mod (dependency list) and go.sum (checksums for security). go get adds/upgrades deps; go mod tidy syncs the files (run before commits). Go uses Semantic Import Versioning: v2+ requires a /v2 path suffix. The module cache is shared at $GOPATH/pkg/mod. go mod vendor creates a vendor/ directory for reproducible, offline, or audited builds.

go
# Initialize a new module
go mod init github.com/user/myproject

# This creates go.mod:
# module github.com/user/myproject
# go 1.21

# Add a dependency (auto-added to go.mod)
go get github.com/gin-gonic/gin@latest
go get github.com/lib/[email protected]  # specific version

# Tidy: add missing deps, remove unused ones
go mod tidy

# Download dependencies to local cache
go mod download

# Verify dependencies haven't been modified
go mod verify

# Vendor: copy deps into ./vendor (for offline builds)
go mod vendor

# Upgrade dependencies
go get -u github.com/lib/pq       # upgrade to latest minor
go get -u=patch github.com/lib/pq # upgrade patches only
go get github.com/lib/[email protected]  # pin specific version

# View dependency graph
go mod graph | head

go test & Benchmarking

go test runs _test.go files; functions named TestXxx(t *testing.T) are tests, BenchmarkXxx(b *testing.B) are benchmarks. -race enables the data race detector (essential for concurrent code). -cover shows test coverage; -coverprofile generates a detailed report. Benchmarks run b.N iterations where N is auto-tuned. Table-driven tests (slices of input/expected) are the idiomatic Go testing style.

go
# Run all tests in the current package
go test ./...

# Verbose output
go test -v ./...

# Run a specific test
go test -run TestAdd -v

# Run benchmarks
go test -bench=. -benchmem

# Race detector (find data races)
go test -race ./...

# Coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out  # view in browser

# --- test file example (xxx_test.go) ---
# func TestAdd(t *testing.T) {
#     got := Add(2, 3)
#     if got != 5 {
#         t.Errorf("Add(2,3) = %d, want 5", got)
#     }
# }
#
# func BenchmarkAdd(b *testing.B) {
#     for i := 0; i < b.N; i++ {
#         Add(2, 3)
#     }
# }

go fmt, go vet & golangci-lint

go fmt/gofmt enforces a single canonical format — Go has no formatting debates. go vet catches common bugs (printf mismatches, lock copies, bad struct tags). golangci-lint aggregates dozens of linters and is the industry standard for CI. Run gofmt and go vet before every commit; add golangci-lint to CI for deeper analysis. Consistent tooling is a major reason Go codebases look uniform.

go
# go fmt: format code (the ONE true Go style)
go fmt ./...              # format all files
gofmt -d main.go          # show diff without changing
gofmt -w main.go          # write changes in place

# go vet: static analysis for common mistakes
go vet ./...              # check all packages
# Catches: printf format mismatches, unreachable code,
# struct tag errors, lock copies, shadowed variables

# golangci-lint: meta-linter (runs many linters)
# Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run         # run all enabled linters
golangci-lint run --enable=gosec,goconst  # enable specific linters

# Common linters:
# errcheck    - check unchecked errors
# gosec       - security issues
# govet       - go vet checks
# staticcheck - advanced static analysis
# ineffassign - detect ineffectual assignments
# unused      - find unused code

# goimports: fmt + auto-manage imports
# go install golang.org/x/tools/cmd/goimports@latest
goimports -w main.go

Profiling & pprof

pprof is Go's built-in profiler — CPU, memory, goroutine, and mutex profiling. For long-running servers, import _ 'net/http/pprof' to expose a /debug/pprof/ endpoint for live profiling without restarts. go tool pprof gives an interactive shell (top, list, web) or a web UI. The execution tracer (go tool trace) visualizes goroutine scheduling and blocking. Profiling is essential for performance-critical Go code.

go
# CPU profiling
go test -cpuprofile=cpu.prof -bench=.
go tool pprof cpu.prof
# Interactive commands: top, list FunctionName, web (graphviz)

# Memory profiling
go test -memprofile=mem.prof -bench=.
go tool pprof mem.prof

# Built-in HTTP pprof endpoint (for production servers)
import _ "net/http/pprof"
# Then visit: http://localhost:8080/debug/pprof/
# Capture a 30-second CPU profile:
# go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

# In code:
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// ... code to profile ...

# Trace (execution tracer)
go test -trace=trace.out -bench=.
go tool trace trace.out  # opens in browser
20

Database (SQL)

database/sql Basics

database/sql is the standard interface for SQL databases. The driver is imported for its side effects (registering itself). sql.Open does not establish a connection; use Ping to verify. Always defer db.Close().

go
import "database/sql"
import _ "github.com/lib/pq"
db, err := sql.Open("postgres", "host=localhost dbname=mydb")
if err != nil { log.Fatal(err) }
defer db.Close()
err = db.Ping()

Query Rows

Query returns multiple rows. Always defer rows.Close() to release resources. Scan copies column values into variables. Check rows.Err() after the loop for iteration errors.

go
rows, err := db.Query("SELECT id, name FROM users WHERE age > $1", 18)
if err != nil { log.Fatal(err) }
defer rows.Close()
for rows.Next() {
    var id int; var name string
    if err := rows.Scan(&id, &name); err != nil { log.Fatal(err) }
    fmt.Printf("%d: %s\n", id, name)
}

Query Single Row

QueryRow returns a single row. Scan returns sql.ErrNoRows if no row matches. Cleaner than Query for single-row lookups. Always handle ErrNoRows explicitly.

go
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = $1", 1).Scan(&name)
if err != nil {
    if err == sql.ErrNoRows { fmt.Println("Not found") } else { log.Fatal(err) }
}

Prepared Statements

Prepare creates a reusable statement, improving performance for repeated queries. Prevents SQL injection. Always defer stmt.Close(). Use for queries executed multiple times.

go
stmt, err := db.Prepare("INSERT INTO users(name) VALUES($1)")
if err != nil { log.Fatal(err) }
defer stmt.Close()
_, err = stmt.Exec("Alice")
_, err = stmt.Exec("Bob")

Transactions

Begin starts a transaction. All operations within tx are atomic. defer Rollback is safe: it is a no-op after Commit. If any operation fails, Rollback undoes all changes.

go
tx, err := db.Begin()
if err != nil { log.Fatal(err) }
defer tx.Rollback()  // Safe to call after commit
_, err = tx.Exec("UPDATE accounts SET bal = bal - 100 WHERE id = 1")
_, err = tx.Exec("UPDATE accounts SET bal = bal + 100 WHERE id = 2")
if err != nil { log.Fatal(err) }
err = tx.Commit()
21

Testing Deep Dive

Basic Test

Test functions start with Test and take *testing.T. t.Errorf logs failure and continues. t.Fatalf logs and stops. Run with go test. Use testify/assert for cleaner assertions.

go
func TestAdd(t *testing.T) {
    got := Add(2, 3)
    want := 5
    if got != want {
        t.Errorf("Add(2,3) = %d; want %d", got, want)
    }
}

Table-Driven Tests

Table-driven tests are idiomatic in Go. Define test cases as a slice of structs. Loop through and run each. t.Run creates subtests for individual failures.

go
func TestAdd(t *testing.T) {
    tests := []struct {
        a, b, want int
    }{
        {1, 2, 3}, {-1, 1, 0}, {0, 0, 0},
    }
    for _, tt := range tests {
        got := Add(tt.a, tt.b)
        if got != tt.want {
            t.Errorf("Add(%d,%d)=%d; want %d", tt.a, tt.b, got, tt.want)
        }
    }
}

Subtests

Subtests use t.Run with a name and function. Run specific subtests with -run pattern. Provides better test organization and output.

go
func TestAdd(t *testing.T) {
    t.Run("positive", func(t *testing.T) {
        if Add(1, 2) != 3 { t.Error("failed") }
    })
    t.Run("negative", func(t *testing.T) {
        if Add(-1, -2) != -3 { t.Error("failed") }
    })
}
// Run specific: go test -run TestAdd/negative

Test Main

TestMain runs once for the package, replacing the default test runner. Use for global setup/teardown. Must call m.Run() to execute tests. os.Exit propagates the exit code.

go
func TestMain(m *testing.M) {
    setup()
    code := m.Run()
    teardown()
    os.Exit(code)
}
func setup() { /* initialize DB, etc. */ }
func teardown() { /* cleanup */ }

Mocking Interfaces

Go mocking relies on interfaces. Define an interface, implement a mock, and inject it. Tools like mockery and mockgen auto-generate mocks. Enables unit testing without external dependencies.

go
type Store interface { Get(id int) (string, error) }
type MockStore struct { data map[int]string }
func (m *MockStore) Get(id int) (string, error) {
    if v, ok := m.data[id]; ok { return v, nil }
    return "", errors.New("not found")
}
// Use in tests by injecting MockStore
22

Benchmarking

Basic Benchmark

Benchmark functions start with Benchmark and take *testing.B. b.N is adjusted by the framework to get stable measurements. Run with go test -bench. Output shows ns/op (nanoseconds per operation).

go
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}
// Run: go test -bench=.
// Output: BenchmarkAdd-8    1000000000    0.3 ns/op

Sub-Benchmarks

Sub-benchmarks use b.Run. ResetTimer excludes setup time. ReportAllocs shows memory allocations. Compare performance across input sizes.

go
func BenchmarkSort(b *testing.B) {
    sizes := []int{100, 1000, 10000}
    for _, size := range sizes {
        b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
            data := generateData(size)
            b.ResetTimer()
            for i := 0; i < b.N; i++ {
                Sort(data)
            }
        })
    }
}

Memory Allocations

ReportAllocs shows memory allocations per operation. Reducing allocations is key to Go performance. Use sync.Pool, pre-allocate slices, and avoid unnecessary string concatenation.

go
func BenchmarkConcat(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        s := "a" + "b" + "c"
        _ = s
    }
}
// Output includes allocs/op and B/op

Parallel Benchmarks

RunParallel runs benchmarks concurrently across multiple goroutines. Useful for testing thread-safe code. pb.Next() iterates the work distribution. Measures throughput under concurrency.

go
func BenchmarkParallel(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Add(2, 3)
        }
    })
}

Benchmark Comparison

benchstat compares benchmark results statistically. Run benchmarks multiple times with -count=10 for reliable comparison. Helps verify performance improvements and detect regressions.

go
# Run with memory stats
benchstat old.txt new.txt
# Compare two runs
# name      old time/op  new time/op  delta
# Add-8     2.3ns        1.8ns        -21.7%
23

Profiling (pprof)

CPU Profiling

StartCPUProfile writes CPU profile to a file. StopCPUProfile flushes data. Analyze with go tool pprof. Use top, list, web commands in pprof. Focus on functions consuming the most CPU.

go
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Run your code here
// Analyze: go tool pprof cpu.prof

Memory Profiling

WriteHeapProfile captures the current heap state. Call runtime.GC() first for accurate results. Set MemProfileRate = 1 to profile every allocation (slower but precise).

go
// Add to code
runtime.GC()
f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()
// Or use runtime.MemProfileRate = 1 for all allocations

HTTP Profiling

net/http/pprof registers profiling endpoints on the default mux. Access profiles via HTTP without restarting. Useful for production diagnostics. Secure the endpoint in production.

go
import _ "net/http/pprof"
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Analyze live:
// go tool pprof http://localhost:6060/debug/pprof/profile

pprof Commands

top shows functions consuming most resources. list shows annotated source code. web opens an SVG call graph. tree shows the call hierarchy. Use focus to filter.

go
# Start interactive pprof
go tool pprof cpu.prof
(pprof) top 10        # Top functions by CPU
(pprof) list Add      # Show source with timings
(pprof) web           # Open graph in browser
(pprof) tree          # Call tree
(pprof) png > out.png # Save graph as PNG

Trace

runtime/trace captures execution traces: goroutine scheduling, GC, syscall blocking. go tool trace opens a web UI. Useful for diagnosing latency and concurrency issues.

go
import "runtime/trace"
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// Run code
// View: go tool trace trace.out
24

Build Tags

Basic Build Tags

Build tags conditionally compile files. The //go:build syntax (Go 1.17+) replaces // +build. Tags can be OS (linux, darwin, windows), arch (amd64, arm64), or custom.

go
//go:build linux
// +build linux

package main
// This file only compiles on Linux

Multiple Tags

&& (comma in old syntax) requires all tags. || (space in old syntax) requires any tag. ! negates. Use for platform-specific implementations.

go
//go:build linux && amd64
// +build linux,amd64

//go:build linux || darwin
// +build linux darwin

//go:build !windows
// +build !windows

Custom Tags

Custom tags enable optional features. Build with -tags flag. Common use: debug builds, experimental features, enterprise vs community editions. Keep tag names lowercase.

go
//go:build debug
// +build debug

package main
const DebugMode = true
// Build with: go build -tags debug

File Suffixes

File suffixes provide implicit build constraints. _linux, _darwin, _windows for OS. _amd64, _arm64 for architecture. Simpler than tags for platform-specific code.

go
// file: utils_linux.go    -> Linux only
// file: utils_darwin.go   -> macOS only
// file: utils_windows.go  -> Windows only
// file: utils_amd64.go    -> amd64 only
// file: utils_debug.go    -> requires -tags debug

Build with Tags

Use -tags to enable conditional compilation. Multiple tags are space-separated. Go version tags (go1.18) enable version-specific code. Verify with go list -tags.

go
# Build with debug tag
go build -tags debug
# Multiple tags
go build -tags "debug verbose"
# List available tags
go list -tags ./...
# Conditional in code
//go:build go1.18  // Requires Go 1.18+
25

CGO Basics

Basic CGO

CGO enables calling C from Go. C code is in comments above import "C". The import must be immediately after the comment. CGO slows builds and prevents cross-compilation.

go
/*
#include <stdio.h>
void hello() {
    printf("Hello from C!\n");
}
*/
import "C"
func main() {
    C.hello()
}

Passing Strings

C.CString allocates a C string (must free with C.free). CString copies the data. Use unsafe.Pointer for conversion. Always free C-allocated memory to avoid leaks.

go
/*
#include <string.h>
int len(const char* s) { return strlen(s); }
*/
import "C"
import "unsafe"
s := "Hello"cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
length := C.len(cs)

Calling C Libraries

#cgo LDFLAGS links C libraries. #cgo CFLAGS sets compiler flags. Include system headers with #include. CGO bridges Go and existing C libraries like libm, libcrypto.

go
/*
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"
result := float64(C.sqrt(16.0))
fmt.Println(result)  // 4

C to Go Callback

Go functions exported with //export can be called from C. The function must be in package main. Enables C libraries to call back into Go. Used in FFI bindings.

go
/*
extern void goCallback(int);
void callGo(int n) { goCallback(n); }
*/
import "C"
//export goCallback
func goCallback(n C.int) {
    fmt.Printf("Called with %d\n", n)
}

Performance Notes

CGO calls have significant overhead compared to Go function calls. Avoid in performance-critical code. Batch operations to reduce cross-boundary calls. Set CGO_ENABLED=0 for pure Go builds.

go
// CGO calls have overhead (~100ns)
// Avoid in hot paths
// Batch C calls to amortize cost
// CGO disables inlining and some optimizations
// Build: CGO_ENABLED=1 go build
26

Web Frameworks

net/http Server

net/http is the standard library HTTP server. HandleFunc registers handlers. ListenAndServe starts the server. Default mux is fine for simple apps; use custom mux for production.

go
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
})
http.HandleFunc("/api", apiHandler)
log.Fatal(http.ListenAndServe(":8080", nil))

Gin Framework

Gin is a high-performance HTTP framework. Param extracts URL parameters. JSON serializes responses. Gin provides routing, middleware, and JSON validation. Faster than net/http due to httprouter.

go
import "github.com/gin-gonic/gin"
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id")
    c.JSON(200, gin.H{"user": id})
})
r.Run(":8080")

Middleware

Middleware wraps handlers to add cross-cutting concerns: logging, auth, CORS. c.Next() calls the next handler. gin.Recovery() prevents crashes from panics. Order matters.

go
func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        fmt.Printf("%s %s %v\n", c.Request.Method, c.URL.Path, time.Since(start))
    }
}
r := gin.New()
r.Use(Logger(), gin.Recovery())

Echo Framework

Echo is another popular framework, similar to Gin. Handlers return errors for centralized error handling. Built-in middleware for CORS, JWT, rate limiting. Clean API design.

go
import "github.com/labstack/echo/v4"
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
    id := c.Param("id")
    return c.JSON(200, map[string]string{"user": id})
})
e.Start(":8080")

Static Files

FileServer serves static files. StripPrefix adjusts the URL path. Useful for serving HTML, CSS, JS, and images. For production, use a CDN or nginx for static assets.

go
// net/http
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// Gin
r.Static("/assets", "./assets")
r.StaticFile("/favicon.ico", "./favicon.ico")
27

gRPC

Define Proto

Protocol Buffers define the service contract. proto3 is the latest syntax. service defines RPC methods. message defines data structures. Generate Go code with protoc.

go
syntax = "proto3";
package greet;
service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }

Generate Code

protoc generates Go code from .proto files. --go_out generates message types. --go-grpc_out generates service stubs. The generated code is not edited manually.

go
# Install protoc and plugins
protoc --go_out=. --go-grpc_out=. \
    greet.proto
# This generates greet.pb.go and greet_grpc.pb.go
# Containing message types and service interfaces

Server Implementation

Embed UnimplementedGreeterServer for forward compatibility. Implement the service methods. grpc.NewServer creates the server. Register the service before serving.

go
type server struct { greet.UnimplementedGreeterServer }
func (s *server) SayHello(ctx context.Context, in *greet.HelloRequest) (*greet.HelloReply, error) {
    return &greet.HelloReply{Message: "Hello " + in.Name}, nil
}
lis, _ := net.Listen("tcp", ":50051")
grpc.NewServer().Serve(lis)

Client

grpc.Dial establishes a connection. WithInsecure disables TLS (use credentials.NewTLS for production). The client stub provides typed methods. Connections are pooled and reused.

go
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := greet.NewGreeterClient(conn)
resp, _ := client.SayHello(context.Background(), &greet.HelloRequest{Name: "Alice"})
fmt.Println(resp.Message)

Streaming

gRPC supports three streaming patterns. stream keyword marks streaming. Server streaming: one request, many responses. Bidirectional: both sides stream. Useful for real-time data.

go
// Server streaming
rpc LotsOfReplies(HelloRequest) returns (stream HelloReply);
// Client streaming
rpc LotsOfGreetings(stream HelloRequest) returns (HelloReply);
// Bidirectional
rpc BidiHello(stream HelloRequest) returns (stream HelloReply);
28

Error Wrapping

Wrapping Errors

Use %w verb to wrap errors, preserving the original. This creates an error chain. Avoid %v for wrapping as it loses the chain. Wrapping adds context without losing the root cause.

go
if err != nil {
    return fmt.Errorf("failed to open config: %w", err)
}

Unwrapping

errors.Is checks if any error in the chain matches. errors.As extracts a specific error type from the chain. Use Is for sentinel values, As for typed errors. Both traverse the wrap chain.

go
err := someOperation()
if errors.Is(err, sql.ErrNoRows) {
    // Handle not found
}
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
    fmt.Println("Path:", pathErr.Path)
}

Custom Errors

Custom error types implement the error interface. They carry structured data for error handling. Use errors.As to extract the custom type. Prefer typed errors over string comparisons.

go
type ValidationError struct {
    Field string
    Msg   string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
func validate(s string) error {
    if s == "" { return &ValidationError{Field: "name", Msg: "required"} }
    return nil
}

Sentinel Errors

Sentinel errors are package-level error variables. Use for expected error conditions. Check with errors.Is, never with ==. Export them for users to check against.

go
var ErrNotFound = errors.New("not found")
func Find(id int) (*User, error) {
    if id > 100 { return nil, ErrNotFound }
    return &User{}, nil
}
// Check with errors.Is
if errors.Is(err, ErrNotFound) { /* ... */ }

Error Handling Patterns

Handle errors immediately when possible. For deferred operations like Close, capture the error. Named return values allow deferred functions to modify the return value. Always check Close errors.

go
// Immediate handling
if err := doSomething(); err != nil {
    return fmt.Errorf("operation failed: %w", err)
}
// Deferred error checking (e.g., Close)
func readFile() (err error) {
    f, e := os.Open("file.txt")
    if e != nil { return e }
    defer func() {
        if cerr := f.Close(); err == nil { err = cerr }
    }()
    // ...
}
29

Common Pitfalls

Goroutine Leaks

Goroutines leak when they block forever. Always provide an exit path: context cancellation, close channels, or buffered channels. Use runtime.NumGoroutine() to detect leaks.

go
// BUG: goroutine leaks if receiver stops
func send(ch chan int) {
    go func() { ch <- 1 }()  // Blocks forever
}
// FIX: use context or buffered channel
func send(ctx context.Context, ch chan int) {
    go func() {
        select {
        case ch <- 1:
        case <-ctx.Done():
        }
    }()
}

Channel Close

Only the sender should close a channel, never the receiver. Closing signals no more values. Receiving from a closed channel returns the zero value. Sending to a closed channel panics.

go
// Only the sender should close a channel
// Closing from receiver causes panic
ch := make(chan int)
go func() {
    defer close(ch)  // Sender closes
    for i := 0; i < 5; i++ { ch <- i }
}()
for v := range ch { fmt.Println(v) }

Loop Variable Capture

Before Go 1.22, loop variables were shared across iterations. Goroutines capturing them see the final value. Go 1.22+ fixes this by creating a new variable per iteration. Pass as parameter for older versions.

go
// BUG (Go < 1.22): all goroutines see last value
for i := 0; i < 3; i++ {
    go func() { fmt.Println(i) }()  // Prints 3,3,3
}
// FIX: pass as parameter
for i := 0; i < 3; i++ {
    go func(i int) { fmt.Println(i) }(i)
}

Map Concurrency

Maps are not safe for concurrent use. Concurrent reads and writes cause a runtime panic. Use sync.Mutex for explicit locking or sync.Map for read-heavy concurrent access.

go
// BUG: concurrent map writes panic
m := map[int]int{}
go func() { m[1] = 1 }()
go func() { m[2] = 2 }()
// FIX: use sync.Map or mutex
var mu sync.Mutex
mu.Lock(); m[1] = 1; mu.Unlock()

Nil Interface

A nil pointer wrapped in an interface is not nil. The interface has a type even if the value is nil. Always return nil directly, not a nil typed pointer. Check with reflect or return nil explicitly.

go
// BUG: nil check fails
type MyError struct{}
func (e *MyError) Error() string { return "err" }
func doSomething() error {
    var err *MyError = nil
    return err  // Non-nil interface!
}
// FIX: return nil explicitly
func doSomething() error {
    return nil
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.