Skip to Content

Go Cheat Sheet

Go toolchain, language patterns, and standard library. Targets Go 1.22+ and C# developers moving to Go.

Versions: Go 1.22+ · azure-sdk-for-go v1.7+ · golang.org/x/sync v0.7+ · Go 1.21+ for slices/maps/slog


Toolchain & Modules

Installation

Bash
# Linux - fetch latest from go.dev
GO_VERSION=$(curl -s https://go.dev/VERSION?m=text | head -1)
wget "https://go.dev/dl/${GO_VERSION}.linux-amd64.tar.gz"
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf "${GO_VERSION}.linux-amd64.tar.gz"
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc && source ~/.bashrc
 
# macOS
brew install go
 
# Windows
winget install GoLang.Go
 
go version
go env GOPATH GOROOT

goenv - Go Version Manager

Bash
# Install goenv (Linux/macOS)
git clone https://github.com/syndbg/goenv.git ~/.goenv
 
# Add to ~/.bashrc / ~/.zshrc
export GOENV_ROOT="$HOME/.goenv"
export PATH="$GOENV_ROOT/bin:$PATH"
eval "$(goenv init -)"
export PATH="$GOROOT/bin:$PATH"
export PATH="$GOPATH/bin:$PATH"
 
source ~/.bashrc
 
# List available Go versions
goenv install --list
 
# Install and activate a version
goenv install 1.24.0
goenv global  1.24.0    # default for the machine
goenv local   1.24.0    # writes .go-version in the current directory
goenv version           # show active version
 
# List installed versions
goenv versions
 
# Install goenv on Windows (g - simpler alternative)
# PowerShell (run as Admin):
winget install GoLang.Go   # baseline install
# then use 'g' for switching: https://github.com/stefanmaric/g

Modules

Bash
# Initialise a new module
go mod init github.com/org/myapp
 
# Add / tidy dependencies
go get github.com/some/package@latest
go mod tidy       # prune unused, add missing
go mod download   # pre-fetch to local cache
 
# Upgrade all direct dependencies
go get -u ./...
 
# Vendor (reproducible offline builds)
go mod vendor
go build -mod=vendor ./...
 
# Check for vulnerabilities
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

go.mod - key directives

Go
module github.com/org/myapp
 
go 1.22
 
require (
    github.com/Azure/azure-sdk-for-go/sdk/azidentity                          v1.7.0
    github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
    golang.org/x/sync                                                          v0.7.0
)
 
// Replace a module with a local fork
replace github.com/some/pkg => ../local-fork

Build, run, test

Bash
# Build
go build ./...                           # all packages
go build -o bin/app ./cmd/app            # named binary
go build -ldflags="-s -w" -o app ./...  # stripped (smaller binary)
 
# Cross-compile
GOOS=linux   GOARCH=amd64 go build -o app-linux   ./cmd/app
GOOS=darwin  GOARCH=arm64 go build -o app-mac     ./cmd/app
GOOS=windows GOARCH=amd64 go build -o app.exe     ./cmd/app
 
# Run
go run ./cmd/app -- --flag value
 
# Test
go test ./...
go test -v -run TestMyFunc ./pkg/...
go test -race ./...                      # data race detector
go test -cover ./...
go test -coverprofile=cov.out ./... && go tool cover -html=cov.out
 
# Benchmarks
go test -bench=. -benchmem ./...
 
# Static analysis
go vet ./...
go install honnef.co/go/tools/cmd/staticcheck@latest && staticcheck ./...

Variables, Types & Constants

Go
// Variable declarations
var name string = "Alice"
var count int                    // zero value: 0
x := 42                          // short declaration (functions only)
a, b := 1, "hello"
 
// Constants
const Pi = 3.14159
const (
    StatusOK       = 200
    StatusNotFound = 404
)
 
// iota - auto-incrementing constant
type Direction int
const (
    North Direction = iota  // 0
    East                    // 1
    South                   // 2
    West                    // 3
)
 
// Basic types
var (
    b   bool    = true
    i   int     = -42          // platform-sized (32 or 64-bit)
    i64 int64   = 9223372036854775807
    f64 float64 = 3.14
    s   string  = "hello"
    r   rune    = 'A'          // alias for int32 (Unicode code point)
    by  byte    = 0xFF         // alias for uint8
)
 
// Type conversion (always explicit - no implicit casting)
f := float64(i64)
u := uint(f)
 
// Pointers
p := &i    // address of i
*p = 99    // dereference to set
fmt.Println(*p, i)  // 99 99
 
// new vs make
ptr := new(int)             // allocates zeroed int, returns *int
sl  := make([]int, 0, 10)  // slice: len=0, cap=10
m   := make(map[string]int) // initialised map (nil map panics on write)

Pointers

Go
// & takes the address of a value; * dereferences it
x := 42
p := &x      // p is *int - holds the memory address of x
*p = 99      // write through the pointer
fmt.Println(x)  // 99
 
// Pointer to a struct - fields accessed with . (auto-deref)
type Point struct{ X, Y int }
pt := &Point{X: 1, Y: 2}
pt.X = 10    // equivalent to (*pt).X = 10
 
// new allocates a zeroed value and returns a pointer
p2 := new(int)   // *int pointing to 0
*p2 = 7
 
// nil pointer - always check before dereferencing
var p3 *int
if p3 != nil {
    fmt.Println(*p3)
}
 
// Pointer receivers - mutate the receiver in place
type Counter struct{ n int }
 
func (c *Counter) Inc()   { c.n++ }
func (c Counter) Value() int { return c.n }  // value receiver - read-only copy
 
c := Counter{}
c.Inc()
c.Inc()
fmt.Println(c.Value())  // 2
 
// Use a pointer receiver when:
//   1. the method mutates the receiver
//   2. the struct is large (avoids copying)
//   3. consistency - if any method has a pointer receiver, prefer all do
 
// Passing pointers to functions - avoids copying, enables mutation
func double(n *int) { *n *= 2 }
 
v := 5
double(&v)
fmt.Println(v)  // 10
 
// Returning a pointer - safe; Go heap-allocates when the address escapes
func newUser(name string) *User {
    return &User{Name: name}   // NOT dangling - compiler handles escape analysis
}
 
// Slices and maps are already reference types - do NOT pass *[]T or *map[K]V
// unless you need to replace the slice/map itself (e.g. append that grows it)
nums := []int{1, 2, 3}
addFour(&nums)           // only needed if addFour does nums = append(nums, 4)
 
func addFour(s *[]int) { *s = append(*s, 4) }
 
// Pointer comparison - two pointers are equal only if they point to the same address
a, b := 1, 1
pa, pb := &a, &b
fmt.Println(pa == pb)   // false (different variables)
fmt.Println(pa == &a)   // true
 
// unsafe.Pointer - only for interop with C or low-level tricks; avoid in normal code

Functions

Go
// Multiple return values
func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("divide by zero")
    }
    return a / b, nil
}
 
