Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Go  Module 6 – Interfaces and type assertions

Enums and sum types: Go patterns that work

Model closed sets in Go with named types, constants and validation, emulate sum types with sealed interfaces and type switches, and catch missing cases.

  • Intermediate
  • 15 minutes
  • Examples run with Go 1.27.1
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: A named type and its constants make an enum, but not a closed one · A sealed interface is Go's sum type. With a pass: 2 in-depth parts and 3 interview answers, about 9 minutes.

What you will learn

  • Model a closed set of values with a named type, its constants and a validity check
  • Build a sum type from a sealed interface and a type switch whose default returns an error
  • Validate values at the boundary with MarshalText and UnmarshalText
  • Write a state machine that refuses every move it does not list
  • Catch missing cases with a check over every variant and with an exhaustiveness linter

Before you start

What you’ll learn · Before you start

What you will learn

  • Model a closed set of values with a named type, its constants and a validity check
  • Build a sum type from a sealed interface and a type switch whose default returns an error
  • Validate values at the boundary with MarshalText and UnmarshalText
  • Write a state machine that refuses every move it does not list
  • Catch missing cases with a check over every variant and with an exhaustiveness linter

Before you start

On this page

In the 2025 Go Developer Survey, 65 % of respondents named a next-favourite language that has type-safe enums, and enums and sum types came up again among the features they missed. Go has neither keyword, yet Go code models closed sets every day, a payment’s status or the outcome of a payment attempt, with tools you already know.

You will build enums from a named type and its constants, see what that type does not stop, and emulate sum types with a sealed interface and a type switch. What each program printed on Go 1.27.1 is shown right below it.

A named type and its constants make an enum, but not a closed one

An enumeration: A fixed set of named values, such as the states of an order. In Go it is a named type, usually an integer type, with one constant for each value, often numbered by iota. More in “Constants, untyped values and iota” in Go is a named type with one constant per value. Go enums are often integers numbered by iota, as Constants and iota shows, and often strings, which read well in JSON and in logs. Here is a payment status as a string type, with a check for the five valid values:

status.go: five statuses, and three values the type did not stop Go · status.go
// status.go: a payment status as a named string type, and what the type alone does not stop.
package main

import (
	"encoding/json"
	"fmt"
	"slices"
)

// Status is the state of a payment.
type Status string

const (
	Created  Status = "created"
	Pending  Status = "pending"
	Success  Status = "success"
	Failed   Status = "failed"
	Refunded Status = "refunded"
)

var allStatuses = []Status{Created, Pending, Success, Failed, Refunded}

// IsValid reports whether s is one of the five statuses.
func (s Status) IsValid() bool { return slices.Contains(allStatuses, s) }

type Payment struct {
	ID     string `json:"id"`
	Status Status `json:"status"`
}

func main() {
	fmt.Println(Success, Success.IsValid())

	odd := Status("settled") // any string converts to Status
	fmt.Println(odd, odd.IsValid())

	var p Payment
	err := json.Unmarshal([]byte(`{"id": "TXN1003", "status": "REFUND_PARTIAL"}`), &p)
	fmt.Println(p.Status, p.Status.IsValid(), err)

	fmt.Println(p.Status == "sucess") // a misspelt constant compiles and is simply false
}

Output

success true
settled false
REFUND_PARTIAL false <nil>
false

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run status.go

Success is valid, but the type stopped nothing else. Status("settled") compiled, because any string converts to Status. json.Unmarshal stored REFUND_PARTIAL without an error, because Status is a string type to json. And p.Status == "sucess" compiled and was false, because an untyped string constant converts to Status as well, typo and all. The check exists, but nothing calls it unless your code does.

The two common shapes differ in what goes wrong:

An int type with iota A string type
stored and sent as a number, such as 2 its name, such as success
readable in logs and JSON only with a String method as it is
reordering the constants gives stored numbers new meanings changes nothing
the zero value the first constant, often a real state the empty string, never a valid name
any value converts to it any int any string

Key idea

Go enums are named types with constants: they document the set, but only your own checks keep other values out.

Checkpoint

Check: Enums from named types and constants

  1. Question 1 of 2 type Status string, with five constants. Which of these compile?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • var s Status = Success
    • if p.Status == "sucess" { … }
    • odd := Status("settled")

    The type keeps out values of other types, not other values of its own underlying type.

  2. Question 2 of 2 A payment decoded from JSON had no status field. Which enum design lets the code tell that apart from a real status?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A string type, whose zero value "" is never a valid name

    Choose a zero value that no real state uses: the empty string, or an Unknown constant first in an iota block.

A sealed interface is Go’s sum type

