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.
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.countChannel 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)
<-donePerformance
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 UserInitialize 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
- staticcheckThis guide provides a comprehensive set of best practices for writing Go code at Uber, helping ensure code maintainability, efficiency, and adherence to Go idioms.
FunTester
10k followers, 1k articles | completely useless
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.