Go Katas

repository·master·Indexed 25 days ago

https://github.com/medunes/go-kata

A collection of standalone coding challenges designed to help developers master idiomatic Go patterns. The repository focuses on concurrency control, memory efficiency, and error semantics through exercises such as building a fail-fast data aggregator, a graceful shutdown server, a context-aware error propagator, a rate-limited fan-out client, and a cache stampede shield.

Tokens
11.1K
Snippets
7
Records
43
Agent score
81%

What's inside go-kata

  1. Browse the Go Katas index

    master

    The Katas are organized into six thematic groups. You can choose a challenge based on the specific Go area you wish to master:

    • 01) Context, Cancellation, and Fail-Fast Concurrency: Focuses on preventing leaks, enforcing backpressure, and handling cancellation (e.g., 01 - The Fail-Fast Data Aggregator, 10 - Worker Pool with Backpressure).
    • 02) Performance, Allocation, and Throughput: Focuses on memory efficiency and high-throughput (e.g., 02 - Concurrent Map with Sharded Locks, 04 - Zero-Allocation JSON Parser).
    • 03) HTTP and Middleware Engineering: Focuses on idiomatic HTTP patterns and middleware (e.g., 06 - Interface-Based Middleware Chain).
    • 04) Errors: Semantics, Wrapping, and Edge Cases: Focuses on modern error handling and pitfalls (e.g., 08 - Retry Policy That Respects Context, 20 - The “nil != nil” Interface Trap).
    • 05) Filesystems, Packaging, and Deployment Ergonomics: Focuses on portable binaries and testable filesystem code (e.g., 13 - Filesystem-Agnostic Config Loader (io/fs)).
    • 06) Testing and Quality Gates: Focuses on idiomatic testing patterns like table-driven tests and fuzzing (e.g., 15 - Go Test Harness).
  2. Understand the "nil != nil" Interface Trap

    master

    In Go, an interface value is only nil when both its dynamic type and its value are nil.

    A common pitfall occurs when a function returns a typed nil pointer (e.g., (*MyError)(nil)) as an error interface. Because the interface now contains a type, the check err != nil evaluates to true, even though the underlying pointer is nil. This leads to misleading error paths, incorrect logging, or panics when attempting to access fields on the nil pointer.

  3. Test filesystem logic using fstest.MapFS

    master

    When building filesystem-agnostic tools in Go, you should avoid using real files on disk for unit testing. Instead, use testing/fstest.MapFS to create an in-memory filesystem. This ensures your tests are fast, deterministic, and do not depend on the host environment.

    By using fstest.MapFS, you can simulate various directory structures and file contents, which is particularly useful for testing how your logic handles *.conf files or invalid paths within an fs.FS context.

  4. How to preserve multiple errors during cleanup

    master

    When performing cleanup in a defer block, a common mistake is to ignore errors returned by Close() or Rollback(), or to overwrite the original error.

    To implement idiomatic error preservation:

    1. Use a named return parameter (e.g., err error) in your function signature.
    2. Inside your defer block, check if the cleanup operation returns an error.
    3. If the cleanup fails, use errors.Join to combine the existing error with the new cleanup error. This ensures the caller receives both the root cause and the cleanup failure.

    Example Pattern

    func DoWork() (err error) {
        res, err := acquireResource()
        if err != nil {
            return err
        }
        defer func() {
            if closeErr := res.Close(); closeErr != nil {
                // Join the original error with the cleanup error
                err = errors.Join(err, closeErr)
            }
        }()
    
        // ... perform work ...
        return nil
    }
  5. Avoid common sync.Pool pitfalls

    master

    When using sync.Pool for high-throughput handlers, avoid these three common mistakes that lead to performance regressions or bugs:

    1. Pooling long-lived objects: sync.Pool is intended for short-lived, frequently allocated objects. Using it for objects with long lifecycles is an anti-pattern.
    2. Forgetting to reset buffers: If you reuse a bytes.Buffer without calling Reset(), the next consumer will see data from the previous request (a data leak).
    3. Storing huge buffers (Memory Bloat): If a buffer grows significantly during a specific request, putting that massive buffer back into the pool can cause the application's memory footprint to explode. Always implement a bound (e.g., only return the buffer to the pool if cap(buf) <= max).
  6. Classify transient vs non-transient errors for retries

    master

    When implementing a retry policy, you must only retry on transient failures. All other errors should cause the Do method to fail immediately.

    According to the kata scenario, transient errors include:

    • net.Error where Timeout() == true
    • HTTP status codes 429 (Too Many Requests) or 503 (Service Unavailable)
    • A sentinel error named ErrTransient

    Use errors.Is and errors.As to perform this classification.

  7. Structure of a Go Kata challenge

    master

    Each kata in this repository follows a standardized template designed to teach Go-specific idioms through practical challenges. When working through a kata, you should follow this structure:

    1. Target Idioms & Difficulty: Identifies the specific Go patterns being tested (e.g., Concurrency Patterns, Interface Pollution) and the expected skill level.
    2. The "Why": Explains the rationale behind the challenge, specifically highlighting why non-Go patterns (like those from Java or Python) are inappropriate for the problem.
    3. The Scenario: Provides a realistic production context for the problem.
    4. The Challenge: Divided into two critical parts:
      • Functional Requirements: The basic features the code must implement.
      • Idiomatic Constraints: The specific Go patterns you must use to pass (e.g., avoiding allocations in loops, using functional options, or error wrapping).
    5. Self-Correction: A guide to help you identify if you have implemented a solution in an un-idiomatic or dangerous way and how to fix it.
    6. Resources: Links to official Go documentation or blog posts to support your learning.
  8. Implement a Go Test Harness with Subtests, Parallelism, and Fuzzing

    master

    This kata challenges you to implement a sanitizer function and a robust test suite using idiomatic Go testing patterns.

    Scenario

    Implement a function func NormalizeHeaderKey(s string) (string, error) that:

    • Allows only ASCII letters, digits, and hyphens.
    • Normalizes input to canonical header form (e.g., content-type becomes Content-Type).
    • Rejects invalid input.

    Functional Requirements

    • Canonicalize valid inputs.
    • Reject invalid characters.
    • Ensure stable behavior (the same input must always produce the same output).

    Idiomatic Constraints (Pass/Fail Criteria)

    To complete the kata successfully, your test suite must meet these criteria:

    1. Table-Driven Tests: Use t.Run to organize test cases.
    2. Parallel Subtests: Use t.Parallel() correctly within subtests, ensuring you avoid loop variable capture bugs.
    3. Fuzz Testing: Implement a fuzz test (go test -fuzz) that verifies:
      • The implementation never panics.
      • The output never contains invalid characters.
      • The operation is idempotent (calling NormalizeHeaderKey twice on the same input returns the same result).
  9. Complete Kata 01: The Fail-Fast Data Aggregator

    master

    Kata 01 is an intermediate-level Go exercise focused on mastering concurrency control, context propagation, and the functional options pattern. The goal is to build a UserAggregator that fetches data from multiple mock services (e.g., Profile and Order services) in parallel, ensuring that if any single request fails or a timeout occurs, all other pending operations are cancelled immediately to save resources.

    Functional Requirements

    • The aggregator must be configurable (e.g., timeout, logger) using the Functional Options Pattern to avoid large constructors.
    • Services must be queried concurrently.
    • The final output must combine results from all services (e.g., "User: Alice | Orders: 5").

    Idiomatic Constraints (Pass/Fail Criteria)

    To successfully complete this kata, you must follow these Go best practices:

    • Use golang.org/x/sync/errgroup: Do not use sync.WaitGroup. errgroup provides the necessary error propagation and context cancellation that WaitGroup lacks.
    • Use Functional Options: Implement the constructor using the pattern New(WithTimeout(2s)) instead of passing many parameters.
    • Context Propagation: Always pass context.Context as the first argument to methods.
    • Immediate Cleanup: Ensure that if one service fails, the context is cancelled so that other service requests abort immediately.
    • Structured Logging: Use the log/slog package for logging.
  10. Implement the NDJSON Reader Kata

    master

    This kata challenges you to implement a streaming NDJSON (Newline Delimited JSON) reader capable of handling arbitrarily large lines (e.g., > 64KB) that would cause a standard bufio.Scanner to fail with bufio.Scanner: token too long.

    Implementation Task

    Implement the following function signature: func ReadNDJSON(ctx context.Context, r io.Reader, handle func([]byte) error) error

    Functional Requirements

    • Call handle(line) for each line, providing the line content without the trailing newline.
    • Stop execution immediately if handle returns an error.
    • Stop execution immediately if the provided context.Context is cancelled (ctx.Done()).

    Idiomatic Constraints (Pass/Fail Criteria)

    To pass this kata, your implementation must meet these specific Go idioms:

    • Avoid bufio.Scanner: Do not rely on the default bufio.Scanner behavior, as it has a fixed buffer limit.
    • Use bufio.Reader: Use bufio.Reader and correctly handle the ErrBufferFull error returned by ReadSlice('\n') to manage lines larger than the buffer.
    • Low Allocation: Avoid per-line allocations by reusing buffers.
    • Error Wrapping: Wrap errors with line number context using the %w verb.
    func ReadNDJSON(ctx context.Context, r io.Reader, handle func([]byte) error) error
  11. Implement a Filesystem-Agnostic Config Loader

    master

    This kata challenges you to implement a configuration loader that is decoupled from the OS filesystem by using the io/fs abstraction. Instead of passing string paths to os.Open, you should design an API that accepts an fs.FS interface. This allows the loader to work seamlessly with local disks, embedded files (via embed), ZIP files, or in-memory filesystems for testing.

    Implementation Task

    Implement the following function signature: func LoadConfigs(fsys fs.FS, root string) (map[string][]byte, error)

    Functional Requirements

    • Recursively walk the root directory.
    • Identify and read all files ending in .conf.
    • Return a map where keys are the file paths and values are the file contents (map[string][]byte).
    • Handle invalid paths gracefully by returning an error.

    Idiomatic Constraints (Pass/Fail Criteria)

    To successfully complete this kata, your implementation must adhere to these Go idioms:

    • Use fs.FS abstraction: The core API must accept an fs.FS interface, not raw OS paths.
    • Use io/fs primitives: You must use fs.WalkDir and fs.ReadFile.
    • Avoid os package coupling: Do NOT use os.Open or filepath.Walk inside the core loader logic.
    • Test with fstest.MapFS: Unit tests must be able to run without touching the real filesystem by using testing/fstest.MapFS to mock the filesystem.
    func LoadConfigs(fsys fs.FS, root string) (map[string][]byte, error)
  12. How to use the Go Katas repository

    master

    This repository is a collection of small, standalone coding challenges (Katas) designed to practice idiomatic Go patterns. To use the repository, follow these steps:

    1. Pick a Kata: Navigate to any XX-kata-yy directory from the index.
    2. Read the Challenge: Open the README.md located inside the specific kata folder. This file defines the goal, constraints, and the specific "idiomatic patterns" required for the solution.
    3. Solve It: Initialize a new Go module inside that specific folder and implement your solution.
    4. Reflect: Compare your implementation against the provided "Reference Implementation" (if available) or the core patterns listed in the challenge description.