Перейти до змісту

Власні типи

Go дозволяє визначати власні іменовані типи. Є два різновиди оголошення — вони виглядають майже однаково, але означають абсолютно різні речі.

type Celsius float64       // визначення типу — новий, окремий тип
type Temp    = float64     // псевдонім типу — просто ще одна назва для float64

Єдина відмінність — знак =. Наслідки значні.

Навіщо створювати власні типи?

Три причини, у порядку важливості:

  1. Компілятор не дасть змішати те, що не слід змішувати. Значення Celsius не можна додати до значення Fahrenheit без явного перетворення.
  2. До них можна прикріплювати методи. Лише визначені типи (не псевдоніми) можуть мати методи — це розглядається пізніше, але саме заради цього й варто вдаватися до type Foo Bar.
  3. Назви передають намір. UserID документує себе краще, ніж черговий int64 у сигнатурі функції.

Визначення типу: type Foo Bar

Зі специфікації Go:

"Визначення типу створює новий, окремий тип із тим самим базовим типом та операціями, що й заданий тип [...]. Він відрізняється від будь-якого іншого типу, включно з тим, від якого був створений."

type Celsius float64
type Fahrenheit float64

var c Celsius = 20
var f Fahrenheit = 68

sum := c + f          // compile error: mismatched types Celsius and Fahrenheit

Celsius і Fahrenheitрізні типи. Обидва розміщені в пам'яті точно як float64 — це їхній спільний базовий тип — але компілятор вважає їх різними, оскільки вони мають різні назви.

Можна перетворювати між ними за допомогою синтаксису T(x) (розглянуто в 03-type-conversions.md):

c2 := Celsius((f - 32) * 5 / 9)   // ok — явне перетворення

Типові патерни

Семантичні обгортки над примітивами:

type UserID int64
type Email  string
type Money  int64    // зберігаємо центи, щоб уникнути float

func sendEmail(to Email, user UserID) error { /* ... */ }

var id UserID = 42
var addr Email = "ada@example.com"
sendEmail(addr, id)              // ok — порядок аргументів відповідає типам

sendEmail(id, addr)              // compile error: неправильний порядок, компілятор ловить помилку

Це безкоштовна мережа безпеки. Без UserID як окремого типу обидва аргументи мали б тип int64/string, і неправильний порядок компілювався б без помилок.

Іменовані типи функцій:

type Handler func(req string) string

func register(h Handler) { /* ... */ }

register(func(req string) string {
    return "echo: " + req
})

Іменовані типи зрізів або мап:

type Headers map[string]string
type Tags    []string

func process(h Headers, t Tags) { /* ... */ }

Константи у стилі enum — зазвичай у поєднанні з iota:

type Weekday int

const (
    Sunday Weekday = iota   // 0
    Monday                  // 1
    Tuesday                 // 2
    Wednesday               // 3
    Thursday                // 4
    Friday                  // 5
    Saturday                // 6
)

func isWeekend(d Weekday) bool {
    return d == Sunday || d == Saturday
}

isWeekend(Monday)            // ok — перевіряється тип
isWeekend(5)                 // ok — 5 є нетипізованою цілою константою, стає Weekday
isWeekend(int(5))            // compile error — int не є Weekday

Псевдонім типу: type Foo = Bar

Зі специфікації:

"Оголошення псевдоніма прив'язує ідентифікатор до заданого типу. У межах видимості ідентифікатора він слугує псевдонімом для цього типу."

Псевдонім — не новий тип, а друга назва того самого типу.

type Celsius = float64        // псевдонім

var c Celsius = 20
var f float64 = c             // ok — c І Є float64
var sum = c + f               // ok — той самий тип, перетворення не потрібне

Порівняйте з формою визначення вище: при type Celsius float64 компілятор відхилив би var f float64 = c. Із псевдонімом він приймає.

Коли використовувати псевдоніми (рідко)

Переважна більшість коду потребує визначення типів, а не псевдонімів. Псевдоніми існують переважно для:

  1. Поступового рефакторингу між пакетами — тимчасово відкривати oldpkg.Foo як newpkg.Foo, доки клієнти мігрують.
  2. Скорочення довгих назв типівtype list = []*linkedListNode[T] для читабельності всередині одного файлу.
  3. Стандартна бібліотека використовує їх обережноbyte = uint8 і rune = int32 є псевдонімами. Саме тому byte і uint8 взаємозамінні скрізь.
var b byte   = 65
var u uint8  = b              // ok — псевдонім означає той самий тип
var c uint32 = uint32(b)      // ok — потрібне явне перетворення (різні типи)

Ключове обмеження: до псевдоніма не можна прикріплювати методи. Псевдонім не має окремої ідентичності, до якої їх прикріпити.

type Celsius = float64
func (c Celsius) Fmt() string { return "..." }   // compile error

Якщо потрібні методи, використовуйте визначення типу.

Методи — головна перевага визначень типів

Після визначення типу до нього можна прикріплювати методи — саме заради цього й варто вдаватися до type Foo Bar. Механіка (отримувачі за значенням та за вказівником, набори методів, вбудовування) розглядається в 10-methods.md. Єдине правило, яке варто зазначити тут: методи можна визначати лише для типів із власного пакету — ніколи для int, string, time.Duration чи будь-якого іншого стороннього типу.

«Базовий тип» — точне правило

Зі специфікації, переказано як процес:

  1. Попередньо оголошені типи (int, string, bool, float64, ...) — їхній базовий тип — вони самі.
  2. Літерали типів ([]int, map[string]int, struct{...}, func(...) ...) — їхній базовий тип — вони самі.
  3. Будь-який інший тип — слідуйте ланцюжку визначень, поки не дійдете до одного з вищезгаданих.
type A = string         // базовий тип: string
type B string           // базовий тип: string  (ланцюжок псевдоніму: B → string)
type C B                // базовий тип: string  (ланцюжок: C → B → string)
type D struct {         // базовий тип: struct{Name string}  (літерал типу)
    Name string
}

Перевірити під час виконання можна через reflect.TypeOf(x).Kind() — повертається базовий різновид, а не іменований тип.

Ідентичність типів в одному абзаці

Два типи є ідентичними з погляду Go, якщо:

  • Це той самий визначений тип (та сама назва, той самий пакет), АБО
  • Обидва є літералами типів зі структурно сумісними базовими типами.

Визначений тип ніколи не є ідентичним своєму базовому типу чи будь-якому іншому визначеному типу, навіть якщо розміщення в пам'яті збігається. Саме тому Celsius і Fahrenheit не можна змішувати.

Коротка довідка

Форма Новий тип? Методи? Сумісний з оригіналом?
type Foo Bar (визначення) так так ні — потрібне явне перетворення
type Foo = Bar (псевдонім) ні ні так — той самий тип

Джерела