result, err := Divide(10, 3)
if err != nil { log.Fatal(err) }
 
// Named return values
func MinMax(nums []int) (min, max int) {
    min, max = nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min { min = n }
        if n > max { max = n }
    }
    return  // naked return
}
 
// Variadic
func Sum(nums ...int) int {
    total := 0
    for _, n := range nums { total += n }
    return total
}
Sum(1, 2, 3)
Sum(nums...)  // spread a slice
 
// First-class functions & closures
func Multiplier(factor int) func(int) int {
    return func(n int) int { return n * factor }
}
double := Multiplier(2)
fmt.Println(double(5))  // 10
 
// defer - runs LIFO when function returns (including panic)
func ReadFile(path string) error {
    f, err := os.Open(path)
    if err != nil { return err }
    defer f.Close()  // guaranteed cleanup
    // ... read
    return nil
}

Structs & Methods

Go
// Struct definition
type User struct {
    ID        string
    Name      string
    Email     string
    CreatedAt time.Time
    role      string    // unexported - package-private
}
 
// Constructor (New* convention)
func NewUser(name, email string) *User {
    return &User{
        ID:        uuid.New().String(),
        Name:      name,
        Email:     email,
        CreatedAt: time.Now().UTC(),
        role:      "member",
    }
}
 
// Value receiver - read-only, receives a copy
func (u User) String() string {
    return fmt.Sprintf("%s <%s>", u.Name, u.Email)
}
 
// Pointer receiver - can mutate, avoids copying large structs
func (u *User) Promote() { u.role = "admin" }
 
// Embedding - composition over inheritance
type Admin struct {
    User                  // all User fields and methods promoted
    Permissions []string
}
 
admin := Admin{
    User:        *NewUser("Alice", "alice@example.com"),
    Permissions: []string{"read", "write", "delete"},
}
fmt.Println(admin.Name)  // promoted from User
admin.Promote()          // promoted method
 
// Struct tags (JSON, DB, validation)
type APIResponse struct {
    ID      string    `json:"id"`
    Message string    `json:"message,omitempty"`
    Error   string    `json:"error,omitempty"`
    At      time.Time `json:"timestamp"`
}

Interfaces

Go
// Interface definition
type Repository[T any] interface {
    FindByID(ctx context.Context, id string) (T, error)
    Save(ctx context.Context, entity T) error
    Delete(ctx context.Context, id string) error
    List(ctx context.Context) ([]T, error)
}
 
