Table of Contents

0. Setup & Environment

Install Go via Homebrew

The most common way to install Go on macOS:

brew install go
go version   # verify: go version go1.22.x darwin/arm64

Go installs to $(brew --prefix)/opt/go. The toolchain (go, gofmt, vet) is available immediately after install.

GOPATH and Modules

Go modules (introduced in 1.11, default since 1.16) replaced GOPATH-based development. You no longer need to keep projects under $GOPATH/src.

# Start a new module anywhere on your filesystem
mkdir myproject && cd myproject
go mod init myproject   # creates go.mod

# go.mod pins the module path and Go version
# go.sum records exact dependency checksums
PATH note: Add $HOME/go/bin to your shell profile so binaries installed with go install are accessible:
export PATH=$PATH:$HOME/go/bin   # add to ~/.zshrc or ~/.bashrc

Editor Setup

VS Code (most popular, free) — install the official Go extension by the Go team. On first open it auto-installs:

GoLand by JetBrains — commercial IDE with deep Go support built-in (no extensions needed).

Tip: Enable Format on Save in VS Code. Go's gofmt is the community-wide standard formatter — there are no tabs-vs-spaces debates in Go.

Quick Verify

Confirm your install end-to-end with a minimal module:

mkdir ~/go-refresher && cd ~/go-refresher
go mod init go-refresher

cat > main.go << 'EOF'
package main
import "fmt"
func main() { fmt.Println("Hello, Go!") }
EOF

go run main.go    # Hello, Go!
go build .        # produces ./go-refresher binary

Program Basics

Every Go program belongs to a package. The main package is the entry point; all other packages are libraries.

Packages and Imports

// Every file starts with a package declaration
package main

// Import stdlib packages by path
import (
    "fmt"
    "os"
    "strings"

    // Aliased import
    str "strings"

    // Blank import — only runs init(), side effects only
    _ "image/png"

    // Dot import — brings names into current scope (avoid in production)
    . "math"
)

// Exported names start with an uppercase letter
// Unexported names start with lowercase — package-private
func main() {
    fmt.Println("Hello, Go!")
    _ = str.ToUpper("hello") // use alias
    _ = Sqrt(4)              // from dot import
}
Visibility Rule
Uppercase first letter = exported (public). Lowercase = unexported (package-private). There are no public/private keywords.

Go Modules

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

# Add a dependency (updates go.mod + go.sum)
go get github.com/some/[email protected]

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

# Vendor dependencies into ./vendor/
go mod vendor

# Show dependency graph
go mod graph

# Upgrade all direct deps to latest minor/patch
go get -u ./...

# Download modules to local cache
go mod download
// go.mod — the module manifest
module github.com/yourname/myproject

go 1.22

require (
    github.com/some/package v1.2.3
    golang.org/x/sync v0.6.0
)

// go.sum — cryptographic checksums, commit to source control
// github.com/some/package v1.2.3 h1:abc...
// github.com/some/package v1.2.3/go.mod h1:xyz...

Project Structure

myproject/
├── cmd/
│   └── myapp/
│       └── main.go        # Entry point (package main)
├── internal/              # Cannot be imported by external packages
│   ├── auth/
│   │   ├── auth.go
│   │   └── auth_test.go
│   └── db/
│       └── db.go
├── pkg/                   # Public library code (importable externally)
│   └── httputil/
│       └── httputil.go
├── api/                   # OpenAPI/proto specs, generated code
├── scripts/               # Build/deploy scripts
├── Makefile
├── go.mod
└── go.sum

Compilation and Running

# Run without building a binary (development)
go run ./cmd/myapp/

# Build a binary
go build -o bin/myapp ./cmd/myapp/

# Install binary to $GOPATH/bin (or $GOBIN)
go install ./cmd/myapp/

# Cross-compile: GOOS and GOARCH env vars
GOOS=linux GOARCH=amd64 go build -o bin/myapp-linux ./cmd/myapp/
GOOS=darwin GOARCH=arm64 go build -o bin/myapp-mac-arm ./cmd/myapp/
GOOS=windows GOARCH=amd64 go build -o bin/myapp.exe ./cmd/myapp/

# Format all code
go fmt ./...

# Vet for common mistakes
go vet ./...

# Show documentation
go doc fmt.Println

Type System

Basic Types

CategoryTypesNotes
Booleanbooltrue / false
Signed integersint, int8, int16, int32, int64int is platform-width (32 or 64-bit)
Unsigned integersuint, uint8, uint16, uint32, uint64, uintptruintptr for pointer arithmetic
Floating-pointfloat32, float64Prefer float64 by default
Complexcomplex64, complex128Rare in practice
Byte / Runebyte (= uint8), rune (= int32)rune represents a Unicode code point
StringstringImmutable UTF-8 byte sequence

Zero Values

Every type has a zero value — the default when declared without initialization.

TypeZero Value
boolfalse
int, float64, etc.0
string"" (empty string)
pointer, slice, map, channel, funcnil
structeach field set to its own zero value
arrayeach element set to its own zero value

Type Definitions vs Aliases

// Type definition — creates a NEW type with its own method set
type Celsius float64
type Fahrenheit float64

func (c Celsius) ToF() Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

// These are DIFFERENT types — no implicit conversion
var c Celsius = 100
var f Fahrenheit = Fahrenheit(c) // explicit conversion required

// Type alias — just another name, SAME type, same method set
type MyFloat = float64 // rarely needed, mostly for gradual refactoring
var x MyFloat = 3.14
var y float64 = x // no conversion needed — they ARE the same type

Type Assertions and Conversions

// Type conversion — between compatible types
var i int = 42
var f float64 = float64(i) // explicit cast
var u uint = uint(f)

// String conversions (not casts!)
s := string(rune(65))  // "A" — int to unicode char
n, _ := strconv.Atoi("42")        // string to int
s2 := strconv.Itoa(42)            // int to string
f2, _ := strconv.ParseFloat("3.14", 64)

// Type assertion — extract concrete type from interface
var val interface{} = "hello"

// Panics if val is not string
s3 := val.(string)

// Safe form — comma-ok idiom
s4, ok := val.(string)
if ok {
    fmt.Println("It's a string:", s4)
}

