Определяемые типы и инкапсуляция
В 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
}