// Compile-time satisfaction check - catches drift at build time
var _ Repository[User] = (*userRepository)(nil)
 
// Implementation
type userRepository struct{ db *sql.DB }
 
func (r *userRepository) FindByID(ctx context.Context, id string) (User, error) {
    var u User
    err := r.db.QueryRowContext(ctx,
        "SELECT id, name, email FROM users WHERE id = $1", id).
        Scan(&u.ID, &u.Name, &u.Email)
    return u, err
}
 
// Type assertion
var v any = "hello"
if s, ok := v.(string); ok {
    fmt.Println("string:", s)
}
 
// Type switch
switch t := v.(type) {
case int:    fmt.Println("int:", t)
case string: fmt.Println("string:", t)
case []byte: fmt.Println("bytes:", len(t))
default:     fmt.Printf("unknown: %T\n", t)
}

Error Handling

Go
// Wrapping - preserves the original error for errors.Is / errors.As
func GetUser(ctx context.Context, id string) (User, error) {
    u, err := db.FindByID(ctx, id)
    if err != nil {
        return User{}, fmt.Errorf("GetUser %s: %w", id, err)
    }
    return u, nil
}
 
// errors.Is - sentinel comparison through the chain
if errors.Is(err, sql.ErrNoRows) { /* not found */ }
 
// Custom error type
type NotFoundError struct {
    Resource string
    ID       string
}
func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s %q not found", e.Resource, e.ID)
}
 
// errors.As - unwrap to concrete type
var nfe *NotFoundError
if errors.As(err, &nfe) {
    fmt.Printf("missing: %s/%s\n", nfe.Resource, nfe.ID)
}
 
// Sentinel errors
var (
    ErrNotFound    = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
)
 
// Multi-error (Go 1.20+)
err1, err2 := errors.New("first"), errors.New("second")
combined := errors.Join(err1, err2)
fmt.Println(errors.Is(combined, err1))  // true

Collections

Go
// Slices
s := []int{1, 2, 3}
s = append(s, 4, 5)
s[1:3]   // [2 3]
s[:2]    // [1 2]
s[2:]    // [3 4 5]
 
dst := make([]int, len(s))
copy(dst, s)
 
// slices package (Go 1.21+)
import "slices"
slices.Sort(s)
slices.Contains(s, 3)
slices.Index(s, 3)
_, found := slices.BinarySearch(s, 3)
 
// Range over integer (Go 1.22+)
for i := range 5 {   // 0 1 2 3 4
    fmt.Println(i)
}
 
// Maps
m := map[string]int{"alice": 30, "bob": 25}
m["carol"] = 28
delete(m, "bob")
count, ok := m["alice"]  // ok is false if absent
 
for k, v := range m {
    fmt.Printf("%s: %d\n", k, v)
}
 
// maps package (Go 1.21+)
import "maps"
cloned := maps.Clone(m)
keys   := slices.Collect(maps.Keys(m))   // unordered []string

Generics

Go
// Generic function
func Map[T, U any](s []T, fn func(T) U) []U {
    out := make([]U, len(s))
    for i, v := range s { out[i] = fn(v) }
    return out
}
names := Map(users, func(u User) string { return u.Name })
 
// Type constraint with tilde (underlying type)
type Number interface {
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}
 
func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums { total += n }
    return total
}
 
// Generic Result type
type Result[T any] struct {
    Value T
    Err   error
}
 
func OK[T any](v T) Result[T]       { return Result[T]{Value: v} }
func Fail[T any](err error) Result[T] { return Result[T]{Err: err} }
 
// Generic thread-safe cache
type Cache[K comparable, V any] struct {
    mu    sync.RWMutex
    items map[K]V
}
 
func NewCache[K comparable, V any]() *Cache[K, V] {
    return &Cache[K, V]{items: make(map[K]V)}
}
 
func (c *Cache[K, V]) Set(key K, val V) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items[key] = val
}
 
func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.items[key]
    return v, ok
}

Goroutines & Channels

Go
// Goroutine
go func() {
    fmt.Println("running concurrently")
}()
 
// Channels
ch  := make(chan int)     // unbuffered - send blocks until received
buf := make(chan int, 10) // buffered  - send blocks only when full
 
go func() { ch <- 42 }()
v := <-ch
 
// Close signals completion; range drains until closed
close(ch)
for v := range ch { fmt.Println(v) }
 
// WaitGroup
var wg sync.WaitGroup
for _, url := range urls {
    wg.Add(1)
    go func(u string) {
        defer wg.Done()
        fetch(u)
    }(url)
}
wg.Wait()
 
