AlgoMaster Logo

Generic Types

Medium Priority22 min readUpdated June 6, 2026

Generic functions are useful, but a lot of the value of generics shows up when you put type parameters on a struct. A Stack that works for orders and products, a Set of any comparable type, a LinkedList whose nodes hold whatever you want. This lesson covers how to declare generic struct types, how to write methods on them, the extra constraints that map keys need, and a small set of rules that come up when first using a Stack[T].

A Generic Stack

A stack is the most useful first example because it's small, familiar, and shows every piece of the generic-type machinery in one go. Here's a Stack[T any] that holds any type and supports Push, Pop, Peek, and Len:

The orders come out in reverse insertion order, which is the defining property of a stack. The interesting line is type Stack[T any] struct. The square brackets after the type name introduce a type parameter list, just like in a generic function. T any means "one type parameter, called T, with no constraints." Inside the struct body, T is a placeholder you can use anywhere a normal type would go. Here it's the element type of the items slice.

Reading the code top to bottom, three things are doing real work:

  • The type parameter list [T any] on the struct definition.
  • The bracketed [T] on each method receiver, which we'll unpack next.
  • The variable declaration var pending Stack[Order], which fills in T with Order and gives us a concrete type.

Once Stack[Order] exists, it behaves exactly like any other struct. There's no runtime cost from being "generic" beyond what the equivalent hand-written OrderStack would have. The compiler stamps out a specialized version for each type argument you use (or shares one when the layouts match).

Methods on Generic Receivers

The line func (s *Stack[T]) Push(v T) looks busy, but every piece is doing a specific job. Read it slowly:

  • (s *Stack[T]) is the receiver. s is the receiver name, *Stack[T] is the receiver type.
  • [T] after Stack is the type parameter list that the method inherits from the type.
  • Push is the method name.
  • (v T) is the parameter list, which can use T because the method is "inside" the generic type.

Notice that the receiver echoes the type parameter list from the struct declaration. You wrote type Stack[T any] struct { ... }, so methods receivers say *Stack[T]. The brackets aren't optional. If you leave them off, the compiler can't link the method back to the generic type.

What happens when you forget it:

The commented-out version produces:

The compiler reaches T in the parameter list and has no idea what it refers to, because the receiver didn't introduce it. The brackets on the receiver are what bring T into scope for the rest of the method.

A second rule that surprises people: methods can't introduce new type parameters of their own. You can use the type parameters from the receiving type, but you can't add fresh ones to a single method.

The Go specification rules this out. The workaround is to write a top-level generic function instead of a method:

Convert is a regular function with two type parameters, T and U. It works on any Cart[T] and produces a slice of any U. The function-level type parameter U is what the method-level one couldn't be.

A Generic Set

Go's standard library doesn't have a Set type, so people build their own. The most idiomatic implementation is a map from the element type to struct{}, which uses zero bytes per value. Once you make that map generic, you hit the first interesting constraint problem:

The notable line is type Set[T comparable] map[T]struct{}. The constraint isn't any here, it's comparable. Why? Because Go map keys must support == and !=. If you wrote type Set[T any], the compiler would refuse the underlying map type, because any doesn't promise that T is comparable.

Try it and you'll see:

comparable is a built-in constraint that means "values of this type can be compared with ==." It's satisfied by all numeric types, strings, booleans, pointers, channels, interface types, and structs or arrays whose fields are all comparable. It's not satisfied by slices, maps, or functions, which is why you can't use those as map keys (or as Set elements here).

Notice another small detail: the methods have value receivers ((s Set[T])), not pointer receivers. Maps already point to shared storage internally, so the value receiver gets a copy of the map header but still operates on the same backing storage. wishlist.Add(...) mutates the underlying map even though s is a copy. This is the same reason you don't need *map[K]V parameters when passing maps to functions.

A Generic Linked List

Linked lists are the textbook example for parametric types, and Go's generics handle them cleanly. The node type is recursive: a Node[T] holds a T value and a pointer to another Node[T].

The pieces here are worth pointing out. Node[T] is a self-referential generic type: the next field has type *Node[T], which means "pointer to a node holding the same T." If you instantiate List[Review], every node in that list is a Node[Review], and you can't accidentally splice in a Node[Order]. The compiler enforces it.

The List[T] struct keeps a head pointer and a size counter, and its methods follow the same pattern as the stack: pointer receivers with bracketed type parameters. Walk takes a func(T) callback, which lets the caller decide what to do with each value without List knowing or caring.

Here's a Mermaid view of what reviews looks like in memory after the three Add calls. The list itself is a struct holding head and size. Each node carries one Review value and a next pointer:

The list's head field points to the first node, each node's next field points to the next, and the final node's next is nil. Adding a value means walking to the tail and linking a new node on. For a textbook linked list, that's O(n) per Add; for a production-grade one you'd keep a tail pointer too, but the structure is the same.

A useful exercise is to think about what changes if Node[T] had a prev *Node[T] field for a doubly linked list. The answer is "nothing about the generics machinery." The type parameter T is still just the element type. Generic types in Go don't care how complicated the surrounding shape is; they only care that T is used consistently.

Multi-Parameter Generic Types

A generic type can have more than one type parameter. The classic example is a map type that takes both the key and value types as parameters, with their own constraints. Here's an OrderedMap[K, V] that behaves like Go's built-in map but remembers insertion order:

