Методи¶
Метод — це функція з отримувачем (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). - Узгодженість: якщо хоча б один метод типу потребує отримувача-вказівника, зробіть всі методи з отримувачами-вказівниками, щоб набір методів типу був узгодженим.
Nil-отримувач — це нормальний випадок¶
Виклик методу з отримувачем-вказівником на nil-вказівнику — не
помилка. Сам виклик цілком коректний; аварійно завершується лише
розіменування nil-вказівника. Тож метод може перевірити nil і трактувати
його як осмислений стан — саме так рекурсивні структури обходяться без
nil-перевірок у кожному місці виклику.
type Tree struct {
Val int
Left, Right *Tree
}
func (t *Tree) Sum() int {
if t == nil { // nil-отримувач — це нормальний випадок, а не баг
return 0
}
return t.Val + t.Left.Sum() + t.Right.Sum()
}
t := &Tree{Val: 1, Left: &Tree{Val: 2}}
fmt.Println(t.Sum()) // output: 3
var empty *Tree
fmt.Println(empty.Sum()) // output: 0
t.Left.Sum() працює, навіть коли Left є nil, — саме це дає рекурсії
завершитися без перевірки кожної гілки перед спуском.
З погляду Python: виклик методу на
None— це завждиAttributeError. У Go отримувач — це просто аргумент, тож nil-вказівник доходить до методу цілим і неушкодженим; ви самі вирішуєте, що це означає.
Автоматичне взяття адреси та розіменування¶
Вам не потрібно писати (&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.
Пастка циклу range¶
Те саме правило адресовності спричиняє найпоширенішу тиху помилку з
отримувачами-вказівниками. for _, x := range s прив'язує x до копії
кожного елемента, тож зміна нічого не робить — і це чисто компілюється:
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
cs := []Counter{{}, {}}
for _, c := range cs {
c.Inc() // c — копія: компілюється, нічого не змінює
}
fmt.Println(cs) // output: [{0} {0}]
for i := range cs {
cs[i].Inc() // cs[i] адресовний — змінює зріз
}
fmt.Println(cs) // output: [{1} {1}]
Звертайтеся за індексом (або зробіть []*Counter), коли зміна має
зберегтися.
З погляду Python:
for x in lstдає сам об'єкт, тожx.inc()зберігається. Go дає копію, і ніщо вас не попереджає.
Методи на нест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
}
Саме так до типів зрізу, мапи, функції або примітивних типів прикріплюється поведінка. Випадок із функцією — найнесподіваніший: функціональний тип може мати методи:
type Handler func(string) string
func (h Handler) Twice(s string) string { return h(h(s)) }
var exclaim Handler = func(s string) string { return s + "!" }
fmt.Println(exclaim.Twice("go")) // output: go!!
exclaim — це значення-функція, та .Twice викликає її двічі: отримувач
h і є цією функцією. (Саме так http.HandlerFunc перетворює звичайну
функцію на інтерфейс.)
Обмеження «той самий пакет»¶
Тип отримувача повинен бути визначений у тому ж пакеті, що й метод:
package mine
func (t time.Time) Foo() { ... } // compile error
func (i int) Double() int { ... } // compile error
Не можна прикріпити методи до int, time.Time або чогось іншого з іншого
пакету. Рішення таке саме, як у
власні типи: визначте власний тип з чужим типом як
базовим типом і прикріпіть метод до нього.
Набори методів — попередній огляд¶
Кожен тип має набір методів (method set): методи, які можна викликати на значеннях цього типу. Правило:
- Набір методів
Tмістить усі методи з типом отримувачаT. - Набір методів
*Tмістить усі методи з типом отримувача*Tта всі методи з типом отримувачаT.
На практиці ви рідко явно думаєте про набори методів — аж поки не починаєте реалізовувати інтерфейси. Інтерфейс (детально розглядається в окремій статті) — це іменований набір сигнатур методів; тип задовольняє інтерфейс, коли його набір методів містить усі ці методи. Інтерфейси розглядатимуться окремо; запам'ятайте правило:
Якщо будь-який метод має отримувач-вказівник, тільки
*T(неT) задовольняє інтерфейси, що включають цей метод.
type Counter int
func (c Counter) Get() int { return int(c) } // отримувач-значення
func (c *Counter) Inc() { *c++ } // отримувач-вказівник
type Incrementer interface{ Inc() }
var c Counter = 5
c.Inc() // ок: c адресовна, тож Go бере &c за вас
fmt.Println(c.Get()) // output: 6
var i Incrementer = &c // лише *Counter задовольняє Incrementer
i.Inc()
fmt.Println(c.Get()) // output: 7
// var bad Incrementer = c // compile error: Counter does not implement
// // Incrementer (method Inc has pointer receiver)
Виклик c.Inc() напряму працює, бо c — адресовна змінна: Go нишком
переписує це на (&c).Inc(). Але зберігання значення в інтерфейсі такої
допомоги не отримує, тож Incrementer задовольняє лише &c.
Значення методу та вирази методу¶
Метод можна від'єднати від його отримувача двома способами.
Значення методу — отримувач вбудований¶
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 захоплений замиканням.
Отримувач обчислюється в момент створення значення методу, а не під час виклику. З отримувачем-значенням це означає, що ви захоплюєте копію, заморожену в ту мить; з отримувачем-вказівником ви захоплюєте адресу, тож подальші зміни видно.
type Counter struct{ n int }
func (c Counter) Get() int { return c.n } // отримувач-значення
func (c *Counter) Inc() { c.n++ } // отримувач-вказівник
c := Counter{}
get := c.Get // копіює c просто зараз
inc := c.Inc // захоплює &c
inc()
inc()
fmt.Println(get(), c.Get()) // output: 0 2
get() усе ще повідомляє 0 — він читає копію, зроблену до інкрементів. Це
класична помилка, коли значення методу зберігають у зворотному виклику чи в
defer.
З погляду Python:
obj.method— це зв'язаний метод, який завжди бачить поточний стан. Значення методу Go з отримувачем-значенням — ні.
Вираз методу — отримувач є першим параметром¶
g має тип func(Celsius) float64. Отримувач стає явним першим параметром
у місці виклику.
Для методу з отримувачем-вказівником треба назвати тип-вказівник — і компілятор вам про це скаже:
inc := (*Counter).Inc // тип: func(*Counter)
c := Counter{}
inc(&c)
inc(&c)
fmt.Println(c.n) // output: 2
// f := Counter.Inc
// compile error: invalid method expression Counter.Inc
// (needs pointer receiver (*Counter).Inc)
Значення методу набагато поширеніші в реальному коді; вирази зустрічаються в допоміжних бібліотеках і тестах.
Вбудовування та просування методів¶
Якщо структура вбудовує інший тип (поле з назвою типу і без назви поля), методи вбудованого типу стають доступними для виклику на зовнішній структурі.
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 немає наслідування.
Що просування додає до набору методів¶
Просування підпорядковується тому самому поділу вказівник/значення, що й
звичайні методи, лише на рівень глибше. Вбудовування значення T
просуває методи T з отримувачем-значенням і до S, і до *S, але методи
T з отримувачем-вказівником потрапляють лише до *S:
type Logger struct{ n int }
func (l *Logger) Log() { l.n++ } // отримувач-вказівник
type Server struct{ Logger } // вбудовує значення
type Loggable interface{ Log() }
var _ Loggable = &Server{} // ок
var _ Loggable = Server{}
// compile error: Server does not implement Loggable (method Log has pointer receiver)
Уперше ця помилка збиває з пантелику, бо Log навіть не оголошено на
Server — його просунуто. Виправлення — використовувати *Server.
Коли два вбудовані типи стикаються¶
Вбудувати два типи, що надають метод з однаковою назвою, оголосити можна. Помилка виникає лише там, де ви його справді вибираєте:
type Reader struct{}
func (Reader) Close() string { return "reader" }
type Writer struct{}
func (Writer) Close() string { return "writer" }
type File struct {
Reader
Writer
}
var f File
fmt.Println(f.Close())
// compile error: ambiguous selector f.Close
Менша глибина перемагає, тож оголошення Close безпосередньо на File
розв'язує конфлікт — і саме так у Go працює «перевизначення». Внутрішні
лишаються доступними за іменем:
func (File) Close() string { return "file" }
fmt.Println(f.Close(), f.Reader.Close()) // output: file reader
Короткий довідник¶
| Потреба | Запис |
|---|---|
| Метод, що читає отримувач | 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 (вираз методу) |