1. Generics in Go

As we've mentioned, Go does not support classes. For a long time, that meant that Go code couldn't easily be reused in many cases. For example, imagine some code that splits a slice into 2 equal parts. The code that splits the slice doesn't care about the types of values stored in the slice. Before generics, we needed to write the same code for each type, which is a very un-DRYarrow-up-right thing to do.

splitIntSlice.go
func splitIntSlice(s []int) ([]int, []int) {
    mid := len(s)/2
    return s[:mid], s[mid:]
}
splitStringSlice.go
func splitStringSlice(s []string) ([]string, []string) {
    mid := len(s)/2
    return s[:mid], s[mid:]
}

In Go 1.18 however, support for genericsarrow-up-right was released, effectively solving this problem!

Type Parameters

Put simply, generics allow us to use variables to refer to specific types. This is an amazing feature because it allows us to write abstract functions that drastically reduce code duplication.

splitAnySlice.go
func splitAnySlice[T any](s []T) ([]T, []T) {
    mid := len(s)/2
    return s[:mid], s[mid:]
}

In the example above, T is the name of the type parameter for the splitAnySlice function, and we've said that it must match the any constraint, which means it can be anything. This makes sense because the body of the function doesn't care about the types of things stored in the slice.

firstInts, secondInts := splitAnySlice([]int{0, 1, 2, 3})
fmt.Println(firstInts, secondInts)

Assignment

At Textio we store all the emails for a campaign in memory as a slice. We store payments for a single user in the same way.

Complete the getLast() function. It should be a generic function that returns the last element from a slice, no matter the types stored in the slice. If the slice is empty, it should return the zero value of the type.

circle-info

Tip: Creating a variable that's the zero value of a type is easy:

It's the same with generics, we just have a variable that represents the type:

Solution

πŸ€” The Problem Generics Solve

Before Generics: Code Duplication Hell

Imagine you want to get the last item from a slice. Without generics, you need a separate function for each type:

This is the EXACT same logic, just different types! 😀


✨ The Solution: Generics

With generics, you write one function that works for ALL types:

Now you can use it with any type:

One function, infinite types! πŸŽ‰


πŸ” Breaking Down the Syntax

Let's dissect this line:

Part 1: [T any] - Type Parameter

  • T is a variable that represents a type (you can name it anything: T, K, ElementType, etc.)

  • any is a constraint meaning "this can be literally any type"

Part 2: (s []T) - Function Parameter

  • []T means "a slice of type T"

  • If T is int, then []T is []int

  • If T is string, then []T is []string

Part 3: T - Return Type

  • The function returns the same type as the slice contains

  • If you pass []int, it returns int

  • If you pass []string, it returns string


πŸ§ͺ How Go Figures Out the Type

When you call a generic function, Go automatically infers the type:

You can also be explicit if you want:

But this is rarely needed because Go is smart enough to figure it out.


🎯 Complete Example with Explanation


🧠 Understanding var myZero T

This is how you create a zero value of a generic type:

What is a zero value?

  • For int: 0

  • For string: ""

  • For bool: false

  • For pointers: nil

  • For structs: all fields set to their zero values

Why do we need it?

If the slice is empty, we need to return something. Since we don't know what type T is at compile time, we return the zero value:


πŸ”„ More Examples to Solidify Understanding

Example 1: Split Slice (from your lesson)

Example 2: Find Maximum


πŸ“š Constraints Explained

The any in [T any] is a constraint. There are other constraints:

  1. any - No restrictions

  1. comparable - Can use == and !=

  1. Custom constraints


πŸŽ“ Mental Model

Think of generics as mad libs for types:

When you call the function, Go fills in the blank with the actual type:


βœ… Key Takeaways

  1. Generics eliminate code duplication by letting you write one function for many types

  2. [T any] means "T is a placeholder for any type"

  3. []T means "a slice of whatever type T is"

  4. Go infers the type from what you pass to the function

  5. var myZero T creates the zero value of type T


πŸš€ Your Solution Explained

This works because:

  • T can be any type (int, string, User, etc.)

  • If slice is empty, return the zero value of that type

  • If not empty, return the last element