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 intThe ~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
EasyLet'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
Metersbased onfloat64to represent distances.Create a custom type
Kilogramsbased onfloat64to represent weights.Define a constraint interface called
Measurementthat accepts any type whose underlying type isfloat64. Use the approximation operator~so your custom types work with this constraint.Implement a generic function
Scale[T Measurement](value T, factor float64) Tthat multiplies the measurement by the given factor and returns the result.Implement a generic function
Total[T Measurement](values []T) Tthat 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 (
metersorkilograms), 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
Totalfunction, then scale that total by2.5using yourScalefunction.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 (
metersorkilograms) - 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.5Your output should be:
Total: 36.0
Scaled (x2.5): 90.0And given:
kilograms
4
2.5
3.0
1.5
4.0Your output should be:
Total: 11.0
Scaled (x2.5): 27.5The 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 intThe ~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
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool