jgarrison929

golang-expert

0
0
# Install this skill:
npx skills add jgarrison929/openclaw-skills --skill "golang-expert"

Install specific skill from multi-skill repository

# Description

Use when writing Go code requiring concurrency patterns, error handling, interfaces, testing, or module management. Invoke for goroutines, channels, performance optimization, or idiomatic Go architecture.

# SKILL.md


name: golang-expert
description: Use when writing Go code requiring concurrency patterns, error handling, interfaces, testing, or module management. Invoke for goroutines, channels, performance optimization, or idiomatic Go architecture.
triggers:
- Go
- Golang
- goroutine
- channel
- go.mod
- go.sum
- gin
- echo
- fiber
- cobra
- gRPC
- protobuf
role: specialist
scope: implementation
output-format: code


Go Expert

Senior Go specialist with deep expertise in concurrent systems, idiomatic patterns, and production-grade Go applications.

Role Definition

You are a senior Go engineer who builds simple, reliable, and efficient systems. You prioritize clarity over cleverness, explicit error handling, and proper use of Go's concurrency primitives. You follow Effective Go and the Go proverbs.

Core Principles

  1. Clear is better than clever — readability wins
  2. Don't communicate by sharing memory; share memory by communicating — channels over mutexes when possible
  3. Errors are values — handle them explicitly, wrap with context
  4. Accept interfaces, return structs — flexible inputs, concrete outputs
  5. A little copying is better than a little dependency — minimize external deps
  6. Make the zero value useful — design structs that work without initialization

Project Structure

myservice/
├── cmd/
│   └── myservice/
│       └── main.go           # Entry point
├── internal/
│   ├── handler/              # HTTP handlers
│   │   └── user.go
│   ├── service/              # Business logic
│   │   └── user.go
│   ├── repository/           # Data access
│   │   └── user.go
│   └── model/                # Domain types
│       └── user.go
├── pkg/                      # Public library code (if any)
│   └── validator/
├── api/                      # OpenAPI specs, proto files
├── migrations/               # SQL migrations
├── go.mod
├── go.sum
├── Makefile
└── README.md

go.mod Setup

module github.com/yourorg/myservice

go 1.22

require (
    github.com/jackc/pgx/v5 v5.5.0
    go.uber.org/zap v1.27.0
)

require (
    // indirect dependencies managed by go mod tidy
)

Error Handling

package service

import (
    "errors"
    "fmt"
)

// Sentinel errors for known failure modes
var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrConflict     = errors.New("conflict")
)

// Custom error type with context
type ValidationError struct {
    Field   string
    Message string
}

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

// Wrapping errors with context
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
    user, err := s.repo.FindByID(ctx, id)
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
        }
        return nil, fmt.Errorf("fetching user %s: %w", id, err)
    }
    return user, nil
}

// Caller checks with errors.Is / errors.As
func handleGetUser(w http.ResponseWriter, r *http.Request) {
    user, err := svc.GetUser(r.Context(), userID)
    if err != nil {
        var valErr *ValidationError
        switch {
        case errors.Is(err, ErrNotFound):
            http.Error(w, "User not found", http.StatusNotFound)
        case errors.As(err, &valErr):
            http.Error(w, valErr.Error(), http.StatusBadRequest)
        default:
            http.Error(w, "Internal error", http.StatusInternalServerError)
        }
        return
    }
    json.NewEncoder(w).Encode(user)
}

Interfaces and Composition

// Small, focused interfaces
type UserReader interface {
    GetUser(ctx context.Context, id string) (*User, error)
    ListUsers(ctx context.Context, filter UserFilter) ([]*User, error)
}

type UserWriter interface {
    CreateUser(ctx context.Context, user *User) error
    UpdateUser(ctx context.Context, user *User) error
    DeleteUser(ctx context.Context, id string) error
}

// Compose interfaces
type UserRepository interface {
    UserReader
    UserWriter
}

// Accept interfaces, return structs
type UserService struct {
    repo   UserRepository
    cache  Cache
    logger *slog.Logger
}

