Определяемые типы и инкапсуляция

В Go можно создавать новые типы на основе существующих. Это не просто алиасы - это полноценные типы с собственными методами. Реализуя метод String(), такой тип автоматически становится fmt.Stringer - см. урок про интерфейсы.

type definition - создание нового типа

type Celsius float64
type Fahrenheit float64
type Meters float64
type Kilometers float64

// Это РАЗНЫЕ типы! Нельзя складывать без преобразования
var temp1 Celsius = 36.6
var temp2 Fahrenheit = 97.88
// var sum = temp1 + temp2  // ОШИБКА: разные типы

Зачем создавать новые типы?

// ПЛОХО: два float64, легко перепутать аргументы
func SetTemperature(celsius float64, humidity float64) {}

// ХОРОШО: типы подсказывают, что передавать
type Celsius float64
type Humidity float64

func SetTemperature(temp Celsius, hum Humidity) {}

// Компилятор поймает ошибку:
// SetTemperature(Humidity(50), Celsius(36.6))  // ОШИБКА

Методы на определяемых типах

Методы можно добавлять к любому типу, объявленному в текущем пакете:

type Celsius float64

func (c Celsius) ToFahrenheit() Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

func (c Celsius) String() string {
    return fmt.Sprintf("%.1f°C", float64(c))
}

// Использование
temp := Celsius(36.6)
fmt.Println(temp)                // 36.6°C (вызывается String())
fmt.Println(temp.ToFahrenheit()) // 97.9°F

Типы-коллекции с методами

type StringSlice []string

func (ss StringSlice) Contains(target string) bool {
    for _, s := range ss {
        if s == target {
            return true
        }
    }
    return false
}

func (ss StringSlice) Filter(fn func(string) bool) StringSlice {
    var result StringSlice
    for _, s := range ss {
        if fn(s) {
            result = append(result, s)
        }
    }
    return result
}

// Использование
tags := StringSlice{"go", "backend", "api", "grpc"}
fmt.Println(tags.Contains("go")) // true
long := tags.Filter(func(s string) bool { return len(s) > 2 })
fmt.Println(long) // [backend api grpc]

type alias vs type definition

// Type DEFINITION - новый тип (несовместим с оригиналом)
type UserID int64

// Type ALIAS - другое имя для того же типа (совместим)
type ID = int64
type UserID int64
type AliasID = int64

var uid UserID = 42
var aid AliasID = 42
var raw int64 = 42

// aid = raw    // OK: alias - тот же тип
// uid = raw    // ОШИБКА: UserID ≠ int64
uid = UserID(raw) // OK: явное преобразование

<ComparisonTable data={{ headers: ["", "type X int (definition)", "type X = int (alias)"], rows: [ ["Новый тип?", "Да", "Нет, тот же тип"], ["Свои методы?", "Да", "Нет"], ["Совместим с оригиналом?", "Нет, нужен cast", "Да, полностью"], ["Когда?", "Доменные типы, безопасность", "Рефакторинг, обратная совместимость"] ] }} />

Enum через iota

В Go нет отдельного типа enum. Используют type + iota:

type OrderStatus int

const (
    OrderPending    OrderStatus = iota // 0
    OrderConfirmed                     // 1
    OrderShipped                       // 2
    OrderDelivered                     // 3
    OrderCancelled                     // 4
)

func (s OrderStatus) String() string {
    names := [...]string{
        "Pending", "Confirmed", "Shipped", "Delivered", "Cancelled",
    }
    if int(s) < len(names) {
        return names[s]
    }
    return "Unknown"
}

// Использование
order := OrderShipped
fmt.Println(order) // Shipped

Инкапсуляция через пакеты

Go инкапсулирует не на уровне классов, а на уровне пакетов - см. урок Область видимости и пакеты:

// account/account.go
package account

type Account struct {
    owner   string  // приватное - только внутри пакета account
    balance float64 // приватное
}

// Конструктор - единственный способ создать Account
func New(owner string, initial float64) (*Account, error) {
    if initial < 0 {
        return nil, fmt.Errorf("начальный баланс не может быть отрицательным")
    }
    return &Account{owner: owner, balance: initial}, nil
}

// Публичные методы - контролируемый доступ
func (a *Account) Owner() string   { return a.owner }
func (a *Account) Balance() float64 { return a.balance }

func (a *Account) Deposit(amount float64) error {
    if amount <= 0 {
        return fmt.Errorf("сумма должна быть положительной")
    }
    a.balance += amount
    return nil
}

func (a *Account) Withdraw(amount float64) error {
    if amount <= 0 {
        return fmt.Errorf("сумма должна быть положительной")
    }
    if amount > a.balance {
        return fmt.Errorf("недостаточно средств: есть %.2f, нужно %.2f", a.balance, amount)
    }
    a.balance -= amount
    return nil
}
// main.go
package main

import "myapp/account"

func main() {
    acc, _ := account.New("Alice", 1000)

    // acc.balance = -500    // ОШИБКА: приватное поле
    // acc.balance           // ОШИБКА: приватное поле

    acc.Deposit(500)         // OK: через публичный метод
    fmt.Println(acc.Balance()) // 1500
}
Инкапсуляция в Go - не классы, а пакеты. Приватные поля + публичные методы = контролируемый доступ. Конструктор `New()` - единственный вход.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.