3. Constraints

Sometimes you need your generic function to know something about the types it operates on. The example we used in the first exercise didn't need to know anything about the types in the slice, so we used the built-in any constraint:

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

Constraints are just interfaces that allow us to write generics that only operate within the constraints of a given interface type. In the example above, the any constraint is the same as the empty interface because it means the type in question can be anything.

Creating a Custom Constraint

Let's take a look at the example of a concat function. It takes a slice of values and concatenates the values into a string. This should work with any type that can represent itself as a string, even if it's not a string under the hood. For example, a user struct can have a .String() that returns a string with the user's name and age.

type stringer interface {
    String() string
}

func concat[T stringer](vals []T) string {
    result := ""
    for _, val := range vals {
        // this is where the .String() method
        // is used. That's why we need a more specific
        // constraint instead of the any constraint
        result += val.String()
    }
    return result
}

What Are Constraints?

Constraints are interfaces that restrict what types can be used with generic functions.

The any Constraint

any means "any type" - no restrictions:

  • T any = T can be any type

  • any = same as empty interface interface{}

  • Works with: int, string, struct, anything!

Why Use Custom Constraints?

When your function needs to call methods or perform operations on the type:

Creating a Custom Constraint

Define an interface with required methods:

How it works:

  • T stringer = T must implement the stringer interface

  • stringer requires a String() string method

  • Any type with String() can be used

Complete Example

Constraint Syntax Breakdown

Reads as: "concat is a function with type parameter T, where T must satisfy the stringer interface"

Built-in Constraints

Go provides some built-in constraints:

Multiple Constraints

You can require multiple interfaces:

Numeric Constraints

For math operations, use constraint package:

Common Constraint Patterns

Pattern 1: Method Requirement

Pattern 2: Comparable

Pattern 3: Ordered (for sorting)

Why Constraints Matter

Without constraints:

With constraints:

Constraint Hierarchy

Key Takeaways

  1. Constraints = interfaces that restrict generic types

  2. any = no restrictions, any type works

  3. Custom constraints = require specific methods

  4. comparable = built-in for types supporting ==

  5. Define constraint = create interface with required methods

  6. Type must satisfy = implement all methods in constraint

Quick Reference

Constraint
Syntax
Allows

Any type

[T any]

Everything

Comparable

[T comparable]

Types with == and !=

Custom

[T myInterface]

Types implementing interface

Multiple

[T interface{A; B}]

Types implementing A and B

Remember: Constraints tell Go what operations your generic function needs to perform on the type!

Generic Function Syntax Breakdown

Let me break down each part of this generic function signature:

Part-by-Part Explanation

Part 1: func splitAnySlice

  • func = function keyword

  • splitAnySlice = name of the function

Part 2: [T any]

This is the generic part:

  • [T any] = type parameter declaration

  • T = placeholder for a type (like a variable, but for types)

  • any = constraint (T can be any type)

  • Square brackets [] indicate this is a generic function

Think of it like:

  • Regular parameter: (x int) - x is a value

  • Type parameter: [T any] - T is a type

Part 3: (s []T)

Regular function parameter:

  • s = parameter name

  • []T = slice of type T (whatever T is)

Examples of what []T could be:

  • If T is int β†’ []int

  • If T is string β†’ []string

  • If T is user β†’ []user

Part 4: ([]T, []T)

Return types:

  • Returns two values

  • Both are slices of type T

Complete Breakdown

How It Works in Practice

Call with integers:

Call with strings:

Comparison: Generic vs Non-Generic

Non-Generic (separate function for each type)

Generic (one function for all types)

Much cleaner!

Type Parameter T Explained

T is like a variable that holds a type instead of a value:

  • T = name (could be anything: T, Type, Element, etc.)

  • any = what types are allowed (constraint)

Common type parameter names:

  • T = generic Type

  • K = Key (for maps)

  • V = Value (for maps)

  • E = Element

The Square Brackets []

Square brackets mean different things in different contexts:

In [T any], the brackets indicate type parameters.

Visual Flow

Complete Example with Types Shown

Key Takeaways

Part
Meaning
Example

[T any]

Type parameter T, any type allowed

T could be int, string, etc.

(s []T)

Input: slice of type T

If T=int, then []int

([]T, []T)

Returns: two slices of type T

If T=int, returns ([]int, []int)

Summary

Reads as: "splitAnySlice is a generic function with type parameter T (which can be any type), that takes a slice of T, and returns two slices of T"

The beauty of generics: write once, use with any type!

Assignment

We have different kinds of "line items" that we charge our customer's credit cards for. Line items can be things like "subscriptions" or "one-time payments" for email usage.

Complete the chargeForLineItem function.

1

Check if the user has a balance with enough funds to be able to pay for the cost of the newItem.

2

If they don't, then return an "insufficient funds" error and zero values for the other return values.

3

If they do have enough funds:

  • Add the line item to the user's history by appending the newItem to the slice of oldItems. This new slice is your first return value.

  • Calculate the user's new balance by subtracting the cost of the new item from their balance. This is your second return value.

Solution