Why do the slices package functions use the constraint [S ~[]E, E any] instead of just []E?
Question 53HardGo 1.22 to 1.25
If a function is declared as func Filter[E any](s []E) []E, then passing a named slice type like type IDs []int returns plain []int. The caller loses the named type and its methods (for example, a sort.Interface implementation). The constraint S ~[]E means "any type whose underlying type is []E". The function then returns S and the caller's type is kept.
type IDs []int
func (ids IDs) String() string { return fmt.Sprint([]int(ids)) }
func FilterLossy[E any](s []E, keep func(E) bool) []E { /* ... */ return s }
func Filter[S ~[]E, E any](s S, keep func(E) bool) S {
out := s[:0:0] // zero-length, zero-cap, same type S
for _, v := range s {
if keep(v) {
out = append(out, v)
}
}
return out
}
func main() {
ids := IDs{1, 2, 3, 4}
even := Filter(ids, func(v int) bool { return v%2 == 0 })
fmt.Printf("%T %v\n", even, even) // main.IDs [2 4]
// FilterLossy(ids, ...) would return []int, losing the String method
}
Type inference derives E from S automatically ("constraint type inference"), so callers never write the type parameters. This is exactly how slices.Clone, slices.Compact, slices.Insert and similar are declared. Similarly, maps.Clone uses [M ~map[K]V, K comparable, V any].
Interviewer signal: you understand the ~ tilde (underlying-type sets) and why it matters for API design.
More on Arrays, Slices, Maps & Strings
- Q51Why can't you compare slices with ==? How do you check equality of slices, maps, and structs containing them?
- Q52How do you correctly allocate a 2D slice, and what are the performance trade-offs?
- Q54Which slices and maps package functions do you use daily, and how do they work with iterators (Go 1.23)?
- Q55Describe how Go maps were implemented before Go 1.24 (buckets, tophash, load factor, growth).
- Q56What changed in Go 1.24 with Swiss Tables? Why are they faster?
- Q57Why is map iteration order random, and how do you iterate deterministically?