Uber Go Style Guide

repository·master·Indexed 10 days ago

https://github.com/uber-go/guide

A collection of patterns and conventions used by Uber engineers to write idiomatic and maintainable Go code, covering topics such as channel sizing, container capacity, resource cleanup with defer, and the use of the go.uber.org/atomic package.

Tokens
30.9K
Snippets
122
Records
150
Agent score
92%

What's inside Uber Go Style Guide

  1. Overview of the Uber Go Style Guide

    master

    The Uber Go Style Guide defines the conventions and idiomatic practices used at Uber to ensure codebases remain manageable and productive. It covers more than just formatting (which is handled by gofmt) and includes guidelines on:

    • Guidelines: Best practices for pointers, interfaces, mutexes, concurrency, and error handling.
    • Performance: Optimization techniques like preferring strconv over fmt and managing container capacity.
    • Style: Conventions for naming, grouping declarations, reducing nesting, and initializing structs/maps.
    • Patterns: Common architectural patterns like Functional Options and Test Tables.

    This guide is based on Effective Go, Go Common Mistakes, and Go Code Review Comments.

  2. Access the Uber Go Style Guide

    master
    The Uber Go Style Guide documents the patterns and conventions used in Go code at Uber. You can access the full guide in style.md to learn about recommended Go coding practices, idiomatic patterns, and structural conventions used within Uber's engineering organization.
  3. How to manage background goroutine lifecycles

    master

    To correctly implement background tasks in a package, follow these patterns:

    1. Encapsulation: Wrap the goroutine logic within a struct.
    2. Controlled Start: Only start the goroutine when the user explicitly calls a constructor like NewWorker().
    3. Signaling Stop: Use a channel (e.g., stop) to signal the goroutine to terminate.
    4. Waiting for Exit: Use a second channel (e.g., done) or a sync.WaitGroup to ensure the Shutdown method blocks until the goroutine has actually exited.

    If the worker manages multiple goroutines, use sync.WaitGroup to coordinate their exit.

  4. Handle nil slices correctly

    master

    In Go, nil is a valid slice of length 0. Follow these rules for slice management:

    • Return nil instead of an explicit empty slice []T{} when returning a zero-length slice.
    • Check for emptiness using len(s) == 0 rather than checking s == nil.
    • Use var to declare empty slices that will be populated via append, as the zero value is immediately usable without make().
    // Return nil for empty slices
    if x == "" {
      return nil
    }
    
    // Check emptiness using len()
    func isEmpty(s []string) bool {
      return len(s) == 0
    }
    
    // Use var for slices to be appended to
    var nums []int
    if add1 {
      nums = append(nums, 1)
    }
  5. Understanding the difference between nil and empty slices

    master
    While nil is a valid slice of length 0, a nil slice is not identical to an allocated slice of length 0. They are distinct in memory (one is nil and the other is not) and may behave differently in specific contexts, such as during JSON serialization.
  6. Pass interfaces as values, not pointers

    master
    You almost never need a pointer to an interface. Pass interfaces as values; the underlying data can still be a pointer. An interface consists of a type pointer and a data pointer. If the underlying data is a value, the interface stores a pointer to that value. Use a pointer to the underlying type only if the interface methods need to modify that data.
  7. When to use `init()`

    master

    While init() should generally be avoided, it may be preferable or necessary in the following scenarios:

    • Complex expressions: When initialization requires logic that cannot be represented as a single variable assignment.
    • Pluggable hooks: For implementing registries, such as database/sql dialects or encoding type registries.
    • Deterministic precomputation: For optimizations in environments like Google Cloud Functions, where using global variables can allow for object reuse across future invocations.
  8. Compare Functional Options vs. Fixed Arguments

    master

    When designing APIs, avoid long lists of mandatory parameters.

    Bad Pattern: Using fixed arguments for optional features. This forces the caller to provide default values or dummy values (like zap.NewNop()) even when they don't want to configure that specific feature.

    Good Pattern: Using Functional Options. This allows the caller to provide only the options they care about, while the constructor handles defaults internally.

    // BAD: Parameters must always be provided
    db.Open(addr, db.DefaultCache, zap.NewNop())
    
    // GOOD: Options are provided only if needed
    db.Open(addr)
    db.Open(addr, db.WithLogger(log))
  9. Avoid breaking zero values with embedding

    master

    Embedding an interface or a pointer type can break the usefulness of a struct's zero value, leading to nil pointer panics when calling embedded methods. To ensure a useful zero value, embed concrete types that are ready to use upon declaration.

    Comparison

    Bad: Embedding an interface (causes panics)

    type Book struct {
        io.ReadWriter
        // other fields
    }
    
    var b Book
    b.Read(...)  // panic: nil pointer

    Good: Embedding a concrete type (safe zero value)

    type Book struct {
        bytes.Buffer
        // other fields
    }
    
    var b Book
    b.Read(...)  // ok
    type Book struct {
        bytes.Buffer
        // other fields
    }
    
    // later
    var b Book
    b.Read(...)  // ok
    b.String()   // ok
    b.Write(...) // ok
  10. Why you should avoid os.Exit in non-main functions

    master

    Calling os.Exit or log.Fatal* inside functions other than main() creates three primary issues:

    1. Non-obvious control flow: It becomes difficult to reason about the program's execution path if any function can abruptly terminate the process.
    2. Difficult to test: A function that calls os.Exit will terminate the entire test process, making it impossible to test the function's error state without crashing the test runner and potentially skipping other tests.
    3. Skipped cleanup: os.Exit terminates the program immediately without running any functions previously scheduled with defer. This can lead to resource leaks or incomplete cleanup tasks.
  11. Best practices for embedding in structs

    master

    Embed types at the top of the field list, followed by an empty line before regular fields.

    Guidelines for Embedding:

    • Use embedding only when it provides tangible benefit (e.g., augmenting functionality).
    • Do not embed sync.Mutex or other synchronization primitives, even on unexported types.
    • Do not embed types purely for cosmetic reasons or to expose implementation details.
    • Ensure embedding does not affect the outer type's zero value or copy semantics in a way that surprises users.
    type countingWriteCloser struct {
        io.WriteCloser
    
        count int
    }
    
    func (w *countingWriteCloser) Write(bs []byte) (int, error) {
        w.count += len(bs)
        return w.WriteCloser.Write(bs)
    }
  12. Avoid embedding types in public structs

    master

    To prevent leaking implementation details and to allow for easier type evolution, avoid using Go's type embedding for public structs. Embedding an internal type (like an AbstractList) into a public struct (like ConcreteList) makes the embedded type's methods and fields part of the public API. This creates several maintenance risks:

    • Breaking Changes: Adding methods to an embedded interface, removing methods from an embedded struct, or replacing the embedded type are all breaking changes for consumers.
    • Obscured Documentation: Embedding makes it harder for developers to discover the full interface of your type through documentation.
    • Leaked Details: It signals to the consumer exactly which internal implementation is being used.

    Recommended Pattern: Instead of embedding, use a private field to hold the implementation and hand-write delegate methods that call the implementation. This encapsulates the internal logic and provides a stable public API.

    // Good: Use a private field and explicit delegate methods
    type ConcreteList struct {
      list *AbstractList
    }
    
    func (l *ConcreteList) Add(e Entity) {
      l.list.Add(e)
    }
    
    func (l *ConcreteList) Remove(e Entity) {
      l.list.Remove(e)
    }