Skip to content
Back to skills

Golang Patterns

ASecurity

Use when designing Go struct/interface APIs or optimizing allocation-heavy hot paths - functional options for constructors, defining narrow consumer-side interfaces, making zero values useful, and reducing allocations with preallocated slices, strings.Builder, and sync.Pool. For error handling, context propagation, or basic package layout use go-service-idioms instead; this skill is about API shape and allocation efficiency, not service plumbing.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 16, 2026
ai-agentsgosqltestingapi

Works with

  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 16, 2026

npx -y skills add shennawardana23/skillme --skill golang-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Golang Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Golang Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/shennawardana23-golang-patterns/badge)](https://www.skillsdirectory.com/skills/shennawardana23-golang-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: golang-patterns
description: Use when designing Go struct/interface APIs or optimizing allocation-heavy hot paths - functional options for constructors, defining narrow consumer-side interfaces, making zero values useful, and reducing allocations with preallocated slices, strings.Builder, and sync.Pool. For error handling, context propagation, or basic package layout use go-service-idioms instead; this skill is about API shape and allocation efficiency, not service plumbing.
license: Apache-2.0
metadata:
  version: "0.1.0"
---

# Go Struct/Interface Design and Allocation Efficiency

This skill covers two things `go-service-idioms` doesn't: how to shape Go
structs and interfaces so an API is hard to misuse, and how to cut
allocations on a hot path once a benchmark (see `golang-testing`) has
identified one. Apply the design guidance when writing new types and
constructors; apply the allocation guidance only where profiling data
justifies it — these are optimizations, not defaults to reach for
everywhere.

## Make the zero value useful

Design a type so `var t T` is immediately usable without a constructor call,
the way `bytes.Buffer` and `sync.Mutex` work.

```go
// Good: zero value works — count starts at 0, mutex starts unlocked
type Counter struct {
    mu    sync.Mutex
    count int
}

// Bad: zero value panics — nil map has no entries and can't be written to
type BadCounter struct {
    counts map[string]int
}
```

If a type genuinely cannot have a useful zero value (it needs a required
dependency), say so with an unexported field and a constructor, not a
runtime nil-check scattered across every method.

## Accept interfaces, return structs

A function should accept the narrowest interface it needs and return a
concrete type, not an interface — returning an interface hides information
the caller may need (which concrete fields exist, which extra methods are
available) for no benefit to the callee.

```go
// Good
func ProcessData(r io.Reader) (*Result, error) { ... }

// Bad — caller loses access to anything beyond io.Reader's own methods,
// and gains nothing since ProcessData already had the concrete type
func ProcessData(r io.Reader) (io.Reader, error) { ... }
```

## Define interfaces where they're consumed, not where they're implemented

The consumer package should declare the small interface it needs; the
implementing package stays unaware such an interface exists.

```go
// package service — defines exactly what it needs, nothing more
type UserStore interface {
    GetUser(id string) (*User, error)
}

type Service struct{ store UserStore }

// package postgres — implements GetUser (and much more) with no import
// of, or awareness of, the service package's interface
type Store struct{ db *sql.DB }
func (s *Store) GetUser(id string) (*User, error) { ... }
```

This keeps interfaces small and consumer-specific instead of one large
`Repository` interface every implementation must satisfy in full, and it
avoids an import cycle between the consumer and provider packages.

## Functional options for constructors with optional config

Use functional options once a constructor has more than two or three
optional parameters, instead of a config struct with exported zero-value
ambiguity (was `Timeout: 0` explicit or just unset?) or a long positional
argument list.

```go
type Server struct {
    addr    string
    timeout time.Duration
    logger  *slog.Logger
}

type Option func(*Server)

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

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

## Embedding for method promotion, not for shortcuts

Embed a type when the outer type genuinely *is-a* superset of the inner
type's behavior (a `Server` that logs is reasonably a `*Logger` plus
networking). Don't embed purely to save writing forwarding methods for a
relationship that isn't really "is-a" — it leaks the embedded type's full
method set into the outer type's API, including methods callers shouldn't
rely on.

```go
type Logger struct{ prefix string }
func (l *Logger) Log(msg string) { fmt.Printf("[%s] %s\n", l.prefix, msg) }

type Server struct {
    *Logger
    addr string
}
// s := &Server{Logger: &Logger{prefix: "SERVER"}, addr: ":8080"}
// s.Log("starting") // promoted from *Logger
```

## Allocation efficiency (apply only where a benchmark justifies it)

**Preallocate slices with a known or estimable size** — `append` on a nil
slice reallocates and copies repeatedly as it grows:

```go
// Bad: repeated reallocation as the slice grows
var results []Result
for _, item := range items {
    results = append(results, process(item))
}

// Good: one allocation up front
results := make([]Result, 0, len(items))
```

**Use `strings.Builder` (or `strings.Join`) instead of `+=` in a loop** —
each `+=` allocates a new string, making the loop O(n²) in total bytes
copied:

```go
var sb strings.Builder
for i, p := range parts {
    if i > 0 {
        sb.WriteString(",")
    }
    sb.WriteString(p)
}
// or, when there's no per-item logic: strings.Join(parts, ",")
```

**Use `sync.Pool` for short-lived objects allocated on a hot path** —
scratch buffers, encoder state — reused across calls instead of allocated
and garbage-collected every time. Only worth the complexity when a
benchmark (`golang-testing`) shows GC pressure from that allocation site:

```go
var bufferPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}

func ProcessRequest(data []byte) []byte {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer func() { buf.Reset(); bufferPool.Put(buf) }()
    buf.Write(data)
    return buf.Bytes()
}
```

A `sync.Pool` is not a cache with guaranteed retention — the runtime is free
to evict pooled items at any GC cycle, so never rely on it for anything
beyond amortizing allocation cost.

## Recommended linter baseline

```yaml
# .golangci.yml
linters:
  enable:
    - errcheck
    - gosimple
    - govet
    - ineffassign
    - staticcheck
    - unused
    - unconvert
    - unparam
```

Run `gofmt -l .` / `goimports -l .` in CI to fail on unformatted code rather
than just relying on editor integration, since a missed local `gofmt` run is
otherwise invisible until review.

## Gotchas

- Loop-variable capture in a goroutine or closure (`for _, id := range ids {
  go func() { use(id) }() }`) needed an explicit `id := id` shadow before Go
  1.22 — as of 1.22 each iteration gets its own variable and the shadow is a
  no-op, not a bug. Check the module's `go` directive in `go.mod` before
  assuming which behavior applies to a given codebase.
- A large embedded interface's full method set becomes part of the outer
  type's public API whether or not that was intended — embedding for
  convenience is an API design decision, not just an implementation detail.
- `sync.Pool`'s `New` function must return a pointer type (or something
  cheap to box) — pooling non-pointer values defeats the purpose, since
  `Get()`/`Put()` traffic in `any` and a value type still allocates on the
  interface conversion.

## Real-world grounding

The standard library's own `fmt` package uses `sync.Pool` internally to
reuse the `pp` printer struct across `Printf`/`Sprintf` calls — the same
pattern recommended above, applied by the stdlib to one of the hottest call
paths in a typical Go program.

Files in this skill

  • SKILL.md7.4 KB
  • evals/evals.json3.3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.