// Type switch — multiple type assertions
func describe(i interface{}) string {
    switch v := i.(type) {
    case int:
        return fmt.Sprintf("int: %d", v)
    case string:
        return fmt.Sprintf("string: %q", v)
    case bool:
        return fmt.Sprintf("bool: %t", v)
    default:
        return fmt.Sprintf("unknown: %T", v)
    }
}

Variables & Constants

Variable Declaration

package main

// Package-level variables (var keyword required)
var globalCounter int = 0
var (
    host = "localhost"
    port = 8080
    debug bool // zero value: false
)

func main() {
    // Explicit type
    var x int = 10

    // Inferred type from initializer
    var y = 3.14

    // Short declaration (most common inside functions)
    z := "hello"

    // Multiple assignment
    a, b := 1, 2
    a, b = b, a // swap — no temp variable needed

    // Blank identifier — discard a value
    result, _ := someFunction()

    // Multiple return values
    q, r := divide(10, 3)
    fmt.Println(x, y, z, a, b, result, q, r)
}

func someFunction() (int, error) { return 42, nil }
func divide(a, b int) (int, int) { return a / b, a % b }
Short Declaration (:=) Gotchas
  • Only works inside functions — not at package level.
  • Requires at least one NEW variable on the left side.
  • In nested scopes, := creates a NEW variable that shadows the outer one — a common source of bugs.

Constants and Iota

// Constants are evaluated at compile time
const Pi = 3.14159
const MaxRetries = 3
const Greeting = "hello"

// Grouped constants
const (
    StatusOK    = 200
    StatusNotFound = 404
)

// iota — auto-incrementing integer within a const block
type Direction int

const (
    North Direction = iota // 0
    East                   // 1
    South                  // 2
    West                   // 3
)

// iota with expressions
type ByteSize float64

const (
    _           = iota             // ignore first value (0)
    KB ByteSize = 1 << (10 * iota) // 1024
    MB                             // 1048576
    GB                             // 1073741824
    TB
)

// iota resets to 0 in each const block
const (
    A = iota // 0
    B        // 1
)
const (
    C = iota // 0 again — new block
    D        // 1
)

// Untyped constants — flexible, fit into any compatible type
const UntypedInt = 1000        // can be used as int, int64, float64...
const UntypedFloat = 3.14      // can be used as float32 or float64
var x float32 = UntypedFloat   // OK
var y float64 = UntypedFloat   // also OK

Functions

Function Basics

// Basic function
func add(a, b int) int {
    return a + b
}

// Multiple return values — idiomatic Go error handling
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

// Named return values — useful for documentation, defer
func minMax(arr []int) (min, max int) {
    min, max = arr[0], arr[0]
    for _, v := range arr[1:] {
        if v < min { min = v }
        if v > max { max = v }
    }
    return // naked return — returns min, max
}

// Variadic function — last param can accept zero or more values
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

// Spreading a slice into variadic
nums := []int{1, 2, 3, 4}
result := sum(nums...) // spread with ...

First-Class Functions and Closures

// Functions as values
type Handler func(string) string

// Function variable
var greet Handler = func(name string) string {
    return "Hello, " + name
}

// Passing functions as arguments
func apply(s string, fn func(string) string) string {
    return fn(s)
}

result := apply("world", greet)

// Returning a function — closure captures surrounding variables
func makeCounter() func() int {
    count := 0 // captured by the closure
    return func() int {
        count++
        return count
    }
}

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

// Immediately invoked function expression (IIFE)
result2 := func(x int) int { return x * x }(5) // 25

Defer

// defer: schedules a function call to run when the enclosing function returns
// Arguments are evaluated IMMEDIATELY, execution is deferred
func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // will run when readFile returns, even if it panics

    // ... read from f
    return nil
}

// Defers execute in LIFO (last-in, first-out) order
func lifoDemo() {
    defer fmt.Println("first defer — runs LAST")
    defer fmt.Println("second defer — runs SECOND")
    defer fmt.Println("third defer — runs FIRST")
    fmt.Println("function body")
}
// Output:
// function body
// third defer — runs FIRST
// second defer — runs SECOND
// first defer — runs LAST

// defer with named returns — can modify return value
func double(n int) (result int) {
    defer func() {
        result *= 2 // modifies the named return
    }()
    result = n
    return // returns n*2
}

// Argument evaluation timing gotcha
x := 0
defer fmt.Println(x) // prints 0 — x is captured NOW
x = 42               // too late, defer already has x=0
Defer in Loops
Do not use defer inside a loop to close resources — the defers accumulate and only fire when the function returns, not each iteration. Use an inner function or close explicitly instead.

The init() Function

// init() runs automatically before main(), after all variable initialization
// A package can have multiple init() functions (even in different files)
// They run in the order they appear

package mypackage

var config map[string]string

func init() {
    config = make(map[string]string)
    config["env"] = os.Getenv("APP_ENV")
    if config["env"] == "" {
        config["env"] = "development"
    }
}

// Execution order within a package:
// 1. Package-level variables initialized
// 2. init() functions run (in source file order, then declaration order)
// 3. main() runs (in main package)

Pointers

// & — address-of operator: gets the pointer to a variable
// * — dereference operator: gets the value at the pointer

x := 42
p := &x       // p is *int, holds the memory address of x
fmt.Println(*p) // 42 — dereference to get the value
*p = 100        // modify x through the pointer
fmt.Println(x)  // 100

// new() allocates zeroed memory and returns a pointer
p2 := new(int)  // *int pointing to 0
*p2 = 7

// make() creates slices, maps, and channels (not a pointer)
s := make([]int, 5)
m := make(map[string]int)
ch := make(chan int, 10)

// No pointer arithmetic in Go (unlike C)
// *(p + 1) — ILLEGAL

// Nil pointer — zero value for any pointer type
var ptr *int // nil
// *ptr = 5  — PANIC: nil pointer dereference

if ptr != nil {
    *ptr = 5
}

// Struct field access through pointer — automatic dereference
type Point struct{ X, Y int }
pt := &Point{1, 2}
fmt.Println(pt.X)   // equivalent to (*pt).X

When to Use Pointers vs Values

