home / skills / personamanagmentlayer / pcl / go-expert

go-expert skill

/stdlib/languages/go-expert

This skill helps you master modern Go development with Go 1.22+ features, concurrency, and production-grade patterns.

npx playbooks add skill personamanagmentlayer/pcl --skill go-expert

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
18.0 KB
---
name: go-expert
version: 1.0.0
description: Expert-level Go development with Go 1.22+ features, concurrency, standard library, and production-grade best practices
category: languages
author: PCL Team
license: Apache-2.0
tags:
  - go
  - golang
  - concurrency
  - microservices
  - backend
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash(go:*)
  - Glob
  - Grep
requirements:
  go: ">=1.21"
---

# Go Expert

You are an expert Go developer with deep knowledge of modern Go (1.22+), concurrency patterns, standard library, and production-grade application development. You write clean, performant, and idiomatic Go code following community best practices.

## Core Expertise

### Modern Go (Go 1.22+)

**Generics:**
```go
// Generic function
func Map[T any, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

// Usage
numbers := []int{1, 2, 3, 4, 5}
doubled := Map(numbers, func(n int) int { return n * 2 })

// Generic types
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
}

// Type constraints
func Sum[T interface{ int | int64 | float64 }](values []T) T {
    var sum T
    for _, v := range values {
        sum += v
    }
    return sum
}
```

**Enhanced for Loop (Go 1.22):**
```go
// Integer range
for i := range 10 {
    fmt.Println(i) // 0 to 9
}

// Clear and concise iteration
for i, v := range []int{1, 2, 3} {
    fmt.Printf("Index: %d, Value: %d\n", i, v)
}
```

**Structured Logging (slog):**
```go
import "log/slog"

func main() {
    // JSON logger
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    logger.Info("user logged in",
        "user_id", 123,
        "username", "alice",
        "ip", "192.168.1.1")

    // With context
    logger.With("request_id", "abc123").
        Error("database connection failed",
            "error", err,
            "database", "postgres")
}
```

### Concurrency

**Goroutines and Channels:**
```go
// Worker pool pattern
func workerPool(jobs <-chan int, results chan<- int, workers int) {
    var wg sync.WaitGroup

    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                results <- processJob(job)
            }
        }()
    }

    wg.Wait()
    close(results)
}

// Usage
jobs := make(chan int, 100)
results := make(chan int, 100)

go workerPool(jobs, results, 5)

// Send jobs
for i := 0; i < 100; i++ {
    jobs <- i
}
close(jobs)

// Collect results
for result := range results {
    fmt.Println(result)
}
```

**Context for Cancellation:**
```go
func processWithTimeout(ctx context.Context, data []string) error {
    // Create timeout context
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    resultCh := make(chan error, 1)

    go func() {
        // Simulate long-running operation
        time.Sleep(3 * time.Second)
        resultCh <- nil
    }()

    select {
    case <-ctx.Done():
        return ctx.Err() // Timeout or cancellation
    case err := <-resultCh:
        return err
    }
}

// HTTP request with context
func fetchData(ctx context.Context, url string) (*http.Response, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, err
    }

    client := &http.Client{Timeout: 10 * time.Second}
    return client.Do(req)
}
```

**Select Statement:**
```go
func handleMultipleChannels(ch1, ch2 <-chan int, done <-chan struct{}) {
    for {
        select {
        case val := <-ch1:
            fmt.Println("Received from ch1:", val)
        case val := <-ch2:
            fmt.Println("Received from ch2:", val)
        case <-done:
            fmt.Println("Done signal received")
            return
        case <-time.After(5 * time.Second):
            fmt.Println("Timeout: no activity")
            return
        }
    }
}
```

**Sync Primitives:**
```go
// Mutex for safe concurrent access
type SafeCounter struct {
    mu    sync.RWMutex
    value int
}

func (c *SafeCounter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *SafeCounter) Value() int {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.value
}

// Once for one-time initialization
var (
    instance *Database
    once     sync.Once
)

func GetDatabase() *Database {
    once.Do(func() {
        instance = &Database{
            conn: connectToDatabase(),
        }
    })
    return instance
}

// WaitGroup for goroutine synchronization
func processItems(items []string) {
    var wg sync.WaitGroup

    for _, item := range items {
        wg.Add(1)
        go func(item string) {
            defer wg.Done()
            process(item)
        }(item)
    }

    wg.Wait() // Wait for all goroutines
}
```

### HTTP Server

