skip to content

Can a Go constraint require both a method and a specific underlying type, and what satisfies such a constraint?

level: middleimportance: nice to knowfreq 32%

answer

  1. elements intersect, they do not add up
  2. one line of types, one line of methods
  3. no method can be declared on a predeclared type
  4. an interface with methods may not be a union term

basics

~20 s

Yes. A constraint interface may hold a union of type terms and method requirements as separate elements, and its type set is their intersection. Only a defined type with one of those underlying types that also declares the method qualifies.

solid answer

~40 s

A constraint interface's type set is the **intersection** of its elements, so listing `~string | ~int64` on one line and `Label() string` on the next means "types whose underlying type is `string` or `int64` *and* which have a `Label` method". A plain `string` can never satisfy it, because methods can only be declared on defined types in their own package — so the set contains exactly the caller's own wrapper types, such as `type PlayerID string` with a `Label` method. In the body you get both halves: the method call, and every operator the union permits. The one restriction to remember is that a union of more than one term may not contain `comparable` or an interface with methods, so `fmt.Stringer | ~int` is rejected; keep the method requirement as its own element instead.

code

go · 15 lines
go
type Key interface {
	~string | ~int64
	Label() string
}

type PlayerID string

func (p PlayerID) Label() string { return "player " + string(p) }

func Rank[K Key](keys []K) {
	for _, k := range keys {
		_ = k.Label() // from the method element
		_ = k < keys[0] // from the union element
	}
}

go deeper

for a junior

Know that a Go constraint is an interface and that it can list both method requirements and permitted types.

for a middle

Explain that the elements intersect, so a type must meet all of them, and that the method half restricts the set to caller-defined types.

for a senior

Be able to say when this shape earns its complexity and when a plain method-only interface or a bare union serves better, and to unpick the unsatisfiable pointer-receiver case.

for a principal

Own what such a constraint demands of every consuming team: it forces them to declare wrapper types with methods, which is a real migration cost you should weigh against taking a function value instead.

## Elements intersect A constraint interface is a list of **elements**, and its type set is the intersection of the type sets of those elements. That single rule explains the whole feature. ```go type Key interface { ~string | ~int64 // element 1: a union Label() string // element 2: a method } ``` Element 1's type set is every type whose underlying type is `string` or `int64`. Element 2's type set is every type with a `Label() string` method. The interface's type set is what is in both. ## Who can actually be in that intersection This is the part that surprises people the first time. Methods in Go may only be declared on defined types in the package that declares them; you cannot attach a method to `string`, `int64`, or to a type from another package. So the intersection above excludes the predeclared types entirely and contains only defined types like: ```go type PlayerID string func (p PlayerID) Label() string { return string(p) } ``` That is usually exactly the intent. The union pins the *representation* the generic code relies on (it can order the values, concatenate them, convert them cheaply) while the method pins the *behaviour* it needs (a display label, a validation hook, a partition key). A caller who wants in must declare a proper domain type — which is often a fine thing for a library to insist on. ## What the body may do Both halves are available. Method requirements let the body call `v.Label()`. The union lets the body use every operator supported by all its terms with the same meaning: for `~string | ~int64` that is `==`, `!=`, the relational operators and `+`. Add a term such as `~[]byte` and you lose all of them, because slices support none. Pointer receivers deserve one caution. If `Label` is declared on `*PlayerID`, then `PlayerID` does not have it in its method set, and `Key`'s type set contains `*PlayerID` instead — but `*PlayerID`'s underlying type is a pointer type, so it fails the union and the constraint becomes unsatisfiable. When you mix a union with methods, declare those methods on value receivers. ## The restriction on unions A union with more than one term may not contain the predeclared `comparable`, nor an interface that specifies methods, nor an interface embedding either of those. So this is rejected: ```go type Bad interface { fmt.Stringer | ~int // illegal union term } ``` The restriction exists because "a type that is either an int or has a String method" would force the compiler to pick, per instantiation, which operations are available — the opposite of the rule that an operation must work for every member. Keeping method requirements as separate elements sidesteps the problem: intersection is well-defined, union across method sets is not. A single-term "union" is just an embedded interface and stays legal, which is why `interface { comparable; ~string | ~int64 }` — comparable embedded as its own element — is fine. ## When this shape is worth it Most constraints need only one kind of element, and mixing should be deliberate. Reach for it when the generic code genuinely needs both: a ranking index that must order raw keys *and* render them, or a serialiser that needs a numeric representation *and* a validation method. If you only need behaviour, a method-only interface is simpler and admits far more types. If you only need representation, the union alone keeps callers from having to declare methods. The cost is discoverability. When a caller's type fails such a constraint, the compiler reports that it does not satisfy the constraint, and the reason may be either half; a doc comment naming both requirements saves the reader a trip into your source. ## Checking it at compile time Because a constraint is not a variable type, you cannot use the usual `var _ Iface = (*T)(nil)` assertion on it. The equivalent is to instantiate something with it in your own package or tests — a one-line `var _ = Rank[PlayerID]` style reference — so that a later change to `PlayerID` breaks the build in your package rather than in a caller's.

  • Why is fmt.Stringer | ~int rejected as a union?
    A union of more than one term may not contain an interface that specifies methods, nor `comparable`. Allowing it would mean the available operations depended on which member was chosen, breaking the rule that an operation must be valid for every type in the set. Put the method requirement on its own element instead.
  • Can a predeclared type like string ever satisfy a constraint that includes a method?
    No. Methods may only be declared on defined types in the declaring package, so `string` has none and never will. The intersection therefore holds only caller-declared types such as `type PlayerID string` that declare the method themselves.
  • What happens if the method is declared on a pointer receiver?
    Then only `*PlayerID` has it in its method set, and `*PlayerID`'s underlying type is a pointer, which fails the union. The constraint becomes unsatisfiable. When mixing a union with method requirements, declare those methods on value receivers.

saying these in an interview costs you the question

  • Says a constraint may hold either methods or type terms, never both
  • Writes fmt.Stringer | ~int as a union
  • Expects plain string to satisfy a constraint with a method
  • Reads several elements as a union rather than an intersection