SituationUse PointerUse Value
Mutate the argument in callerYesNo
Large struct (>64 bytes)Yes (avoid copy)No
Nullable / optional valueYes (nil means absent)No
Small struct / scalarNoYes (simpler)
Shared state across goroutinesYes + syncNo
Method needs to modify receiverYes (pointer receiver)No
Implementing an interface consistentlyYes (all methods)Depends

Structs & Methods

Struct Definition and Field Tags

// Struct definition
type User struct {
    ID        int64     `json:"id"       db:"id"`
    Name      string    `json:"name"     db:"name"`
    Email     string    `json:"email"    db:"email"`
    CreatedAt time.Time `json:"created_at" db:"created_at"`
    password  string    // unexported — not accessible outside package
}

// Struct literal — named fields (preferred)
u := User{
    ID:    1,
    Name:  "Alice",
    Email: "[email protected]",
}

// Anonymous / positional (fragile, avoid)
u2 := User{1, "Bob", "[email protected]", time.Now(), "secret"}

// Zero value struct
var u3 User // all fields zero-valued

// Pointer to struct
up := &User{Name: "Carol"}
up.Email = "[email protected]" // auto-dereference

// Anonymous fields (embedding by type name)
type Address struct {
    Street string
    City   string
}

type Employee struct {
    User              // promoted fields and methods
    Address           // another embedded type
    Department string
}

e := Employee{}
e.Name = "Dave"       // promoted from User
e.Street = "Main St"  // promoted from Address
e.Department = "Eng"

Methods and Receivers

type Rectangle struct {
    Width, Height float64
}

// Value receiver — operates on a copy, cannot mutate
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Pointer receiver — can mutate the original
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func (r *Rectangle) String() string {
    return fmt.Sprintf("Rectangle(%.1f x %.1f)", r.Width, r.Height)
}

// Usage
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area())      // 50 — value receiver, called on value
rect.Scale(2)                 // Go auto-takes address: (&rect).Scale(2)
fmt.Println(rect.Width)       // 20
Consistency Rule
If any method of a type uses a pointer receiver, all methods should use pointer receivers. Mixing leads to confusion about which methods are in the pointer's method set vs. the value's method set.

Embedding vs Inheritance

// Go has NO inheritance. Use embedding for composition.

type Logger struct {
    prefix string
}

func (l *Logger) Log(msg string) {
    fmt.Printf("[%s] %s\n", l.prefix, msg)
}

type Server struct {
    *Logger              // embedded — Logger's methods promoted to Server
    host string
    port int
}

s := &Server{
    Logger: &Logger{prefix: "SERVER"},
    host:   "localhost",
    port:   8080,
}

s.Log("starting up")   // calls s.Logger.Log("starting up")
s.Logger.Log("direct") // also valid — explicit access

// Embedding resolves ambiguity at shallowest depth
// If two embedded types have the same method name, you must be explicit

Interfaces

Implicit Implementation

// Interface definition
type Animal interface {
    Sound() string
    Name() string
}

// Implicit implementation — no "implements" keyword
type Dog struct{ name string }

func (d Dog) Sound() string { return "Woof" }
func (d Dog) Name() string  { return d.name }

type Cat struct{ name string }

func (c Cat) Sound() string { return "Meow" }
func (c Cat) Name() string  { return c.name }

// Any value that has all the interface's methods satisfies the interface
func makeSound(a Animal) {
    fmt.Printf("%s says %s\n", a.Name(), a.Sound())
}

makeSound(Dog{name: "Rex"})
makeSound(Cat{name: "Whiskers"})

Common Standard Library Interfaces

// io.Reader — anything that can be read from
type Reader interface {
    Read(p []byte) (n int, err error)
}

// io.Writer — anything that can be written to
type Writer interface {
    Write(p []byte) (n int, err error)
}

// error — the built-in error interface
type error interface {
    Error() string
}

// fmt.Stringer — controls how a value is printed
type Stringer interface {
    String() string
}

// sort.Interface — makes a type sortable
type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

// Example: custom sort
type ByLength []string

func (b ByLength) Len() int           { return len(b) }
func (b ByLength) Less(i, j int) bool { return len(b[i]) < len(b[j]) }
func (b ByLength) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }

words := ByLength{"banana", "apple", "kiwi"}
sort.Sort(words) // uses sort.Interface methods

Interface Composition

// Interfaces can embed other interfaces
type ReadWriter interface {
    io.Reader
    io.Writer
}

type ReadWriteCloser interface {
    io.Reader
    io.Writer
    io.Closer
}

// Empty interface — satisfied by every type
// Use sparingly; prefer generics or concrete types
func printAnything(v interface{}) {
    fmt.Printf("%T: %v\n", v, v)
}

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

Interface Values and Nil

// An interface value has two components: (type, value)
// It is nil only if BOTH type and value are nil

type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }

// GOTCHA: this function returns a non-nil interface holding a nil pointer
func problematic() error {
    var err *MyError // nil pointer to MyError
    // ... some logic that might set err
    return err // returns interface{(*MyError, nil)} — NOT nil!
}

err := problematic()
fmt.Println(err == nil) // false! The interface is not nil

// Correct: return untyped nil directly
func correct() error {
    var err *MyError
    if err != nil {
        return err
    }
    return nil // returns interface{nil, nil} — truly nil
}
Nil Interface Trap
Never return a typed nil pointer as an interface. Return untyped nil directly. A typed nil pointer stored in an interface is NOT equal to nil.

Error Handling

Creating and Returning Errors

import (
    "errors"
    "fmt"
)

// errors.New — simple, static error message
var ErrNotFound = errors.New("not found")         // sentinel error
var ErrUnauthorized = errors.New("unauthorized")

// fmt.Errorf — formatted error message
func findUser(id int) (*User, error) {
    if id <= 0 {
        return nil, fmt.Errorf("invalid user id: %d", id)
    }
    // ...
    return nil, fmt.Errorf("findUser %d: %w", id, ErrNotFound) // %w wraps
}

// Check errors
user, err := findUser(0)
if err != nil {
    // Check for specific sentinel error
    if errors.Is(err, ErrNotFound) {
        fmt.Println("user does not exist")
    }
    log.Printf("error: %v", err)
    return
}
_ = user

Error Wrapping and Unwrapping

// errors.Is — checks if any error in the chain matches the target
// Walks the Unwrap() chain
err := fmt.Errorf("database: %w", ErrNotFound)
fmt.Println(errors.Is(err, ErrNotFound)) // true

// errors.As — finds the first error in chain matching the target type
type ValidationError struct {
    Field   string
    Message string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation: %s — %s", e.Field, e.Message)
}

wrappedErr := fmt.Errorf("handler: %w", &ValidationError{
    Field:   "email",
    Message: "invalid format",
})

var ve *ValidationError
if errors.As(wrappedErr, &ve) {
    fmt.Printf("field %q failed: %s\n", ve.Field, ve.Message)
}

// Custom error type with Unwrap()
type DBError struct {
    Query string
    Err   error
}

func (e *DBError) Error() string { return fmt.Sprintf("db query %q: %v", e.Query, e.Err) }
func (e *DBError) Unwrap() error { return e.Err } // allows errors.Is/As to traverse chain

Panic and Recover

// panic — stops normal execution, begins unwinding the stack
// recover — captures a panic in a deferred function

func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    return a / b, nil // panics if b == 0
}

result, err := safeDivide(10, 0)
// err = "recovered from panic: runtime error: integer divide by zero"

// When to panic vs return error:
// panic:  programmer error, invariant violation, init failures
// error:  expected failure conditions, IO, user input, external calls
Error Handling Pattern
Handle errors at the right level. At the application boundary (e.g., HTTP handler), log the error with context and return a user-friendly message. Lower layers should wrap errors with context and return them upward.

Concurrency

Go's Concurrency Philosophy
"Do not communicate by sharing memory; instead, share memory by communicating." — Go team. Channels are the preferred way to pass data between goroutines.

Goroutines

// A goroutine is a lightweight thread managed by the Go runtime
// Start with 'go' keyword before a function call
go someFunction()

go func(msg string) {
    fmt.Println(msg)
}("hello from goroutine")

// Goroutines are cheap — Go programs can run millions
// They're multiplexed onto OS threads by the scheduler (M:N threading)

Channels

// Unbuffered channel — send blocks until receiver is ready
ch := make(chan int)
go func() { ch <- 42 }() // send
v := <-ch                 // receive

// Buffered channel — send only blocks when buffer is full
bch := make(chan string, 3)
bch <- "one"   // doesn't block
bch <- "two"   // doesn't block
bch <- "three" // doesn't block
// bch <- "four" // would block — buffer full

// Directional channel types (enforce at compile time)
func producer(out chan<- int) {  // send-only
    out <- 42
    close(out)
}

func consumer(in <-chan int) {  // receive-only
    for v := range in {        // range closes when channel is closed
        fmt.Println(v)
    }
}

// Closing a channel
close(ch)             // signals no more values
v, ok := <-ch         // ok=false when channel is closed and empty
for v := range ch {}  // range exits when channel is closed

// Check if channel has value without blocking
select {
case v := <-ch:
    fmt.Println("received:", v)
default:
    fmt.Println("no value ready")
}

Select Statement

// select: like switch but for channel operations
// Blocks until one case is ready, then executes that case
// If multiple are ready, picks one at random

func fanIn(ch1, ch2 <-chan string) <-chan string {
    out := make(chan string)
    go func() {
        defer close(out)
        for {
            select {
            case v, ok := <-ch1:
                if !ok { ch1 = nil } else { out <- v }
            case v, ok := <-ch2:
                if !ok { ch2 = nil } else { out <- v }
            }
            if ch1 == nil && ch2 == nil {
                return
            }
        }
    }()
    return out
}

// Timeout with select
func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
    resultCh := make(chan string, 1)
    go func() {
        // ... do fetch
        resultCh <- "result"
    }()

    select {
    case result := <-resultCh:
        return result, nil
    case <-time.After(timeout):
        return "", fmt.Errorf("timeout after %v", timeout)
    }
}

Sync Primitives

import "sync"

// WaitGroup — wait for a collection of goroutines to finish
var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        fmt.Printf("worker %d done\n", id)
    }(i)
}
wg.Wait() // blocks until all Done() called

// Mutex — mutual exclusion lock
var (
    mu      sync.Mutex
    counter int
)

func increment() {
    mu.Lock()
    defer mu.Unlock()
    counter++
}

// RWMutex — multiple readers, single writer
var rwmu sync.RWMutex
var cache map[string]string

func readCache(key string) string {
    rwmu.RLock()         // multiple goroutines can hold RLock simultaneously
    defer rwmu.RUnlock()
    return cache[key]
}

func writeCache(key, val string) {
    rwmu.Lock()          // exclusive write lock
    defer rwmu.Unlock()
    cache[key] = val
}

// Once — run a function exactly once
var (
    once     sync.Once
    instance *MyService
)

func getInstance() *MyService {
    once.Do(func() {
        instance = &MyService{} // runs only the first time
    })
    return instance
}

// sync.Map — concurrent-safe map (use when reads >> writes)
var sm sync.Map
sm.Store("key", "value")
v, ok := sm.Load("key")
sm.Delete("key")
sm.Range(func(k, v any) bool {
    fmt.Println(k, v)
    return true // return false to stop iteration
})

errgroup and Context

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

// errgroup — WaitGroup + error collection
func fetchAll(ctx context.Context, urls []string) error {
    g, ctx := errgroup.WithContext(ctx)

    results := make([]string, len(urls))
    for i, url := range urls {
        i, url := i, url // capture loop vars
        g.Go(func() error {
            result, err := fetch(ctx, url)
            if err != nil {
                return fmt.Errorf("fetch %s: %w", url, err)
            }
            results[i] = result
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        return err // returns first non-nil error
    }
    return nil
}

// Context for cancellation
func doWork(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err() // context.Canceled or context.DeadlineExceeded
        default:
            // do some work
            time.Sleep(100 * time.Millisecond)
        }
    }
}

Common Patterns

Worker Pool
func workerPool(numWorkers int, jobs <-chan Job, results chan<- Result) {
    var wg sync.WaitGroup
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                results <- process(job)
            }
        }()
    }
    go func() {
        wg.Wait()
        close(results)
    }()
}

// Usage
jobs := make(chan Job, 100)
results := make(chan Result, 100)
go workerPool(5, jobs, results)

