Go
Overview
Go programming patterns including concurrency, error handling, and idiomatic Go code.
Basic Patterns
Structs and Methods
package main
import ( "encoding/json" "fmt" "time" )
// Struct definition
type User struct {
ID string json:"id"
Email string json:"email"
Name string json:"name"
CreatedAt time.Time json:"created_at"
metadata map[string]interface{} // unexported (private)
}
// Constructor function func NewUser(email, name string) *User { return &User{ ID: generateID(), Email: email, Name: name, CreatedAt: time.Now(), metadata: make(map[string]interface{}), } }
// Value receiver (for read-only) func (u User) FullName() string { return u.Name }
// Pointer receiver (for mutations or large structs) func (u *User) SetMetadata(key string, value interface{}) { u.metadata[key] = value }
// Embedding (composition) type Admin struct { User // Embedded struct Permissions []string }
func (a *Admin) HasPermission(perm string) bool { for _, p := range a.Permissions { if p == perm { return true } } return false }
Interfaces
// Interface definition type Repository interface { Find(id string) (*User, error) FindAll() ([]*User, error) Create(user *User) error Update(user *User) error Delete(id string) error }
// Interface implementation (implicit) type MemoryRepository struct { users map[string]*User }
func NewMemoryRepository() *MemoryRepository { return &MemoryRepository{ users: make(map[string]*User), } }
func (r *MemoryRepository) Find(id string) (*User, error) { user, ok := r.users[id] if !ok { return nil, ErrNotFound } return user, nil }
func (r *MemoryRepository) Create(user *User) error { r.users[user.ID] = user return nil }
// Compile-time interface check var _ Repository = (*MemoryRepository)(nil)
// Empty interface (any type) func PrintAny(v interface{}) { fmt.Printf("%v\n", v) }
// Type assertion func ProcessValue(v interface{}) { switch val := v.(type) { case string: fmt.Println("String:", val) case int: fmt.Println("Int:", val) case *User: fmt.Println("User:", val.Name) default: fmt.Println("Unknown type") } }
Error Handling
import ( "errors" "fmt" )
// Sentinel errors var ( ErrNotFound = errors.New("not found") ErrBadRequest = errors.New("bad request") )
// Custom error type type ValidationError struct { Field string Message string }
func (e *ValidationError) Error() string { return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message) }
// Error wrapping func GetUser(id string) (*User, error) { user, err := repository.Find(id) if err != nil { return nil, fmt.Errorf("getting user %s: %w", id, err) } return user, nil }
// Error checking func ProcessUser(id string) error { user, err := GetUser(id) if err != nil { if errors.Is(err, ErrNotFound) { return fmt.Errorf("user not found: %s", id) } var validationErr *ValidationError if errors.As(err, &validationErr) { return fmt.Errorf("validation failed: %s", validationErr.Field) } return err } // Process user... return nil }
// Multi-error handling type MultiError struct { Errors []error }
func (m *MultiError) Error() string { var msgs []string for _, err := range m.Errors { msgs = append(msgs, err.Error()) } return strings.Join(msgs, "; ") }
func (m *MultiError) Add(err error) { if err != nil { m.Errors = append(m.Errors, err) } }
func (m *MultiError) HasErrors() bool { return len(m.Errors) > 0 }
Concurrency
Goroutines and Channels
// Basic goroutine func main() { go func() { fmt.Println("Hello from goroutine") }()
time.Sleep(100 * time.Millisecond)
}
// Channel basics func worker(jobs <-chan int, results chan<- int) { for job := range jobs { results <- job * 2 } }
func main() { jobs := make(chan int, 100) results := make(chan int, 100)
// Start workers
for w := 0; w < 3; w++ {
go worker(jobs, results)
}
// Send jobs
for j := 0; j < 9; j++ {
jobs <- j
}
close(jobs)
// Collect results
for r := 0; r < 9; r++ {
fmt.Println(<-results)
}
}
// Select for multiple channels func main() { ch1 := make(chan string) ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "one"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
case <-time.After(500 * time.Millisecond):
fmt.Println("Timeout")
}
}
}
Concurrency Patterns
import ( "context" "sync" )
// Worker pool type WorkerPool struct { numWorkers int jobs chan func() wg sync.WaitGroup }
func NewWorkerPool(numWorkers int) WorkerPool { pool := &WorkerPool{ numWorkers: numWorkers, jobs: make(chan func(), numWorkers2), } pool.Start() return pool }
func (p *WorkerPool) Start() { for i := 0; i < p.numWorkers; i++ { go func() { for job := range p.jobs { job() p.wg.Done() } }() } }
func (p *WorkerPool) Submit(job func()) { p.wg.Add(1) p.jobs <- job }
func (p *WorkerPool) Wait() { p.wg.Wait() }
func (p *WorkerPool) Close() { close(p.jobs) }
// Fan-out, fan-in func FanOut(ctx context.Context, input <-chan int, workers int) []<-chan int { outputs := make([]<-chan int, workers) for i := 0; i < workers; i++ { outputs[i] = worker(ctx, input) } return outputs }
func FanIn(ctx context.Context, channels ...<-chan int) <-chan int { var wg sync.WaitGroup merged := make(chan int)
output := func(c <-chan int) {
defer wg.Done()
for v := range c {
select {
case merged <- v:
case <-ctx.Done():
return
}
}
}
wg.Add(len(channels))
for _, c := range channels {
go output(c)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
// Rate limiter type RateLimiter struct { ticker *time.Ticker tokens chan struct{} }
func NewRateLimiter(rate int, burst int) *RateLimiter { rl := &RateLimiter{ ticker: time.NewTicker(time.Second / time.Duration(rate)), tokens: make(chan struct{}, burst), }
// Fill initial burst
for i := 0; i < burst; i++ {
rl.tokens <- struct{}{}
}
// Refill tokens
go func() {
for range rl.ticker.C {
select {
case rl.tokens <- struct{}{}:
default:
}
}
}()
return rl
}
func (rl *RateLimiter) Wait(ctx context.Context) error { select { case <-rl.tokens: return nil case <-ctx.Done(): return ctx.Err() } }
Context
import ( "context" "time" )
// Context with timeout func FetchWithTimeout(url string) ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// Context with values type contextKey string
const userIDKey contextKey = "userID"
func WithUserID(ctx context.Context, userID string) context.Context { return context.WithValue(ctx, userIDKey, userID) }
func GetUserID(ctx context.Context) (string, bool) { userID, ok := ctx.Value(userIDKey).(string) return userID, ok }
// Passing context through layers func Handler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() ctx = WithUserID(ctx, r.Header.Get("X-User-ID"))
result, err := ProcessRequest(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}
func ProcessRequest(ctx context.Context) (*Result, error) { // Check for cancellation select { case <-ctx.Done(): return nil, ctx.Err() default: }
userID, ok := GetUserID(ctx)
if !ok {
return nil, errors.New("user ID not found in context")
}
return fetchData(ctx, userID)
}
Generics (Go 1.18+)
// Generic function func Map[T, U any](items []T, fn func(T) U) []U { result := make([]U, len(items)) for i, item := range items { result[i] = fn(item) } return result }
func Filter[T any](items []T, predicate func(T) bool) []T { var result []T for _, item := range items { if predicate(item) { result = append(result, item) } } return result }
func Reduce[T, U any](items []T, initial U, fn func(U, T) U) U { result := initial for _, item := range items { result = fn(result, item) } return result }
// Generic type constraint type Number interface { ~int | ~int32 | ~int64 | ~float32 | ~float64 }
func Sum[T Number](items []T) T { var sum T for _, item := range items { sum += item } return sum }
// Generic struct type Stack[T any] struct { items []T }
func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) }
func (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T return zero, false } item := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return item, true }
// Usage stack := &Stack[int]{} stack.Push(1) stack.Push(2) val, ok := stack.Pop() // val = 2, ok = true
Testing
import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" )
// Basic test func TestSum(t *testing.T) { result := Sum([]int{1, 2, 3}) if result != 6 { t.Errorf("expected 6, got %d", result) } }
// Table-driven tests func TestSumTableDriven(t *testing.T) { tests := []struct { name string input []int expected int }{ {"empty", []int{}, 0}, {"single", []int{5}, 5}, {"multiple", []int{1, 2, 3}, 6}, {"negative", []int{-1, 1}, 0}, }
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Sum(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// Test with testify func TestUser(t *testing.T) { user := NewUser("test@example.com", "Test User")
require.NotNil(t, user)
assert.Equal(t, "test@example.com", user.Email)
assert.Equal(t, "Test User", user.Name)
assert.NotEmpty(t, user.ID)
}
// Benchmark func BenchmarkSum(b *testing.B) { items := make([]int, 1000) for i := range items { items[i] = i }
b.ResetTimer()
for i := 0; i < b.N; i++ {
Sum(items)
}
}
Related Skills
-
[[backend]] - Go web services
-
[[cloud-platforms]] - Cloud-native Go
-
[[system-design]] - System architecture