# Go Coding

> Create or modify Go skipper code

- Skill: `zalando/go-coding` (Agent Skill)
- Install (CLI): `npx skillmds@latest add zalando/go-coding`
- Raw SKILL.md: https://api.skillmd.com/api/skills/zalando/go-coding/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: zalando (https://skillmd.com/u/zalando)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/zalando/go-coding

---

## What I do

- focus on Go code and architecture

## When to use me

Use this when you are reading or writing Go code

## Quick start

Use `gopls` to navigate the code.

### Package and type organization

Group related types together using a single `type (...)` block. Use iota for
enumerated constants. Keep unexported types lowercase; only export what callers
need.

```go
type (
    matchBehavior int
    matchMode     int
)

const (
    matchBehaviorAll matchBehavior = iota
    matchBehaviorAny

    matchModeExact matchMode = iota
    matchModeRegexp
)

type (
    spec struct {
        name          string
        matchBehavior matchBehavior
        matchMode     matchMode
        reg           *registry
    }
    predicate struct {
        kv            map[string][]valueMatcher
        matchBehavior matchBehavior
        cache         sync.Map
    }
)
```

### Precompute in Create/New, not in the hot path

`CreateFilter` and `Create` run at route-build time. Precompute everything
costly there (compile regexps, build maps, normalize config). The hot path
(`Request`, `Response`, `Match`) must be allocation-free on the common case.

```go
func (s *spec) Create(args []any) (routing.Predicate, error) {
    if len(args) == 0 || len(args)%2 != 0 {
        return nil, predicates.ErrInvalidPredicateParameters
    }
    kv := make(map[string][]valueMatcher)
    for i := 0; i < len(args); i += 2 {
        key, keyOk := args[i].(string)
        value, valueOk := args[i+1].(string)
        if !keyOk || !valueOk {
            return nil, predicates.ErrInvalidPredicateParameters
        }
        re, err := regexp.Compile(value)
        if err != nil {
            return nil, predicates.ErrInvalidPredicateParameters
        }
        kv[key] = append(kv[key], regexMatcher{regexp: re})
    }
    return &predicate{kv: kv}, nil
}
```

### Cache expensive Create results to survive frequent route table rebuilds

Route tables rebuild every few seconds. Cache `CreateFilter`/`Create` results
keyed by config args to avoid recreating identical instances and leaking memory.
Use `sync.Mutex` around the map; check-then-insert is fine for idempotent
objects.

```go
type spec struct {
    mu        sync.Mutex
    filterMap map[string]*myFilter
}

func (s *spec) CreateFilter(args []interface{}) (filters.Filter, error) {
    key, err := keyFromArgs(args)
    if err != nil {
        return nil, err
    }
    s.mu.Lock()
    if f, ok := s.filterMap[key]; ok {
        s.mu.Unlock()
        return f, nil
    }
    s.mu.Unlock()
    // ... build f ...
    s.mu.Lock()
    s.filterMap[key] = f
    s.mu.Unlock()
    return f, nil
}
```

### Cache hot-path lookups with sync.Map

For repeated lookups in `Match`/`Request`/`Response` (e.g. parsed tokens,
validated values), use `sync.Map` — it avoids a single global mutex bottleneck
under heavy read concurrency. Periodically clear it rather than tracking
per-entry TTLs; infrequent full clears are cheap.

```go
type predicate struct {
    kv    map[string][]valueMatcher
    cache sync.Map // wire-string → bool
}

// in a background goroutine started at spec construction time:
func (r *registry) clean() {
    tick := time.NewTicker(time.Hour)
    defer tick.Stop()
    for {
        select {
        case <-r.quit:
            return
        case <-tick.C:
            r.mu.Lock()
            for _, p := range r.predicateMap {
                p.cache.Clear()
            }
            r.mu.Unlock()
        }
    }
}
```

### Avoid allocations in string parsing

Prefer `strings.Index` / `strings.Cut` over `strings.Split` to avoid
allocating a slice. Use iterator-style helpers (or `strings.SplitSeq` in
go1.24+) to range over parts without materializing them.

```go
// zero-allocation split iteration (backport of strings.SplitSeq)
func splitSeq(s, sep string) func(yield func(string) bool) {
    return func(yield func(string) bool) {
        for {
            i := strings.Index(s, sep)
            if i < 0 {
                break
            }
            if !yield(s[:i]) {
                return
            }
            s = s[i+len(sep):]
        }
        yield(s)
    }
}

// usage
for part := range splitSeq(header, ",") {
    token, value, found := strings.Cut(strings.TrimSpace(part), "=")
    _ = token; _ = value; _ = found
}
```

### Skip work when input hasn't changed

For dataclients and watchers, record the last successful input (e.g. file
bytes, ETag) and return early without parsing when nothing changed.

```go
func (c *WatchClient) loadUpdates() watchResponse {
    content, err := os.ReadFile(c.fileName)
    if err != nil { ... }

    if bytes.Equal(content, c.lastContent) {
        return watchResponse{} // no change
    }
    r, err := eskip.Parse(string(content))
    if err != nil {
        c.lastContent = nil
        return watchResponse{err: err}
    }
    c.lastContent = content
    ...
}
```

### Error handling

Wrap errors with context using `fmt.Errorf("...: %w", err)`. Return sentinel
errors (`predicates.ErrInvalidPredicateParameters`) from `Create`/`CreateFilter`
for invalid config so callers can distinguish config errors from runtime errors.
Never swallow errors in the hot path.

```go
req, endpointMetrics, err := p.mapRequest(ctx, requestContext)
if err != nil {
    return nil, &proxyError{err: fmt.Errorf("could not map backend request: %w", err)}
}
```