for _, j := range allJobs {
    jobs <- j
}
close(jobs) // signal workers no more jobs

for r := range results {
    // consume results
}
Pipeline Pattern
// Each stage takes an input channel and returns an output channel
func generate(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            out <- n
        }
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            out <- n * n
        }
    }()
    return out
}

// Compose the pipeline
naturals := generate(1, 2, 3, 4, 5)
squares := square(naturals)
for v := range squares {
    fmt.Println(v) // 1, 4, 9, 16, 25
}
Rate Limiting
// Basic rate limiting with time.Ticker
limiter := time.NewTicker(200 * time.Millisecond) // 5 req/sec
defer limiter.Stop()

for _, req := range requests {
    <-limiter.C // wait for tick
    go handle(req)
}

// Bursty rate limiting with buffered channel
burstLimiter := make(chan time.Time, 3) // allow burst of 3
for i := 0; i < 3; i++ {
    burstLimiter <- time.Now() // fill initial burst
}

go func() {
    for t := range time.Tick(200 * time.Millisecond) {
        burstLimiter <- t
    }
}()

for _, req := range requests {
    <-burstLimiter
    go handle(req)
}

Generics Go 1.18+

Type Parameters

// Generic function — type parameter [T constraint]
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
}

doubled := Map([]int{1, 2, 3}, func(n int) int { return n * 2 })
lengths := Map([]string{"a", "bb", "ccc"}, func(s string) int { return len(s) })

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

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

Type Constraints

import "golang.org/x/exp/constraints"

// Built-in constraints
// any       — alias for interface{}, accepts all types
// comparable — types that support == and !=

// Custom constraint using type set
type Number interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

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

fmt.Println(Sum([]int{1, 2, 3}))       // 6
fmt.Println(Sum([]float64{1.1, 2.2}))  // 3.3

// ~ prefix: includes all types whose underlying type is T
type Ordered interface {
    ~int | ~float64 | ~string
}

func Min[T Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

// Generic struct
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
    }
    last := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return last, true
}

intStack := &Stack[int]{}
intStack.Push(1)
intStack.Push(2)
v, _ := intStack.Pop() // 2
When to Use Generics
Use generics for data structures (Stack, Queue, Set) and algorithms operating on collections (Map, Filter, Reduce). Do NOT use generics just to avoid writing a specific function — concrete types with interfaces are often clearer.

Collections

Slices

// Array — fixed length, value type (copied on assignment)
var arr [5]int             // [0 0 0 0 0]
arr2 := [3]string{"a", "b", "c"}
arr3 := [...]int{1, 2, 3} // compiler counts

// Slice — dynamic, reference type (shares underlying array)
s := []int{1, 2, 3, 4, 5}

// make([]T, length, capacity)
s2 := make([]int, 3)       // [0 0 0], cap=3
s3 := make([]int, 0, 10)   // [], cap=10 (pre-allocated)

// Slicing — [low:high:max]
s4 := s[1:3]    // [2 3] — shares underlying array!
s5 := s[2:]     // [3 4 5]
s6 := s[:3]     // [1 2 3]
s7 := s[:]      // full slice

// append — may allocate new backing array
s = append(s, 6, 7)
s = append(s, []int{8, 9}...) // spread another slice

// copy — always copies min(len(dst), len(src)) elements
dst := make([]int, len(s))
n := copy(dst, s)

// Delete element at index i (order-preserving)
i := 2
s = append(s[:i], s[i+1:]...)

// Delete element (fast, doesn't preserve order)
s[i] = s[len(s)-1]
s = s[:len(s)-1]

// Capacity growth: typically doubles until 1024, then ~1.25x
fmt.Printf("len=%d cap=%d\n", len(s3), cap(s3))
Slice Sharing Gotcha
Slices share the underlying array. Modifying a sub-slice modifies the original. Use copy() or append(s[:i:i], ...) (three-index slice) to get an independent copy.

Maps

// Map — unordered key-value store
m := map[string]int{
    "alice": 30,
    "bob":   25,
}

// make
m2 := make(map[string][]int)

// Read — always safe; missing key returns zero value
age := m["alice"]    // 30
age2 := m["nobody"]  // 0 — no panic

// Comma-ok idiom — distinguish missing from zero
age3, ok := m["alice"]
if !ok {
    fmt.Println("key not found")
}

// Write
m["carol"] = 35

// Delete
delete(m, "bob")

// Iteration — order is RANDOM each run
for k, v := range m {
    fmt.Printf("%s: %d\n", k, v)
}

// Keys only
for k := range m {
    fmt.Println(k)
}

// Nil map — reads return zero, writes panic
var nilMap map[string]int
_ = nilMap["key"]   // ok, returns 0
// nilMap["key"] = 1 // PANIC: assignment to entry in nil map

// Maps are not safe for concurrent use
// Use sync.Mutex or sync.Map for concurrent access

Range Keyword

// range over slice — index, value
for i, v := range []int{10, 20, 30} {
    fmt.Println(i, v)
}

// range over map — key, value (random order)
for k, v := range map[string]int{"a": 1, "b": 2} {
    fmt.Println(k, v)
}

// range over string — index (byte position), rune (Unicode code point)
for i, r := range "Hello, 世界" {
    fmt.Printf("%d: %c (%d)\n", i, r, r)
}
// Note: i advances by byte count, not character count

// range over channel — receive until closed
for v := range ch {
    fmt.Println(v)
}

// Discard index or value
for _, v := range slice { }  // value only
for i := range slice { }     // index only

Strings & Runes

// Strings are immutable sequences of bytes (not characters)
s := "Hello, 世界"
fmt.Println(len(s))        // 13 — byte count, not character count
fmt.Println(s[0])          // 72 — byte value, not character

// Iterate by rune (Unicode code point)
for i, r := range s {
    fmt.Printf("%d: %c\n", i, r)
}

// Convert to []rune for index-safe operations
runes := []rune(s)
fmt.Println(len(runes))    // 9 — character count
fmt.Println(string(runes[7])) // "世"

// rune literals
r := 'A'              // rune (int32), value 65
r2 := '世'            // rune, value 19990