// errgroup (golang.org/x/sync) - WaitGroup with error propagation
g, ctx := errgroup.WithContext(ctx)
for _, url := range urls {
    url := url
    g.Go(func() error { return fetch(ctx, url) })
}
if err := g.Wait(); err != nil { log.Fatal(err) }
 
// Select - multiplex channels
select {
case msg := <-ch1:
    fmt.Println("ch1:", msg)
case msg := <-ch2:
    fmt.Println("ch2:", msg)
case <-ctx.Done():
    return ctx.Err()
case <-time.After(5 * time.Second):
    return errors.New("timeout")
default:
    // non-blocking: no channel ready
}
 
// Worker pool
func WorkerPool[T, R any](ctx context.Context, jobs []T, workers int, fn func(T) R) []R {
    jobCh := make(chan T, len(jobs))
    resCh := make(chan R, len(jobs))
 
    var wg sync.WaitGroup
    for range workers {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobCh {
                resCh <- fn(job)
            }
        }()
    }
 
    for _, j := range jobs { jobCh <- j }
    close(jobCh)
    go func() { wg.Wait(); close(resCh) }()
 
    var results []R
    for r := range resCh { results = append(results, r) }
    return results
}

sync Package

Go
// Mutex - exclusive lock
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
 
// RWMutex - multiple concurrent readers, one writer
var rw sync.RWMutex
rw.RLock(); defer rw.RUnlock()   // read path
rw.Lock();  defer rw.Unlock()    // write path
 
// Once - run exactly once regardless of concurrent callers
var (
    once sync.Once
    db   *Database
)
func GetDB() *Database {
    once.Do(func() { db = connect() })
    return db
}
 
// atomic.Int64 - lock-free counter
var counter atomic.Int64
counter.Add(1)
fmt.Println(counter.Load())
 
// atomic.Value - arbitrary type, lock-free reads
var cfg atomic.Value
cfg.Store(Config{MaxRetries: 3})
current := cfg.Load().(Config)

Context

Go
// Always pass context as the first parameter
func ProcessRequest(ctx context.Context, id string) error {
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
    }
    return db.QueryContext(ctx, id)
}
 
// Timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()   // always cancel - releases timer resources
 
// Cancellation
ctx, cancel = context.WithCancel(context.Background())
go func() { time.Sleep(2 * time.Second); cancel() }()
 
// Values (request-scoped only - trace IDs, auth tokens, etc.)
type ctxKey string
const requestIDKey ctxKey = "requestID"
 
ctx = context.WithValue(ctx, requestIDKey, "req-abc123")
rid := ctx.Value(requestIDKey).(string)

HTTP Server

Go
// Method-based routing (Go 1.22+)
mux := http.NewServeMux()
 
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")     // extracts {id} from path
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"id": id})
})
 
mux.HandleFunc("POST /users",      createUserHandler)
mux.HandleFunc("DELETE /users/{id}", deleteUserHandler)
 
// Middleware
func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        slog.Info("request", "method", r.Method, "path", r.URL.Path, "dur", time.Since(start))
    })
}
 
func RequireAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !validateToken(r.Header.Get("Authorization")) {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}
 
// Chain middleware
handler := Logging(RequireAuth(mux))
 
srv := &http.Server{
    Addr:         ":8080",
    Handler:      handler,
    ReadTimeout:  10 * time.Second,
    WriteTimeout: 30 * time.Second,
    IdleTimeout:  120 * time.Second,
}
 
// Graceful shutdown
go func() {
    if err := srv.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatal(err)
    }
}()
 
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
 
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
srv.Shutdown(ctx)

JSON

Go
// Marshal (Go → JSON)
data, err  := json.Marshal(user)
pretty, err := json.MarshalIndent(user, "", "  ")
 
// Unmarshal (JSON → Go)
var u User
if err := json.Unmarshal(data, &u); err != nil {
    return fmt.Errorf("unmarshal: %w", err)
}
 
// Streaming encoder/decoder (preferred for HTTP handlers)
func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}
 
func readJSON(r *http.Request, dst any) error {
    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields()
    return dec.Decode(dst)
}
 
// Custom marshaling
type Duration struct{ time.Duration }
 
func (d Duration) MarshalJSON() ([]byte, error) {
    return json.Marshal(d.String())
}
 
func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil { return err }
    dur, err := time.ParseDuration(s)
    d.Duration = dur
    return err
}

Structured Logging (slog)

🛠️ Deeper reference - this section covers slog setup, structured fields, context propagation, ReplaceAttr redaction, LogValuer, and the logger decorator pattern. It’s a mini-guide, not a quick-reference snippet.

