Skip to content

Generic Limitations — Interview Q&A

How to use this file

Each question has a short answer for memorization and a long answer for understanding. Practice both. In a real interview, give the short version first and expand only if asked.

Difficulty: - 🟢 Beginner - 🟡 Mid-level - 🔴 Senior - 🟣 Expert


Beginner 🟢

Q1. Can methods on a generic type declare their own type parameters?

Short: No. Methods may use only the receiver's type parameters.

Long: This is a deliberate language decision. Proposal 47781 asked for it; the Go team rejected the proposal as "not justified by the cost". The standard workaround is a free function: func MapBox[T, U any](b Box[T], f func(T) U) Box[U].

Q2. Can you switch v.(type) where v has a type parameter type?

Short: No. The type-switch operand must be an interface; a type parameter is not an interface.

Long: The compiler error is "cannot use type switch on type parameter value". Workaround: switch any(v).(type). This boxes v into an interface{} so the switch becomes well-typed — at the cost of reintroducing a runtime type check.

Q3. Is []Cat assignable to []Animal if Cat satisfies Animal?

Short: No. Go has no covariance.

Long: Each generic instantiation is a distinct type. Even when Cat satisfies Animal, the slice types []Cat and []Animal are unrelated. You must copy element-by-element.

Q4. Can a method type parameter eventually be added to Go?

Short: Possibly, but not in any planned release. Proposal 47781 is closed.

Long: The Go team has rejected the proposal multiple times. Future revival is conceivable but not on any current roadmap. Senior engineers should not design code anticipating it.

Q5. What predeclared functions work on a bare T any?

Short: Only new(T). Everything else needs a constraint guarantee.

Long: len, cap, make, append, copy, delete, close all require the type set to support the operation. A constraint like [T ~[]E] enables len, cap, append. new(T) is the exception because it just allocates sizeof(T) bytes regardless of what T is.

Q6. Can you use a type parameter as a constant type?

Short: No. const x T = 1 is rejected.

Long: Type parameters are runtime constructs (well, instantiated at compile time but not at constant-evaluation time). Constants are evaluated before instantiation, so the compiler has no T to use.

Q7. Can two distinct instantiations of a generic type be assigned to each other?

Short: No. Box[int] and Box[int64] are unrelated types.

Long: Even when underlying memory layouts coincide, the type system treats them as distinct. There is no implicit conversion; you must convert explicitly.

Q8. Can you embed a type parameter in a struct?

Short: No. Only defined types can be embedded.

Long: A struct field declared as just T (the type parameter) is rejected. You can have a field Value T, but not embed T directly.

Q9. Can you write ~Stringer to mean "any type with the underlying interface Stringer"?

Short: No. ~T requires T to be a non-interface type.

Long: The ~ operator widens to all types with the given underlying type. Interfaces have no meaningful underlying type in this sense, so ~Stringer is rejected.

Q10. Can interface methods have type parameters?

Short: No. Same rule as concrete methods.

Long: An interface declaration like interface { Map[U any](f func(T) U) } is refused. Free functions take their place.


Mid-level 🟡

Q11. What is the workaround for a method that "needs" a new type parameter?

Short: Make it a free function.

Long: func MethodEquivalent[T, U any](b Box[T], f func(T) U) Box[U] compiles to the same code a method would have. Call site is MethodEquivalent(b, f) instead of b.Method(f). No method chaining is the cost; no perf cost.

Q12. Why is the type-switch workaround any(v).(type) slow in hot loops?

Short: It boxes v into an interface{} and dispatches at runtime.

Long: Each call constructs the (type, data) interface header. For non-pointer types this may allocate. The type switch then walks the dynamic type. In tight loops over millions of items, this is measurable. Pull the switch out of the loop, or redesign to avoid the switch.

Q13. How do you simulate covariance for slices?

Short: Element-by-element copy with explicit conversion.

Long:

animals := make([]Animal, len(cats))
for i, c := range cats { animals[i] = c }
The cost is O(n). For large slices accessed in hot paths, this is a real concern. Options: avoid the conversion, work with []Cat throughout, or design the abstraction with Animal from the start.

Q14. Why does func Len[T any](v T) int { return len(v) } fail?

Short: The constraint any does not guarantee len is defined.

Long: len works on strings, slices, maps, channels, arrays. A bare T any includes int, *Foo, struct{}, etc. — types where len is not defined. Add a constraint like [T ~string | ~[]E] to make the operation legal.

Q15. Why can't you write ~Stringer?

Short: ~T requires T to be a non-interface concrete type.

Long: ~T denotes "all types whose underlying type is T". Interfaces have no underlying type for this purpose; allowing the syntax would require the compiler to compute structural subtyping at constraint-evaluation time. The spec rejects it.

Q16. What is the difference between a generic type alias and a generic type definition?

Short: Alias is the same type; definition is a new named type with its own method set.

