Backend Development 16 min read

Uber Go Coding Style Guide and Best Practices

This guide outlines Uber's conventions and best practices for writing Go code, covering linting, formatting, interface usage, error handling, concurrency, performance, style, patterns, and code‑checking tools to ensure maintainable, efficient, and idiomatic backend services.

FunTester
FunTester
FunTester
Uber Go Coding Style Guide and Best Practices

Introduction

This guide summarizes the conventions and best practices for writing Go code at Uber. Its goal is to manage code complexity, ensure maintainability of the codebase, and enable engineers to effectively leverage Go's features.

All code should be checked with golint and go vet . It is recommended to run goimports on save and use golint and go vet to catch errors.

Guide

Pointer to Interface

Almost never use a pointer to an interface; even if the underlying data is a pointer, the interface should be passed by value.

Validate Interface Compliance

Validate interface compliance at compile time where appropriate to ensure a type implements the required interface.

type Handler struct {
    // ...
}

var _ http.Handler = (*Handler)(nil)

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // ...
}

Receiver and Interface

Methods with value receivers can be called on both values and pointers, while methods with pointer receivers can only be called on pointers or addressable values.

Zero‑Value Mutex Is Valid

The zero values of sync.Mutex and sync.RWMutex are usable, so pointers to mutexes are rarely needed.

var mu sync.Mutex
mu.Lock()

Copy Slices and Maps at Boundaries

Slices and maps contain pointers to underlying data; copy them carefully to avoid unintended side effects.

Use defer to Clean Up Resources

Use defer to release files, locks, and other resources so they are correctly cleaned up even on error.

p.Lock()
defer p.Unlock()

if p.count < 10 {
    return p.count
}

p.count++
return p.count

Channel Size Should Be One or Unbuffered

Channels are usually sized to one or left unbuffered; avoid large buffers unless absolutely necessary.

c := make(chan int, 1) // or
c := make(chan int)

Enumerations Start at 1

Start enums at 1 to avoid zero being a valid but unintended state.

type Operation int

const (
    Add Operation = iota + 1
    Subtract
    Multiply
)

Use time Package for Time Handling

Always use the time package for time‑related operations to avoid common pitfalls.

Error Handling

Error Types

For static error messages use errors.New ; for dynamic messages use fmt.Errorf . Use custom error types when matching is required.

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
    return ErrCouldNotOpen
}

Error Wrapping

Wrap errors with fmt.Errorf and the %w verb to provide context.

if err != nil {
    return fmt.Errorf("new store: %w", err)
}

Error Naming

Prefix exported error values with Err and unexported ones with err .

var (
    ErrBrokenLink = errors.New("link is broken")
    errNotFound   = errors.New("not found")
)

Handle an Error Only Once

Avoid logging an error and then returning it; handle it a single time.

if err := emitMetrics(); err != nil {
    log.Printf("Could not emit metrics: %v", err)
}

Handle Type Assertion Failures

Always use the "comma ok" idiom when performing type assertions to avoid panics.

t, ok := i.(string)
if !ok {
    // gracefully handle error
}

Do Not Panic

Avoid using panic in production code; instead return errors and let callers decide how to handle them.

Use go.uber.org/atomic

Prefer go.uber.org/atomic for atomic operations to avoid common mistakes with sync/atomic .

type foo struct {
    running atomic.Bool
}

func (f *foo) start() {
    if f.running.Swap(true) {
        return
    }
    // start Foo
}

Avoid Mutable Global Variables

Do not modify global variables; use dependency injection instead.

Avoid Embedding Types in Exported Structs

Do not embed types in exported structs to prevent leaking implementation details.

Avoid Using Built‑in Names

Avoid using Go's predeclared identifiers as variable names to prevent shadowing and confusion.

Avoid init() When Possible

Minimize use of init() ; if required, keep it deterministic and free of external state.

Exit Only in main()

Call os.Exit or log.Fatal only from main() ; all other functions should return errors.

func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() error {
    // ...
}

Use Field Tags When Serializing Structs

Apply JSON/YAML tags to struct fields when they are serialized.

type Stock struct {
    Price int `json:"price"`
    Name  string `json:"name"`
}

Do Not Start Goroutines That Never Exit

Ensure goroutines have a clear exit point and clean up properly.

var (
    stop = make(chan struct{})
    done = make(chan struct{})
)

go func() {
    defer close(done)
    for {
        select {
        case <-ticker.C:
            flush()
        case <-stop:
            return
        }
    }
}()

close(stop)
<-done

Performance

Prefer strconv Over fmt

Use strconv for converting basic types to strings for better performance.

Avoid Repeated String‑to‑Byte Conversions

Convert a string to a byte slice once and reuse the result.

Specify Container Capacity

Specify the capacity of slices and maps whenever possible to avoid unnecessary allocations.

data := make([]int, 0, size)

Style

Avoid Overly Long Lines

Keep line length under 99 characters to avoid horizontal scrolling.

Maintain Consistency

Follow the same style throughout the codebase.

Group Similar Declarations

Group similar declarations together for readability.

const (
    a = 1
    b = 2
)

var (
    a = 1
    b = 2
)

Import Group Order

Separate imports into standard library and third‑party groups.

import (
    "fmt"
    "os"

    "go.uber.org/atomic"
)

Package Naming

Choose short, descriptive, all‑lowercase, non‑plural package names.

Function Naming

Use MixedCaps for function names; test functions may contain underscores for grouping.

Import Aliases

Use import aliases only when necessary to resolve naming conflicts.

Function Grouping and Ordering

Group functions by receiver and order them by call sequence.

Reduce Nesting

Handle error and special cases early to reduce nesting.

Avoid Unnecessary else

When a variable can be set in a single if statement, omit the redundant else block.

Top‑Level Variable Declarations

Use var for top‑level declarations unless the type is obvious.

Underscore Prefix for Unexported Globals

Prefix unexported top‑level variables and constants with _ to avoid accidental use.

Embedding in Structs

Embed types in structs only when it provides a real benefit; avoid embedding mutexes.

Local Variable Declarations

Prefer short variable declarations ( := ) for locals.

nil Is a Valid Slice

Use nil to represent an empty slice instead of returning an explicit empty slice.

Reduce Variable Scope

Limit variable scope as much as possible for readability.

Avoid Bare Parameters

Do not pass bare parameters; use named types or comments to clarify.

Use Raw String Literals to Avoid Escapes

Prefer raw string literals to avoid escape characters.

Struct Initialization

Initialize with Field Names

Always use field names when initializing structs.

k := User{
    FirstName: "John",
    LastName:  "Doe",
}

Omit Zero‑Value Fields

Omit fields that would be set to their zero value.

user := User{
    FirstName: "John",
    LastName:  "Doe",
}

Declare Zero‑Value Structs with var

Use var to declare a zero‑value struct.

var user User

Initialize Struct Pointers with &T{}

Prefer &T{} over new(T) for struct pointers.

sptr := &T{Name: "bar"}

Map Initialization

Use make for empty maps and map literals for fixed elements.

m := make(map[T1]T2, size)

Declare Format Strings Outside Printf ‑Style Functions

Declare format strings as const values outside of Printf -style functions.

Name Printf -Style Functions with f Suffix

Use an f suffix for Printf -style functions to enable go vet checks.

Patterns

Table‑Driven Tests

Use table‑driven tests with sub‑tests to avoid code duplication.

tests := []struct {
    give      string
    wantHost  string
    wantPort  string
}{
    // ...
}

for _, tt := range tests {
    t.Run(tt.give, func(t *testing.T) {
        host, port, err := net.SplitHostPort(tt.give)
        require.NoError(t, err)
        assert.Equal(t, tt.wantHost, host)
        assert.Equal(t, tt.wantPort, port)
    })
}

Functional Options

Use functional options in constructors and public APIs to handle optional parameters.

type Option interface {
    apply(*options)
}

func WithCache(c bool) Option { return cacheOption(c) }

func Open(addr string, opts ...Option) (*Connection, error) {
    // ...
}

Code Checks

Use a consistent set of code‑checking tools across the codebase. Recommended tools include:

errcheck

goimports

golint

govet

staticcheck

Code‑Check Runner

Use golangci-lint as the runner for Go code checks. It supports many linters and can be configured via a .golangci.yml file.

linters:
  enable:
    - errcheck
    - goimports
    - golint
    - govet
    - staticcheck

This guide provides a comprehensive set of best practices for writing Go code at Uber, helping ensure code maintainability, efficiency, and adherence to Go idioms.

backendGobest practicescoding standardsError handlingstyle guide
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.