log/slog (Go 1.21+) is the standard structured logger. Use it instead of log.Println / fmt.Println for anything you’d ever read in a log aggregator. JSON in prod, text in dev, levels and context propagation built in.

Sensible defaults

Go
import (
    "log/slog"
    "os"
)
 
func setupLogger() *slog.Logger {
    level := slog.LevelInfo
    if os.Getenv("LOG_LEVEL") == "debug" {
        level = slog.LevelDebug
    }
 
    opts := &slog.HandlerOptions{
        Level:     level,
        AddSource: false,                       // set true in dev to include file:line
        ReplaceAttr: redactSensitive,           // see "Redaction" below
    }
 
    var handler slog.Handler
    if os.Getenv("ENV") == "development" {
        handler = slog.NewTextHandler(os.Stdout, opts)
    } else {
        handler = slog.NewJSONHandler(os.Stdout, opts)
    }
 
    logger := slog.New(handler).With(
        "service", "my-api",
        "version", buildVersion,
    )
    slog.SetDefault(logger)
    return logger
}

Log lines - always structured fields, never fmt.Sprintf

Go
// CORRECT - each field is queryable in your log backend
slog.Info("order processed",
    "order_id",    order.ID,
    "customer_id", order.CustomerID,
    "elapsed_ms",  elapsed.Milliseconds())
 
// WRONG - destroys the structure
slog.Info(fmt.Sprintf("order %s processed for %s", order.ID, order.CustomerID))

Prefer slog.LogAttrs(ctx, level, msg, attrs...) on hot paths - it skips reflection and is allocation-free for typed attrs:

Go
slog.LogAttrs(ctx, slog.LevelInfo, "order processed",
    slog.String("order_id",    order.ID),
    slog.String("customer_id", order.CustomerID),
    slog.Int64("elapsed_ms",   elapsed.Milliseconds()),
)

Context propagation - one correlation ID across every log line

slog’s methods take a context.Context. Stash the request-scoped logger in the context once at the HTTP boundary; every nested call pulls it back out.

Go
type ctxKey struct{}
 
func WithLogger(ctx context.Context, l *slog.Logger) context.Context {
    return context.WithValue(ctx, ctxKey{}, l)
}
 
func L(ctx context.Context) *slog.Logger {
    if l, ok := ctx.Value(ctxKey{}).(*slog.Logger); ok {
        return l
    }
    return slog.Default()
}
 
// HTTP middleware - bind correlation_id + user_id for every request
func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        cid := r.Header.Get("X-Correlation-ID")
        if cid == "" { cid = uuid.NewString() }
 
        logger := slog.Default().With("correlation_id", cid, "method", r.Method, "path", r.URL.Path)
        ctx    := WithLogger(r.Context(), logger)
 
        start := time.Now()
        next.ServeHTTP(w, r.WithContext(ctx))
        logger.InfoContext(ctx, "request handled", "elapsed_ms", time.Since(start).Milliseconds())
    })
}
 
// Anywhere downstream:
func handleOrder(ctx context.Context, id string) {
    L(ctx).Info("handling order", "order_id", id)
}

Redaction via ReplaceAttr

HandlerOptions.ReplaceAttr runs on every attribute before it’s emitted - the right place to mask tokens, passwords, or any field name you don’t want in logs.

Go
var sensitiveKeys = map[string]bool{
    "password": true, "token": true, "authorization": true,
    "connection_string": true, "api_key": true,
}
 
func redactSensitive(groups []string, a slog.Attr) slog.Attr {
    if sensitiveKeys[strings.ToLower(a.Key)] {
        return slog.String(a.Key, "[REDACTED]")
    }
    return a
}

LogValuer - control how a type renders itself

Implement slog.LogValuer so a struct logs its safe fields automatically, without callers having to remember to redact.

Go
type User struct {
    ID       string
    Email    string
    Password string  // never log this
}
 
func (u User) LogValue() slog.Value {
    return slog.GroupValue(
        slog.String("id",    u.ID),
        slog.String("email", u.Email),
        // Password is deliberately omitted
    )
}
 
slog.Info("user created", "user", user)   // password never appears

Logger decorator - timing + error context around a service

The decorator pattern works here as in C#/TypeScript: same interface, wrap inner, log around each call.

Go
type OrderService interface {
    Place(ctx context.Context, req PlaceOrderRequest) (Order, error)
}
 
type loggingOrderService struct {
    inner  OrderService
    logger *slog.Logger
}
 
func NewLoggingOrderService(inner OrderService, logger *slog.Logger) OrderService {
    return &loggingOrderService{inner: inner, logger: logger}
}
 
