Why does `slices.Clone` declare `[S ~[]E, E any]` instead of one `[E any]` with a `[]E` parameter?
answer
- two names, two different jobs
- one is the slice, one the element
- the caller had a named slice type
- look at the result type, not the parameter
- the whole bracket list is one scope
basics
~20 sTwo type parameters name both the caller's slice type and its element type. S is the actual argument type, including a named type such as Names, so the result keeps that type instead of a plain []string.
solid answer
~40 s`func Clone[S ~[]E, E any](s S) S` declares two type parameters where the first is constrained in terms of the second: `S` is whatever slice type the caller passed, and `E` is its element type. `~[]E` means "any type whose underlying type is `[]E`", which admits both `[]string` and a defined type like `type Names []string`. The payoff is the **result**: `Clone` returns `S`, so cloning a `Names` gives back a `Names`, with the methods declared on `Names` still available. The single-parameter form `func Clone[E any](s []E) []E` accepts a `Names` argument through ordinary assignability, but its result is statically `[]string`, so `Names`' method set is gone. It also shows that a type parameter list is one scope: `S`'s constraint may mention `E` even though `E` is written after it.
code
go · 18 linestype Names []string
func (n Names) First() string { return n[0] }
func Clone[S ~[]E, E any](s S) S {
out := make(S, len(s))
copy(out, s)
return out
}
func CloneE[E any](s []E) []E {
out := make([]E, len(s))
copy(out, s)
return out
}
// Clone(Names{"a"}).First() compiles: the result is a Names
// CloneE(Names{"a"}).First() does not: the result is a []stringgo deeper
You are unlikely to be asked to write this signature, but recognise it when reading the slices package: the first parameter is the slice type, the second its element type.
Explain what each of the two parameters binds to for a call with a defined slice type, and that a type parameter list is a single scope so a constraint may refer forward.
Show the judgment: adopt the two-parameter form when the function returns the collection it was given or when callers' named slice types carry methods, and reject it where it only adds noise.
Set the house style for exported generic helpers - how far you mirror the standard library's signatures, and how many interdependent type parameters a public API may carry before readability outweighs the fidelity.
## The two shapes Compare these declarations, both of which copy a slice: ```go func CloneE[E any](s []E) []E // one type parameter func Clone[S ~[]E, E any](s S) S // two, the first constrained by the second ``` The second is the shape the standard library's `slices` package uses throughout — `slices.Clone`, `slices.Sort`, `slices.Contains` and friends all begin `[S ~[]E, ...]`. It looks like ceremony until you see what it buys. ## What each parameter names In `[S ~[]E, E any]`: - `E` is the **element** type. `any` places no requirement on it. - `S` is the **slice** type the caller actually used. Its constraint, `~[]E`, is a type set: every type whose *underlying* type is `[]E`. That includes the slice type literal `[]E` itself and every defined type built on it. So for `type Names []string`, calling `Clone(Names{"a", "b"})` binds `S = Names` and `E = string`. The function's parameter is typed `S`, and — the point of the whole exercise — so is its result. ## One list, one scope Notice that `S`'s constraint refers to `E`, which is declared later in the same bracket list. That is legal, and it is a direct consequence of how type parameter scope works: **all the type parameters in one list are in scope across the entire list**, as well as across the rest of the signature and the body. Declaration order inside the brackets carries no meaning, and mutual references between constraints are permitted. Writing `[E any, S ~[]E]` compiles just as well; the `slices` convention of putting the slice type first is stylistic. ## What you lose with the single-parameter form It is tempting to conclude that `CloneE[E any](s []E) []E` simply cannot take a `Names`. It can: `Names` and `[]string` have identical underlying types and one of them is an unnamed type literal, so Go's assignability rules let a `Names` value be passed as a `[]string` argument, and let the returned `[]string` be assigned back into a `Names` variable. The loss is subtler and shows up in the **static type of the expression**: ```go type Names []string func (n Names) First() string { return n[0] } var n Names _ = Clone(n).First() // fine: Clone returns Names // _ = CloneE(n).First() // does not compile: CloneE returns []string, which has no methods ``` A `[]string` has no method set, so any method you declared on `Names` is unavailable on the result until you assign it to a variable of type `Names` or convert it. The same demotion propagates into further inference: passing `CloneE(n)` on to another generic function infers `[]string`, not `Names`. ## Where it also matters The pattern is not only about methods. Anywhere a function is expected to *return the type it was given* — a sort helper that returns the sorted slice, a filter, a deduplicator, a builder that chains — the two-parameter form preserves the caller's vocabulary. Functions that merely *consume* a slice and return something unrelated (a count, a bool, an index) gain much less from it; `slices` still uses the two-parameter form there for consistency of signature. ## Writing the body Inside the function, `S` behaves as a slice type: because every type in its type set has the same underlying slice type, `len`, indexing, `range`, `append` and `make` all work on an `S`: ```go func Clone[S ~[]E, E any](s S) S { out := make(S, len(s)) copy(out, s) return out } ``` `make(S, len(s))` produces a value of the caller's own slice type, which is exactly what the result type promises. Note that `copy` returns the number of elements copied, `min(len(dst), len(src))`, which here is `len(s)`. ## The cost The honest downside is readability. `[S ~[]E, E any]` is more to parse than `[E any]`, and a signature with three or four interdependent parameters gets hard to hold in your head. Reach for the second parameter when the function *returns* the collection type it was handed, or when a caller's named slice type carries methods that must survive. Otherwise the simpler declaration is the better one — and if the standard library already ships the helper, use that instead of writing either.
- Can you still pass a `type Names []string` value to `func CloneE[E any](s []E) []E`?Yes. `Names` and `[]string` share an underlying type and one side is an unnamed type literal, so assignability lets the call through and lets the result be assigned back into a `Names` variable. What you lose is the static type of the *expression*: it is `[]string`, so methods declared on `Names` are not available on the result.
- Is it legal for `S`'s constraint to mention `E` when `E` is declared after it?Yes. Every type parameter in a list is in scope across the whole list, so order is irrelevant and constraints may reference each other. `[E any, S ~[]E]` is equally valid; the `slices` package simply writes the slice type first by convention.
- Why can the body write `make(S, len(s))` and `append` to an `S`?Because every type in `S`'s type set has the same underlying slice type, `[]E`. That gives `S` a slice core type, which is what the built-ins require, so `len`, indexing, `range`, `copy`, `append` and `make` all apply and produce values of the caller's own slice type.
saying these in an interview costs you the question
- Says the two-parameter form is only stylistic
- Thinks a defined slice type cannot be passed as []string at all
- Believes E must be declared before S may reference it
- Reads ~[]E as a pointer to a slice
- Cannot say where the named type is actually lost - the result