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:
- Don't communicate by sharing memory, share memory by communicating
- Concurrency is not parallelism
- Channels orchestrate; mutexes serialize
- The bigger the interface, the weaker the abstraction
- Make the zero value useful
- A little copying is better than a little dependency
- Clear is better than clever
- Errors are values
- Don't just check errors, handle them gracefully
- Documentation is for users