The signature type OrderedMap[K comparable, V any] lists two parameters separated by a comma. K has the comparable constraint because it's used as a map key, and V has any because it can be anything. The constraint applies to the parameter immediately preceding it, just like in function definitions.

  • keys []K preserves insertion order. values map[K]V gives O(1) lookup. The two together get us "ordered map" behavior.
  • Add only appends to keys if the key is new. Re-adding the same key updates the value but keeps its original position. That's a design decision, you could just as easily move re-added keys to the back, but this matches what most people expect.
  • Keys() returns a copy so callers can't mutate the internal slice. Returning the internal slice directly would leak the invariant.
  • NewOrderedMap is a constructor function with its own type parameters. Without it, callers would have to write OrderedMap[string, *Order]{values: make(map[string]*Order)} every time, because the zero value of a generic map type has a nil inner map and Add would panic on the first call.

The two-type-parameter pattern shows up wherever you have a relationship between types: function pipelines (Map[T, U]), bidirectional maps, result types like Result[T, E]. Once you can read one of them, you can read all of them.

Instantiating Generic Types

Instantiating a generic type means filling in its type parameters with concrete types so you can actually allocate one. The syntax is the same as instantiating a generic function: the type name followed by square brackets with the type arguments.

Four different ways to bring a Stack into existence, and all of them include the type argument explicitly: Stack[int], Stack[string], Stack[Product], Stack[float64]. Once written, each is a distinct concrete type. You can't assign a Stack[int] to a variable of type Stack[string]; the compiler treats them as unrelated.

There's an important asymmetry between functions and types here. With a generic function, you can often omit the type arguments because the compiler infers them from the actual arguments you pass. With a generic type, you almost always have to write the type arguments out, because there's nothing for the compiler to infer from. Consider:

Here the compiler sees []int and concludes T = int. But for a generic type:

There's nothing for the compiler to look at; the type literal Box{val: 42} doesn't give it enough information to pick T. So you spell out Box[int] and move on. Go 1.21 improved type inference in some places, but for plain struct instantiation, the type argument is still required.

A nice consequence of this rule is that you can read any concrete generic type instantiation in isolation and know exactly what it holds. Stack[Order] is unambiguous. Set[string] is unambiguous. There's no "guess what T is from context" guessing game.

Zero Values and Generic Types

Every Go type has a zero value, and generic types are no exception. var s Stack[int] gives you a Stack[int] whose items field is the zero value of []int, which is nil. The interesting question is whether that zero-valued struct is actually usable.

For most well-designed generic types, the answer is yes:

The freshly declared Stack[int] has items == nil, and len(nil) == 0, and append(nil, 10) returns a new one-element slice. So Push works without any explicit initialization. This is a Go-wide pattern: lots of standard types (sync.Mutex, bytes.Buffer, strings.Builder) are designed so the zero value is immediately usable. When you write a generic type, aim for the same.

Some generic types can't follow this pattern because their inner state isn't a slice. The OrderedMap from earlier is the canonical example: its values field is a map, and the zero value of a map is nil. Writing to a nil map panics, so the zero OrderedMap is broken:

Two ways to fix this. The first is a constructor function that initializes the map, as we did earlier with NewOrderedMap. The second is lazy initialization inside the methods:

Now var m OrderedMap[string, int] is usable. The first Add allocates the inner map; later calls reuse it. Lazy initialization makes the zero value useful at the cost of a one-time check inside every method.

The Mermaid below summarizes the choice. The zero-value-usable design (left) skips the constructor and lazy check entirely, which works when the underlying types (slices, integers, pointers) all behave correctly when zero. The lazy-init design (middle) handles types like maps that need allocation. The constructor design (right) makes the requirement explicit at the call site:

Pick the design that fits the type. A Stack[T] backed by a slice should always be zero-value usable. An OrderedMap[K, V] or any type holding a channel, mutex, or non-trivial inner state usually needs either lazy init or a constructor. The decision affects every line of code that touches the type, so make it consciously.

A small note on type aliases versus generic types. Go 1.24 added generic type aliases (type MySet[T comparable] = map[T]struct{}), but they're aliases, not new types: a MySet[int] is exactly the same type as map[int]struct{} and shares its method set. The named generic types in this lesson (type Set[T comparable] map[T]struct{} without the =) define a brand-new type with its own methods. For most code you'll want the named version, since that's what lets you write Add and Has methods on it.

Common Pitfalls

Generic types share most of their pitfalls with non-generic types, but a few are unique to the bracketed syntax. Each of these compiles or panics in surprising ways the first time you hit them.

Forgetting the brackets on the receiver

The receiver type has to echo the type parameter list of the underlying type, brackets included. The brackets are required even though the type already declared them.

The error message points at the T on the second line, not at the missing brackets. The fix:

You can rename the receiver's type parameter if you want, but most code keeps the same letter. (c *Cache[T]) is the convention.

Trying to add type parameters to a method

Methods inherit the type parameters of the receiver type and can't add new ones. The compiler error is "method must have no type parameters." The workaround is to write a top-level function: func MapCartTo[T, U any](c *Cart[T], fn func(T) U) []U. The function-level form gives you both T and U to work with.

Pointer versus value receivers on generic types

The same rules that apply to non-generic types apply here, with one extra consideration. Methods that mutate the struct need pointer receivers, like always. But for generic types backed by a slice or map, value receivers can still work, because the slice header or map header gets copied while the backing storage is shared.

AddByValue modifies a local copy of b; the change is lost when the method returns. AddByPointer modifies the original. For a Set backed by a map, value receivers happen to work because the map header points to shared storage, but for a Stack or Bag backed by a slice that may grow, you need a pointer receiver. When in doubt, prefer pointer receivers for any generic type with mutating methods. The cost is negligible and the bugs are nasty.

Mixing instantiations

Stack[int] and Stack[int32] are different types. Pretty obvious. Less obvious: Stack[Order] and Stack[*Order] are also different types. Choosing element types thoughtfully matters more for generic types than for non-generic types, because every choice shows up in every method signature.

You can convert between them only by writing an explicit copy function. The compiler won't let you smuggle elements across.

Quiz

Generic Types Quiz

10 quizzes