A sum type: A type whose values are each exactly one of a fixed set of kinds, each kind with its own data, such as Paid, Declined or Pending. Go has no sum types; a sealed interface and a type switch emulate them. says that a value is exactly one of a few kinds, each with its own data: a payment attempt ended Paid with a reference, Declined with a code, or Pending with a time to retry. The Go FAQ explains that variant types were left out because they overlap with interfaces in confusing ways, and that an interface value with a type switch already covers much of what they would do. Two details close the set: an unexported method, which makes the interface sealed interface: An interface with an unexported method, such as isResult(). Only the interface's own package can write that method, so no other package can declare a new type that has it., and a default case that refuses what it does not know:

The payments module: three results and one Describe

cmd/demo/main.go · payments/cmd/demo/main.go

// The demo: one message for each of the three results.
package main

import (
	"fmt"
	"time"

	"example.com/payments/result"
)

func main() {
	for _, r := range []result.Result{
		result.Paid{Ref: "UTR0412", Paise: 49900},
		result.Declined{Code: "51"},
		result.Pending{RetryAfter: 30 * time.Second},
	} {
		msg, err := result.Describe(r)
		fmt.Printf("%-17T %s %v\n", r, msg, err)
	}
}

Output

result.Paid       paid 499.00 rupees, reference UTR0412 <nil>
result.Declined   declined by the bank, code 51 <nil>
result.Pending    pending, checking again in 30s <nil>

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run main.go

result/result.go · payments/result/result.go

// Package result describes how a payment attempt ended: exactly one of Paid, Declined or Pending.
package result

import (
	"fmt"
	"time"
)

// Result is a sealed interface: its method isResult is unexported, so no other package can declare it.
type Result interface{ isResult() }

// Paid carries the bank's reference and the amount in paise.
type Paid struct {
	Ref   string
	Paise int64
}

// Declined carries the bank's reason code.
type Declined struct{ Code string }

// Pending says when to ask again.
type Pending struct{ RetryAfter time.Duration }

func (Paid) isResult()     {}
func (Declined) isResult() {}
func (Pending) isResult()  {}

// Describe writes the message a customer sees. Its default case returns an error, never a guess.
func Describe(r Result) (string, error) {
	switch v := r.(type) {
	case Paid:
		return fmt.Sprintf("paid %d.%02d rupees, reference %s", v.Paise/100, v.Paise%100, v.Ref), nil
	case Declined:
		return "declined by the bank, code " + v.Code, nil
	case Pending:
		return fmt.Sprintf("pending, checking again in %v", v.RetryAfter), nil
	default:
		return "", fmt.Errorf("result: no message for %T", r)
	}
}

Each variant carries its own fields, and Describe’s type switch gave each its message. A type in another package cannot declare isResult for itself, because the method has an unexported name:

cmd/outside/main.go: a Refund from another package Go · payments/cmd/outside/main.go
// outside: another package tries to add a fourth result.
package main

import "example.com/payments/result"

// Refund wants to be a Result, with an isResult method of its own.
type Refund struct{}

func (Refund) isResult() {}

var _ result.Result = Refund{}

func main() {}

Output (exit status 1)

Printed as an error (standard error)

# command-line-arguments
./main.go:11:23: cannot use Refund{} (value of struct type Refund) as result.Result value in variable declaration: Refund does not implement result.Result (unexported method isResult)

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run main.go

Refund does not implement result.Result (unexported method isResult): an unexported method name belongs to its own package, so the isResult that main declares is a different method. Step through who can be a Result:

Interactive · step by step A sealed Result and the types that can be one Step through the payments module. The unexported method decides which types can be a Result, and Describe covers each.
A sealed Result and the types that can be one, step 5 of 5. Showing Result, Paid, Describe, Declined, Refund, in main, Pending. In focus: Result, Refund, in main.isResultisResultisResultswitches onrefusedResultisResult(), unexportedPaidRef and PaiseDescribea type switchDeclineda reason codeRefund, in mainits own isResultPendingRetryAfter

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. An unexported method. Result lists one method, isResult, whose name starts with a small letter. Only code in package result can declare a method by that name that counts.
  2. Three variants. Paid, Declined and Pending each declare isResult with an empty body. That is all it takes, and each carries its own fields.
  3. One switch covers them. Describe switches on the dynamic type, with one case per variant and a default that returns an error instead of a guess.
  4. Another package tries. Package main declares Refund with an isResult method of its own. An unexported name belongs to its package, so it is a different method.
  5. Refused. The compiler says Refund does not implement result.Result: unexported method isResult. Only package result can declare that method, so the variants it declares are the ones its switches list.

