agentsclimarketplace

Go generics

Skill ctoth/golang-skills-plugin/plugins/golang/skills/go-generics

Agent skill plugins for Go code quality and performance work.

Install
npx -y skills add ctoth/golang-skills-plugin --skill go-generics

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Guides Go generics restraint and constraint design — reach for a type parameter only when you would otherwise write the exact same code for multiple types (a container, or an algorithm over a slice/map/channel element type like map/filter/reduce), never when you only call a method on the value (that is an interface), and prefer the stdlib slices/maps/cmp helpers over hand-rolled generics. Covers constraints (any, comparable, cmp.Ordered, the ~T underlying-type element, unions like int | int64), type inference, generic type aliases (1.24), and self-referential constraints (1.26). Auto-invokes when writing or editing type parameters ([T any]), type constraints, generic functions or types, and on "should this be generic", "generic or interface here", or "make this generic". A type parameter earns its place only on real duplication; clarity first.

SKILL.md

15.6 KB, as published. Nobody here has run it

Go Generics

"If you find yourself writing the exact same code multiple times, where the only difference between the copies is that the code uses different types, consider whether you can use a type parameter." — When To Use Generics

"If all you need to do with a value of some type is call a method on that value, use an interface type, not a type parameter." — When To Use Generics

"Write Go programs by writing code, not by defining types. ... if you start writing your program by defining type parameter constraints, you are probably on the wrong path. Start by writing functions." — When To Use Generics

Type parameters (added in Go 1.18) let one function or type work over a set of types. They are powerful and easy to over-reach for. This skill is mostly about restraint: a type parameter earns its place only when the alternative is writing the same code for several types. When all you do is call a method, the right tool is an interface, not a generic — that boundary is the central decision here.


1. The Headline Rule: Don't Reach for Generics First

The default is no type parameter. Write the concrete function. Generics come later, and only on proven duplication: "you should avoid type parameters until you notice that you are about to write the exact same code multiple times" (When To Use Generics). The policy root states this as axis 2 — over-abstraction — and routes the depth here (go-idiomatic-discipline: "Clear is better than clever").

// WRONG — a type parameter with one concrete caller, reachable for "flexibility"
func First[T any](s []T) T { return s[0] }
func firstUser(us []User) User { return First(us) } // the only call site

// RIGHT — write the concrete function; add the type parameter when a SECOND
// element type actually needs the identical code.
func firstUser(us []User) User { return us[0] }

The starting guideline is blunt: "write Go programs by writing code, not by defining types ... Start by writing functions. It's easy to add type parameters later when it's clear that they will be useful" (When To Use Generics).


2. The Rules and Their Sources

RuleThe disciplineSource
Don't reach firstNo type parameter until you'd write the same code for multiple types"avoid type parameters until you notice that you are about to write the exact same code multiple times" (When Generics)
Method call → interfaceIf you only call a method on the value, use an interface"If all you need to do with a value of some type is call a method on that value, use an interface type, not a type parameter" (When Generics)
Don't swap interfaces for type paramsA working interface signature stays an interface"Omitting the type parameter makes the function easier to write, easier to read, and the execution time will likely be the same" (When Generics)
Same impl → type param; different impl → interfaceDecide by whether the method body is identical across types"if the implementation of a method is the same for all types, use a type parameter ... if the implementation is different for each type, then use an interface type" (When Generics)
Element-type functionsGeneric fits functions over slice/map/channel elements that assume nothing about the element"writing functions that operate on the special container types ... slices, maps, and channels ... the function code doesn't make any particular assumptions about the element types" (When Generics)
Prefer the stdlibUse slices/maps/cmp before rolling your ownslices.Contains, slices.Sort, maps.Keys (pkg.go.dev/slices)
Constraints are interfacesA constraint is an interface defining the permitted type set"In Go, type constraints must be interfaces" (Intro Generics)
~T for named typesUse ~T so types whose underlying type is T qualify"The expression ~string means the set of all types whose underlying type is string" (Intro Generics)

3. Generic or Interface? The Central Boundary

This is the decision that goes wrong most often, and the rule is mechanical. Look at what the body does with the value:

  • It only calls a method on the value → use an interface. "If all you need to do with a value of some type is call a method on that value, use an interface type, not a type parameter" (When To Use Generics).
  • The method body would be identical for every type, and the code operates on the data (indexing a slice, comparing with <, using as a map key) → use a type parameter. "if the implementation of a method is the same for all types, use a type parameter. Inversely, if the implementation is different for each type, then use an interface type and write different method implementations, don't use a type parameter" (When To Use Generics).
// WRONG — type parameter, but the body only calls String(); this is interface work
func describe[T fmt.Stringer](v T) string { return "value: " + v.String() }

// RIGHT — an interface says exactly this and reads simpler
func describe(v fmt.Stringer) string { return "value: " + v.String() }

And the explicit prohibition, so the model never "modernizes" a fine interface signature into a generic one: "do not replace interface types with type parameters" — "Don't make that kind of change" (When To Use Generics). The reciprocal rule lives in go-interfaces §5: any says nothing, and behavior is named with an interface, not a type parameter.


4. Constraints: any, comparable, cmp.Ordered, ~T, Unions

A constraint is an interface — "type constraints must be interfaces" (Intro Generics) — and it controls which operations the body may use. Pick the narrowest one that the code needs:

  • any — no operations beyond assignment and passing around (the element of a container you only store and retrieve).
  • comparable — allows == / != and use as a map key. It "denotes the set of all non-interface types that are strictly comparable" (Spec — Type constraints). Do not use any and then ==; that does not compile.
  • cmp.Ordered — allows < <= >= >. It "permits any ordered type: any type that supports the operators < <= >= >" (cmp). Prefer it over hand-writing the union.
  • ~T (approximation element)~int is "the set of all types whose underlying type is" int (Intro Generics). Without the tilde, a named type like type Celsius float64 would not satisfy a plain float64 term. The standard library uses ~ everywhere (cmp.Ordered is all ~ terms), and you should too.
  • Union elementsint | int64 is the union of the listed type sets (Spec — Type constraints). Combine with ~ for named types: ~int | ~int64.
// A constraint that admits named types built on the listed kinds, and allows <:
type Number interface {
	~int | ~int64 | ~float64
}

func Max[T cmp.Ordered](a, b T) T { // cmp.Ordered, not a hand-rolled union
	if a > b {
		return a
	}
	return b
}

comparable and constraints embedding it "may only be used as type constraints. They cannot be the types of values or variables" (Spec — Type constraints).


5. Type Inference — You Usually Don't Write the Type Arguments

Calling a generic function rarely needs explicit [T]: "In many cases the compiler can infer the type argument for T from the ordinary arguments," and when it does, "calling generic functions looks no different than calling ordinary functions" (Intro Generics).

xs := []int{3, 1, 2}
slices.Sort(xs)            // inferred [int]; not slices.Sort[int](xs)
m := Max(3, 7)            // inferred [int]
f := Max(2.5, 1.5)       // inferred [float64]

Write the explicit type argument only when inference can't succeed (e.g. make-style functions with no argument to infer from). Spelling out an inferable type argument is noise.


6. Prefer the Standard Library: slices, maps, cmp

Before writing a generic helper, check whether slices, maps, or cmp already has it. These are the type parameters the Go team already wrote and tested:

// WRONG — hand-rolled generic Contains, reinventing the stdlib
func Contains[T comparable](s []T, v T) bool {
	for _, x := range s {
		if x == v {
			return true
		}
	}
	return false
}

// RIGHT — the standard library function (Go 1.21+)
found := slices.Contains(s, v)             // func Contains[S ~[]E, E comparable](s S, v E) bool
slices.Sort(xs)                            // func Sort[S ~[]E, E cmp.Ordered](x S)
top := slices.Max(xs)                      // func Max[S ~[]E, E cmp.Ordered](x S) E
keys := slices.Sorted(maps.Keys(m))        // iterate keys, then sort

Note the stdlib signatures use the [S ~[]E, E any] shape: S ~[]E lets a named slice type (type IDs []int) pass while E names the element. The data operations on slices and maps — append-aliasing, nil-vs-empty, preallocation, clear — are owned by go-slices-and-maps; this skill only says "reach for those packages instead of your own type parameters."


7. Generic Data Structures — the Legitimate Case

The clearest place a type parameter earns its keep is a reusable container the language doesn't provide: "A general purpose data structure is something like a slice or map, but one that is not built into the language, such as a linked list, or a binary tree." Replacing an interface element with a type parameter "can permit data to be stored more efficiently ... it can also permit the code to avoid type assertions, and to be fully type checked at build time" (When To Use Generics).

// A type-safe stack: the same code for every element type, fully checked at build time.
type Stack[T any] struct{ items []T }

func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) {
	var zero T
	if len(s.items) == 0 {
		return zero, false
	}
	v := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return v, true
}

The same goes for functions over container element types — a Map/Filter over a slice, or MapKeys over a map — where "the function code doesn't make any particular assumptions about the element types" (When To Use Generics). Note var zero T is the idiom for the zero value of a type parameter; see go-zero-values-and-construction.


8. Generic Aliases (1.24) and Self-Referential Constraints (1.26)

Two language features gate on the module's go directive — confirm the version before using them (go-version-feature-map owns the table):

  • Generic type aliases (Go 1.24): "Go 1.24 now fully supports generic type aliases: a type alias may be parameterized like a defined type" (Go 1.24 release notes). type Set[T comparable] = map[T]struct{} is now legal.
  • Self-referential constraints (Go 1.26): "The restriction that a generic type may not refer to itself in its type parameter list has been lifted" (Go 1.26 release notes). This enables the recursive-constraint idiom:
type Adder[A Adder[A]] interface {
	Add(A) A
}

func sum[A Adder[A]](x, y A) A { return x.Add(y) }

Both are advanced; reach for them only when a concrete type or a plain constraint can't express the relationship.


9. Who Suffers When Generics Are Done Badly

The cost of a needless type parameter is paid downstream, never by the author at write time:

  • The reviewer forced to decode a func Process[T constraint, U any, V ~[]U](...) signature — three type parameters and a custom constraint — to make a one-line change, when a concrete function or a small interface said the same thing plainly. "Clear is better than clever" (Go Proverbs) is the empathy rule: the clever generic offloads its complexity onto whoever reads it next.
  • The teammate who copies your hand-rolled generic Contains/Map into a third package because it was easier than finding it — duplicating what slices already ships, tested.
  • The next caller of a func(v T) string whose body only calls v.String(): they get a confusing instantiation error where a plain fmt.Stringer parameter would have just worked.

Generics also carry a real implementation cost (monomorphization vs dictionaries, slower builds, harder-to-read errors), so the bar is "clear AND correct," not "as general as possible." When in doubt, write the concrete code.


10. Routing to the Specific Skills

  • go-idiomatic-discipline — the policy root. Premature generics are its axis 2 (over-abstraction); this skill holds the depth behind "don't reach for generics first."
  • go-interfaces — the other half of the boundary. If you only call a method, an interface is simpler than a type parameter; any says nothing. Coordinate the generic-vs-interface decision with §5 there.
  • go-slices-and-maps — the slices/maps data operations the helpers in §6 belong to (aliasing, nil-vs-empty, preallocation, clear).
  • go-zero-values-and-constructionvar zero T for a type parameter's zero value, and useful zero values generally.
  • go-version-feature-map — generic aliases (1.24) and self-referential constraints (1.26) are gated on the go directive.

11. Reference Files

High-frequency generics anti-patterns in LLM-generated Go, each with wrong/right code and citations:

${CLAUDE_SKILL_DIR}/references/common-mistakes.md

Source provenance for every claim in this skill:

${CLAUDE_SKILL_DIR}/references/sources.yaml

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.