// Raw string literals — backticks, no escape sequences
raw := `C:\Users\name\file.txt`
multiline := `line 1
line 2
line 3`

// strings package
import "strings"

strings.Contains("seafood", "foo")      // true
strings.HasPrefix("seafood", "sea")     // true
strings.HasSuffix("seafood", "food")    // true
strings.Count("cheese", "e")            // 3
strings.Index("chicken", "ken")         // 4, -1 if not found
strings.ToUpper("hello")                // "HELLO"
strings.ToLower("HELLO")                // "hello"
strings.TrimSpace("  hello  ")          // "hello"
strings.Trim("--hello--", "-")          // "hello"
strings.Split("a,b,c", ",")            // ["a", "b", "c"]
strings.Join([]string{"a", "b"}, "-")  // "a-b"
strings.Replace("oink oink", "oink", "moo", 1)  // "moo oink"
strings.ReplaceAll("oink oink", "oink", "moo")  // "moo moo"
strings.Fields("  foo bar  ")           // ["foo", "bar"] — split on whitespace

// strings.Builder — efficient string concatenation
var b strings.Builder
for i := 0; i < 5; i++ {
    fmt.Fprintf(&b, "item%d ", i)
}
result := b.String()

// fmt formatting verbs
fmt.Sprintf("%v", val)    // default format
fmt.Sprintf("%+v", val)   // struct: includes field names
fmt.Sprintf("%#v", val)   // Go syntax representation
fmt.Sprintf("%T", val)    // type
fmt.Sprintf("%d", 42)     // decimal integer
fmt.Sprintf("%f", 3.14)   // floating point
fmt.Sprintf("%e", 3.14)   // scientific notation
fmt.Sprintf("%s", "hi")   // string
fmt.Sprintf("%q", "hi")   // quoted string: "hi"
fmt.Sprintf("%x", 255)    // hexadecimal: ff
fmt.Sprintf("%b", 42)     // binary: 101010
fmt.Sprintf("%p", &x)     // pointer address

I/O

File Operations

import (
    "bufio"
    "encoding/json"
    "io"
    "os"
)

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

// Write entire file
err = os.WriteFile("out.txt", []byte("hello\n"), 0644)

// Open file for reading
f, err := os.Open("file.txt")
if err != nil {
    return err
}
defer f.Close()

// Buffered reading line by line
scanner := bufio.NewScanner(f)
for scanner.Scan() {
    line := scanner.Text()
    _ = line
}
if err := scanner.Err(); err != nil {
    return err
}

// Create / write file
f2, err := os.Create("new.txt")
if err != nil {
    return err
}
defer f2.Close()

writer := bufio.NewWriter(f2)
fmt.Fprintln(writer, "line 1")
fmt.Fprintln(writer, "line 2")
writer.Flush() // must flush buffered writer!

// Append to file
f3, err := os.OpenFile("log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
defer f3.Close()

// Copy between reader and writer
n, err := io.Copy(dst, src) // copies until EOF, returns bytes written

JSON Encoding/Decoding

type Person struct {
    Name    string   `json:"name"`
    Age     int      `json:"age"`
    Email   string   `json:"email,omitempty"` // omit if zero value
    private string   // unexported — not marshaled
}

// Marshal (Go → JSON)
p := Person{Name: "Alice", Age: 30}
data, err := json.Marshal(p)
// data = []byte(`{"name":"Alice","age":30}`)

// Pretty print
data2, err := json.MarshalIndent(p, "", "  ")

// Unmarshal (JSON → Go) — must pass pointer
var p2 Person
err = json.Unmarshal([]byte(`{"name":"Bob","age":25}`), &p2)

// Stream encoding/decoding (more memory efficient for large data)
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", "  ")
encoder.Encode(p)

decoder := json.NewDecoder(os.Stdin)
var p3 Person
decoder.Decode(&p3)

// Dynamic JSON with map
var result map[string]interface{}
json.Unmarshal(data, &result)

// Use json.RawMessage to delay or preserve JSON
type Envelope struct {
    Type    string          `json:"type"`
    Payload json.RawMessage `json:"payload"`
}

// Handling unknown fields
decoder2 := json.NewDecoder(strings.NewReader(jsonStr))
decoder2.DisallowUnknownFields() // return error for unknown fields

HTTP Basics

import "net/http"

// HTTP server
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
    // Read query params
    name := r.URL.Query().Get("name")
    // Read request body
    body, _ := io.ReadAll(r.Body)
    defer r.Body.Close()
    // Write response
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{"greeting": "Hello, " + name})
})

log.Fatal(http.ListenAndServe(":8080", nil))

// HTTP client
client := &http.Client{Timeout: 10 * time.Second}

resp, err := client.Get("https://api.example.com/users")
if err != nil {
    return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
    return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}

var users []User
json.NewDecoder(resp.Body).Decode(&users)

// POST with JSON body
payload := map[string]string{"name": "Alice"}
buf, _ := json.Marshal(payload)
resp2, err := client.Post("https://api.example.com/users",
    "application/json", bytes.NewReader(buf))

Testing

Test Functions

// File: math_test.go (same package or _test suffix package)
package math_test

import (
    "testing"
    "github.com/yourname/myproject/math"
)

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

// t.Fatal — fails the test and stops execution of this test function
// t.Error — fails the test but continues execution
// t.Log   — log without failing (use -v to see)
// t.Skip  — skip the test

Table-Driven Tests

func TestDivide(t *testing.T) {
    tests := []struct {
        name    string
        a, b    float64
        want    float64
        wantErr bool
    }{
        {"positive", 10, 2, 5, false},
        {"negative dividend", -10, 2, -5, false},
        {"divide by zero", 10, 0, 0, true},
        {"float", 7, 2, 3.5, false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := math.Divide(tt.a, tt.b)
            if (err != nil) != tt.wantErr {
                t.Errorf("Divide() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !tt.wantErr && got != tt.want {
                t.Errorf("Divide() = %v, want %v", got, tt.want)
            }
        })
    }
}

// Run tests
// go test ./...             — all packages
// go test -v ./...          — verbose
// go test -run TestDivide   — specific test
// go test -run TestDiv/zero — specific subtest
// go test -count=1 ./...    — disable caching

Parallel Tests and Benchmarks

