JavaRush /Courses /Go SELF /Type parameters: simple generics functions

Type parameters: simple generics functions

Go SELF
Level 27, Lesson 0
Available

1. Why generics are needed and why this is not magic

When you first see code like func First[T any](xs []T) ..., your brain often reacts the same way it does to a Wi‑Fi password written in chalk on a board: the letters seem familiar, but you just want to take a photo and leave. That is normal. Generics in Go are designed to be as “down to earth” as possible: they are a way to describe a single algorithm that works the same for different types, while still being checked by the compiler.

Formally, a type parameter is a “placeholder” for a future concrete type: at compile time, the compiler will substitute a type argument in its place and then verify that everything is correct. It is similar to a variable, but it lives among types rather than values: a “type variable” is known during compilation.

Imagine you have already written two almost identical functions:

  • FirstInt([]int) (int, bool)
  • FirstString([]string) (string, bool)

Both do the same thing: take the first element or say “empty”. Generics let you write one function, First, that works for []int, for []string, and for []Task.

Important: generics in Go are not “reflection on steroids” and not “turn everything into any”. On the contrary, generics usually preserve type safety: if a function returns T, the calling code receives exactly the type it worked with, not a mysterious interface{}.

How to recognize generics by the signature: [...]

If you want to quickly “spot” a generics function, look for one simple sign: square brackets immediately after the function or type name.

Here is what a normal function looks like:

func FirstInt(xs []int) (int, bool) { ... }

And here is a generic one:

func First[T any](xs []T) (T, bool) { ... }

Key point: the square brackets do not belong to the slice, not to an index, not to an array. This is the list of type parameters.

It is useful to lock a mini table into your head (it is handy to recall when your eyes “catch” on the brackets):

What you see in code What it means
[]T
a slice of elements of type T (values)
[N]T
an array of length N (values)
F[T any](...)
function F with type parameter T (type level)

And one more important idea: T is a type name, not a variable. You cannot “print it” like a number. It exists so the compiler understands the relationship: “the input is []T, so the output T will be of the same type”.

Type parameter vs function parameter: types and values

In generics it is very easy to confuse two similar phrases: “type parameter” and “function parameter”. The difference is fundamental: function parameters are values, while type parameters are types.

Look at the example and read it literally, as a contract:

package main

import "fmt"

func Echo[T any](v T) T {
	return v
}

func main() {
	fmt.Println(Echo(10))      // 10
	fmt.Println(Echo("go"))    // go
}

Here:

  • v is a function parameter; it “lives” at runtime.
  • T is a type parameter; it “lives” at compile time.

The phrase Echo[T any] means: “Echo can work with any type T, but inside the function I am not promising that I can do absolutely anything with T”. Formally, the constraint is what defines which operations are allowed on T. Today we take the simplest option: any.

Type argument and type inference: when you do not need to write F[int](...)

The next beginner trap: see First[T any] and start writing “just in case” First[int](...) everywhere. In practice, Go most often handles it on its own.

There are two ways to use generics:

  • you explicitly specify the type argument;
  • the compiler infers the type argument from the ordinary function arguments.

An example where the compiler infers everything by itself:

package main

import "fmt"

func First[T any](xs []T) (T, bool) {
	if len(xs) == 0 {
		var zero T
		return zero, false
	}
	return xs[0], true
}

func main() {
	n, ok := First([]int{10, 20})
	fmt.Println(n, ok) // 10 true

	s, ok := First([]string{"go"})
	fmt.Println(s, ok) // go true
}

We did not write First[int] anywhere. The compiler saw []int and understood: “okay, so T = int”. This is called type inference (inferring the type argument).

Explicit type arguments are more often needed in ambiguous cases, for example when the compiler does not have enough information. But for a start, keep this rule: if the compiler can infer the type, let it infer. That makes the code shorter and less “noisy”.

Zero value for T: how to return “empty”

When you write First[T any], you do not know inside what T will be: int, string, Task, or something else. But sometimes you need to return a “default value” when there is no data.

In Go, this is solved in a standard pattern:

var zero T
return zero, false

This works because every type has a zero value: for int it is 0, for string it is "", for bool it is false, for a struct it is a struct with zero values for its fields.

Let us carefully integrate this into our learning project. Suppose we have a task model (we already know how to make a struct):

package main

type Task struct {
	ID    int
	Title string
	Done  bool
}

Now First can work for []Task as well:

package main

import "fmt"

type Task struct {
	ID    int
	Title string
	Done  bool
}

func First[T any](xs []T) (T, bool) {
	if len(xs) == 0 {
		var zero T
		return zero, false
	}
	return xs[0], true
}

func main() {
	t, ok := First([]Task{{ID: 1, Title: "learn generics"}})
	fmt.Println(t.Title, ok) // learn generics true
}

Notice a very “Go-ish” contract idea: instead of nil we return (T, bool). For meaningful types (like Task) this is often more convenient than trying to build “nullability” where it does not exist.

2. Generics utilities: Ptr and Must

Now for a pleasant moment: generics are especially good for small utilities that you previously either copied for each type or wrote in a clumsy way.

Ptr: a pointer to a value of any type

Sometimes you want to quickly make *T from T (for example, when a data model contains pointer fields or you want to distinguish “not set” from the zero value). For each type you can write &x, but sometimes it is convenient to have a small helper:

package main

import "fmt"

func Ptr[T any](v T) *T {
	return &v
}

