How do you declare a generic type such as `type Cache[K comparable, V any] struct`, and what must its methods repeat?
answer
- list goes after the type name
- fields may use the parameters
- the receiver declares them again
- names matched by position, not spelling
- two instantiations are two types
basics
~20 sPut the type parameter list right after the type name; the parameters are usable in every field. Each method must redeclare them on the receiver, as in func (c *Cache[K, V]) Get(k K), and every instantiation is a distinct type.
solid answer
~40 sA generic type carries its type parameter list immediately after the type name: `type Cache[K comparable, V any] struct { entries map[K]*entry[V] }`. `K` and `V` are in scope through the whole type definition, so fields, embedded types and nested generic types may all use them. Methods do not get to reuse those names for free — the **receiver** must redeclare them: `func (c *Cache[K, V]) Put(k K, v V)`. The receiver's identifiers are fresh binding occurrences matched positionally, so `func (c *Cache[Key, Val])` is equally valid as long as the count matches; the blank identifier is allowed for ones the method ignores. Instantiating gives ordinary, distinct types: `Cache[string, []byte]` and `Cache[string, int]` are unrelated types with their own method sets and are not assignable to each other.
code
go · 17 linestype entry[V any] struct {
value V
hits int
}
type Cache[K comparable, V any] struct {
entries map[K]*entry[V]
order []K
}
func (c *Cache[K, V]) Put(k K, v V) {
c.entries[k] = &entry[V]{value: v}
c.order = append(c.order, k)
}
// Same method, different receiver parameter names - also legal:
func (c *Cache[Key, Val]) Len() int { return len(c.entries) }go deeper
Be ready to write the declaration and one method from memory, and to say that a field's type may use K and V. Knowing the brackets go straight after the type name is most of the answer here.
Explain that the receiver's bracket list declares new names bound by position, that instantiations are distinct types, and why a nested generic type inside the definition must carry its own type arguments.
An interviewer expects you to review someone's generic container: catch a receiver missing its parameters, spot an attempt to pin a type argument on a receiver, and know when a plain function beats a method.
Decide what your packages export: how many type parameters a public container may carry, whether the parameters belong on the type or on its constructor, and what naming convention keeps a generic file readable for the team.
## Declaring the type A generic type declaration is an ordinary `type` declaration with a type parameter list wedged between the name and the definition: ```go type entry[V any] struct { value V hits int } type Cache[K comparable, V any] struct { entries map[K]*entry[V] order []K // least-recently-used first } ``` Two things are worth spelling out here. First, `K` and `V` are in scope for the *entire* definition — every field, every embedded type, every nested instantiation. Second, when the definition names another generic type it must instantiate it: the field is `map[K]*entry[V]`, never `map[K]*entry`. A generic type name on its own is not a type. `K` is constrained by `comparable` here only because it is used as a map key and map keys must support `==`; the interesting part for this discussion is the *shape* of the declaration, not that particular constraint. ## Methods must redeclare the parameters on the receiver Methods on a generic type are declared the usual way, except that the receiver names the type parameters: ```go func (c *Cache[K, V]) Put(k K, v V) { c.entries[k] = &entry[V]{value: v} c.order = append(c.order, k) } func (c *Cache[K, V]) Get(k K) (V, bool) { e, ok := c.entries[k] if !ok { var missing V return missing, false } e.hits++ return e.value, true } ``` The identifiers in `Cache[K, V]` on the receiver are **declarations**, not references back to the type declaration. They are matched by position, so their spelling is yours to choose: ```go func (c *Cache[Key, Val]) Len() int { return len(c.entries) } ``` compiles exactly as well, with `Key` bound to the first parameter and `Val` to the second. The count must match the type's parameter list, and you may not add constraints on the receiver — the constraints live on the type declaration and are simply inherited. In practice teams keep the same names across the type and all its methods, because reading a file where every method renames the parameters is miserable. ## Every instantiation is its own type `Cache[string, []byte]` is a defined type in its own right. So is `Cache[string, int]`. They share source but nothing else: - they are **not assignable** to one another, and no conversion between them exists; - each has its own method set, with `K` and `V` resolved; - each has its own zero value, its own layout, its own `%T` output. This is the property that makes a generic container safe: a `Cache[string, []byte]` cannot accidentally receive an `int`, and there is no place where the compiler quietly widens one instantiation into another. ## Recursion is fine A generic type may refer to itself as long as it does so with its own type parameters: ```go type node[K comparable, V any] struct { key K val V next *node[K, V] } ``` That is the ordinary recursive-struct rule; the instantiation `node[K, V]` inside the definition is the same instantiation as the one being defined, so there is nothing infinite about it. ## Where it usually goes wrong when reading a codebase Someone new to a generic package tends to hit three things in order: 1. They write `var c Cache` and get a compile error, because a generic type name needs type arguments before it is a type. 2. They add a method and forget the receiver's bracket list — `func (c *Cache) Len() int` — and are told the receiver is a generic type without instantiation. 3. They try to write `func (c *Cache[string, []byte]) Len() int`, putting *type arguments* on a receiver. The receiver declares parameters; it cannot pin them. If you want a method that only exists for one instantiation, write a plain function taking `*Cache[string, []byte]` instead. ## Constructors Because a generic type never infers its own type arguments, most packages export a constructor function, which *can* have its arguments inferred at the call site in the usual way: ```go func NewCache[K comparable, V any](capacity int) *Cache[K, V] { return &Cache[K, V]{entries: make(map[K]*entry[V], capacity)} } ``` Note that this particular constructor still has to be called as `NewCache[string, []byte](128)`, since nothing in its argument list mentions `K` or `V`. Where a constructor does take a value of the element type, the call reads like any other Go call.
- Must the receiver use the same identifiers as the type declaration?No. The receiver's `Cache[Key, Val]` declares fresh names bound positionally to the type's parameters, so any spelling works and the blank identifier is allowed for ones the method ignores. Only the count has to match. Keeping the names consistent is a readability convention rather than a language rule.
- Are `Cache[string, int]` and `Cache[string, string]` assignable to each other?No. Each instantiation is a distinct defined type with its own method set and layout; there is no assignability and no conversion between them. That separation is the point — it is what stops a `Cache[string, []byte]` from ever holding an `int`.
- Can a method's receiver pin one type argument, as in `func (c *Cache[string, V]) Dump()`?No. A receiver *declares* type parameters, it cannot supply type arguments, so `string` there is read as a parameter name and rejected. If you need behaviour for exactly one instantiation, write an ordinary function that takes `*Cache[string, V]` or `*Cache[string, []byte]` as a parameter.
saying these in an interview costs you the question
- Writes methods with a bare *Cache receiver
- Thinks the receiver names must match the type declaration's names
- Puts type arguments such as [string, int] on a receiver
- Says all instantiations of one generic type are assignable
- Names a nested generic type without its type arguments in a field