func (s *loggingOrderService) Place(ctx context.Context, req PlaceOrderRequest) (Order, error) {
    log := s.logger.With("method", "Place", "customer_id", req.CustomerID)
    start := time.Now()
    log.InfoContext(ctx, "starting")
 
    order, err := s.inner.Place(ctx, req)
    if err != nil {
        log.ErrorContext(ctx, "failed", "err", err, "elapsed_ms", time.Since(start).Milliseconds())
        return order, err
    }
    log.InfoContext(ctx, "ok", "order_id", order.ID, "elapsed_ms", time.Since(start).Milliseconds())
    return order, nil
}

Log-level guidance

LevelUse for
DebugLocal diagnostics, payload dumps - off in prod
InfoBusiness events: request handled, job complete
WarnRecoverable issue, retry succeeded, fallback used
ErrorAn operation failed; caller will see a failure

Custom levels (slog.Level(12) for “Notice”, slog.Level(-2) for “Trace”) are supported but rarely worth it - the four above cover every case I’ve seen in practice.

Sensible defaults checklist

  • One setupLogger() call in main; everything else uses slog.Default() or L(ctx).
  • JSON in prod, text in dev - flip on ENV.
  • LOG_LEVEL env var to change verbosity without code changes.
  • ReplaceAttr to redact known-sensitive keys globally.
  • Bind context (correlation_id, user_id) in middleware, not at each call site.
  • Pass error values as the "err" attr - slog calls .Error() on them automatically.
  • Use LogValuer for any type with sensitive fields.

Common pitfalls

  • Don’t use log.Println / fmt.Println in production code - unstructured, no levels, no redaction.
  • Don’t pass context.Context as a logger attr - use InfoContext/ErrorContext to read attributes from it, don’t serialise the whole context.
  • Don’t build the message with fmt.Sprintf - it defeats the point of structured logging and runs even when the level is disabled.
  • Don’t create a new logger per request without With(...) - it allocates on every call. Build a child via logger.With(...) and stash it in context.
  • slog.Default() is set once - if you call SetDefault mid-program, code holding a previous reference still uses the old one. Configure at startup only.

Testing

Go
// Table-driven tests (idiomatic Go)
func TestDivide(t *testing.T) {
    tests := []struct {
        name    string
        a, b    float64
        want    float64
        wantErr bool
    }{
        {"normal",      10, 2, 5.0, false},
        {"float result", 7, 2, 3.5, false},
        {"div by zero",  5, 0, 0,   true},
    }
 
    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            got, err := Divide(tc.a, tc.b)
            if (err != nil) != tc.wantErr {
                t.Fatalf("err = %v; wantErr %v", err, tc.wantErr)
            }
            if !tc.wantErr && got != tc.want {
                t.Errorf("got %v; want %v", got, tc.want)
            }
        })
    }
}
 
// Parallel subtests
func TestFetch(t *testing.T) {
    t.Parallel()
    t.Run("success", func(t *testing.T) {
        t.Parallel()
        // ...
    })
}
 
// Benchmark
func BenchmarkSum(b *testing.B) {
    data := make([]int, 10_000)
    b.ResetTimer()
    for range b.N {
        Sum(data...)
    }
}
 
// Fuzz testing (Go 1.18+)
func FuzzParse(f *testing.F) {
    f.Add("valid-input")
    f.Fuzz(func(t *testing.T, input string) {
        _, _ = Parse(input)  // must not panic
    })
}
Bash
go test ./...
go test -v -run TestDivide ./...
go test -race ./...                         # race detector
go test -fuzz=FuzzParse -fuzztime=30s       # fuzz for 30s
go test -bench=. -benchmem ./...
go test -coverprofile=cov.out ./... && go tool cover -html=cov.out

Azure SDK for Go

Bash
go get github.com/Azure/azure-sdk-for-go/sdk/azidentity
go get github.com/Azure/azure-sdk-for-go/sdk/azcore
go get github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources
go get github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
go get github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets

Authentication

Go
import "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
 
// ✅ DefaultAzureCredential - env vars → workload identity → managed identity → CLI
// Best choice for code that must run in both CI and locally (as of 2026)
cred, err := azidentity.NewDefaultAzureCredential(nil)
 
// ✅ Managed identity (user-assigned) - preferred auth for Azure-hosted workloads (as of 2026)
cred, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{
    ID: azidentity.ClientID("YOUR-UAMI-CLIENT-ID"),
})
 
// ⚠️ Service principal (client secret) - legacy, avoid in new projects
cred, err = azidentity.NewClientSecretCredential(
    os.Getenv("AZURE_TENANT_ID"),
    os.Getenv("AZURE_CLIENT_ID"),
    os.Getenv("AZURE_CLIENT_SECRET"),
    nil,
)
 