func main() {
	p := Ptr(7)
	fmt.Println(*p) // 7

	s := Ptr("go")
	fmt.Println(*s) // go
}

Here generics do not give you “superpowers”, but they make the code more compact in places where you often create pointers to literals.

Must: extract a value from (T, error)

In our course we are used to the (value, error) style. Sometimes (especially in small examples, prototypes, or tests) you want to write shorter code: “if there is an error, let it crash”. Yes, crashing is not always good, but for demonstrations and tests it can be appropriate.

The generic version looks like this:

package main

import (
	"fmt"
	"strconv"
)

func Must[T any](v T, err error) T {
	if err != nil {
		panic(err)
	}
	return v
}

func main() {
	n := Must(strconv.Atoi("42"))
	fmt.Println(n) // 42
}

What matters here is that Must preserves the type: strconv.Atoi returns (int, error), so Must(...) returns int. Not any, not interface{}, but a concrete int.

And that is a nice difference between generics and “universal functions on interface{}”, which often force you to do type assertions afterward.

3. Generic types: Box and Stack

So far we talked about generic functions. But generics also work for types. The syntax is similar: square brackets appear after the type name.

Here is a simple “container” Box[T]:

package main

import "fmt"

type Box[T any] struct {
	Value T
}

func main() {
	a := Box[int]{Value: 10}
	b := Box[string]{Value: "hello"}

	fmt.Println(a.Value) // 10
	fmt.Println(b.Value) // hello
}

This is not a “mandatory life pattern”, but it is very good practice for reading: when you see Box[int], you literally read “Box where T = int”. Inside Box, the field Value will have type int.

Now let us build something a bit more “practical” for our learning app: a command history stack. Suppose we want to store the user’s action history (for example, added a task, marked it done). For now, we can store the actions as strings—it is simpler.

package main

type Stack[T any] struct {
	items []T
}

func (s *Stack[T]) Push(v T) {
	s.items = append(s.items, v)
}

func (s *Stack[T]) Pop() (T, bool) {
	if len(s.items) == 0 {
		var zero T
		return zero, false
	}
	last := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return last, true
}

This structure already looks like a “real type with behavior”: methods, encapsulation, a contract via (T, bool).

And a small usage example:

package main

import "fmt"

func main() {
	var h Stack[string]
	h.Push("add task #1")
	h.Push("done task #1")

	x, ok := h.Pop()
	fmt.Println(x, ok) // done task #1 true
}

Yes, this is only 10 lines of “pure usefulness”, but you already know how to read and write Stack[T], and that is our goal: to confidently parse generics code with your eyes.

4. How to read generics at a glance: a type-substitution scheme

When reading generics code, it helps to mentally do one simple operation: “substitute a concrete type in place of T and see what you get”. This helps remove the feeling of “abstractness”.

For example, we have:

func First[T any](xs []T) (T, bool)

If you call First([]int{1,2,3}), then the mental substitution is:

  • T = int
  • the signature turns into func First(xs []int) (int, bool)

If you call First([]Task{...}), then:

  • T = Task
  • the signature turns into func First(xs []Task) (Task, bool)

This idea matches how type parameters are described in Go: they are “fillers” for a future type argument known at compile time.

A small flowchart (not because you cannot live without it, but because sometimes the brain likes a picture more than text):

flowchart TD
    A["Code: First[T any](xs []T) (T, bool)"] --> B["Call: First([]int{1,2})"]
    B --> C["Compiler infers: T = int"]
    C --> D["Type check: xs is []int, return (int, bool)"]
    A --> E["Call: First([]Task{...})"]
    E --> F["Compiler infers: T = Task"]
    F --> G["Type check: xs is []Task, return (Task, bool)"]

That is exactly why generics in Go are usually perceived as “regular Go, just with a type template”, not as a separate language inside the language.

5. Common mistakes when first learning type parameters

Mistake #1: confusing []T and [T any].
This happens all the time: you see lots of brackets, and the brain decides they are all the same. In reality, []T is a slice of values, while [T any] is a list of type parameters. A good way to fix this is to look at where the brackets are: in generics they go immediately after the function/type name.

Mistake #2: treating T as a value rather than a type.
Beginners sometimes try to mentally “execute” T, as if it were a variable: “what is T equal to?”. The correct question is: “what concrete type will be substituted for T in this call?”. T does not live at runtime; it is for the compiler.

Mistake #3: writing F[int](...) everywhere “for reliability”.
Most of the time, the compiler infers type arguments from the regular function parameters. If you start specifying type arguments explicitly everywhere, the code becomes noisy, and generics stop saving you effort (and start costing it).

Mistake #4: trying to do operations on T that are not guaranteed.
Today we used T any, and that is intentional: any means “any type”, but it does not promise any operations except those allowed for all values (pass through, store, return). When you want to compare T or sort []T, you will need constraints. But that is a separate topic, and it is better to approach it calmly rather than at a sprint.

Mistake #5: returning any instead of T, losing type safety.
Sometimes you want to write a “universal” function, but the easiest thing to end up with is func First(xs []any) any. This is almost always worse: you lose the concrete result type and then have to do casts and checks at runtime. Generics are good precisely because they let you return T and keep the result type at compile time.

1
Task
Go SELF, level 27, lesson 0
Locked
Value Mirror
Value Mirror
1
Task
Go SELF, level 27, lesson 0
Locked
First Signal
First Signal
1
Task
Go SELF, level 27, lesson 0
Locked
Token Search
Token Search
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION