Go Best Practices and Idioms

Writing idiomatic Go means following community conventions and the language's philosophy.

Code Style

Use go fmt

Always format your code. No debates about style.

go fmt ./...
# Or use gofmt -w .

Use goimports

Automatically manage imports:

goimports -w .

Naming Conventions

// Package names: short, lowercase, no underscores
package http
package user
package userservice  // Not user_service

// Variables and functions: camelCase
var userName string
func getUserByID(id int) *User

// Exported names: PascalCase
type User struct {}
func NewUser() *User

// Acronyms: keep consistent case
var userID string   // Not userId
var httpClient      // Not hTTPClient
type HTTPHandler    // Not HttpHandler

// Interface names: often end in -er
type Reader interface
type Validator interface
type UserService interface  // OK for larger interfaces

// Getter/Setter: just Name(), not GetName()
func (u *User) Name() string     // Not GetName()
func (u *User) SetName(n string) // Setter needs Set prefix

Comments

// Package-level comments
// Package http provides HTTP client and server implementations.
package http

// Exported function comments start with the function name
// NewServer creates a new HTTP server with the given address.
func NewServer(addr string) *Server

// Good comment: explains WHY
// We retry 3 times because the upstream API has transient failures
for i := 0; i < 3; i++ {

// Bad comment: explains WHAT (obvious from code)
// Increment i by 1
i++

Error Handling

Always Handle Errors

// Bad: ignoring errors
data, _ := json.Marshal(user)

// Good: handle or propagate
data, err := json.Marshal(user)
if err != nil {
    return fmt.Errorf("marshaling user: %w", err)
}

Handle Once

// Bad: handling multiple times
if err != nil {
    log.Printf("error: %v", err)  // Logged here
    return err                      // And caller logs again
}

// Good: handle at the top
func main() {
    if err := run(); err != nil {
        log.Fatal(err)  // Single place
    }
}

func run() error {
    if err := doThing(); err != nil {
        return fmt.Errorf("doing thing: %w", err)  // Just propagate
    }
    return nil
}

Add Context to Errors

// Bad: loses context
return err

// Good: adds context
return fmt.Errorf("reading config file %s: %w", path, err)

Check Specific Errors

// Good: use errors.Is for sentinel errors
if errors.Is(err, os.ErrNotExist) {
    // File doesn't exist
}

// Good: use errors.As for error types
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    log.Printf("path: %s", pathErr.Path)
}

Function Design

Accept Interfaces, Return Structs

// Good: flexible input
func Process(r io.Reader) error

// Good: concrete output
func NewBuffer() *bytes.Buffer

Keep Functions Focused

// Bad: does too much
func ProcessUserAndSendEmail(u *User) error

// Good: single responsibility
func ProcessUser(u *User) error
func SendWelcomeEmail(email string) error

Use Named Parameters for Clarity

// Confusing
createUser(true, false, 100)

// Better: use named types
type CreateUserOptions struct {
    IsAdmin   bool
    SendEmail bool
    Quota     int
}
createUser(CreateUserOptions{IsAdmin: true, Quota: 100})

// Or for simple cases, use named variables
isAdmin := true
sendEmail := false
createUser(isAdmin, sendEmail, 100)

Struct Design

Use Constructors

type Server struct {
    addr    string
    timeout time.Duration
}

// Don't allow direct construction if validation needed
func NewServer(addr string) (*Server, error) {
    if addr == "" {
        return nil, errors.New("addr required")
    }
    return &Server{
        addr:    addr,
        timeout: 30 * time.Second,  // Default
    }, nil
}

Make Zero Values Useful

// Good: zero value is ready to use
type Counter struct {
    mu    sync.Mutex
    count int  // Zero is valid
}

// Good: sync.Mutex zero value is an unlocked mutex
var mu sync.Mutex  // Ready to use

// Good: bytes.Buffer zero value is empty buffer
var buf bytes.Buffer  // Ready to use

Embed for Behavior, Not Just Fields

// Good: embedding adds behavior
type CountingWriter struct {
    io.Writer
    count int
}

// Now CountingWriter has all Writer methods

Concurrency

Use Channels for Communication

// Good: share by communicating
resultChan := make(chan Result)
go func() {
    result := expensiveOperation()
    resultChan <- result
}()
result := <-resultChan

// Avoid: communicating by sharing
var result Result
var done bool
go func() {
    result = expensiveOperation()
    done = true
}()
for !done { /* spin */ }  // Bad!

Always Cancel Contexts

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()  // Always call cancel

Avoid Goroutine Leaks

// Bad: goroutine leaks if nobody receives
func leaky() {
    ch := make(chan int)
    go func() {
        ch <- expensiveOperation()  // Blocked forever
    }()
    return  // Leaked goroutine!
}