// ✅ Workload identity (GitHub Actions / AKS pod) - current preferred CI/CD auth pattern (as of 2026)
cred, err = azidentity.NewWorkloadIdentityCredential(nil)

Resource groups & Blob Storage

Go
import (
    "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
    "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
    "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
    "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
)
 
// List resource groups
rgClient, err := armresources.NewResourceGroupsClient(subscriptionID, cred, nil)
pager := rgClient.NewListPager(nil)
for pager.More() {
    page, err := pager.NextPage(ctx)
    if err != nil { return err }
    for _, rg := range page.Value {
        fmt.Printf("%s  %s\n", *rg.Name, *rg.Location)
    }
}
 
// Create resource group
_, err = rgClient.CreateOrUpdate(ctx, "rg-myapp-prod",
    armresources.ResourceGroup{Location: to.Ptr("uksouth")}, nil)
 
// Key Vault secrets
kvClient, err := azsecrets.NewClient("https://my-vault.vault.azure.net", cred, nil)
 
secret, err := kvClient.GetSecret(ctx, "my-secret", "", nil)
fmt.Println(*secret.Value)
 
_, err = kvClient.SetSecret(ctx, "my-secret",
    azsecrets.SetSecretParameters{Value: to.Ptr("new-value")}, nil)
 
// Blob storage
blobClient, err := azblob.NewClient(
    "https://myaccount.blob.core.windows.net", cred, nil)
 
// Upload
_, err = blobClient.UploadBuffer(ctx, "my-container", "path/file.json",
    []byte(`{"key":"value"}`), nil)
 
// Download
stream, err := blobClient.DownloadStream(ctx, "my-container", "path/file.json", nil)
data, err := io.ReadAll(stream.Body)
defer stream.Body.Close()
 
// List blobs
listPager := blobClient.NewListBlobsFlatPager("my-container", nil)
for listPager.More() {
    page, _ := listPager.NextPage(ctx)
    for _, item := range page.Segment.BlobItems {
        fmt.Println(*item.Name)
    }
}

Azure Functions Custom Handler

Go runs in Azure Functions as a custom handler - the Functions runtime forwards triggers to a plain HTTP server you write. No SDK required; just net/http and encoding/json.

Project structure

PLAINTEXT
myfunction/
├── host.json
├── local.settings.json
├── HttpTrigger/
│   └── function.json
├── QueueTrigger/
│   └── function.json
├── main.go
└── go.mod

host.json

JSON
{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.*, 5.0.0)"
  },
  "customHandler": {
    "description": {
      "defaultExecutablePath": "handler",
      "workingDirectory": "",
      "arguments": []
    },
    "enableForwardingHttpRequest": true
  }
}

enableForwardingHttpRequest: true passes the raw HTTP request directly to your server - use this for HTTP triggers. Set it to false for non-HTTP triggers (queue, timer, Service Bus) which use the JSON envelope protocol instead.

function.json - HTTP trigger

