When is `func F[S ~[]E, E any](s S)` worth its second type parameter over `func F[E any](s []E)`?
answer
- does S appear anywhere else
- who wants their named type back
- the return test
- explicit instantiation names every parameter
- assignability already handles the argument side
basics
~10 sThe extra parameter earns its place only when the function must produce the caller's named slice type. If nothing of type S comes back out, take a plain []E and keep the signature readable.
solid answer
~40 s`S` exists so the caller's *named* slice type survives the call. `func Clone[S ~[]E, E any](s S) S` hands a `FlagList` back to someone who passed a `FlagList`, rather than a bare `[]Flag` they must convert. If `S` never appears in a result, in a second parameter, or in a field, the second type parameter buys nothing and costs plenty: it shows up in the doc comment, in every compiler error message, and anyone who has to instantiate explicitly must write both arguments as `F[FlagList, Flag](fl)`. The standard library's `slices` package takes `S` throughout partly for package-wide consistency, which is a defensible reason for a package of forty functions and a bad reason for one helper in your own code.
code
go · 11 linestype Flag uint32
type FlagList []Flag
func Clone[S ~[]E, E any](s S) S {
out := make(S, len(s))
copy(out, s)
return out
}
var fl FlagList
c := Clone(fl) // c is a FlagList, not a []Flaggo deeper
Know that a defined type like FlagList can be passed straight to a []Flag parameter. Most helpers you write need no slice type parameter at all.
Apply the return test out loud: if S appears only where it is declared, drop it. Be able to say why Clone keeps it and a counting helper does not.
Weigh the reader cost against the caller cost. Deep parameter lists surface in doc headers, compiler errors and explicit instantiations, and they invite copy-paste into helpers that do not need them.
Set the house rule and the exception. A uniform package-wide signature shape can be worth the extra parameter; a scattering of them across internal helpers is a review target, not a style preference.
## What the two type parameters actually do ```go func F[E any](s []E) // one parameter: the element type func F[S ~[]E, E any](s S) // two: the slice type and the element type ``` In the second form the parameter's type is `S`, a type parameter whose constraint admits any type whose underlying type is `[]E`. That includes both the unnamed `[]Flag` and a defined type such as: ```go type Flag uint32 type FlagList []Flag ``` The question is what you gain by being able to *name* that slice type inside the signature, because naming it is the only thing `S` adds. ## The case where it pays: the named type must come back out ```go func Clone[S ~[]E, E any](s S) S { out := make(S, len(s)) copy(out, s) return out } var fl FlagList c := Clone(fl) // c is a FlagList ``` With the single-parameter form, `func Clone[E any](s []E) []E`, the result would be a plain `[]Flag`. The caller who cares about `FlagList` — because it has methods on it, say a `String()` for logging — has to convert: `FlagList(Clone(fl))`. Multiply that across a package and the conversion becomes the visible cost, which is exactly why the standard library's `slices.Clone` is declared `func Clone[S ~[]E, E any](s S) S`. The same argument applies when `S` appears in a second parameter (`func Append[S ~[]E, E any](dst, src S) S`) or in a generic struct field that must hold the caller's slice type. ## The case where it does not pay If the function only reads the slice, or mutates it in place and returns nothing of that type, `S` is write-only in the signature. Consider a helper that counts set flags: ```go func CountSet[S ~[]Flag](s S) int // S never appears again func CountSet(s []Flag) int // says the same thing, no generics at all ``` A defined slice type is **assignable** to a parameter of its underlying type, so a `FlagList` can be passed to `func CountSet(s []Flag)` with no ceremony at all. The generic version has added a type parameter to buy something the language already gave you. ## The readability cost, concretely The cost is not aesthetic hand-waving; it lands in four places a reader actually meets: 1. **The doc header.** `func Map[S ~[]E, E any, R any](s S, f func(E) R) []R` is the first line a reader sees, and it takes real effort before they learn that this is `map`. 2. **Compiler errors.** A failed call reports the full instantiation, with both parameters and the constraint, rather than one mismatched element type. 3. **Explicit instantiation.** Where inference does not apply — passing the function as a value, or an argument the compiler cannot unify — the caller must supply *every* type argument in order: `F[FlagList, Flag]`. You cannot supply only the ones you want. 4. **Review load.** Each additional parameter is another thing a reviewer must check for consistency, and `[S ~[]E, E any]` invites people to copy the pattern into helpers that do not need it. ## The rule of thumb **Apply the return test.** Write the signature; ask whether `S` appears anywhere other than the parameter it declares. If it does not, delete it and take `[]E`. If the function is not otherwise generic at all, delete both and take the concrete slice type — the plainest signature that compiles is usually the right one at an API boundary, and it is the one you can change later without renegotiating type arguments with every caller. One honest exception: **consistency inside a package**. `slices` takes `S` even in functions like `Sort`, declared `func Sort[S ~[]E, E cmp.Ordered](x S)`, which returns nothing. Uniformity across a large package of related functions is worth something to readers, and it means a named slice type behaves identically everywhere. That reasoning applies to a package with dozens of parallel functions. It does not justify the second type parameter on the one helper you are adding to an internal package this afternoon.
- Why does `slices.Sort` take `S` even though it returns nothing?Package-wide consistency. Every function in `slices` takes the slice type parameter, so readers meet one shape and a defined slice type behaves identically across the package. That argument is worth paying for in a large uniform package and rarely worth it for a single helper.
- Can a caller supply only the type argument they care about?No. Explicit instantiation supplies type arguments positionally from the left, so a caller who must name `S` writes `F[FlagList, Flag](fl)` in full. Partial instantiation of a later parameter is not possible, which is part of the second parameter's cost.
- Does `func CountSet(s []Flag) int` reject a `FlagList` argument?No — a defined type is assignable to a parameter of its underlying type, so `CountSet(fl)` compiles with no generics at all. That is why a read-only helper rarely needs the slice type parameter in the first place.
saying these in an interview costs you the question
- Adds S ~[]E by habit because the standard library does
- Claims a defined slice type cannot be passed to a []E parameter
- Thinks the extra parameter improves performance
- Cannot say where S must appear to be worth declaring
- Believes a caller can instantiate only the second type argument