When the compiler rejects your type
You're writing a generic function to handle multiple types. You define a constraint, pass an argument, and the build fails. The compiler stops you with type constraint not satisfied. You look at the code and the types seem compatible. The methods exist. The names match. The compiler insists otherwise.
This error isn't a bug. It's the compiler enforcing a contract. Generics in Go rely on constraints to define what a type can do. When the type you pass doesn't meet the requirements of the constraint, the compiler rejects the program. This check happens at compile time, preventing runtime panics where a method call would crash because the method doesn't exist.
Constraints are contracts
A type constraint tells the compiler which types are allowed to fill a type parameter. In Go, constraints are usually interfaces. If you write a function with func Process[T fmt.Stringer](v T), you are declaring that T must be a type that implements fmt.Stringer. The fmt.Stringer interface requires a String() string method. Any type passed to Process must have that exact method.
Think of a constraint like a power outlet. The outlet requires a plug with a specific shape. You can plug in a lamp or a charger, provided they have the right plug. You cannot plug in a device with a different plug shape. The constraint defines the required shape. The type is the device. If the shape doesn't match, the connection fails.
The compiler checks this contract at every call site. When you call the generic function, the compiler substitutes the type parameter with the concrete type you passed. It then verifies that the concrete type satisfies the constraint. If the verification fails, you get the error.
Minimal example
This example shows a generic function that requires a type to implement fmt.Stringer. It demonstrates a working call and a failing call.
package main
import (
"fmt"
)
// FormatValue prints the string representation of a value.
// The constraint T fmt.Stringer requires the type to have a String() string method.
func FormatValue[T fmt.Stringer](v T) {
// The compiler guarantees v has a String() method because of the constraint.
// Calling v.String() is safe and will not panic.
fmt.Println("Formatted:", v.String())
}
// CustomInt is an int that knows how to describe itself.
// It implements fmt.Stringer by providing a String() method.
type CustomInt int
// String returns the string representation of CustomInt.
// The receiver is a value receiver, so CustomInt satisfies the interface.
func (c CustomInt) String() string {
return fmt.Sprintf("CustomInt(%d)", c)
}
func main() {
// This works: CustomInt implements fmt.Stringer.
// The compiler checks CustomInt against the constraint and finds the String() method.
FormatValue(CustomInt(42))
// This fails: int does not have a String() method.
// Uncommenting this line causes a compile error.
// FormatValue(42) // Error: type constraint not satisfied
}
Walkthrough
When the compiler processes FormatValue(CustomInt(42)), it substitutes T with CustomInt. It checks if CustomInt satisfies fmt.Stringer. The fmt.Stringer interface requires String() string. CustomInt has a method String() string. The check passes. The compiler generates code for CustomInt.
When the compiler processes FormatValue(42), it substitutes T with int. It checks if int satisfies fmt.Stringer. The int type has no methods. It lacks String() string. The check fails. The compiler emits type constraint not satisfied.
The error message points to the call site. It tells you the argument type doesn't match the constraint. The fix is to pass a type that satisfies the constraint, or to change the constraint to match the types you need.
Constraints are contracts. Keep them small.
Realistic example
In real code, you often need to constrain types to ensure they have multiple methods. This example shows a generic function that saves entities to a database. The constraint requires the entity to have an ID method and a Validate method.
package main
import (
"context"
"fmt"
)
// Entity defines the interface for database records.
// Types must provide an ID and a validation method.
type Entity interface {
ID() string
Validate() error
}
// SaveEntity validates and saves an entity.
// The constraint ensures the entity has the required methods.
// Context is the first parameter, following Go convention.
func SaveEntity[T Entity](ctx context.Context, e T) error {
// Check if the context is cancelled before doing work.
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Validate the entity before saving.
// The constraint guarantees e has a Validate() method.
if err := e.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Simulate saving to the database.
// The constraint guarantees e has an ID() method.
fmt.Printf("Saved entity with ID: %s\n", e.ID())
return nil
}
// User represents a user in the system.
// It implements the Entity interface.
type User struct {
id string
name string
}
// ID returns the user's unique identifier.
// The receiver name is a short letter matching the type.
func (u User) ID() string {
return u.id
}
// Validate checks if the user data is valid.
func (u User) Validate() error {
if u.name == "" {
return fmt.Errorf("name is required")
}
return nil
}
// Product represents a product.
// It has an ID but no Validate method.
type Product struct {
id string
}
// ID returns the product's unique identifier.
func (p Product) ID() string {
return p.id
}
func main() {
ctx := context.Background()
// This works: User implements Entity.
// User has both ID() and Validate() methods.
SaveEntity(ctx, User{id: "u1", name: "Alice"})
// This fails: Product does not implement Validate().
// Product satisfies ID() but lacks Validate().
// Uncommenting this line causes a compile error.
// SaveEntity(ctx, Product{id: "p1"}) // Error: type constraint not satisfied
}
The receiver name is usually one or two letters matching the type. Use (u User), not (this User) or (self User). This convention keeps code concise and readable.
Context is plumbing. Run it through every long-lived call site.
Common pitfalls
The type constraint not satisfied error often hides subtle issues. Here are the most common causes.
Pointer vs value receivers
Method sets differ between values and pointers. If a type has a method on a pointer receiver, the value type does not have that method. The constraint check sees only the methods available on the type you pass.
package main
import "fmt"
// Counter has methods on pointer receiver.
type Counter struct {
count int
}
// Inc increments the counter.
// This method is on the pointer receiver, not the value.
// Only *Counter has this method.
func (c *Counter) Inc() {
c.count++
}
// Incrementable requires the Inc method.
type Incrementable interface {
Inc()
}
// ProcessCounter requires a type that implements Incrementable.
func ProcessCounter[T Incrementable](c T) {
c.Inc()
}
func main() {
c := Counter{}
// This fails: Counter does not have Inc(), *Counter does.
// The compiler rejects this with type constraint not satisfied.
// ProcessCounter(c)
// This works: *Counter has Inc().
// Passing a pointer satisfies the constraint.
ProcessCounter(&c)
}
The compiler rejects this with type constraint not satisfied when you pass a value type that lacks a method available only on the pointer type. Pass a pointer to satisfy the constraint, or move the method to a value receiver if the method doesn't modify the struct.
Pointers change method sets. Check the receiver.
The comparable constraint
Go has a built-in constraint called comparable. It means the type can be used with == and !=. You can use comparable directly in a constraint.
package main
import "fmt"
// Max returns the larger of two values.
// The constraint T comparable requires the type to support equality comparison.
func Max[T comparable](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
// This works: int is comparable.
fmt.Println(Max(10, 20))
// This fails: slices are not comparable.
// You cannot use == or != with slices.
// Uncommenting this line causes a compile error.
// Max([]int{1}, []int{2}) // Error: type constraint not satisfied
}
Slices, maps, and functions are not comparable. If you pass a slice to a function constrained by comparable, the compiler rejects it with type constraint not satisfied. Use reflect.DeepEqual or a custom comparison function for slices.
Type aliases vs new types
Type aliases and new types behave differently regarding constraints. A type alias is just another name for the same type. A new type creates a distinct type with no methods.
package main
import "fmt"
// MyStringAlias is an alias for string.
// It is identical to string in every way.
type MyStringAlias = string
// MyStringNew is a new type based on string.
// It is distinct from string and has no methods.
type MyStringNew string
// Stringer requires a String() method.
type Stringer interface {
String() string
}
// ProcessStringer requires a type that implements Stringer.
func ProcessStringer[T Stringer](v T) {
fmt.Println(v.String())
}
// CustomString implements Stringer.
type CustomString string
// String returns the string value.
func (c CustomString) String() string {
return string(c)
}
func main() {
// This works: CustomString implements Stringer.
ProcessStringer(CustomString("hello"))
// This fails: MyStringNew is a new type and has no methods.
// It does not inherit methods from string.
// ProcessStringer(MyStringNew("hello")) // Error: type constraint not satisfied
// This also fails: string does not have a String() method.
// ProcessStringer("hello") // Error: type constraint not satisfied
}
Aliases satisfy constraints of the underlying type. New types do not. If you define a new type, you must add the methods yourself to satisfy constraints.
Generics reduce duplication. Constraints ensure safety.
Decision matrix
Use a generic function with a type constraint when you need compile-time type safety across multiple types and want to avoid code duplication. Use a plain interface parameter when the function only needs one type and you don't need to define a generic type alias or struct. Use any as a constraint when the function treats the value as a black box and performs type assertions or reflection internally. Use a type switch when you need different behavior for each concrete type rather than a uniform interface. Use a custom type definition when you need to attach methods to a primitive type to satisfy a constraint.
Trust the compiler. Fix the type.