Long: type Vec[T any] = []T (1.24+) makes Vec[int] the same as []int — no new methods, no distinct identity. type Vec[T any] []T (1.18+) makes Vec[int] a defined type, which can have methods and is incompatible with []int for assignment.

Q17. Can you specialize a generic function for a specific type?

Short: No. One body per shape.

Long: Workarounds: a separate non-generic function (func SumInt(s []int) int), runtime dispatch with any(v), or PGO (which devirtualizes hot paths automatically without code changes). True compile-time specialization is not in the language.

Q18. Why is there no overloading by type parameter set?

Short: Go has no function overloading at all.

Long: Go rejects overloading even for non-generic functions. Generics inherit the same restriction. Multiple Process[T] for different constraint sets is not allowed. Use different names (ProcessInt, ProcessString) or runtime dispatch.

Q19. What is the proposal number for type parameters in methods?

Short: 47781.

Long: Closed without action. The Go team's stated reason is implementation cost outweighs benefit, given that every concrete use case has a free-function workaround.

Q20. What is the proposal number for generic type aliases?

Short: 46477. Shipped in Go 1.24.

Long: Originally listed as future work in the Type Parameters proposal (43651). Took six years to design and ship because of identity-preservation concerns — aliases are supposed to be transparent, and adding type parameters introduced edge cases that needed careful resolution.


Senior 🔴

Q21. Why does Go not have higher-kinded types?

Short: They were excluded from proposal 43651 to keep the language small.

Long: HKTs (F[_] style abstractions) would let you write Functor[F], Monad[M], etc. They require a kind system, which complicates the type checker and compile times. The Go team judged the gain too small for the cost, especially because typical Go workloads do not benefit from HKT-style abstractions.

Q22. How do mature libraries work around the lack of HKTs?

Short: Per-container free functions. Libraries like samber/lo define Map, Filter, etc. once per container kind.

Long: Without HKTs, you cannot abstract "any container". Libraries enumerate the container kinds explicitly: MapSlice, MapMap, MapChan. The repetition is ergonomic to use even if verbose to define.

Q23. Why does generic instantiation produce distinct types?

Short: Type identity in Go is name + parameterization. Different parameters → different types.

Long: This rule keeps the type system simple and makes invariance natural. It also enables predictable compile-time dispatch — there is no need to compute structural relationships between instantiations.

Q24. When should you use codegen instead of generics?

Short: When the method set varies per type, the type space is open, or you need compile-time specialization.

Long: Codegen handles cases generics cannot: per-type methods (different signatures per instantiation), open type spaces (third parties define new types), and specialization (one optimized body per type). Modern Go often combines both: codegen for the public per-type API, generics for the shared core.

Q25. What is the cost of any(v).(type) for T = int?

Short: A boxing conversion (one heap allocation if escape analysis cannot prove otherwise) plus a type-switch dispatch.

Long: For pointer-shaped T, any(v) is essentially free. For value types like int or small structs, escape analysis may decide the boxed value escapes, causing a heap allocation. Inside a hot loop this matters. Mitigations: pull the switch out, or use an interface from the start.

Q26. Why are constraints with type elements barred from being runtime interface values?

Short: Type elements break the standard interface satisfaction model.

Long: A regular interface value carries a runtime type tag and dispatches methods by name. Type elements (int | string, ~int) constrain the type set without specifying methods. The compiler enforces them at instantiation only. Allowing them as runtime values would require a parallel dispatch mechanism, which the spec deliberately avoids.

Q27. How does PGO interact with generic dispatch?

Short: It can devirtualize a hot dictionary lookup into a direct call.

Long: Profile-guided optimization (Go 1.21+) records which instantiation is most common in production. The compiler can then emit a fast path that bypasses the runtime dictionary and uses the hot type's operations directly. This is the closest Go gets to specialization, and it is automatic — no code changes required.

Q28. What is the relationship between proposal 43651, 47781, and 46477?

Short: 43651 introduced generics. 47781 asked to extend them with method type parameters (rejected). 46477 asked for generic type aliases (accepted, shipped 1.24).

Long: Each subsequent proposal is measured against 43651's "small and simple" doctrine. Some are rejected outright (47781), some are accepted and take multiple releases to design (46477). The pattern shows the Go team's careful incrementalism.

Q29. How do you handle "T must implement either A or B" in Go?

Short: You can't with a constraint. Use runtime checks or two separate functions.

Long: Type elements in unions must be types, not interfaces. interface { A | B } (where A, B are interfaces) is refused. Workarounds: write two functions, one constrained to A, one to B; or accept any and check with reflection or type-switch at runtime.

Q30. What is invariance and why does Go have it?

Short: Different type arguments produce unrelated types.