func NewUserService(repo UserRepository, cache Cache, logger *slog.Logger) *UserService {
    return &UserService{
        repo:   repo,
        cache:  cache,
        logger: logger,
    }
}

// Functional options pattern for complex construction
type Option func(*Server)

func WithPort(port int) Option {
    return func(s *Server) { s.port = port }
}

func WithTimeout(d time.Duration) Option {
    return func(s *Server) { s.timeout = d }
}

func WithLogger(l *slog.Logger) Option {
    return func(s *Server) { s.logger = l }
}

func NewServer(opts ...Option) *Server {
    s := &Server{
        port:    8080,
        timeout: 30 * time.Second,
        logger:  slog.Default(),
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

Concurrency Patterns

// Worker pool
func processItems(ctx context.Context, items []Item, workers int) []Result {
    in := make(chan Item)
    out := make(chan Result, len(items))

    // Start workers
    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for item := range in {
                out <- process(item)
            }
        }()
    }

    // Send work
    go func() {
        for _, item := range items {
            select {
            case in <- item:
            case <-ctx.Done():
                break
            }
        }
        close(in)
    }()

    // Wait and collect
    go func() {
        wg.Wait()
        close(out)
    }()

    var results []Result
    for r := range out {
        results = append(results, r)
    }
    return results
}

// Fan-out / fan-in
func fanOut(ctx context.Context, urls []string) <-chan *http.Response {
    results := make(chan *http.Response, len(urls))
    var wg sync.WaitGroup

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            req, _ := http.NewRequestWithContext(ctx, "GET", u, nil)
            resp, err := http.DefaultClient.Do(req)
            if err == nil {
                results <- resp
            }
        }(url)
    }

    go func() {
        wg.Wait()
        close(results)
    }()
    return results
}

// Semaphore for rate limiting
type Semaphore struct {
    ch chan struct{}
}

func NewSemaphore(max int) *Semaphore {
    return &Semaphore{ch: make(chan struct{}, max)}
}

func (s *Semaphore) Acquire(ctx context.Context) error {
    select {
    case s.ch <- struct{}{}:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

func (s *Semaphore) Release() {
    <-s.ch
}

// errgroup for structured concurrency
import "golang.org/x/sync/errgroup"

func fetchAll(ctx context.Context, urls []string) ([]string, error) {
    g, ctx := errgroup.WithContext(ctx)
    results := make([]string, len(urls))

    for i, url := range urls {
        i, url := i, url // capture loop vars
        g.Go(func() error {
            body, err := fetch(ctx, url)
            if err != nil {
                return fmt.Errorf("fetching %s: %w", url, err)
            }
            results[i] = body
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        return nil, err
    }
    return results, nil
}

// Context with timeout and cancellation
func processWithTimeout(parentCtx context.Context) error {
    ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
    defer cancel()

    select {
    case result := <-doWork(ctx):
        fmt.Println("Got result:", result)
        return nil
    case <-ctx.Done():
        return fmt.Errorf("timed out: %w", ctx.Err())
    }
}

HTTP Server Patterns

// Standard library HTTP server (Go 1.22+ routing)
mux := http.NewServeMux()

mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)

// Middleware
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        next.ServeHTTP(wrapped, r)
        slog.Info("request",
            "method", r.Method,
            "path", r.URL.Path,
            "status", wrapped.statusCode,
            "duration", time.Since(start),
        )
    })
}

type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (w *responseWriter) WriteHeader(code int) {
    w.statusCode = code
    w.ResponseWriter.WriteHeader(code)
}

// Graceful shutdown
func main() {
    srv := &http.Server{
        Addr:         ":8080",
        Handler:      loggingMiddleware(mux),
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 30 * time.Second,
        IdleTimeout:  120 * time.Second,
    }

    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    srv.Shutdown(ctx)
}

Testing

// Table-driven tests
func TestParseAmount(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    int
        wantErr bool
    }{
        {name: "valid integer", input: "100", want: 100},
        {name: "valid decimal", input: "10.50", want: 1050},
        {name: "zero", input: "0", want: 0},
        {name: "empty string", input: "", wantErr: true},
        {name: "negative", input: "-5", wantErr: true},
        {name: "not a number", input: "abc", wantErr: true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := ParseAmount(tt.input)
            if tt.wantErr {
                if err == nil {
                    t.Fatal("expected error, got nil")
                }
                return
            }
            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if got != tt.want {
                t.Errorf("got %d, want %d", got, tt.want)
            }
        })
    }
}

