How would you write a generic helper `Map[T, U any](s []T, f func(T) U) []U`, and how should it allocate its result?
answer
- two element types, two type parameters
- you already know the final length
- capacity zero-length, then append
- make with a length plus append doubles it
- filtering cannot know its length
basics
~20 sTwo type parameters carry the input and output element types. Allocate with make([]U, 0, len(s)) and append one result per element: the length is known exactly, so preallocating capacity avoids regrowth. Using make([]U, len(s)) with append instead doubles the result.
solid answer
~50 sThe transform has two independent element types, so it needs two type parameters: `T` for the input elements and `U` for whatever `f` produces. The body is a `range` loop that appends `f(v)` to an output slice. Because the output length equals `len(s)` exactly, allocate `make([]U, 0, len(s))` — one allocation, no regrowth — and then `append`. The classic bug is `make([]U, len(s))` followed by `append`, which produces `2*len(s)` elements with the zero values in front; if you allocate with a length you must assign by index instead. A filter is different: the surviving count is unknown, so starting from a `var out []T` avoids reserving a full-length array when few elements survive. Document what an empty or nil input returns — `make` gives a non-nil empty slice, which encodes as `[]` rather than `null`.
code
go · 17 linesfunc Map[T, U any](s []T, f func(T) U) []U {
out := make([]U, 0, len(s)) // capacity, length stays 0
for _, v := range s {
out = append(out, f(v))
}
return out
}
func Filter[T any](s []T, keep func(T) bool) []T {
var out []T // unknown final length; nil grows fine
for _, v := range s {
if keep(v) {
out = append(out, v)
}
}
return out
}go deeper
Be able to write the loop: allocate, range, append, return. Know that appending to a slice created with make and a length adds elements after the zero values already there.
Explain why the helper needs two type parameters, why capacity is preallocated rather than length, and how a filter differs because its output length is unknown until the loop ends.
Show the API judgment: document nil versus empty, choose the fallible variant's error policy deliberately, and be willing to say the helper should not exist when a plain loop reads better and inlines.
Own the question of how much of a functional toolkit belongs in a shared package. Every helper is API surface other teams depend on, and one that only saves a three-line loop costs more in review and onboarding than it returns.
## The shape ```go func Map[T, U any](s []T, f func(T) U) []U { out := make([]U, 0, len(s)) for _, v := range s { out = append(out, f(v)) } return out } ``` Three things are worth spelling out. **Two type parameters, because there are two independent types.** A transform maps elements of one type to elements of another: `[]time.Duration` to `[]float64`, `[]CoverageRow` to `[]string`. `T` appears in the input slice and in `f`'s parameter; `U` appears in `f`'s result and in the returned slice. Both are ordinary type parameters constrained by `any`, because the body never does anything to a `T` or a `U` except move it around — it does not compare them, add them or call methods on them, so it needs no stronger constraint. **Capacity, not length.** `make([]U, 0, len(s))` reserves the backing array up front and leaves the length at zero, so the `append` calls fill it without a single regrowth. `append` on a slice with spare capacity writes in place; on a full slice it allocates a bigger array and copies. For a transform you know the final length exactly, so there is no reason to pay for repeated growth. The bug this section exists to prevent is: ```go out := make([]U, len(s)) // length, not capacity for _, v := range s { out = append(out, f(v)) // appends AFTER the zeroed elements } ``` That returns `2*len(s)` elements: `len(s)` zero values followed by the real results. If you prefer allocating with a length, assign by index instead — `out[i] = f(v)` with `for i, v := range s` — and drop `append` entirely. Both forms are correct; mixing them is not. **Filtering is not the same problem.** A filter's output length is unknown, and `len(s)` is only an upper bound: ```go func Filter[T any](s []T, keep func(T) bool) []T { var out []T for _, v := range s { if keep(v) { out = append(out, v) } } return out } ``` Starting from a nil slice means a selective filter over a million samples does not reserve a million-element array. If you expect most elements to survive, `make([]T, 0, len(s))` is the better trade; that is a measured choice, not a default. Note also that `append` on a nil slice is perfectly legal — a nil slice has length and capacity zero and grows like any other. ## nil versus empty The two versions above differ on empty input. `Map` returns a non-nil, zero-length slice even for a nil input, because `make` always returns a non-nil slice. `Filter` returns nil when nothing survives. Both have `len` 0, both range over nothing, and `append` works on both — but `encoding/json` marshals the first as `[]` and the second as `null`, and a test comparing with `reflect.DeepEqual` distinguishes them. Whichever you choose, say so in the doc comment, because callers that serialise the result will notice. ## Fallible transforms If `f` can fail, the signature changes rather than the body: ```go func MapErr[T, U any](s []T, f func(T) (U, error)) ([]U, error) ``` Decide and document whether it stops at the first error or collects them; `errors.Join` (Go 1.21) makes the collecting version practical. Do not smuggle errors out through a captured variable in the callback — the caller cannot see it and the helper's signature lies. ## Does this helper deserve to exist? Go's standard library deliberately does not ship `Map`, `Filter` or `Reduce`. The three-line `for` loop is idiomatic, is inlined, and reads without the reader having to open your helper's source. A shared helper earns its place when the transform is genuinely repeated across a package and the callback is small; it does not earn its place when it replaces one loop with one call plus a closure. In a batch job that runs the transform over millions of elements, the closure is also an indirect call per element, so the helper can be measurably slower than the loop it replaced. Write it if you use it; do not build a functional toolkit on principle. What a reviewer wants to see is that you knew the output length, allocated once for it, chose nil-versus-empty on purpose, and did not reach for a generic abstraction where a loop would have said the same thing more plainly.
- What does your Map return for a nil input slice, and why might a caller care?It returns a non-nil, zero-length `[]U`, because `make` never returns nil. That matters at the edges: `encoding/json` writes `[]` for an empty non-nil slice and `null` for a nil one, and `reflect.DeepEqual` tells them apart. Both have length zero and both range over nothing, so pick one and document it.
- Why start Filter from a nil slice rather than make([]T, 0, len(s))?Because `len(s)` is only an upper bound on survivors. Reserving it for a filter that keeps 1% of a million samples allocates a hundred times the memory that is needed. When most elements survive, preallocating is the better trade — but that is a measured decision, not the default.
- How does the signature change if the transform can fail?Make it `func(T) (U, error)` and return `([]U, error)`. Decide explicitly whether it stops at the first failure or processes everything and combines the failures with `errors.Join`, and document that choice. Never let the callback report failure through a captured variable — the helper's signature would then be lying to its callers.
saying these in an interview costs you the question
- Allocates with make([]U, len(s)) and then appends
- Grows the result from nil when the length is known
- Thinks one type parameter can cover input and output
- Ignores whether empty input returns nil or an empty slice
- Assumes the standard library ships Map and Filter
- Reports callback errors through a captured variable