Long: Invariance avoids the PutItemBack problem (a covariantly-typed list could accept items that are not actually of the element type, causing runtime errors). Go's invariance is the simplest discipline: no implicit conversion, no surprises, just an explicit copy when conversion is needed.


Expert 🟣

Q31. Could method type parameters be added without breaking the runtime?

Short: Possibly, but it would require non-trivial changes to the dictionary mechanism.

Long: Today, the runtime dictionary is keyed by the receiver type's parameters. Adding a per-method dictionary layer is implementable but complicates devirtualization and reflection. The performance and complexity cost is the main reason proposal 47781 was rejected. A hypothetical future proposal would need to demonstrate the cost is manageable.

Q32. Why is ~T for interfaces forbidden when "underlying interface" could be defined?

Short: The spec authors avoided structural relationships in constraints to keep the type-set computation simple.

Long: Allowing ~Stringer would require the compiler to compute "all types whose method set is a structural superset of Stringer's". This is computable but adds a non-trivial subtype relation to the constraint solver. The Go team's guideline is "constraints state explicit type sets", which forbids this.

Q33. How would you design a generic library with high-performance hot paths and uniform fallbacks?

Short: Generic core, specialized non-generic wrappers for hot types, callers explicitly pick.

Long: The library exposes Sum[T Number](s []T) T and SumInt(s []int) int. The latter is hand-optimized. Callers in performance-critical code use SumInt; everyone else uses the generic version. Document this explicitly. PGO may also auto-specialize the generic at the call site of frequent users.

Q34. What is the failure mode of "deeply nested generics"?

Short: Each level adds dictionary layers; binary size and compile time grow super-linearly.

Long: A type like Cache[K, V] parameterized over Wrapper[Inner[T]] causes cascading instantiations. Each new shape grows the binary by a stencil, and each instantiation costs a dictionary. Real-world depth limits are usually 2-3 levels; deeper designs benefit from flattening.

Q35. How do generics interact with unsafe and the GC?

Short: GC shape stenciling makes pointer-vs-scalar distinctions; unsafe.Pointer can break the assumed shape.

Long: A generic body assumes its T matches the GC shape it was stenciled for. Using unsafe to reinterpret a T as something with a different pointer layout is undefined behavior. The runtime dictionary will not match. This is a corner case that almost no real Go code hits, but it is in the spec for completeness.

Q36. Why was the original 1.18 design centered on interfaces-with-type-elements rather than a separate "constraints" feature?

Short: Reusing interfaces is simpler to teach and implement.

Long: Earlier proposals (2016, 2018) introduced "contracts" — a separate construct. Users found contracts confusing because they overlapped with interfaces. The 2020 proposal combined them: an interface can have methods, type elements, or both, and serves as a constraint when used in [T C]. Trade-off: some interfaces (comparable, those with type elements) cannot be used as runtime values.

Q37. What is the strategic argument for keeping invariance forever?

Short: Removing it later is impossible. Variance can be added if needed; Go has chosen not to.

Long: Once code depends on covariance, removing it breaks every user. Once code depends on invariance, adding covariance is backward-compatible. Go's "do less now, more later" ethos favors the latter. There is no current pressure to add variance, so it remains future-work-in-perpetuity.

Q38. How do generic limits manifest in pprof and the debugger?

Short: Generic functions show up with [go.shape.X] suffixes; per-instantiation behavior is not directly visible.

Long: Stenciled bodies share a name across instantiations. pprof flame graphs distinguish them by GC shape, which is useful for performance analysis but not for "which type is this?" debugging. Modern dlv versions handle generics correctly, but profiling generic code requires reading shape suffixes.

Q39. Could Go ever support overloading just for generics?

Short: Extremely unlikely.

Long: Adding overloading "only for generics" would create an inconsistency between generic and non-generic functions. The Go design philosophy is uniformity. Overloading is broadly disliked for its effect on readability and tooling. The most realistic path is the existing one: different names for different specializations.

Q40. What is the long-term outlook for generic limitations in Go?

Short: Most current limits will persist. Some (type aliases) have been resolved. Big features (HKT, method type params, variance) are unlikely.

Long: The Go team's track record is conservative incrementalism. Type aliases took six years; specifications-shaping proposals get long discussion. Senior engineers should plan code as if the current limits are permanent. If a limit becomes a real cost, the right response is a workaround, not waiting.


Summary

Memorize the short answers for fluency. Practice the long answers for depth. The most common interview themes are:

  • Method type parameters are forbidden (proposal 47781)
  • Type switches require any(v) boxing
  • Invariance — no covariance/contravariance
  • No HKT, no specialization, no overloading
  • Generic type aliases shipped in 1.24 (proposal 46477)
  • Free functions are the canonical method-type-parameter workaround
  • Codegen is still useful for per-type method sets
  • Reflection is the last-resort escape hatch

A confident candidate cites proposal numbers, explains why the limit exists, and proposes the right workaround for the situation.