← Назад к вопросам

Как реализовать Тип-сумму в Go?

1.3 Junior🔥 141 комментариев
#Основы Go

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Реализация Типов-Сумм (Sum Types) в Go

В Go нет встроенной поддержки типов-сумм (также известных как размеченные объединения или алгебраические типы данных) как в функциональных языках (Haskell, Rust, TypeScript). Однако существует несколько идиоматических способов эмулировать их поведение, используя возможности интерфейсов и перечислений.

Основные подходы

1. Интерфейсы с закрытым методом (Sealed Interfaces)

Самый распространённый способ — создать интерфейс и набор реализующих его типов в отдельном пакете, контролируя множество возможных вариантов.

// shapes/shape.go
package shapes

type Shape interface {
    isShape() // закрытый метод
}

type Circle struct {
    Radius float64
}

func (c Circle) isShape() {}

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) isShape() {}

type Triangle struct {
    A, B, C float64
}

func (t Triangle) isShape() {}

Использование с проверкой типа через type switch:

func Area(s shapes.Shape) float64 {
    switch v := s.(type) {
    case shapes.Circle:
        return math.Pi * v.Radius * v.Radius
    case shapes.Rectangle:
        return v.Width * v.Height
    case shapes.Triangle:
        // Формула Герона
        p := (v.A + v.B + v.C) / 2
        return math.Sqrt(p * (p - v.A) * (p - v.B) * (p - v.C))
    default:
        // Этот случай никогда не должен произойти благодаря закрытости
        panic("неизвестный тип фигуры")
    }
}

2. Маркированные структуры с дискриминантом

Подход с явным полем-тегом для идентификации типа:

type Result struct {
    value interface{}
    tag   resultTag
}

type resultTag int

const (
    successTag resultTag = iota
    errorTag
)

func Success(value int) Result {
    return Result{value: value, tag: successTag}
}

func Error(err string) Result {
    return Result{value: err, tag: errorTag}
}

func (r Result) Match(onSuccess func(int), onError func(string)) {
    switch r.tag {
    case successTag:
        onSuccess(r.value.(int))
    case errorTag:
        onError(r.value.(string))
    }
}

3. Использование указателей на структуры с nil-вариантами

Для представления опциональных значений (Maybe/Option):

type Option[T any] struct {
    value *T
}

func Some[T any](v T) Option[T] {
    return Option[T]{value: &v}
}

func None[T any]() Option[T] {
    return Option[T]{value: nil}
}

func (o Option[T]) IsSome() bool {
    return o.value != nil
}

func (o Option[T]) Unwrap() T {
    if o.value == nil {
        panic("попытка развернуть None")
    }
    return *o.value
}

4. Генераторы кода и библиотеки

Для сложных случаев можно использовать:

  • go-sumtype: генератор для проверки exhaustiveness в type switch
  • go-algebraic: библиотека с готовыми алгебраическими типами
  • go.uber.org/fx: содержит реализации Either, Result

Практический пример: Result тип

package result

type Result[T any] interface {
    isResult()
}

type Ok[T any] struct {
    Value T
}

func (Ok[T]) isResult() {}

type Err[T any] struct {
    Error error
}

func (Err[T]) isResult() {}

func HandleResult[T any](r Result[T]) (T, error) {
    switch v := r.(type) {
    case Ok[T]:
        return v.Value, nil
    case Err[T]:
        var zero T
        return zero, v.Error
    default:
        panic("исчерпывающая проверка типов")
    }
}

Ключевые преимущества и ограничения

Преимущества:

  • Безопасность типов: компилятор Go помогает избежать ошибок
  • Исчерпывающая проверка: type switch с default panic гарантирует обработку всех случаев
  • Расширяемость: легко добавлять новые методы для интерфейса
  • Читаемость: явное представление всех возможных состояний

Ограничения:

  • Нет проверки на этапе компиляции: исчерпывающая проверка вариантов требует discipline или внешних инструментов
  • Сравнение с nil: интерфейсы могут быть nil, что добавляет дополнительное состояние
  • Отсутствие pattern matching: нет встроенного механизма декомпозиции как в Rust или Scala

Рекомендации по использованию

  1. Для опциональных значений используйте встроенные указатели или Optional[T] из Go 1.18+
  2. Для ошибок предпочитайте стандартный (T, error) кроме случаев, когда нужен тип-сумма с >2 вариантами
  3. Сложные доменные модели с фиксированным набором вариантов хорошо реализуются через sealed interfaces
  4. Библиотечный код может выиграть от Result/Either типов для явного представления ошибок

Итог: Хотя Go не предоставляет нативных типов-сумм, комбинация интерфейсов, type switch и правильной организации пакетов позволяет эффективно моделировать аналогичную функциональность с сохранением типобезопасности и читаемости кода. Выбор конкретного подхода зависит от сложности доменной области и требований к безопасности типов.

Как реализовать Тип-сумму в Go? | PrepBro