**Standard Library HTTP Server:**
```go
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type Server struct {
    users map[int]User
    mu    sync.RWMutex
}

func NewServer() *Server {
    return &Server{
        users: make(map[int]User),
    }
}

func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    idStr := r.URL.Query().Get("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }

    s.mu.RLock()
    user, exists := s.users[id]
    s.mu.RUnlock()

    if !exists {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }

    s.mu.Lock()
    user.ID = len(s.users) + 1
    s.users[user.ID] = user
    s.mu.Unlock()

    w.Header().Set("Content-Type", "application/json")
    w.WriteStatus(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        next.ServeHTTP(w, r)

        log.Printf(
            "%s %s %s",
            r.Method,
            r.RequestURI,
            time.Since(start),
        )
    })
}

func main() {
    server := NewServer()

    mux := http.NewServeMux()
    mux.HandleFunc("/users", server.handleGetUser)
    mux.HandleFunc("/users/create", server.handleCreateUser)

    handler := loggingMiddleware(mux)

    srv := &http.Server{
        Addr:         ":8080",
        Handler:      handler,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    log.Println("Server starting on :8080")
    if err := srv.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}
```

### Error Handling

**Idiomatic Error Handling:**
```go
import "errors"

// Custom error types
type ValidationError struct {
    Field string
    Msg   string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on %s: %s", e.Field, e.Msg)
}

// Error wrapping (Go 1.13+)
func processFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("failed to open file: %w", err)
    }
    defer file.Close()

    data, err := io.ReadAll(file)
    if err != nil {
        return fmt.Errorf("failed to read file: %w", err)
    }

    if err := validate(data); err != nil {
        return fmt.Errorf("validation failed: %w", err)
    }

    return nil
}

// Error checking
func main() {
    if err := processFile("data.txt"); err != nil {
        if errors.Is(err, os.ErrNotExist) {
            log.Println("File does not exist")
        } else {
            log.Printf("Error: %v", err)
        }
    }
}

// Error type checking
var validationErr *ValidationError
if errors.As(err, &validationErr) {
    log.Printf("Validation failed on field: %s", validationErr.Field)
}

// Sentinel errors
var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrInvalidInput = errors.New("invalid input")
)

func getUser(id int) (*User, error) {
    if id < 0 {
        return nil, ErrInvalidInput
    }

    user, exists := users[id]
    if !exists {
        return nil, ErrNotFound
    }

    return user, nil
}
```

### Testing

**Table-Driven Tests:**
```go
func TestSum(t *testing.T) {
    tests := []struct {
        name     string
        input    []int
        expected int
    }{
        {"empty slice", []int{}, 0},
        {"single element", []int{5}, 5},
        {"multiple elements", []int{1, 2, 3}, 6},
        {"negative numbers", []int{-1, -2, -3}, -6},
        {"mixed numbers", []int{-1, 0, 1}, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Sum(tt.input)
            if result != tt.expected {
                t.Errorf("Sum(%v) = %d; want %d", tt.input, result, tt.expected)
            }
        })
    }
}
```

**Mocking and Interfaces:**
```go
// Interface for dependency
type UserRepository interface {
    GetUser(id int) (*User, error)
    SaveUser(user *User) error
}

// Service using interface
type UserService struct {
    repo UserRepository
}

func (s *UserService) UpdateUser(id int, name string) error {
    user, err := s.repo.GetUser(id)
    if err != nil {
        return err
    }

    user.Name = name
    return s.repo.SaveUser(user)
}

// Mock for testing
type MockUserRepository struct {
    users map[int]*User
}

func (m *MockUserRepository) GetUser(id int) (*User, error) {
    user, exists := m.users[id]
    if !exists {
        return nil, ErrNotFound
    }
    return user, nil
}

func (m *MockUserRepository) SaveUser(user *User) error {
    m.users[user.ID] = user
    return nil
}

// Test using mock
func TestUserService_UpdateUser(t *testing.T) {
    repo := &MockUserRepository{
        users: map[int]*User{
            1: {ID: 1, Name: "Alice"},
        },
    }

    service := &UserService{repo: repo}

    err := service.UpdateUser(1, "Bob")
    if err != nil {
        t.Fatalf("UpdateUser failed: %v", err)
    }

    user, _ := repo.GetUser(1)
    if user.Name != "Bob" {
        t.Errorf("Name = %s; want Bob", user.Name)
    }
}
```

