skip to content

Why does `slices.Clone` declare `[S ~[]E, E any]` instead of one `[E any]` with a `[]E` parameter?

level: middleimportance: nice to knowfreq 34%

answer

  1. two names, two different jobs
  2. one is the slice, one the element
  3. the caller had a named slice type
  4. look at the result type, not the parameter
  5. the whole bracket list is one scope

basics

~20 s

Two 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 lines
go
type 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 []string

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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