These patterns go further in two directions. An enum can refuse unknown values itself, through MarshalText and UnmarshalText, and a state machine can refuse every move it does not list. And a missing case can be caught before production, by a check over every variant and by exhaustiveness linters.

Checkpoint

Check: Sealed interfaces as sum types

  1. Question 1 of 2 Why could package main not add Refund as a fourth Result?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Result's method isResult is unexported, so only package result can declare a method that counts

    An unexported name belongs to its package, so another package's isResult is a different method.

  2. Question 2 of 2 Describe has a case for each of the three results. Why does its default still return an error?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: So that a result Describe does not know fails loudly instead of producing an empty message

    A default that returns an error turns a forgotten case into a visible failure.

Enums that refuse bad values, and a state machine that names every move Pro: opens with a Pro, Premium or Ultimate pass

An enum that only documents its values lets bad ones in at every boundary. This part gives Status the two text methods that json and flag call, deals with the status that never arrived, and builds a state machine whose table of allowed moves refuses everything else.

In this part:

  • UnmarshalText turns an unknown status into an error
  • A missing field is the zero value, so validate after decoding
  • A state machine with a table of allowed moves

2 runnable examples (Go) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Catching the missing case before production Pro: opens with a Pro, Premium or Ultimate pass

Go reports no missing case in a switch, so the day a variant is added, every switch over the set needs a human to remember it. This part runs every variant through every switch, opens the seal from outside to show why the default matters, and sets up linters that know the closed set.

In this part:

  • A check that runs every variant through every switch
  • Embedding opens the seal from outside
  • Linters that know the closed set

2 runnable examples (Go) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise

Practice asks about enums and sealed interfaces, starting with whichever your answers show is weaker:

Practice that adapts to you

Practise this lesson

4 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 4 What is an enumeration in Go made of?

    Choose one answer.

    Show the answer: Question 1 of 4

    Answer: A named type and one constant of that type per value

    type Status string with const Success Status = "success" is a complete enum, if not a closed one.

  2. Question 2 of 4 Someone inserts a constant in the middle of an iota block of statuses. What happens to statuses already stored as numbers?

    Choose one answer.

    Show the answer: Question 2 of 4

    Answer: Every constant after the insertion gets a new number, so stored numbers now mean other statuses

    Store names, or give each number explicitly, so reordering changes nothing outside the program.

  3. Question 3 of 4 What makes an interface sealed?

    Choose one answer.

    Show the answer: Question 3 of 4

    Answer: An unexported method in it, which only its own package can declare

    A method name that starts with a small letter belongs to its package, so a type of another package that declares isResult has a different method, and the compiler refused Refund.

  4. Question 4 of 4 A parser's result is exactly one of Number{Value}, Word{Text} or Error{Pos}. Which Go design fits?

    Choose one answer.

    Show the answer: Question 4 of 4

    Answer: A sealed interface with three variant structs, and type switches with a default that errors

    An enum closes a set of names; a sealed interface closes a set of types that carry data.

Interview questions

Warm-up (fresher): How do you write an enum in Go?

Declare a named type, usually on int or string, and one constant per value: type Status string with const Success Status = "success", or an int type with iota. Add a method that checks validity, and a String method for an int type. The type documents the set, but any value of its underlying type converts to it, so check values that come from outside.

Three longer questions of the module bank build on this lesson:

  1. Fresher to mid

    A partner's webhook sends payment statuses as strings, and one day a new value, CHARGEBACK, arrives. Your service decoded it into its string-typed Status without an error, and a daily report counted those payments as successful. Explain how, and design the type so that unknown statuses are handled on purpose.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Your package defines a sealed Event interface with an unexported marker method, and Apply switches over every variant with a default that panics. A team's test double in another package embeds Event, and a staging run panics inside Apply. Explain how the double got past the seal, and what you change.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    Payments move through created, pending, success, failed and refunded, and bugs keep appearing where a failed payment is refunded or a refunded one is confirmed. Design the transitions in Go so that an invalid move cannot go unnoticed, and say how you test them.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Go enums are named types with one constant per value; any value of the underlying type converts to them, so they document a set without closing it.
  • String enums read well and survive reordering, while int enums with iota need a String method and must never be renumbered once stored.
  • A sealed interface, with an unexported method, and a type switch whose default returns an error, emulate a sum type.

Terms in this lesson

sealed interface
An interface with an unexported method, such as isResult(). Only the interface's own package can write that method, so no other package can declare a new type that has it.
sum type
A type whose values are each exactly one of a fixed set of kinds, each kind with its own data, such as Paid, Declined or Pending. Go has no sum types; a sealed interface and a type switch emulate them.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.