func TestParallelSafe(t *testing.T) {
    tests := []struct{ name, input, want string }{
        {"upper", "hello", "HELLO"},
        {"lower", "WORLD", "world"},
    }
    for _, tt := range tests {
        tt := tt // capture loop variable — REQUIRED before Go 1.22
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // run subtests in parallel
            // ...
        })
    }
}

// Benchmarks — measure performance
func BenchmarkFibonacci(b *testing.B) {
    // b.N automatically adjusted for stable measurement
    for i := 0; i < b.N; i++ {
        Fibonacci(20)
    }
}

// go test -bench=. -benchmem    — run benchmarks, show memory
// go test -bench=BenchmarkFib   — specific benchmark
// go test -benchtime=5s         — run for 5 seconds

Testify and Mocking

import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    "github.com/stretchr/testify/mock"
)

func TestWithTestify(t *testing.T) {
    // assert — continues on failure
    assert.Equal(t, 5, math.Add(2, 3))
    assert.NoError(t, err)
    assert.Contains(t, "seafood", "foo")
    assert.Len(t, slice, 3)
    assert.Nil(t, ptr)
    assert.True(t, condition)

    // require — stops test on failure (use for preconditions)
    require.NoError(t, err)
    require.NotNil(t, result)
}

// Mocking with interfaces — no magic needed in Go
type EmailSender interface {
    Send(to, subject, body string) error
}

// Mock implementation
type MockEmailSender struct {
    mock.Mock
}

func (m *MockEmailSender) Send(to, subject, body string) error {
    args := m.Called(to, subject, body)
    return args.Error(0)
}

func TestUserCreation(t *testing.T) {
    mockSender := &MockEmailSender{}
    mockSender.On("Send", "[email protected]", mock.Anything, mock.Anything).
        Return(nil)

    svc := NewUserService(mockSender)
    err := svc.CreateUser("Alice", "[email protected]")
    assert.NoError(t, err)
    mockSender.AssertExpectations(t)
}

Fuzzing

// Fuzz testing — Go 1.18+
// go test -fuzz=FuzzReverse -fuzztime=30s
func FuzzReverse(f *testing.F) {
    // Seed corpus
    f.Add("hello")
    f.Add("world")
    f.Add("")
    f.Add("!@#$")

    f.Fuzz(func(t *testing.T, s string) {
        rev := Reverse(s)
        // Invariant: reversing twice gives original
        if Reverse(rev) != s {
            t.Errorf("double reverse of %q = %q", s, Reverse(rev))
        }
        // Invariant: same length
        if len(rev) != len(s) {
            t.Errorf("len mismatch: %d vs %d", len(rev), len(s))
        }
    })
}

Context

import "context"

// context.Background — top-level, never cancelled
ctx := context.Background()

// context.TODO — placeholder when unsure which context to use
ctx2 := context.TODO()

// WithCancel — manual cancellation
ctx3, cancel := context.WithCancel(ctx)
defer cancel() // always defer cancel to avoid goroutine leak

go func() {
    // ... do work
    cancel() // signal cancellation
}()

// WithTimeout — auto-cancel after duration
ctx4, cancel4 := context.WithTimeout(ctx, 5*time.Second)
defer cancel4()

// WithDeadline — auto-cancel at absolute time
deadline := time.Now().Add(5 * time.Second)
ctx5, cancel5 := context.WithDeadline(ctx, deadline)
defer cancel5()

// WithValue — pass request-scoped values (use sparingly)
type contextKey string
const userIDKey contextKey = "userID"

ctx6 := context.WithValue(ctx, userIDKey, 42)
userID := ctx6.Value(userIDKey).(int) // type assertion needed

// Check cancellation
select {
case <-ctx.Done():
    return ctx.Err() // context.Canceled or context.DeadlineExceeded
default:
    // continue
}

// HTTP handler example
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    result, err := database.Query(ctx, "SELECT ...")
    if err != nil {
        if errors.Is(err, context.Canceled) {
            // client disconnected — normal, don't log as error
            return
        }
        http.Error(w, "internal error", 500)
        return
    }
    json.NewEncoder(w).Encode(result)
}
Context Best Practices
  • Always pass ctx as the first parameter of functions that do I/O.
  • Never store a context in a struct — pass it explicitly each call.
  • Use context.WithValue only for request-scoped metadata (e.g., request ID, user ID), not for optional parameters.
  • Always call the cancel function returned by WithCancel/WithTimeout — defer it immediately after creation.

Common Patterns

Functional Options

// Problem: configuring a struct with many optional parameters
// Solution: functional options — clean API, backwards-compatible

type Server struct {
    host    string
    port    int
    timeout time.Duration
    maxConn int
}

type Option func(*Server)

func WithHost(host string) Option {
    return func(s *Server) { s.host = host }
}

func WithPort(port int) Option {
    return func(s *Server) { s.port = port }
}

func WithTimeout(d time.Duration) Option {
    return func(s *Server) { s.timeout = d }
}

func WithMaxConnections(n int) Option {
    return func(s *Server) { s.maxConn = n }
}

func NewServer(opts ...Option) *Server {
    s := &Server{ // sensible defaults
        host:    "localhost",
        port:    8080,
        timeout: 30 * time.Second,
        maxConn: 100,
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// Usage
s := NewServer(
    WithPort(9090),
    WithTimeout(10 * time.Second),
)

Middleware Pattern

// HTTP middleware wraps a handler
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 next 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) {
        token := r.Header.Get("Authorization")
        if !isValid(token) {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

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

// Usage
mux := http.NewServeMux()
mux.HandleFunc("/api/data", dataHandler)
handler := Chain(mux, Logging, Auth)
http.ListenAndServe(":8080", handler)

Graceful Shutdown

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

    // Start server in goroutine
    go func() {
        if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
            log.Fatalf("HTTP server error: %v", err)
        }
        log.Println("server stopped accepting connections")
    }()

    // Wait for interrupt signal
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
    <-sigCh

    log.Println("shutting down...")

    // Give active requests 30 seconds to complete
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        log.Printf("shutdown error: %v", err)
    }
    log.Println("server exited cleanly")
}

Dependency Injection via Interfaces