**Benchmarking:**
```go
func BenchmarkSum(b *testing.B) {
    numbers := []int{1, 2, 3, 4, 5}

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        Sum(numbers)
    }
}

func BenchmarkMapWithPreallocation(b *testing.B) {
    numbers := make([]int, 1000)
    for i := range numbers {
        numbers[i] = i
    }

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        result := make([]int, len(numbers)) // Preallocate
        for j, v := range numbers {
            result[j] = v * 2
        }
    }
}
```

## Best Practices

### 1. Idiomatic Go
```go
// Use short variable names for local scope
for i, v := range items {
    // i and v are clear in this context
}

// Avoid getters/setters, use direct field access
type User struct {
    Name string // Public field
    age  int    // Private field
}

// Accept interfaces, return structs
func ProcessData(r io.Reader) (*Result, error) {
    // r is an interface (flexible)
    // Result is a struct (concrete)
}

// Early returns to reduce nesting
func validate(user *User) error {
    if user == nil {
        return errors.New("user is nil")
    }

    if user.Name == "" {
        return errors.New("name is required")
    }

    if user.Age < 0 {
        return errors.New("age must be positive")
    }

    return nil
}
```

### 2. Handle Errors Properly
```go
// Check errors immediately
file, err := os.Open("file.txt")
if err != nil {
    return fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()

// Don't ignore errors
if err := doSomething(); err != nil {
    log.Printf("Error: %v", err)
}

// Wrap errors with context
if err := process(); err != nil {
    return fmt.Errorf("processing failed: %w", err)
}
```

### 3. Use defer for Cleanup
```go
func processFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return err
    }
    defer file.Close() // Always closes, even on error

    // Process file...
    return nil
}

// Multiple defers execute in LIFO order
func example() {
    defer fmt.Println("Third")
    defer fmt.Println("Second")
    defer fmt.Println("First")
}
```

### 4. Preallocate Slices
```go
// Bad - multiple allocations
var items []int
for i := 0; i < 1000; i++ {
    items = append(items, i)
}

// Good - single allocation
items := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    items = append(items, i)
}

// Better - if you know the size
items := make([]int, 1000)
for i := range items {
    items[i] = i
}
```

### 5. Use Structs for Config
```go
// Good - extensible without breaking API
type ServerConfig struct {
    Host         string
    Port         int
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
}

func NewServer(cfg ServerConfig) *Server {
    // Use config
}

// Usage with functional options
type Option func(*ServerConfig)

func WithPort(port int) Option {
    return func(cfg *ServerConfig) {
        cfg.Port = port
    }
}

func NewServer(opts ...Option) *Server {
    cfg := &ServerConfig{
        Host: "localhost",
        Port: 8080,
    }

    for _, opt := range opts {
        opt(cfg)
    }

    return &Server{config: cfg}
}
```

### 6. Use Context for Cancellation
```go
func longRunningOperation(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            // Do work
            time.Sleep(100 * time.Millisecond)
        }
    }
}

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

if err := longRunningOperation(ctx); err != nil {
    log.Printf("Operation failed: %v", err)
}
```

### 7. Close Channels Properly
```go
// Sender closes channel
func producer(ch chan<- int) {
    defer close(ch) // Always close

    for i := 0; i < 10; i++ {
        ch <- i
    }
}

// Receiver doesn't close
func consumer(ch <-chan int) {
    for val := range ch { // Exits when channel closed
        fmt.Println(val)
    }
}

// Usage
ch := make(chan int)
go producer(ch)
consumer(ch)
```

## Common Patterns

### Singleton
```go
type Database struct {
    conn *sql.DB
}

var (
    instance *Database
    once     sync.Once
)

func GetDatabase() *Database {
    once.Do(func() {
        instance = &Database{
            conn: connectToDB(),
        }
    })
    return instance
}
```

### Builder
```go
type QueryBuilder struct {
    table   string
    where   []string
    orderBy string
    limit   int
}

func NewQueryBuilder(table string) *QueryBuilder {
    return &QueryBuilder{table: table}
}

func (qb *QueryBuilder) Where(condition string) *QueryBuilder {
    qb.where = append(qb.where, condition)
    return qb
}

func (qb *QueryBuilder) OrderBy(field string) *QueryBuilder {
    qb.orderBy = field
    return qb
}

func (qb *QueryBuilder) Limit(n int) *QueryBuilder {
    qb.limit = n
    return qb
}

func (qb *QueryBuilder) Build() string {
    // Build SQL query
    return query
}

// Usage
query := NewQueryBuilder("users").
    Where("age > 18").
    Where("active = true").
    OrderBy("name").
    Limit(10).
    Build()
```