JSON
{
  "bindings": [
    {
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["get", "post"],
      "authLevel": "anonymous",
      "route": "users/{id}"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

function.json - Queue trigger

JSON
{
  "bindings": [
    {
      "type": "queueTrigger",
      "direction": "in",
      "name": "queueItem",
      "queueName": "my-queue",
      "connection": "AzureWebJobsStorage"
    }
  ]
}

main.go

Go
package main
 
import (
    "encoding/json"
    "fmt"
    "log/slog"
    "net/http"
    "os"
)
 
// ── HTTP trigger (enableForwardingHttpRequest: true) ─────────────────────────
// The runtime forwards the raw request; respond as you would any HTTP handler.
func httpTrigger(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")   // route parameter defined in function.json
 
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{
        "id":      id,
        "message": fmt.Sprintf("Hello, %s!", id),
    })
}
 
// ── Non-HTTP trigger envelope (enableForwardingHttpRequest: false) ────────────
// The runtime wraps the trigger payload in a JSON envelope; you must respond
// with an Outputs map so the runtime can bind output bindings.
 
type InvokeRequest struct {
    Data     map[string]json.RawMessage `json:"Data"`
    Metadata map[string]json.RawMessage `json:"Metadata"`
}
 
type InvokeResponse struct {
    Outputs     map[string]any `json:"Outputs"`
    Logs        []string       `json:"Logs"`
    ReturnValue any            `json:"ReturnValue,omitempty"`
}
 
func queueTrigger(w http.ResponseWriter, r *http.Request) {
    var req InvokeRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
 
    var message string
    if err := json.Unmarshal(req.Data["queueItem"], &message); err != nil {
        http.Error(w, "bad payload", http.StatusBadRequest)
        return
    }
 
    slog.Info("processing queue message", "message", message)
 
    resp := InvokeResponse{
        Outputs: map[string]any{},
        Logs:    []string{fmt.Sprintf("processed: %s", message)},
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}
 
func main() {
    port := os.Getenv("FUNCTIONS_CUSTOMHANDLER_PORT")
    if port == "" {
        port = "8080"
    }
 
    mux := http.NewServeMux()
 
    // Route name must match the function folder name
    mux.HandleFunc("/api/HttpTrigger", httpTrigger)
    mux.HandleFunc("/QueueTrigger",    queueTrigger)
 
    slog.Info("custom handler listening", "port", port)
    if err := http.ListenAndServe(":"+port, mux); err != nil {
        slog.Error("server error", "err", err)
        os.Exit(1)
    }
}

Build & deploy

Bash
# Build for Linux (Functions runtime is Linux-based)
GOOS=linux GOARCH=amd64 go build -o handler .
 
# Run locally with Azure Functions Core Tools
func start
 
# Deploy to Azure
func azure functionapp publish <your-function-app-name>
 
# Or deploy via zip
zip -r deploy.zip handler host.json HttpTrigger/ QueueTrigger/
az functionapp deployment source config-zip \
  --resource-group rg-myapp \
  --name <your-function-app-name> \
  --src deploy.zip

local.settings.json

JSON
{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "custom",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true"
  }
}

See also: Azure - Az CLI commands for provisioning the Azure resources this SDK interacts with (storage, Key Vault, resource groups).


Useful Patterns

Functional options

Go
type serverConfig struct {
    host    string
    port    int
    timeout time.Duration
    tls     bool
}
 
type Option func(*serverConfig)
 
func WithHost(h string) Option           { return func(c *serverConfig) { c.host = h } }
func WithPort(p int) Option              { return func(c *serverConfig) { c.port = p } }
func WithTimeout(d time.Duration) Option { return func(c *serverConfig) { c.timeout = d } }
func WithTLS() Option                    { return func(c *serverConfig) { c.tls = true } }
 
func NewServer(opts ...Option) *Server {
    cfg := &serverConfig{host: "localhost", port: 8080, timeout: 30 * time.Second}
    for _, o := range opts { o(cfg) }
    return &Server{cfg: cfg}
}
 
// Usage
srv := NewServer(
    WithPort(9090),
    WithTimeout(60*time.Second),
    WithTLS(),
)

Retry with exponential backoff

Go
func Retry(ctx context.Context, attempts int, fn func() error) error {
    var err error
    for i := range attempts {
        if err = fn(); err == nil { return nil }
        wait := time.Duration(1<<uint(i)) * 100 * time.Millisecond  // 100ms, 200ms, 400ms…
        select {
        case <-time.After(wait):
        case <-ctx.Done():
            return fmt.Errorf("retry cancelled: %w", ctx.Err())
        }
    }
    return fmt.Errorf("after %d attempts: %w", attempts, err)
}

Pipeline with context cancellation

Go
func generate(ctx context.Context, nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            select {
            case out <- n:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}
 
func square(ctx context.Context, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            select {
            case out <- n * n:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}
 
ctx := context.Background()
for v := range square(ctx, generate(ctx, 1, 2, 3, 4, 5)) {
    fmt.Println(v)  // 1 4 9 16 25
}

Anti-patterns

  • ⚠️ log.Println / fmt.Println in production code - unstructured output with no levels, no fields, and no redaction. Use log/slog with slog.Info(...) and structured attrs.
  • 🔬 fmt.Sprintf in slog messages - slog.Info(fmt.Sprintf("order %s processed", id)) destroys structure and runs even when the level is disabled. Pass fields as attrs: slog.Info("order processed", "order_id", id).
  • 🚨 Writing to a nil map - var m map[string]int; m["key"] = 1 panics at runtime. Always initialise with make(map[K]V) or a map literal before writing.
  • ⚠️ Ignoring errors with _ - result, _ := doSomething() silently discards failures that may corrupt downstream state. Handle or explicitly wrap every error.
  • ⚠️ Goroutine leak - launching go func() without a termination signal (context cancellation, channel close, or WaitGroup) leaks goroutines indefinitely. Always provide a way to stop.
  • 🔬 Copying a sync.Mutex - a sync.Mutex embedded in a struct must never be copied after first use; copying breaks the lock invariant. Pass structs containing mutexes by pointer.
  • 🔬 interface{} instead of any - interface{} is the pre-Go 1.18 spelling; any is the canonical alias. Use any in all new code.
Last updated on