What can a `[T fmt.Stringer]` type parameter express that a plain `fmt.Stringer` parameter cannot?
answer
- one of the two forgets your type
- where else can the type appear
- try passing a slice of your concrete type
- Go has no variance on composites
- one T per call means homogeneous
basics
~10 sA type parameter keeps the caller's concrete type, so it can appear in the result, in a second parameter, or as a slice element type. An interface parameter erases it, leaving only the methods.
solid answer
~40 sAn interface parameter says *I only need these methods, and I will forget what you gave me*. A type parameter says *whatever concrete type you pass, I can name it again elsewhere in this signature*. That matters in three places: returning the same type the caller passed, forcing two parameters to be the same type, and — most often in practice — taking a slice. Go has no variance, so a `[]*User` is not assignable to a `[]fmt.Stringer` even when `*User` has a `String` method; a caller would have to build a new slice element by element. `func Join[T fmt.Stringer](xs []T) string` accepts `[]*User` directly with `T` inferred. The flip side: the interface version accepts a genuinely mixed slice, and the generic one cannot, because there is one `T` per call.
code
go · 11 linestype User struct{ Name string }
func (u *User) String() string { return u.Name }
func joinAll(xs []fmt.Stringer) string { /* ... */ }
func Join[T fmt.Stringer](xs []T) string { /* ... */ }
users := []*User{{Name: "ana"}}
// joinAll(users) // compile error: []*User is not []fmt.Stringer
Join(users) // T is inferred as *Usergo deeper
Remember the concrete fact that trips everyone up: a slice of a concrete type cannot be passed where a slice of an interface is expected, even when the type implements it.
Explain the rule rather than the example: a type parameter can be named again in the result, in another parameter, or inside a composite type, and an interface parameter cannot.
Show you would reject an unnecessary type parameter in review. Cross out the parameter; if the signature means the same thing, the interface version is the one to merge.
Frame it as a commitment. An interface parameter leaves you room to accept new implementations later; a type parameter in an exported signature propagates into consumers' own code and is expensive to walk back.
## Two ways to say *any type with a String method* ```go func joinAll(xs []fmt.Stringer) string // interface element type func Join[T fmt.Stringer](xs []T) string // type parameter ``` Both constrain to types that satisfy `fmt.Stringer` (a single-method interface: `String() string`). They are not interchangeable, and the difference is about **what the signature can still say about the caller's type after it crosses the boundary**. ### An interface parameter erases the type Inside `joinAll`, every element has static type `fmt.Stringer`. You may call `String()`; you may not use the element as a `*User`, store it in a `[]*User`, or return it as the caller's type. That erasure is often exactly right — it is why `io.Writer` is such a good parameter type — but it has consequences the caller feels. ### A type parameter preserves the type Inside `Join`, `T` is one specific type for the whole call, and `T` can be written again anywhere in the signature. Three concrete abilities follow. **1. Return what you were given.** `func First[T fmt.Stringer](xs []T) T` returns a `*User` to a caller who passed `[]*User`. An interface version can only return `fmt.Stringer`, and the caller must assert to get their type back — the boilerplate problem one rung down the ladder. **2. Tie two parameters together.** `func Eq[T comparable](a, b T) bool` rejects `Eq(1, "x")` at compile time. `func Eq(a, b any) bool` accepts it happily and fails, if at all, at run time. **3. Accept the caller's slice.** This is the one that decides most real cases, and it is the sharpest corner in Go's type system for people arriving from other languages. ```go type User struct{ Name string } func (u *User) String() string { return u.Name } users := []*User{{Name: "ana"}} joinAll(users) // compile error: []*User is not []fmt.Stringer Join(users) // fine: T is inferred as *User ``` Go has **no variance**. `*User` implements `fmt.Stringer`, but `[]*User` and `[]fmt.Stringer` are unrelated types, and the same holds for `map[string][]*User`, `chan *User` and every other composite. The reason is representational: a `[]fmt.Stringer` is a slice of two-word interface values, while a `[]*User` is a slice of pointers, so no conversion can be free. To call `joinAll` the caller must build a second slice: ```go ss := make([]fmt.Stringer, len(users)) for i, u := range users { ss[i] = u } joinAll(ss) ``` That loop is the tax the type parameter removes. Every caller writes it, it is O(n), and it allocates a second slice the callee immediately throws away. ## When the interface parameter is the better answer The rule is not *generics win*. Prefer the interface parameter when: - **the collection is genuinely heterogeneous.** `[]fmt.Stringer` can hold a `*User` and a `time.Duration` in the same slice. `[]T` cannot: there is one `T` per call, so a generic function enforces homogeneity whether you wanted it or not. - **you only consume behaviour and return nothing of that type.** A function that takes an `io.Writer` and writes to it gains nothing from `[W io.Writer](w W)`; the type parameter is noise in the doc comment and in every compiler error. - **the value is stored in a field.** A struct field of interface type can hold different implementations over its life; a field of type `T` fixes it when the enclosing generic type is instantiated. ## The decision rule worth memorising **Does the concrete type need to appear anywhere else in the signature — the result, a second parameter, or inside a composite type like `[]T`?** If yes, that is what a type parameter is for. If no — you only call methods and hand nothing of that type back — the interface parameter is simpler, reads better, and stays flexible for callers with mixed collections. A useful sanity check on a proposed generic signature: cross out the type parameter and see whether the signature still says the same thing. `func Log[T fmt.Stringer](v T)` and `func Log(v fmt.Stringer)` say the same thing, so the first one is a worse version of the second. `func Clone[T fmt.Stringer](v T) T` and `func Clone(v fmt.Stringer) fmt.Stringer` do not say the same thing at all, and there the type parameter is earning its place.
- How would a caller pass a `[]*User` to the `[]fmt.Stringer` version anyway?By building a second slice: `ss := make([]fmt.Stringer, len(users))` and assigning each element in a loop. It is O(n), it allocates a slice the callee discards, and every caller repeats it. That loop is precisely what the type-parameter version removes.
- Does `[T fmt.Stringer]` force every element to be the same concrete type?Yes. There is one `T` per call, so `[]T` is homogeneous. If the collection genuinely mixes types — a `*User` and a `time.Duration` in one slice — the interface element type is the correct answer and the generic version cannot express it.
- Give a signature where the type parameter is pure noise.`func Log[T fmt.Stringer](v T)`. The body can only call `String()`, nothing of type `T` comes back out, so it says exactly what `func Log(v fmt.Stringer)` says while adding a type parameter to the docs and to every error message.
An interface parameter is a receipt that says only what the item can do; a type parameter is a receipt that still has the item's name on it, so you can be handed the same thing back.
saying these in an interview costs you the question
- Assumes []*User converts to []fmt.Stringer because *User implements it
- Says a type parameter is always the more modern choice
- Thinks a generic slice parameter can hold mixed concrete types
- Cannot name a case where the interface parameter is better
- Adds a type parameter that appears exactly once in the signature