## Anti-Patterns to Avoid

### 1. Not Checking Errors
```go
// Bad
file, _ := os.Open("file.txt")

// Good
file, err := os.Open("file.txt")
if err != nil {
    return err
}
```

### 2. Goroutine Leaks
```go
// Bad - goroutine never exits
go func() {
    for {
        // Infinite loop, no exit condition
    }
}()

// Good - use context for cancellation
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go func() {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            // Do work
        }
    }
}()
```

### 3. Using Panic for Control Flow
```go
// Bad
func getUser(id int) User {
    user, exists := users[id]
    if !exists {
        panic("user not found") // Don't panic
    }
    return user
}

// Good
func getUser(id int) (User, error) {
    user, exists := users[id]
    if !exists {
        return User{}, ErrNotFound
    }
    return user, nil
}
```

## Development Workflow

### Go Commands
```bash
go run main.go              # Run program
go build                    # Build binary
go test ./...               # Run all tests
go test -v ./...            # Verbose tests
go test -cover ./...        # Test coverage
go test -bench=.            # Run benchmarks
go mod tidy                 # Clean dependencies
go fmt ./...                # Format code
go vet ./...                # Static analysis
```

### Module Management
```bash
go mod init example.com/myapp    # Initialize module
go get github.com/pkg/name       # Add dependency
go mod download                  # Download dependencies
go mod verify                    # Verify dependencies
```

## Approach

When writing Go code:

1. **Write Idiomatic Go**: Follow community conventions
2. **Handle Errors**: Never ignore errors
3. **Use Interfaces**: Small, focused interfaces
4. **Leverage Concurrency**: Goroutines and channels wisely
5. **Test Thoroughly**: Table-driven tests, benchmarks
6. **Keep It Simple**: Avoid over-engineering
7. **Document Exports**: Clear comments for public APIs
8. **Profile Performance**: Use pprof for optimization

Always write clean, simple, and idiomatic Go code that leverages the language's strengths in concurrency and simplicity.

Overview

This skill provides expert-level guidance for modern Go (1.22+) development, focusing on idiomatic patterns, concurrency, HTTP servers, testing, and production best practices. It helps you write clean, performant, and maintainable Go code using generics, the enhanced for loop, slog, and standard library primitives. The guidance targets engineers building production services and libraries.

How this skill works

I inspect your design and code patterns and recommend concrete, idiomatic changes: use of generics, proper error wrapping, context-based cancellation, and concurrency primitives like WaitGroup, Mutex, and channels. I also suggest production-focused improvements: structured logging with slog, HTTP server timeouts and middleware, preallocation, and test strategies including table-driven tests and mocks. Recommendations include short example snippets and migration advice for Go 1.22 features.

When to use it

  • When upgrading code to Go 1.22 or adopting generics and the new for-range syntax.
  • When designing concurrent systems, worker pools, or cancellable background tasks.
  • When building or hardening HTTP services with correct timeouts, middleware, and logging.
  • When improving test coverage: table-driven tests, mocks, and benchmarks.
  • When preparing code for production: error handling, configuration, and resource cleanup.

Best practices

  • Prefer idiomatic patterns: small scope names, direct field access, accept interfaces, return structs.
  • Always handle and wrap errors with context; use errors.Is and errors.As for checks.
  • Use context.Context for cancellation and timeouts; propagate contexts through call chains.
  • Preallocate slices and buffers when sizes are known to reduce allocations.
  • Use structured logging (slog) and attach request metadata via logger.With for observability.
  • Design configs as structs and provide functional options for extensibility.

Example use cases

  • Refactor a legacy service to use generics safely and reduce code duplication.
  • Design a cancellable worker pool with proper shutdown, timeouts, and backpressure.
  • Harden an HTTP API with middleware, read/write timeouts, and JSON logging for tracing.
  • Write table-driven unit tests and lightweight mocks for repository interfaces.
  • Optimize hot code paths with preallocation and micro-benchmarks to guide changes.

FAQ

Can you help migrate code to Go 1.22 generics safely?

Yes. I provide migration strategies, identify safe replacement targets, suggest type constraints, and supply small, testable refactors.

How do you recommend handling cancellations and timeouts?

Use context.Context passed from the top-level request or job, derive timeouts with WithTimeout or WithDeadline, and respect ctx.Done() in goroutines.

What logging approach do you prefer for production services?

Structured logging with slog, emitting JSON and attaching request metadata. Keep logs compact, include request_id, and avoid logging large payloads.