sumlint adds exhaustiveness checks for your interface "sum types" using a lightweight
naming convention + go vet.
oneoflint provides the same check for gRPC oneof.
// Non‑exhaustive: missing nonexhaustive.B, nonexhaustive.C.
func bad(x SumFoo) {
switch x.(type) { // want `non-exhaustive type switch on SumFoo: missing cases for: nonexhaustive.B, nonexhaustive.C`
case A:
default:
}
}
// Non‑exhaustive: missing one_of.Msg_B
func process(msg *Msg) {
switch msg.GetPayload().(type) { // want `non-exhaustive type switch on isMsg_Payload: missing cases for: one_of.Msg_B`
case *Msg_A:
default:
}
}Install:
go install github.com/gomoni/sumlint/cmd/sumlintRun:
go vet -vettool="$(go env GOPATH)/bin/sumlint" ./...A sum type (enum / tagged / disjoint union / oneof) is a type that can hold exactly one value chosen from a fixed set of alternative types.
Go does not provide first–class sum types. It has almost all the needed ingredients: interfaces, type switches, generics. But it lacks:
- Native pattern matching syntax
- Compiler‑enforced exhaustiveness
sumlint fills the exhaustiveness gap by turning a naming convention into checks at go vet time.
- Ability to represent set of types via single one
- Pattern matching
- Exhaustive match
Go already covers (1) with interfaces and (in a different way) with generic type sets. Example of a generic type set resembling a sum:
type SumType interface {
structA | structB
}
This has a major caveat. One can't have var SumType aSum anywhere in a code. This declaration can be used as a type parameter constraint only.
For (2) Go only offers the type switch. You can discriminate on the dynamic type, but you cannot express "match on value plus structure" the way functional languages allow.
For (3) (the critical piece) Go provides no built‑in exhaustiveness checking for interfaces or switches. Adding a new implementation silently produces partial handling.
sumlint is a go vet analyzer that enforces exhaustive handling of “declared sum interfaces” plus a mandatory default case (to make nil handling explicit).
In Go community there is a great precedent. The Test,
Bench and Fuzz prefixes are merely a convention, which all have a
actual meaning for testing code.
Thus sumlint recognizes a sum interface by
- Public interface name starts with
Sumprefix. - Contains unexported method with same name as an interface itself. That means the prefix of the method is
sum.
And that's it. This is enough for sumlint to recognize and handle
variables and having a method unexported means, the set of types
implementing this interface is closed. Example is.
// SumFoo declares a sum type, which is recognized by sumlint
type SumFoo interface {
sumFoo()
}
// A is an implementation of a SumFoo
type A struct{}
func (A) sumFoo() {}It turned out code generated by protoc for Go uses almost same convention.
message Msg {
oneof payload {
A a = 1;
B b = 2;
}
}type isMsg_Payload interface {
isMsg_Payload()
}The oneoflint is the linter configured to check and report exactly this.
sumlint inspects type switches of all detected Sum* interfaces and reports two distinct problems.
- The switch is not exhaustive - it is a problem and code should be fixed.
- The switch does not have
default:branch, so it can't handlenilinterface.
The (2) is not present in functional languages, but is necessary in Go as interfaces can be nil.
Examples (see tests/ directory):
// all cases handled
func good(x SumFoo) {
switch x.(type) {
case A, B:
default:
}
}
// missing default, is reported
func noDefault(x SumFoo) {
switch x.(type) {
case A, B:
}
}
// missing B
func noB(x SumFoo) {
switch x.(type) {
case A:
default:
}
}$ cd tests; go vet -vettool=${HOME}/go/bin/sumlint .
# github.com/gomoni/sumlint/tests
./src.go:23:2: missing default case on SumFoo: code cannot handle nil interface
./src.go:29:2: non-exhaustive type switch on SumFoo: missing cases for: github.com/gomoni/sumlint/tests.Bsumlint and oneoflint lets you use exhaustively checked type switches in plain Go using a
lightweight naming convention and go vet or other tool compatible with golang.org/x/tools/go/analysis.
Happy vetting!
- Only switches are analyzed (no if‑chains).
- Variants across multiple files in the same package are supported.
- Generics: works on ordinary interface values, not on constraint-only type sets.
- It is a linter. It has no support for auto generating marshal/unmarshal code.
- One can't opt-out from exhaustiveness check using a panic