By using this site, you agree to the Privacy Policy and Terms of Use.
Accept
World of SoftwareWorld of SoftwareWorld of Software
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Search
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
Reading: Breaking Down and Explaining Type Parameters | HackerNoon
Share
Sign In
Notification Show More
Font ResizerAa
World of SoftwareWorld of Software
Font ResizerAa
  • Software
  • Mobile
  • Computing
  • Gadget
  • Gaming
  • Videos
Search
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Have an existing account? Sign In
Follow US
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
World of Software > Computing > Breaking Down and Explaining Type Parameters | HackerNoon
Computing

Breaking Down and Explaining Type Parameters | HackerNoon

News Room
Last updated: 2025/07/12 at 8:38 AM
News Room Published 12 July 2025
Share
SHARE

slices package function signatures

The slices.Clone function is pretty simple: it makes a copy of a slice of any type.

func Clone[S ~[]E, E any](s S) S {
    return append(s[:0:0], s...)
}

This works because appending to a slice with zero capacity will allocate a new backing array. The function body winds up being shorter than the function signature, which is in part because the body is short, but also because the signature is long. In this blog post we’ll explain why the signature is written the way that it is.

Simple Clone

We’ll start by writing a simple generic Clone function. This is not the one in the slices package. We want to take a slice of any element type, and return a new slice.

func Clone1[E any](s []E) []E {
    // body omitted
}

The generic function Clone1 has a single type parameter E. It takes a single argument s which is a slice of type E, and it returns a slice of the same type. This signature is straightforward for anybody familiar with generics in Go.

However, there is a problem. Named slice types are not common in Go, but people do use them.

// MySlice is a slice of strings with a special String method.
type MySlice []string

// String returns the printable version of a MySlice value.
func (s MySlice) String() string {
    return strings.Join(s, "+")
}

Let’s say that we want to make a copy of a MySlice and then get the printable version, but with the strings in sorted order.

func PrintSorted(ms MySlice) string {
    c := Clone1(ms)
    slices.Sort(c)
    return c.String() // FAILS TO COMPILE
}

Unfortunately, this doesn’t work. The compiler reports an error:

c.String undefined (type []string has no field or method String)

We can see the problem if we manually instantiate Clone1 by replacing the type parameter with the type argument.

func InstantiatedClone1(s []string) []string

The Go assignment rules allow us to pass a value of type MySlice to a parameter of type []string, so calling Clone1 is fine. But Clone1 will return a value of type []string, not a value of type MySlice. The type []string doesn’t have a String method, so the compiler reports an error.

Flexible Clone

To fix this problem, we have to write a version of Clone that returns the same type as its argument. If we can do that, then when we call Clone with a value of type MySlice, it will return a result of type MySlice.

We know that it has to look something like this.

func Clone2[S ?](s S) S // INVALID

This Clone2 function returns a value that is the same type as its argument.

Here I’ve written the constraint as ?, but that’s just a placeholder. To make this work we need to write a constraint that will let us write the body of the function. For Clone1 we could just use a constraint of any for the element type. For Clone2 that won’t work: we want to require that s be a slice type.

Since we know we want a slice, the constraint of S has to be a slice. We don’t care what the slice element type is, so let’s just call it E, as we did with Clone1.

func Clone3[S []E](s S) S // INVALID

This is still invalid, because we haven’t declared E. The type argument for E can be any type, which means it also has to be a type parameter itself. Since it can be any type, its constraint is any.

func Clone4[S []E, E any](s S) S

This is getting close, and at least it will compile, but we’re not quite there yet. If we compile this version, we get an error when we call Clone4(ms).

MySlice does not satisfy []string (possibly missing ~ for []string in []string)

The compiler is telling us that we can’t use the type argument MySlice for the type parameter S, because MySlice does not satisfy the constraint []E. That’s because []E as a constraint only permits a slice type literal, like []string. It doesn’t permit a named type like MySlice.

Underlying type constraints

As the error message hints, the answer is to add a ~.

func Clone5[S ~[]E, E any](s S) S

To repeat, writing type parameters and constraints [S []E, E any] means that the type argument for S can be any unnamed slice type, but it can’t be a named type defined as a slice literal. Writing [S ~[]E, E any], with a ~, means that the type argument for S can be any type whose underlying type is a slice type.

For any named type type T1 T2 the underlying type of T1 is the underlying type of T2. The underlying type of a predeclared type like int or a type literal like []string is just the type itself. For the exact details, see the language spec. In our example, the underlying type of MySlice is []string.

Since the underlying type of MySlice is a slice, we can pass an argument of type MySlice to Clone5. As you may have noticed, the signature of Clone5 is the same as the signature of slices.Clone. We’ve finally gotten to where we want to be.

Before we move on, let’s discuss why the Go syntax requires a ~. It might seem that we would always want to permit passing MySlice, so why not make that the default? Or, if we need to support exact matching, why not flip things around, so that a constraint of []E permits a named type while a constraint of, say, =[]E, only permits slice type literals?

To explain this, let’s first observe that a type parameter list like [T ~MySlice] doesn’t make sense. That’s because MySlice is not the underlying type of any other type. For instance, if we have a definition like type MySlice2 MySlice, the underlying type of MySlice2 is []string, not MySlice.