// Define interfaces for dependencies
type UserRepository interface {
    FindByID(ctx context.Context, id int64) (*User, error)
    Save(ctx context.Context, user *User) error
}

type EmailService interface {
    SendWelcome(ctx context.Context, email, name string) error
}

// Service depends on interfaces, not concrete types
type UserService struct {
    repo  UserRepository
    email EmailService
    log   *slog.Logger
}

func NewUserService(repo UserRepository, email EmailService, log *slog.Logger) *UserService {
    return &UserService{repo: repo, email: email, log: log}
}

func (s *UserService) Register(ctx context.Context, name, email string) error {
    user := &User{Name: name, Email: email}
    if err := s.repo.Save(ctx, user); err != nil {
        return fmt.Errorf("save user: %w", err)
    }
    if err := s.email.SendWelcome(ctx, email, name); err != nil {
        s.log.Warn("welcome email failed", "email", email, "err", err)
        // non-fatal — don't return error
    }
    return nil
}

// Wire up in main
func main() {
    db := mustConnectDB()
    repo := postgres.NewUserRepo(db)
    emailSvc := sendgrid.NewEmailService(os.Getenv("SENDGRID_KEY"))
    logger := slog.Default()
    userSvc := NewUserService(repo, emailSvc, logger)
    _ = userSvc
}

Build & Tooling

Cross-Compilation

# Cross-compile
GOOS=linux   GOARCH=amd64  go build -o bin/app-linux-amd64  ./cmd/app/
GOOS=linux   GOARCH=arm64  go build -o bin/app-linux-arm64  ./cmd/app/
GOOS=darwin  GOARCH=amd64  go build -o bin/app-darwin-amd64 ./cmd/app/
GOOS=darwin  GOARCH=arm64  go build -o bin/app-darwin-arm64 ./cmd/app/
GOOS=windows GOARCH=amd64  go build -o bin/app.exe          ./cmd/app/

# Common GOOS values: linux, darwin, windows, freebsd
# Common GOARCH values: amd64, arm64, 386, arm

# Inject version at build time with ldflags
go build -ldflags="-X main.Version=1.2.3 -X main.BuildTime=$(date -u +%Y%m%dT%H%M%S)" \
    -o bin/app ./cmd/app/

# Strip debug info (smaller binary)
go build -ldflags="-s -w" -o bin/app ./cmd/app/
// Read version set by ldflags
var (
    Version   = "dev"   // overridden at build time
    BuildTime = "unknown"
)

func main() {
    if len(os.Args) > 1 && os.Args[1] == "version" {
        fmt.Printf("version %s built at %s\n", Version, BuildTime)
        os.Exit(0)
    }
    // ...
}

Build Tags

// Go 1.17+ syntax (//go:build)
// Must be before package declaration, separated by blank line

//go:build linux || darwin
// +build linux darwin     // old syntax (keep for compatibility pre-1.17)

package main

// Platform-specific file — only compiled on linux
// file: network_linux.go

//go:build !windows

package net

// Feature flags
//go:build integration

package integration_test

// Exclude from builds
//go:build ignore

package tools // won't be compiled normally

go generate

// go generate runs commands specified in source files
// Run with: go generate ./...

//go:generate stringer -type=Direction
type Direction int

const (
    North Direction = iota
    East
    South
    West
)
// stringer generates a String() method for the type

//go:generate mockgen -source=interfaces.go -destination=mocks/mock_interfaces.go

//go:generate go run github.com/sqlc-dev/sqlc/cmd/sqlc generate

Linting and Formatting

# Format all code (always run before commit)
go fmt ./...

# Static analysis — catches common mistakes
go vet ./...

# golangci-lint — aggregates many linters
# Install: brew install golangci-lint
golangci-lint run ./...
golangci-lint run --fix ./...  # auto-fix where possible

# .golangci.yml — configuration
# linters:
#   enable:
#     - errcheck     # check that errors are handled
#     - gosimple     # simplification suggestions
#     - govet        # go vet checks
#     - ineffassign  # detect ineffectual assignments
#     - staticcheck  # advanced static analysis
#     - unused       # check for unused code

# Run specific linter
golangci-lint run --disable-all --enable errcheck ./...

# Coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
go test -cover ./...                          # quick summary

Module Management

# Initialize
go mod init github.com/you/project

# Add dependency (latest)
go get github.com/some/pkg

# Add specific version
go get github.com/some/[email protected]

# Add at commit
go get github.com/some/pkg@abc1234

# Upgrade to latest minor/patch for all direct deps
go get -u ./...

# Upgrade specific package
go get -u github.com/some/pkg

# Remove a dependency
go get github.com/some/pkg@none  # then go mod tidy

# Clean up: remove unused, add missing
go mod tidy

# Vendor all dependencies
go mod vendor
go build -mod=vendor ./...  # build using vendor dir

# Verify checksums
go mod verify

# Why is this dep needed?
go mod why github.com/some/pkg

# Replace a module (useful for local development)
# In go.mod:
# replace github.com/you/mylib => ../mylib

# Workspace mode (Go 1.18+): develop multiple modules together
go work init
go work use ./moduleA ./moduleB
# Creates go.work — do not commit to VCS

Structured Logging (slog)

import "log/slog" // Go 1.21+

// Default logger (text format to stderr)
slog.Info("server started", "port", 8080)
slog.Warn("high memory", "used_mb", 512)
slog.Error("request failed", "err", err, "url", r.URL.Path)

// JSON handler for production
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
    AddSource: true, // includes file:line in output
}))
slog.SetDefault(logger)

// With common attributes (fields added to every log call)
requestLogger := logger.With("request_id", "abc-123", "user_id", 42)
requestLogger.Info("processing request")

// Grouped attributes
slog.Info("user action",
    slog.Group("user",
        "id", 42,
        "name", "Alice",
    ),
    "action", "login",
)
Quick Reference: go commands
CommandPurpose
go run ./cmd/appCompile and run without saving binary
go build ./...Build all packages (checks for errors)
go test ./...Run all tests
go test -race ./...Run tests with race detector
go vet ./...Static analysis
go fmt ./...Format source code
go doc pkg.SymbolShow documentation
go envShow Go environment variables
go list -m allList all modules (transitive)
go clean -cacheClear build cache