Menu
Coddy logo textTech

Type Constraints

Part of the Object Oriented Programming section of Coddy's GO journey — lesson 69 of 107.

While any accepts all types, it limits what operations you can perform inside your function. Type constraints restrict type parameters to types that support specific operations, enabling you to use those operations safely.

You define constraints using interfaces. A constraint specifies what methods or underlying types the type parameter must have:

type Numeric interface {
    int | int64 | float64
}

func Sum[T Numeric](values []T) T {
    var total T
    for _, v := range values {
        total += v  // Works because all Numeric types support +
    }
    return total
}

The Numeric interface uses the union operator | to list acceptable types. Since all listed types support addition, you can use += inside the function. Without this constraint, the compiler would reject the code because any doesn't guarantee arithmetic support.

You can also use the approximation operator ~ to include custom types based on underlying types:

type Integer interface {
    ~int | ~int32 | ~int64
}

type UserID int  // Custom type based on int

func Double[T Integer](n T) T {
    return n * 2
}

var id UserID = 5
result := Double(id)  // Works because UserID's underlying type is int

The ~int means "any type whose underlying type is int," making your generic functions work with custom type definitions. This flexibility is essential when working with domain-specific types in real applications.

challenge icon

Challenge

Easy

Let's build a measurement converter that uses type constraints to work with different numeric types, including custom domain-specific types! You'll create constraint interfaces that allow your generic functions to perform arithmetic operations safely.

You'll organize your code across two files:

  • converter.go: Define your type constraints and generic conversion functions.

    Create a custom type Meters based on float64 to represent distances.

    Create a custom type Kilograms based on float64 to represent weights.

    Define a constraint interface called Measurement that accepts any type whose underlying type is float64. Use the approximation operator ~ so your custom types work with this constraint.

    Implement a generic function Scale[T Measurement](value T, factor float64) T that multiplies the measurement by the given factor and returns the result.

    Implement a generic function Total[T Measurement](values []T) T that calculates and returns the sum of all measurements in the slice.

  • main.go: Read input and demonstrate your constrained generic functions with custom types.

    Read a measurement type (meters or kilograms), then read a count followed by that many values. Parse the values and create a slice of the appropriate custom type.

    Calculate the total of all values using your Total function, then scale that total by 2.5 using your Scale function.

    Print the results in this format:

    Total: [value]
    Scaled (x2.5): [value]

    Display values with one decimal place.

The following inputs will be provided:

  • Line 1: Measurement type (meters or kilograms)
  • Line 2: Count of values (integer)
  • Following lines: One value per line (floating-point numbers)

For example, given:

meters
3
10.5
20.0
5.5

Your output should be:

Total: 36.0
Scaled (x2.5): 90.0

And given:

kilograms
4
2.5
3.0
1.5
4.0

Your output should be:

Total: 11.0
Scaled (x2.5): 27.5

The key here is that your Measurement constraint with ~float64 allows both Meters and Kilograms to work with your generic functions, even though they're distinct custom types. This demonstrates how type constraints enable arithmetic operations while maintaining type safety for domain-specific types.

Cheat sheet

Type constraints restrict type parameters to types that support specific operations. You define constraints using interfaces.

Use the union operator | to list acceptable types:

type Numeric interface {
    int | int64 | float64
}

func Sum[T Numeric](values []T) T {
    var total T
    for _, v := range values {
        total += v
    }
    return total
}

Use the approximation operator ~ to include custom types based on underlying types:

type Integer interface {
    ~int | ~int32 | ~int64
}

type UserID int

func Double[T Integer](n T) T {
    return n * 2
}

var id UserID = 5
result := Double(id)  // Works because UserID's underlying type is int

The ~int means "any type whose underlying type is int," allowing generic functions to work with custom type definitions.

Try it yourself

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)

	// Read measurement type
	scanner.Scan()
	measurementType := scanner.Text()

	// Read count of values
	scanner.Scan()
	count, _ := strconv.Atoi(scanner.Text())

	// Read the values
	values := make([]float64, count)
	for i := 0; i < count; i++ {
		scanner.Scan()
		values[i], _ = strconv.ParseFloat(scanner.Text(), 64)
	}

	// TODO: Based on measurementType, create a slice of the appropriate custom type
	// (Meters or Kilograms) and populate it with the values

	// TODO: Use the Total function to calculate the sum of all measurements

	// TODO: Use the Scale function to scale the total by 2.5

	// TODO: Print the results in the required format:
	// Total: [value]
	// Scaled (x2.5): [value]
	// Use fmt.Printf with %.1f for one decimal place

	_ = measurementType // Remove this line when you use the variable
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming