This section summarizes the most common Go idioms and patterns that every Go developer should know. These patterns represent idiomatic Go code.
1. Error Handling
// ✅ IDIOMATIC: Check errors immediately
result, err := doSomething()
if err != nil {
return err
}
// ✅ IDIOMATIC: Early return
if err != nil {
return fmt.Errorf("context: %w", err)
}
Why this is idiomatic
%w to wrap errors (Go 1.13+)2. Defer for Cleanup
// ✅ IDIOMATIC: Always use defer for cleanup
file, err := os.Open("file.txt")
if err != nil {
return err
}
defer file.Close() // Always executed
Why defer is idiomatic
3. Interface Satisfaction
// ✅ IDIOMATIC: Interfaces are satisfied implicitly
type Writer interface {
Write([]byte) (int, error)
}
type MyWriter struct{}
func (w *MyWriter) Write(data []byte) (int, error) {
// MyWriter implements Writer automatically
return len(data), nil
}
Why implicit satisfaction is idiomatic
4. Zero Values
// ✅ IDIOMATIC: Use zero values
var buffer bytes.Buffer // Ready to use
var mu sync.Mutex // Ready to use
var wg sync.WaitGroup // Ready to use
Why zero values are idiomatic
5. Short Variable Declarations
// ✅ IDIOMATIC: Use := for local variables
x := 42
name := "Alice"
// ✅ IDIOMATIC: Multiple assignments
x, y := 10, 20
Why := is idiomatic
var6. Named Returns
// ✅ IDIOMATIC: Named returns for clarity
func divide(a, b float64) (result float64, err error) {
if b == 0 {
return 0, errors.New("division by zero")
}
result = a / b
return // Naked return
}
Why named returns are idiomatic
1. Table-Driven Tests
// ✅ PATTERN: Table-driven tests
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive", 2, 3, 5},
{"negative", -2, -3, -5},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
}
})
}
}
Why table-driven tests
2. Options Pattern
// ✅ PATTERN: Options for configuration
type Server struct {
host string
port int
}
type Option func(*Server)
func WithHost(host string) Option {
return func(s *Server) {
s.host = host
}
}
func NewServer(opts ...Option) *Server {
s := &Server{host: "localhost", port: 8080}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
server := NewServer(WithHost("example.com"), WithPort(9000))
Why options pattern
3. Functional Options
// ✅ PATTERN: Functional options (similar to options pattern)
type Config struct {
timeout time.Duration
retries int
}
func WithTimeout(d time.Duration) func(*Config) {
return func(c *Config) {
c.timeout = d
}
}
func NewConfig(opts ...func(*Config)) *Config {
c := &Config{timeout: 5 * time.Second, retries: 3}
for _, opt := range opts {
opt(c)
}
return c
}
4. Worker Pool
// ✅ PATTERN: Worker pool
func processWithWorkers(jobs []Job, numWorkers int) {
jobChan := make(chan Job, len(jobs))
var wg sync.WaitGroup
// Start workers
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobChan {
processJob(job)
}
}()
}
// Send jobs
for _, job := range jobs {
jobChan <- job
}
close(jobChan)
wg.Wait()
}
Why worker pools
5. Context Pattern
// ✅ PATTERN: Context for cancellation
func processWithContext(ctx context.Context, items []Item) error {
for _, item := range items {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := processItem(ctx, item); err != nil {
return err
}
}
}
return nil
}
Why context pattern
6. Error Wrapping
// ✅ PATTERN: Wrap errors with context
func processFile(filename string) error {
data, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("reading file %s: %w", filename, err)
}
if err := validate(data); err != nil {
return fmt.Errorf("validating %s: %w", filename, err)
}
return nil
}
Why error wrapping
errors.Is and errors.As work with wrapped errors7. Interface Segregation
// ✅ PATTERN: Small, focused interfaces
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
// Not: ReadWriter interface with both methods
// Better: Compose when needed
type ReadWriter interface {
Reader
Writer
}
Why small interfaces
1. Error Handling
errors.Is and errors.As for error checking2. Concurrency
sync.WaitGroup to wait for goroutines3. Code Organization
4. Performance
sync.Pool for temporary objects5. Testing
6. Documentation
7. Naming
8. Memory Management
These idioms and patterns represent the collective wisdom of the Go community. Following them makes your code more readable, maintainable, and idiomatic.