Что такое тип-сумма (sum type) и как его эмулировать в Go?

Ответ

Тип-сумма (sum type), также известный как "помеченное объединение" (tagged union) или "вариантный тип" (variant), — это тип данных, который в любой момент времени может хранить значение одного из нескольких заранее определенных, различных типов.

Классические примеры из других языков — enum в Rust или алгебраические типы данных в Haskell. Например, тип Result может быть либо Ok(value), либо Err(error).

В Go нет встроенной поддержки типов-сумм, но их можно эффективно эмулировать с помощью "запечатанных" интерфейсов (sealed interfaces).

Идея состоит в том, чтобы определить интерфейс и набор типов, которые его реализуют, в одном пакете. При этом сам интерфейс содержит неэкспортируемый метод-маркер. Это не позволяет типам из других пакетов реализовывать этот интерфейс, делая набор вариантов исчерпывающим в рамках вашего пакета.

Пример эмуляции:

package shapes

import "math"

// Shape - это наш тип-сумма.
// Он может быть либо Circle, либо Rectangle.
type Shape interface {
    // isShape() - неэкспортируемый метод-маркер.
    // Он "запечатывает" интерфейс, не позволяя типам
    // из других пакетов его реализовывать.
    isShape()
}

// Circle - один из возможных вариантов для Shape
type Circle struct {
    Radius float64
}
func (c Circle) isShape() {}

// Rectangle - второй возможный вариант для Shape
type Rectangle struct {
    Width, Height float64
}
func (r Rectangle) isShape() {}

// Для работы с типом-суммой используется type switch,
// который позволяет безопасно проверить конкретный тип.
func Area(s Shape) float64 {
    switch shape := s.(type) {
    case Circle:
        return math.Pi * shape.Radius * shape.Radius
    case Rectangle:
        return shape.Width * shape.Height
    default:
        // Этот случай теоретически недостижим, если мы контролируем
        // все реализации интерфейса Shape.
        panic("unknown shape")
    }
}

Ключевые моменты:

  • Интерфейс-контейнер: Shape определяет общий тип.
  • Метод-маркер: Неэкспортируемый метод isShape() гарантирует, что только типы из текущего пакета (Circle, Rectangle) могут удовлетворять интерфейсу.
  • Исчерпывающая проверка: type switch позволяет компилятору (через статические анализаторы, например go vet) проверить, что все возможные варианты типа обработаны.

Современные альтернативы (Go 1.18+):

Хотя дженерики не создают полноценных типов-сумм, они позволяют реализовать похожие по духу контейнеры, такие как Result[T, E] или Optional[T], которые решают проблему представления одного из двух состояний (например, значение или ошибка).