agentleFS
AllThingsSmittyAllThingsSmitty/proven-skills/skills/go-types/SKILL.md

go-types

Skill1 starsChanged 27 days agoMITView on GitHub

Go type system advisor. Always use this skill when designing Go interfaces, working with structs and embedding, using generics, performing type assertions or type switches, defining custom error types, choosing between named types and aliases, or thinking about zero values. Use when the user says "how do I type this in Go", "when should I use an interface", "should I embed or compose", "how do I write a generic function", "how do I assert a type", "how do I wrap an error", "named type vs type alias", or "is my interface too big". Read this skill before answering any Go type system question.

We split this skill into the rules it gives an agent

8 rules. About 0 are common, 5 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Interface pollution warning: If you're defining an interface before you have two implementations, you probably don't need it yet.

    line 49
  2. Distinctive:

    Use generics when the logic is genuinely type-agnostic and duplicating it for each type would be worse than the added complexity.

    line 80
  3. Distinctive:

    Always use the comma-ok form for type assertions.

    line 130
  4. Distinctive:

    Use a type switch when branching on multiple types:

    line 145
  5. Very specific:

    If you find yourself writing many type assertions against an interface{} or any, that's a design signal: the types probably deserve a real …

    line 162
  6. Very specific:

    Check errors with errors.Is (sentinel values) and errors.As (typed unwrapping):

    line 188
  7. Very specific:

    Never compare errors with == unless they are sentinel values you control and don't wrap.

    line 201
  8. Distinctive:

    Default to named types for domain concepts.

    line 225

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Go Types

Go's type system rewards restraint. The most common mistakes (large interfaces, premature generics, type assertions without comma-ok) all come from over-engineering. Prefer concrete types until abstraction earns its place.

Interfaces

Interfaces are satisfied implicitly. No implements keyword; if a type has the methods, it satisfies the interface. This makes interfaces powerful and dangerous: it's easy to define one that nobody actually satisfies.

Keep interfaces small. The standard library's best interfaces have one or two methods: io.Reader, io.Writer, fmt.Stringer. One method = one behavior = easy to satisfy, easy to compose.

// Good: focused, composable
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Bad: eight methods means eight things your mock, stub, or test double must implement
type UserService interface {
    GetUser(id string) (*User, error)
    ListUsers() ([]*User, error)
    CreateUser(u *User) error
    UpdateUser(u *User) error
    DeleteUser(id string) error
    SearchUsers(query string) ([]*User, error)
    GetUserByEmail(email string) (*User, error)
    CountUsers() (int, error)
}

Define interfaces at the point of use, not the point of definition. The package that uses an abstraction should own the interface. The package that implements it should expose a concrete type. This avoids unnecessary coupling and import cycles.

// Bad: defined in the implementing package — forces callers to import it even if they want a subset
// package userrepo
type UserRepository interface { ... }

// Good: defined in the consuming package — only asks for what it needs
// package billing
type UserLookup interface {
    GetUser(id string) (*User, error)
}

Interface pollution warning. If you're defining an interface before you have two implementations, you probably don't need it yet. Concrete types are easier to understand and refactor. Add the interface when the second implementation arrives.

Structs and Embedding

Embedding promotes methods and fields from an inner type to the outer type. Use it to compose behavior, not to model inheritance.

// Embedding: Logger's methods are promoted to Server
type Server struct {
    *log.Logger
    addr string
}

s := Server{Logger: log.New(os.Stdout, "", 0), addr: ":8080"}
s.Printf("listening on %s", s.addr) // promoted from *log.Logger

Embed for behavior promotion; use a named field for has-a relationships.

// Use a named field when you want to control access or the relationship is conceptual
type Order struct {
    User    *User   // named field: an order has a user, not is-a user
    db      *sql.DB // named field: implementation detail
}

Embedding an interface inside a struct satisfies that interface at compile time and is useful for partial mocking in tests, but it's a footgun in production code: unimplemented methods panic at runtime.

Generics (Go 1.18+)

Use generics when the logic is genuinely type-agnostic and duplicating it for each type would be worse than the added complexity. Don't use them to avoid writing two functions.

