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

Методи

Метод — це функція з отримувачем (receiver) — типізованим параметром, який з'являється між func і назвою методу. Методи надають визначеному типу певну поведінку.

type Celsius float64

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

func main() {
    c := Celsius(100)
    fmt.Println(c.Fahrenheit())     // 212
}

Отримувач (c Celsius) — це звичайний параметр; єдина відмінність — його позиція. Всередині тіла методу c поводиться як будь-яка інша змінна типу Celsius.

З досвіду Python: ≈ метод класу, з однією тонкою відмінністю — класу немає. Отримувач — це просто додатковий параметр, який компілятор прив'язує до конкретного типу. Ви також самі даєте йому ім'я — ніякого неявного self.

Отримувачі-значення

func (c Celsius) — це отримувач-значення (value receiver). Метод отримує копію значення. Зміна отримувача всередині методу не впливає на оригінал.

type Counter int

func (c Counter) Inc() {        // отримувач-значення — працює з копією
    c++
}

func main() {
    var n Counter = 0
    n.Inc()
    n.Inc()
    fmt.Println(n)              // 0 — Inc жодного разу не торкнувся n у викликача
}

Використовуйте отримувач-значення, коли:

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

Отримувачі-вказівники

func (c *Counter) — це отримувач-вказівник (pointer receiver). Метод отримує вказівник на оригінал, і записи через нього змінюють значення у викликача.

type Counter int

func (c *Counter) Inc() {       // отримувач-вказівник — записує через вказівник
    *c++
}

func main() {
    var n Counter = 0
    n.Inc()
    n.Inc()
    fmt.Println(n)              // 2
}

Використовуйте отримувач-вказівник, коли:

  • Метод повинен змінювати отримувач.
  • Отримувач великий (структура з багатьма полями, яку не хочеться копіювати при кожному виклику).
  • Структура містить поле, яке не можна копіювати (наприклад, sync.Mutex).
  • Узгодженість: якщо хоча б один метод типу потребує отримувача-вказівника, зробіть всі методи з отримувачами-вказівниками, щоб набір методів типу був узгодженим.

Автоматичне взяття адреси та розіменування

Вам не потрібно писати (&n).Inc() чи (*p).Inc(). Go сам підставляє & або *, коли в місці виклику є значення одного виду, а метод очікує інший.

type Counter int
func (c *Counter) Inc() { *c++ }

func main() {
    var n Counter = 0
    n.Inc()                     // Go непомітно переписує як (&n).Inc()
    p := &n
    p.Inc()                     // вже вказівник; переписування не потрібне
    fmt.Println(n)              // 2
}

Умова для переписування: значення повинно бути адресованим (іменована змінна, поле адресованої структури або результат * чогось). Елемент мапи або значення, що повертає функція, не є адресованим.

type Counter int
func (c *Counter) Inc() { *c++ }

m := map[string]Counter{"x": 0}
m["x"].Inc()                    // compile error: cannot call pointer method Inc on Counter

Рішення: зчитати в локальну змінну, змінити, записати назад; або змінити мапу так, щоб вона зберігала значення *Counter.

Методи на нестruct-типах

Тип отримувача може бути будь-яким визначеним типом у вашому пакеті — не лише структурами.

type Names []string

func (n Names) Contains(s string) bool {
    for _, x := range n {
        if x == s {
            return true
        }
    }
    return false
}

func main() {
    n := Names{"Ada", "Linus"}
    fmt.Println(n.Contains("Ada"))      // true
    fmt.Println(n.Contains("Grace"))    // false
}

Саме так до типів зрізу, мапи, функції або примітивних типів прикріплюється поведінка.

Обмеження «той самий пакет»

Тип отримувача повинен бути визначений у тому ж пакеті, що й метод:

package mine
func (t time.Time) Foo() { ... }        // compile error
func (i int) Double() int { ... }       // compile error

Не можна прикріпити методи до int, time.Time або чогось іншого з іншого пакету. Рішення таке саме, як у 09-custom-types.md: визначте власний тип з чужим типом як базовим типом і прикріпіть метод до нього.

type Stamp time.Time

func (s Stamp) Unix() int64 {
    return time.Time(s).Unix()
}

Набори методів — попередній огляд

Кожен тип має набір методів (method set): методи, які можна викликати на значеннях цього типу. Правило:

  • Набір методів T містить усі методи з типом отримувача T.
  • Набір методів *T містить усі методи з типом отримувача *T та всі методи з типом отримувача T.

На практиці ви рідко явно думаєте про набори методів — аж поки не починаєте реалізовувати інтерфейси. Інтерфейс (детально розглядається в окремій статті) — це іменований набір сигнатур методів; тип задовольняє інтерфейс, коли його набір методів містить усі ці методи. Інтерфейси розглядатимуться окремо; запам'ятайте правило:

Якщо будь-який метод має отримувач-вказівник, тільки *T (не T) задовольняє інтерфейси, що включають цей метод.

Значення методу та вирази методу

Метод можна від'єднати від його отримувача двома способами.

Значення методу — отримувач вбудований

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

c := Celsius(100)
f := c.Fahrenheit                       // *значення* методу — c захоплено
fmt.Println(f())                        // 212

f має тип func() float64. Отримувач c захоплений замиканням.

Вираз методу — отримувач є першим параметром

g := Celsius.Fahrenheit                 // *вираз* методу
fmt.Println(g(Celsius(100)))            // 212

g має тип func(Celsius) float64. Отримувач стає явним першим параметром у місці виклику.

Значення методу набагато поширеніші в реальному коді; вирази зустрічаються в допоміжних бібліотеках і тестах.

Вбудовування та просування методів

Якщо структура вбудовує інший тип (поле з назвою типу і без назви поля), методи вбудованого типу стають доступними для виклику на зовнішній структурі.

type Logger struct{ prefix string }
func (l Logger) Log(msg string) { fmt.Println(l.prefix, msg) }

type Server struct {
    Logger              // вбудовано — без назви поля
    addr string
}

func main() {
    s := Server{Logger: Logger{prefix: "[srv]"}, addr: ":8080"}
    s.Log("starting")                   // [srv] starting
}

s.Log(...) — це скорочення для s.Logger.Log(...). Метод просунуто до Server. Компонуйте поведінку за допомогою вбудовування; в Go немає наслідування.

Короткий довідник

Потреба Запис
Метод, що читає отримувач func (c Celsius) F() float64 (отримувач-значення)
Метод, що змінює отримувач func (c *Counter) Inc() (отримувач-вказівник)
Виклик методу з отримувачем-вказівником на змінній-значенні Просто пишіть n.Inc() — Go підставить &
Метод на зрізі / мапі / типі на основі int Визначте type X []int, потім func (x X) Foo() {}
Прив'язати метод до фіксованого отримувача f := c.Fahrenheit (значення методу)
Використати метод як незв'язану функцію g := Celsius.Fahrenheit (вираз методу)

Джерела