// Test with mock interface
type mockRepo struct {
    users map[string]*User
}

func (m *mockRepo) GetUser(_ context.Context, id string) (*User, error) {
    u, ok := m.users[id]
    if !ok {
        return nil, ErrNotFound
    }
    return u, nil
}

func TestUserService_GetUser(t *testing.T) {
    repo := &mockRepo{
        users: map[string]*User{
            "1": {ID: "1", Name: "Alice"},
        },
    }
    svc := NewUserService(repo, nil, slog.Default())

    t.Run("existing user", func(t *testing.T) {
        user, err := svc.GetUser(context.Background(), "1")
        if err != nil {
            t.Fatal(err)
        }
        if user.Name != "Alice" {
            t.Errorf("got %s, want Alice", user.Name)
        }
    })

    t.Run("missing user", func(t *testing.T) {
        _, err := svc.GetUser(context.Background(), "999")
        if !errors.Is(err, ErrNotFound) {
            t.Errorf("got %v, want ErrNotFound", err)
        }
    })
}

// Benchmarks
func BenchmarkProcessItems(b *testing.B) {
    items := generateItems(1000)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        processItems(context.Background(), items, 4)
    }
}

// Test helpers
func newTestServer(t *testing.T) (*httptest.Server, *UserService) {
    t.Helper()
    repo := &mockRepo{users: make(map[string]*User)}
    svc := NewUserService(repo, nil, slog.Default())
    mux := http.NewServeMux()
    registerRoutes(mux, svc)
    srv := httptest.NewServer(mux)
    t.Cleanup(srv.Close)
    return srv, svc
}

Generics (Go 1.18+)

// Generic filter/map/reduce
func Filter[T any](items []T, pred func(T) bool) []T {
    var result []T
    for _, item := range items {
        if pred(item) {
            result = append(result, item)
        }
    }
    return result
}

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
}

// Generic cache
type Cache[K comparable, V any] struct {
    mu    sync.RWMutex
    items map[K]V
}

func NewCache[K comparable, V any]() *Cache[K, V] {
    return &Cache[K, V]{items: make(map[K]V)}
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.items[key]
    return v, ok
}

func (c *Cache[K, V]) Set(key K, value V) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items[key] = value
}

Common Anti-Patterns

// ❌ BAD: Ignoring errors
data, _ := json.Marshal(user)

// ✅ GOOD: Handle every error
data, err := json.Marshal(user)
if err != nil {
    return fmt.Errorf("marshaling user: %w", err)
}

// ❌ BAD: Goroutine leak — no way to stop
go func() {
    for {
        doWork()
        time.Sleep(time.Second)
    }
}()

// ✅ GOOD: Respect context cancellation
go func(ctx context.Context) {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()
    for {
        select {
        case <-ticker.C:
            doWork()
        case <-ctx.Done():
            return
        }
    }
}(ctx)

// ❌ BAD: Passing sync.Mutex by value (copies the lock)
func process(m sync.Mutex) { ... }

// ✅ GOOD: Pass pointer or embed
func process(m *sync.Mutex) { ... }

// ❌ BAD: Using init() for complex setup
func init() {
    db, err = sql.Open("postgres", os.Getenv("DB_URL"))
    // Can't return error!
}

// ✅ GOOD: Explicit initialization in main
func main() {
    db, err := sql.Open("postgres", os.Getenv("DB_URL"))
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
}

slog (Structured Logging — Go 1.21+)

import "log/slog"

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))
slog.SetDefault(logger)

slog.Info("processing request",
    "method", r.Method,
    "path", r.URL.Path,
    "user_id", userID,
)

// With context
logger = logger.With("service", "user-api")
logger.ErrorContext(ctx, "failed to fetch user",
    "user_id", id,
    "error", err,
)

Adapted from buildwithclaude by Dave Poon (MIT)

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.