// Good: logic is identical regardless of type, constraint is meaningful
func Map[T, U any](s []T, f func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

// Probably not worth it: the concrete version is clearer
func MaxInt(a, b int) int {
    if a > b { return a }
    return b
}

Use type constraints, not any, when you need operations on the type.

import "golang.org/x/exp/constraints"

func Max[T constraints.Ordered](a, b T) T {
    if a > b { return a }
    return b
}

Custom interface constraints work for domain-specific requirements:

type Numeric interface {
    ~int | ~int64 | ~float64
}

func Sum[T Numeric](vals []T) T {
    var total T
    for _, v := range vals {
        total += v
    }
    return total
}

Don't over-generify. Generics add cognitive overhead. If a function only ever handles one or two concrete types, write those functions. Readable, concrete code beats clever, generic code.

Type Assertions and Type Switches

Always use the comma-ok form for type assertions. A bare assertion panics if the type doesn't match.

var i interface{} = "hello"

// Bad: panics if i is not a string
s := i.(string)

// Good: safe
s, ok := i.(string)
if !ok {
    // handle the mismatch
}

Use a type switch when branching on multiple types:

func describe(i interface{}) string {
    switch v := i.(type) {
    case string:
        return fmt.Sprintf("string: %q", v)
    case int:
        return fmt.Sprintf("int: %d", v)
    case error:
        return fmt.Sprintf("error: %s", v.Error())
    default:
        return fmt.Sprintf("unknown: %T", v)
    }
}

If you find yourself writing many type assertions against an interface{} or any, that's a design signal: the types probably deserve a real interface or a concrete wrapper.

Error Types

error is an interface with one method: Error() string. Custom error types let you carry structured information and support errors.Is and errors.As.

// Custom error type — carry structured data
type NotFoundError struct {
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s %q not found", e.Resource, e.ID)
}

Wrap errors with %w to preserve the chain:

if err != nil {
    return fmt.Errorf("loading user %s: %w", id, err)
}

Check errors with errors.Is (sentinel values) and errors.As (typed unwrapping):

var nfe *NotFoundError
if errors.As(err, &nfe) {
    log.Printf("missing %s %s", nfe.Resource, nfe.ID)
}

if errors.Is(err, ErrTimeout) {
    // retry
}

Never compare errors with == unless they are sentinel values you control and don't wrap. Wrapping breaks == but not errors.Is.

Named Types vs Type Aliases

A named type (type Celsius float64) creates a distinct type. Assignment between the named type and its underlying type requires an explicit conversion. Use named types to prevent misuse, add methods, or express domain meaning.

type UserID string
type OrderID string

// Compiler stops you from mixing these up
func GetUser(id UserID) (*User, error) { ... }

var oid OrderID = "order-123"
GetUser(oid) // compile error: cannot use OrderID as UserID

A type alias (type Celsius = float64) is an exact synonym: the two names are interchangeable. Use aliases for gradual refactoring or to re-export a type from another package.

// Alias: allows migrating callers incrementally
type OldName = NewPackage.NewName

Default to named types for domain concepts. Use aliases only when you need referential identity across package boundaries.

Zero Values

Design structs so the zero value is either useful or clearly invalid. A zero value that silently misbehaves is a source of hard-to-find bugs.

// Good: zero value is immediately usable
var buf bytes.Buffer
buf.WriteString("hello") // no initialization required

// Good: zero value of sync.Mutex is an unlocked mutex
var mu sync.Mutex
mu.Lock()

// Bad: zero value panics — requires explicit initialization
type Cache struct {
    m map[string]string // nil map — writes will panic
}

// Fixed: initialize in a constructor or use sync.Map
func NewCache() *Cache {
    return &Cache{m: make(map[string]string)}
}

For types that cannot have a useful zero value, use a constructor function (NewFoo) and make the type's fields unexported so callers cannot bypass it.

Code Review Checklist

  • Interface defined in the implementing package rather than the consuming package
  • Interface with more than 3 methods (consider splitting)
  • Type assertion without the comma-ok form
  • Generic function constrained with any that uses operators (<, +, etc.) on the type parameter
  • Generic function written where a pair of concrete functions would be clearer
  • fmt.Errorf wrapping an error without %w
  • Error comparison with == instead of errors.Is / errors.As
  • Struct with a nil-map or nil-slice field that is not initialized before use
  • Named type used where a type alias (or vice versa) is more appropriate
  • Embedding an interface inside a struct in non-test code

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.