Go Language Idioms
Go favors simplicity, readability, and explicit behavior. Work with the language's conventions, not against them.
Core Philosophy
- Simplicity over cleverness — clear, boring code is better than clever, obscure code
- Explicit over implicit — errors are values, not exceptions
- Composition over inheritance — embed types, implement interfaces
- Share by communicating — don't communicate by sharing memory
Formatting
Let gofmt handle it. Don't fight the formatter.
gofmt -w . # Format all files
go fmt ./... # Format package
All Go code in the ecosystem follows gofmt. There is no debate.
Naming
General Rules
| Scope | Style | Example |
|---|
| Exported | PascalCase | ReadFile, HTTPClient |
| Unexported | camelCase | readFile, httpClient |
| Packages | lowercase, single word | bytes, http, bufio |
| Acronyms | All caps | HTTP, URL, ID (not Http, Url, Id) |
Package Names
- Short, concise, evocative
- No underscores or mixedCaps
- The package name is part of the type name:
bufio.Reader, not bufio.BufReader
Getters and Setters
// ❌ Don't prefix getters with "Get"
func (u *User) GetName() string
// ✅ Just use the field name
func (u *User) Name() string
// ✅ Setters use "Set" prefix
func (u *User) SetName(name string)
Interface Names
One-method interfaces use method name + -er suffix:
| Method | Interface |
|---|
Read | Reader |
Write | Writer |
Close | Closer |
String | Stringer |
Functions
Multiple Return Values
Go functions return (result, error) instead of throwing:
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}
Named Return Values
Use for documentation, but avoid naked returns in non-trivial functions:
// Named returns document what's being returned
func ParseDuration(s string) (duration time.Duration, err error) {
// ...
return duration, nil // Explicit, not naked
}
Defer
Use defer for cleanup. Defers execute LIFO at function exit:
func CopyFile(dst, src string) error {
r, err := os.Open(src)
if err != nil {
return err
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return err
}
defer w.Close()
_, err = io.Copy(w, r)
return err
}
Interfaces
Keep Interfaces Small
Accept interfaces, return concrete types:
// ✅ Function accepts interface (flexible)
func Process(r io.Reader) error
// ✅ Function returns concrete type (useful)
func NewClient() *Client
Interface Satisfaction
Interfaces are satisfied implicitly—no implements keyword:
type Stringer interface {
String() string
}
type User struct{ Name string }
// User satisfies Stringer automatically
func (u User) String() string {
return u.Name
}
Compile-Time Check
Verify interface satisfaction at compile time:
var _ io.Reader = (*MyReader)(nil)
Pointers vs. Values
When to Use Pointer Receivers
| Use Pointer | When |
|---|
*T | Method modifies the receiver |
*T | Struct is large (avoid copying) |
*T | Consistency (if any method uses pointer, all should) |
When to Use Value Receivers
| Use Value | When |
|---|
T | Method doesn't modify receiver |
T | Type is small (int, small struct) |
T | Type is immutable by design |
Concurrency
Goroutines
Lightweight, start with go:
go func() {
// runs concurrently
}()
Channels
Use channels for communication between goroutines:
ch := make(chan int) // Unbuffered
ch := make(chan int, 100) // Buffered
ch <- value // Send
v := <-ch // Receive
Share by Communicating
"Don't communicate by sharing memory; share memory by communicating."
// ❌ Shared state with mutex
var count int
var mu sync.Mutex
// ✅ Channel-based coordination
type result struct{ value int }
results := make(chan result)
Common Patterns
| Pattern | Use Case |
|---|
sync.WaitGroup | Wait for N goroutines to complete |
context.Context | Cancellation and timeouts |
select | Multiplex channel operations |
sync.Once | One-time initialization |
Error Handling
Errors Are Values
Check explicitly. Never discard with _:
// ❌ Never ignore errors
result, _ := doSomething()
// ✅ Always check
result, err := doSomething()
if err != nil {
return fmt.Errorf("doing something: %w", err)
}
Error Wrapping
Use %w to wrap errors with context:
if err != nil {
return fmt.Errorf("failed to parse config %s: %w", path, err)
}
Custom Errors
For libraries, define sentinel errors or custom types:
// Sentinel errors
var ErrNotFound = errors.New("not found")
// Custom error types
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
When to Panic
| Context | Panic? | Alternative |
|---|
| Library code | ❌ Never | Return error |
| Impossible states | ✅ Acceptable | Assert invariants |
| Init failures | ✅ Acceptable | log.Fatal |
Anti-Patterns
| Anti-Pattern | Description | Remedy |
|---|
| Ignoring errors | Using _ to discard errors | Always check or explicitly document why ignored |
| Naked returns | return without values in complex functions | Use explicit return values |
| Interface pollution | Defining interfaces before needed | Define interfaces at point of use |
| Premature channels | Using channels when mutex suffices | Start simple; add concurrency primitives as needed |
| Giant interfaces | Interfaces with many methods | Keep interfaces small; compose when needed |
Tooling
go fmt ./... # Format code
go vet ./... # Static analysis
golangci-lint run # Comprehensive linting
go test ./... # Run tests
go test -race ./... # Detect race conditions
Quick Reference
- Errors are values — check explicitly, wrap with context
- Accept interfaces, return structs — flexible inputs, concrete outputs
- Use
gofmt — no formatting debates
- Prefer composition — embed types, small interfaces
- Channels for coordination — share by communicating
defer for cleanup — runs at function exit, LIFO
- No naked returns — be explicit in non-trivial functions
These idioms refine but are subordinate to the Code-Edit Constraints.