// Good: use context for cancellation
func safe(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case ch <- expensiveOperation():
        case <-ctx.Done():
            return
        }
    }()
}

Use sync.WaitGroup for Goroutine Coordination

var wg sync.WaitGroup
for _, item := range items {
    wg.Add(1)
    go func(item Item) {
        defer wg.Done()
        process(item)
    }(item)
}
wg.Wait()

Testing

Use Table-Driven Tests

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -1, -2},
        {"zero", 0, 0, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := Add(tt.a, tt.b); got != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.expected)
            }
        })
    }
}

Use Interfaces for Testability

// Production
type UserStore interface {
    GetUser(id int) (*User, error)
}

type Service struct {
    store UserStore
}

// Test
type MockStore struct {
    users map[int]*User
}

func (m *MockStore) GetUser(id int) (*User, error) {
    return m.users[id], nil
}

Use t.Helper() for Test Helpers

func assertEqual(t *testing.T, got, want interface{}) {
    t.Helper()  // Points errors to caller, not this function
    if got != want {
        t.Errorf("got %v; want %v", got, want)
    }
}

Performance

Preallocate Slices When Possible

// Bad: grows slice multiple times
var result []int
for i := 0; i < n; i++ {
    result = append(result, i)
}

// Good: preallocate
result := make([]int, 0, n)
for i := 0; i < n; i++ {
    result = append(result, i)
}

Use strings.Builder for Concatenation

// Bad: creates many intermediate strings
s := ""
for _, word := range words {
    s += word + " "
}

// Good: efficient string building
var b strings.Builder
for _, word := range words {
    b.WriteString(word)
    b.WriteString(" ")
}
s := b.String()

Avoid Premature Optimization

// Start simple
func process(items []Item) {
    for _, item := range items {
        handle(item)
    }
}

// Only optimize after profiling shows it's a bottleneck
// go test -bench=. -cpuprofile=cpu.out
// go tool pprof cpu.out

Common Patterns

Functional Options

type Server struct {
    addr    string
    timeout time.Duration
}

type Option func(*Server)

func WithTimeout(d time.Duration) Option {
    return func(s *Server) {
        s.timeout = d
    }
}

func NewServer(addr string, opts ...Option) *Server {
    s := &Server{addr: addr, timeout: 30 * time.Second}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// Usage
server := NewServer("localhost:8080", WithTimeout(60*time.Second))

Dependency Injection

// Define interfaces where used
type UserService struct {
    db     Database
    cache  Cache
    logger Logger
}

func NewUserService(db Database, cache Cache, logger Logger) *UserService {
    return &UserService{db: db, cache: cache, logger: logger}
}

Graceful Shutdown

func main() {
    server := &http.Server{Addr: ":8080"}

    go func() {
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    // Graceful shutdown with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatal(err)
    }
}

Anti-Patterns to Avoid

Don't Use init() for Complex Logic

// Bad: hidden initialization
func init() {
    db = connectToDatabase()  // What if this fails?
}

// Good: explicit initialization
func main() {
    db, err := connectToDatabase()
    if err != nil {
        log.Fatal(err)
    }
}

Don't Panic in Libraries

// Bad: library panics
func ParseConfig(data []byte) *Config {
    var c Config
    if err := json.Unmarshal(data, &c); err != nil {
        panic(err)  // Caller can't handle this!
    }
    return &c
}

// Good: return error
func ParseConfig(data []byte) (*Config, error) {
    var c Config
    if err := json.Unmarshal(data, &c); err != nil {
        return nil, err
    }
    return &c, nil
}

Don't Overuse Empty Interface

// Bad: loses type safety
func Process(data interface{}) interface{}

// Good: use generics or specific types
func Process[T any](data T) T
func ProcessUser(u *User) *Result

Don't Return Nil Interfaces

// Surprising behavior
type Writer interface {
    Write([]byte) (int, error)
}

func GetWriter() Writer {
    var w *bytes.Buffer  // nil
    return w  // Returns non-nil interface with nil value!
}

w := GetWriter()
if w != nil {  // true!
    w.Write([]byte("oops"))  // panic!
}

// Good: return nil explicitly
func GetWriter() Writer {
    var w *bytes.Buffer
    if w == nil {
        return nil  // Nil interface
    }
    return w
}

Go Proverbs

From Rob Pike's Go Proverbs:

  1. Don't communicate by sharing memory, share memory by communicating
  2. Concurrency is not parallelism
  3. Channels orchestrate; mutexes serialize
  4. The bigger the interface, the weaker the abstraction
  5. Make the zero value useful
  6. A little copying is better than a little dependency
  7. Clear is better than clever
  8. Errors are values
  9. Don't just check errors, handle them gracefully
  10. Documentation is for users

Resources