So either [T ~MySlice] would permit no types at all, or it would be the same as [T MySlice] and only match MySlice. Either way, [T ~MySlice] isn’t useful. To avoid this confusion, the language prohibits [T ~MySlice], and the compiler produces an error like

invalid use of ~ (underlying type of MySlice is []string)

If Go didn’t require the tilde, so that [S []E] would match any type whose underlying type is []E, then we would have to define the meaning of [S MySlice].

We could prohibit [S MySlice], or we could say that [S MySlice] only matches MySlice, but either approach runs into trouble with predeclared types. A predeclared type, like int, is its own underlying type. We want to permit people to be able to write constraints that accept any type argument whose underlying type is int. In the language today, they can do that by writing [T ~int]. If we don’t require the tilde we would still need a way to say “any type whose underlying type is int”. The natural way to say that would be [T int]. That would mean that [T MySlice] and [T int] would behave differently, although they look very similar.

We could perhaps say that [S MySlice] matches any type whose underlying type is the underlying type of MySlice, but that makes [S MySlice] unnecessary and confusing.

We think it’s better to require the ~ and be very clear about when we are matching the underlying type rather than the type itself.

Type inference

Now that we’ve explained the signature of slices.Clone, let’s see how actually using slices.Clone is simplified by type inference. Remember, the signature of Clone is

func Clone[S ~[]E, E any](s S) S

A call of slices.Clone will pass a slice to the parameter s. Simple type inference will let the compiler infer that the type argument for the type parameter S is the type of the slice being passed to Clone. Type inference is then powerful enough to see that the type argument for E is the element type of the type argument passed to S.

This means that we can write

    c := Clone(ms)

without having to write

    c := Clone[MySlice, string](ms)

If we refer to Clone without calling it, we do have to specify a type argument for S, as the compiler has nothing it can use to infer it. Fortunately, in that case, type inference is able to infer the type argument for E from the argument for S, and we don’t have to specify it separately.

That is, we can write

    myClone := Clone[MySlice]

without having to write

    myClone := Clone[MySlice, string]

Deconstructing type parameters

The general technique we’ve used here, in which we define one type parameter S using another type parameter E, is a way to deconstruct types in generic function signatures. By deconstructing a type, we can name, and constrain, all aspects of the type.

For example, here is the signature for maps.Clone.

func Clone[M ~map[K]V, K comparable, V any](m M) M

Just as with slices.Clone, we use a type parameter for the type of the parameter m, and then deconstruct the type using two other type parameters K and V.

In maps.Clone we constrain K to be comparable, as is required for a map key type. We can constrain the component types any way we like.

func WithStrings[S ~[]E, E interface { String() string }](s S) (S, []string)

This says that the argument of WithStrings must be a slice type for which the element type has a String method.

Since all Go types can be built up from component types, we can always use type parameters to deconstruct those types and constrain them as we like.


Ian Lance Taylor

Photo by Robin Jonathan Deutsch on Unsplash

This article is available on The Go Blog under a CC BY 4.0 DEED license.

Sign Up For Daily Newsletter

Be keep up! Get the latest breaking news delivered straight to your inbox.
By signing up, you agree to our Terms of Use and acknowledge the data practices in our Privacy Policy. You may unsubscribe at any time.
Share This Article
Facebook Twitter Email Print
Share
What do you think?
Love0
Sad0
Happy0
Sleepy0
Angry0
Dead0
Wink0
Previous Article Apple @ Work: Passkey portability is finally here in iOS 26 and macOS Tahoe 26 – 9to5Mac
Next Article Tiny earbuds, major ZZZs for just $20
Leave a comment

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Stay Connected

248.1k Like
69.1k Follow
134k Pin
54.3k Follow

Latest News

Multiple Samsung Galaxy S25 phones from the winter Unpacked event are still on sale for Prime Day
News
Top Stories: iPhone 17 Pro Rumors, iOS 26 Beta 3, and More
News
This company could save NASA’s doomed Martian Sample Return mission
News
24 hours with Alexa Plus: we cooked, we chatted, and it kinda lied to me
News

You Might also Like

Computing

Wine-Staging 10.2 Release Brings Patch For 11 Year Old Bug

2 Min Read
Computing

Over 600 Laravel Apps Exposed to Remote Code Execution Due to Leaked APP_KEYs on GitHub

6 Min Read
Computing

How to Handle Log Spikes Like the Pros: How Top DevOps Teams Tame Bursty Workloads | HackerNoon

12 Min Read
Computing

A Peek at How Regulation Molds the Scoreboard – Crypto’s Global Ledger | HackerNoon

5 Min Read
//

World of Software is your one-stop website for the latest tech news and updates, follow us now to get the news that matters to you.

Quick Link

  • Privacy Policy
  • Terms of use
  • Advertise
  • Contact

Topics

  • Computing
  • Software
  • Press Release
  • Trending

Sign Up for Our Newsletter

Subscribe to our newsletter to get our newest articles instantly!

World of SoftwareWorld of Software
Follow US
Copyright © All Rights Reserved. World of Software.
Welcome Back!

Sign in to your account

Lost your password?