192 Patterns from Effective Go Recipes (2024)

Updated

July 30, 2026

192 Patterns from Effective Go Recipes (2024)

This source emphasizes practical recipes across I/O, JSON streaming, HTTP middleware/timeouts, text normalization, function options, errors, concurrency, sockets, C interop, testing, and shipping.

What This Adds to Our Book

  • Recipe-level production patterns that are small, composable, and immediately reusable.
  • Better treatment of streaming and incremental processing.
  • Stronger shipping pipeline topics: build tags, static builds, version injection, Docker packaging.

Streaming-Oriented Mental Model

source -> decoder -> transform -> validator -> sink
          (stream)               (bounded memory)

Deep Integration Example: Streaming JSON Pipeline with Backpressure

package pipeline

import (
    "bufio"
    "context"
    "encoding/json"
    "errors"
    "io"
    "sync"
    "time"
)

type Event struct {
    ID   string `json:"id"`
    Type string `json:"type"`
}

func DecodeEvents(ctx context.Context, r io.Reader) (<-chan Event, <-chan error) {
    out := make(chan Event, 128)
    errCh := make(chan error, 1)

    go func() {
        defer close(out)
        defer close(errCh)

        dec := json.NewDecoder(bufio.NewReader(r))
        for {
            var e Event
            if err := dec.Decode(&e); err != nil {
                if errors.Is(err, io.EOF) {
                    return
                }
                errCh <- err
                return
            }

            select {
            case <-ctx.Done():
                errCh <- ctx.Err()
                return
            case out <- e:
            }
        }
    }()

    return out, errCh
}

func ProcessEvents(ctx context.Context, in <-chan Event, workers int, fn func(Event) error) error {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    errCh := make(chan error, workers)
    var wg sync.WaitGroup

    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for e := range in {
                opCtx, stop := context.WithTimeout(ctx, 250*time.Millisecond)
                err := fn(e)
                stop()
                if opCtx.Err() != nil {
                    errCh <- opCtx.Err()
                    cancel()
                    return
                }
                if err != nil {
                    errCh <- err
                    cancel()
                    return
                }
            }
        }()
    }

    wg.Wait()
    close(errCh)
    for err := range errCh {
        if err != nil {
            return err
        }
    }
    return nil
}

Why This Matters

Modern Go systems increasingly process streams instead of loading full datasets in memory. Recipe-style patterns are especially effective for this style because they focus on one robust tactic at a time.

More examples

Functional options recipe

mkdir -p /tmp/go-egr-opts && cd /tmp/go-egr-opts
go mod init example.com/egr-opts

Save as main.go:

package main

import (
    "fmt"
    "time"
)

type Client struct {
    timeout time.Duration
    retries int
}

type Option func(*Client)

func WithTimeout(d time.Duration) Option {
    return func(c *Client) { c.timeout = d }
}
func WithRetries(n int) Option {
    return func(c *Client) { c.retries = n }
}

func NewClient(opts ...Option) *Client {
    c := &Client{timeout: time.Second, retries: 1}
    for _, o := range opts {
        o(c)
    }
    return c
}

func main() {
    c := NewClient(WithTimeout(200*time.Millisecond), WithRetries(3))
    fmt.Printf("timeout=%s retries=%d\n", c.timeout, c.retries)
}
go run .

Expected output:

timeout=200ms retries=3

Table-driven validation recipe

mkdir -p /tmp/go-egr-table && cd /tmp/go-egr-table
go mod init example.com/egr-table

Save as main.go:

package main

import (
    "fmt"
    "strings"
)

func validEmail(s string) bool {
    return strings.Count(s, "@") == 1 && !strings.HasPrefix(s, "@") && !strings.HasSuffix(s, "@")
}

func main() {
    cases := []struct {
        in   string
        want bool
    }{
        {"a@b.co", true},
        {"@x", false},
        {"nope", false},
    }
    for _, tc := range cases {
        got := validEmail(tc.in)
        fmt.Printf("%q got=%v want=%v ok=%v\n", tc.in, got, tc.want, got == tc.want)
    }
}
go run .

Expected output:

"a@b.co" got=true want=true ok=true
"@x" got=false want=false ok=true
"nope" got=false want=false ok=true

Runnable example

Recipe-style patterns: functional options for config, streaming json.Decoder over a reader, and a context-aware worker fan-in.

mkdir -p /tmp/go-recipes && cd /tmp/go-recipes
go mod init example.com/recipes

Save as main.go:

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "strings"
    "sync"
    "time"
)

type Config struct {
    Workers int
    Timeout time.Duration
}

type Option func(*Config)

func WithWorkers(n int) Option {
    return func(c *Config) { c.Workers = n }
}
func WithTimeout(d time.Duration) Option {
    return func(c *Config) { c.Timeout = d }
}

func NewConfig(opts ...Option) Config {
    c := Config{Workers: 2, Timeout: time.Second}
    for _, o := range opts {
        o(&c)
    }
    return c
}

func main() {
    cfg := NewConfig(WithWorkers(3), WithTimeout(200*time.Millisecond))
    fmt.Printf("config: workers=%d timeout=%s\n", cfg.Workers, cfg.Timeout)

    // Streaming JSON values (NDJSON)
    r := strings.NewReader("{\"n\":1}\n{\"n\":2}\n{\"n\":3}\n")
    dec := json.NewDecoder(r)
    var sum int
    for dec.More() {
        var row struct {
            N int `json:"n"`
        }
        if err := dec.Decode(&row); err != nil {
            panic(err)
        }
        sum += row.N
    }
    fmt.Println("stream_sum:", sum)

    // Bounded workers
    ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
    defer cancel()
    jobs := make(chan int, 8)
    var wg sync.WaitGroup
    var mu sync.Mutex
    var out int
    for i := 0; i < cfg.Workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := range jobs {
                select {
                case <-ctx.Done():
                    return
                default:
                    mu.Lock()
                    out += j
                    mu.Unlock()
                }
            }
        }()
    }
    for i := 1; i <= 5; i++ {
        jobs <- i
    }
    close(jobs)
    wg.Wait()
    fmt.Println("worker_sum:", out)
}
go run .

Expected output:

config: workers=3 timeout=200ms
stream_sum: 6
worker_sum: 15

What to notice

  • Options keep defaults + overrides readable without telescoping constructors.
  • json.Decoder streams; it does not require ReadAll of a huge payload.
  • Workers always honor ctx.Done() for operational cancellation.

Try next

  • Add WithName option and validate Workers > 0 in NewConfig.
  • Decode a large file with os.Open + Decoder in a loop.