Basic vs General Interfaces
Understand the difference between basic and general interface types in Go, why general interfaces cannot be used as values, and how this split supports the generics system.
With the introduction of generics, Go’s interface types took on a new role. Before Go 1.18, every interface defined a set of methods, and any value whose concrete type implemented those methods could be stored in that interface. After 1.18, interfaces can also embed type elements — unions, approximations, and other interface types with type elements — to act as constraints on type parameters. This created a fundamental split in the language: basic interfaces (method-only) can still be used as ordinary values, while general interfaces (those with type elements) exist solely to constrain type parameters and cannot appear where a concrete value is expected.
What a Basic Interface Is
A basic interface is an interface that contains only method specifications and embedded interfaces. Its type set is determined entirely by the methods it requires; any non-interface type that has all those methods belongs to the set.
type Writer interface {
Write([]byte) (int, error)
}
This definition is a basic interface. It can be used anywhere a type is expected: as the type of a variable, a function parameter, a return type, a map value, or inside a composite type.
var w Writer = os.Stdout // valid
Because Writer is a basic interface, the variable w can hold any concrete value whose type has a Write([]byte) (int, error) method. The runtime representation of w is a pair of a type pointer and a data pointer, which Go can always construct for method-only interfaces.
Basic interfaces are value types:
Any interface you wrote before Go 1.18 is a basic interface and can be used as a value. You do not need to change existing code to keep that behavior.
What a General Interface Is
A general interface is an interface that includes at least one type element. Type elements can be type literals (int, string), type unions (int | float64), approximations (~int), or other general interfaces. The presence of any of these makes the interface “general” — it is now a type set definition, not a method set.
type Unsigned interface {
uint | uint8 | uint16 | uint32 | uint64 | uintptr
}
Unsigned is a general interface. Its type set is the finite set of specific integer types listed. It cannot be used as a value type.
var x Unsigned = uint(5) // compile error
The compiler rejects this because a general interface does not have the runtime machinery to hold a value whose concrete type could be one of several distinct, potentially different-sized types.
Embedding a general interface makes the embedding interface general too:
If a new interface embeds Unsigned, even alongside methods, that new interface is also general and cannot be used as a value. The rule applies recursively.
General Interfaces Are Constraints Only
General interfaces exist to be used in type parameter lists — as constraints on the types that a generic function or type can accept.
func Sum[U Unsigned](values ...U) U {
var total U
for _, v := range values {
total += v
}
return total
}
result := Sum(uint16(10), uint16(20)) // valid, U = uint16
Here Unsigned restricts U to be one of the allowed unsigned integer types. The compiler instantiates a concrete version of Sum for uint16. The constraint interface is never used as a runtime value; it guides the type checker and code generation.
Restrictions on General Interfaces as Value Types
The core of the basic vs. general split is this: a general interface cannot appear in any position where a concrete value must be stored. That means you cannot use a general interface as:
- the type of a variable
- the type of a function parameter or return value (outside of a generic parameter list)
- the element type of a slice, array, or map value
- the type of a struct field
- the target type of an interface conversion or type assertion
Attempting any of these results in a compile‑time error. The most common message is “interface contains type constraints”.
type Number interface {
int | float64
}
func double(n Number) Number { // invalid
return n + n
}
The function signature alone is illegal. The error stems from using Number as a value type, not from the body.
Compile‑time rejection:
A general interface cannot be used as a value type under any circumstances. The compiler will stop immediately; there is no runtime fallback.
If you need a function that accepts values of various types without generics, you must use a basic interface (method‑based). If you need a constraint that groups certain types together, you must use a general interface as a type parameter constraint.
// Valid: basic interface as value type
type Stringer interface {
String() string
}
func Print(s Stringer) {
fmt.Println(s.String())
}
// Valid: general interface as constraint
type Addable interface {
int | float64
}
func Add[T Addable](a, b T) T {
return a + b
}
Why This Distinction Exists
The split is not an arbitrary limitation. It follows from how Go represents interface values at runtime.
An interface value (basic) is a two‑word structure: a pointer to type information and a pointer to the actual data. For a method‑only interface, all concrete types that satisfy it share the same runtime calling convention — you call a method by looking up the function pointer in a method table. The size and shape of the underlying data do not matter after the interface is created; the method call always works.
A general interface, by contrast, may describe a set of types with different memory representations. A constraint like int | float64 includes a 64‑bit integer and a 64‑bit floating‑point number, but those types are not interchangeable. The compiler cannot generate a single runtime representation that works for both, because operations on them are fundamentally different. Generics solve this by monomorphisation: the compiler creates a separate function for each concrete type that satisfies the constraint, so no single runtime interface value is ever needed.
This design keeps Go’s interface values fast and predictable while allowing type constraints to be expressive.
How Beginners Should Think About It
Think of basic interfaces as “boxes” that can hold any value that knows the right behaviors (methods). Think of general interfaces as “checklists” that the compiler reads to decide which concrete types are allowed inside a generic function.
If you want to store something in a variable and call methods on it, you need a basic interface. If you are writing a generic function and need to restrict the type parameter to certain sets of types, you need a general interface.
You will rarely need to define a general interface unless you are writing generic code. Most day‑to‑day Go still relies on basic interfaces for polymorphism.
Common Mistakes and Misconceptions
Believing that any interface can be a variable type
The single most frequent error is to define a constraint with type elements and then try to use it as a regular type.
type Comparable interface {
int | string
}
var c Comparable = 5 // compile error
The fix is to lift the operation into a generic function or to use a basic interface with methods instead.
Forgetting that embedding a general interface propagates the restriction
If you embed a general interface inside another interface, the outer interface becomes general as well, even if you add methods.
type Signed interface {
int | int8 | int16 | int32 | int64
}
type StringableSigned interface {
Signed
String() string
}
StringableSigned is a general interface. You cannot use it as a value type, despite the presence of a method specification. This is by design; the type set of StringableSigned is int | int8 | int16 | int32 | int64, and those types do not share a common runtime representation.
Method elements in general interfaces:
A general interface can still specify methods. The constraint means the type parameter must both belong to the type set and implement the given methods. The interface itself, however, remains non‑value.
Assuming any is a general interface
any is a built‑in alias for interface{}, which is a basic interface (it contains no type elements). You can always use any as a value type. The same is true for all pre‑1.18 interfaces, which are basic by definition.
Confusing comparable with a basic interface
comparable is a predeclared interface that is not a basic interface. It cannot be used as a value type. It exists solely as a constraint.
Practical Real-World Usage
In practice, you will often need both basic and general interfaces in the same package. A common pattern is to define a basic interface that captures the methods you need for value‑level polymorphism, and a separate general constraint that extends it with type elements when you need a type‑parameterized API.
For example, a data structure that stores elements and requires them to be ordered might provide two variants: a basic interface version that uses a Compare method, and a generic version constrained by a type set.
// Basic interface for value-level usage
type Comparer interface {
Compare(Comparer) int
}
This basic interface lets you write functions that accept any value whose type knows how to compare itself to another Comparer. But it forces every implementation to embed a type assertion.
A generic constraint version removes that friction:
// General interface for generic usage
type Comparer[T any] interface {
Compare(T) int
}
type Tree[T Comparer[T]] struct {
root *node[T]
}
Comparer[T] is a general interface because it has a type parameter, but when instantiated with a concrete type (e.g., Comparer[time.Time]) it acts as a constraint. It is not used as a value; the Tree struct is parameterized by T and stores values of type T directly, not as an interface.
This design preserves zero‑value usability:
A Tree[time.Time] can be created with its zero value because no internal function pointer needs initialization. The comparison is resolved at compile time through the generic constraint.
When writing libraries, provide basic interfaces for callers who want to use interface values, and general constraints for callers who can leverage generics for performance and type safety. The standard library itself follows this pattern: for instance, io.Reader is a basic interface, and cmp.Ordered (Go 1.21+) is a general constraint.
The distinction between basic and general interfaces keeps Go’s type system simple at the surface while enabling the compiler to generate efficient code. If you need a value that can hold different types based on methods, reach for a basic interface. If you need to write one algorithm that works for a fixed set of concrete types, use a general interface as a constraint.