Вступление
Functions позволяет организовать логику в повторяющиеся процедуры, которые могут использовать разные аргументы при каждом запуске. В процессе определения функций вы часто обнаруживаете, что несколько функций могут работать с одним и тем же фрагментом данных каждый раз. Go распознает этот шаблон и позволяет вам определять специальные функции, называемыеmethods, цель которых - работать с экземплярами определенного типа, называемогоreceiver. Добавление методов к типам позволяет сообщать не только о том, что это за данные, но и о том, как эти данные следует использовать.
Определение метода
Синтаксис определения метода аналогичен синтаксису определения функции. Единственное отличие состоит в добавлении дополнительного параметра после ключевого словаfunc
для указания получателя метода. Получатель - это объявление типа, для которого вы хотите определить метод. В следующем примере определяется метод для типа структуры:
package main
import "fmt"
type Creature struct {
Name string
Greeting string
}
func (c Creature) Greet() {
fmt.Printf("%s says %s", c.Name, c.Greeting)
}
func main() {
sammy := Creature{
Name: "Sammy",
Greeting: "Hello!",
}
Creature.Greet(sammy)
}
Если вы запустите этот код, вывод будет:
OutputSammy says Hello!
Мы создали структуру под названиемCreature
с полямиstring
дляName
иGreeting
. Для этогоCreature
определен единственный метод,Greet
. В объявлении получателя мы присвоили экземплярCreature
переменнойc
, чтобы мы могли ссылаться на поляCreature
при сборке приветственного сообщения вfmt.Printf
. .
В других языках получатель вызовов методов обычно упоминается по ключевому слову (например, this
илиself
). Go считает, что получатель является переменной, как и любая другая, поэтому вы можете называть ее как угодно. Стиль, предпочтительный сообществом для этого параметра, является строчной версией первого символа типа получателя. В этом примере мы использовалиc
, потому что тип приемника былCreature
.
В телеmain
мы создали экземплярCreature
и указали значения для его полейName
иGreeting
. Мы вызвали здесь методGreet
, объединив имя типа и имя метода с.
и указав экземплярCreature
в качестве первого аргумента.
Go предоставляет другой, более удобный способ вызова методов для экземпляров структуры, как показано в этом примере:
package main
import "fmt"
type Creature struct {
Name string
Greeting string
}
func (c Creature) Greet() {
fmt.Printf("%s says %s", c.Name, c.Greeting)
}
func main() {
sammy := Creature{
Name: "Sammy",
Greeting: "Hello!",
}
sammy.Greet()
}
Если вы запустите это, результат будет таким же, как в предыдущем примере:
OutputSammy says Hello!
Этот пример идентичен предыдущему, но на этот раз мы использовалиdot notation для вызова методаGreet
, используяCreature
, хранящийся в переменнойsammy
, в качестве получателя. Это сокращенное обозначение вызова функции в первом примере. Стандартная библиотека и сообщество Go предпочитают этот стиль настолько, что вы редко увидите стиль вызова функции, показанный ранее.
Следующий пример показывает одну причину, почему точечные обозначения более распространены:
package main
import "fmt"
type Creature struct {
Name string
Greeting string
}
func (c Creature) Greet() Creature {
fmt.Printf("%s says %s!\n", c.Name, c.Greeting)
return c
}
func (c Creature) SayGoodbye(name string) {
fmt.Println("Farewell", name, "!")
}
func main() {
sammy := Creature{
Name: "Sammy",
Greeting: "Hello!",
}
sammy.Greet().SayGoodbye("gophers")
Creature.SayGoodbye(Creature.Greet(sammy), "gophers")
}
Если вы запустите этот код, вывод будет выглядеть так:
OutputSammy says Hello!!
Farewell gophers !
Sammy says Hello!!
Farewell gophers !
Мы изменили предыдущие примеры, чтобы ввести другой метод с именемSayGoodbye
, а также изменилиGreet
, чтобы он возвращалCreature
, чтобы мы могли вызывать дополнительные методы в этом экземпляре. В телеmain
мы вызываем методыGreet
иSayGoodbye
для переменнойsammy
сначала с использованием точечной нотации, а затем с использованием функционального стиля вызова.
Оба стиля выдают одинаковые результаты, но пример с точечной нотацией гораздо удобнее для чтения. Цепочка точек также сообщает нам последовательность, в которой будут вызываться методы, где функциональный стиль инвертирует эту последовательность. Добавление параметра к вызовуSayGoodbye
еще больше скрывает порядок вызовов методов. Четкость нотации с точками является причиной того, что это предпочтительный стиль для вызова методов в Go, как в стандартной библиотеке, так и среди сторонних пакетов, которые вы найдете во всей экосистеме Go.
Определение методов для типов, в отличие от определения функций, работающих с определенным значением, имеет другое особое значение для языка программирования Go. Методы являются основной концепцией интерфейсов.
Интерфейсы
Когда вы определяете метод для любого типа в Go, этот метод добавляется кmethod set типа. Набор методов представляет собой набор функций, связанных с этим типом в качестве методов и используемых компилятором Go, чтобы определить, можно ли назначить некоторый тип переменной с типом интерфейса. interface type - это спецификация методов, используемых компилятором, чтобы гарантировать, что тип предоставляет реализации для этих методов. Любой тип, который имеет методы с тем же именем, теми же параметрами и теми же возвращаемыми значениями, что и в определении интерфейса, называетсяimplement этого интерфейса и может быть назначен переменным с этим типом интерфейса. Ниже приводится определение интерфейсаfmt.Stringer
из стандартной библиотеки:
type Stringer interface {
String() string
}
Чтобы тип мог реализовать интерфейсfmt.Stringer
, он должен предоставить методString()
, который возвращаетstring
. Реализация этого интерфейса позволит печатать ваш тип точно так, как вы хотите (иногда это называется «красиво напечатано»), когда вы передаете экземпляры вашего типа функциям, определенным в пакетеfmt
. В следующем примере определяется тип, который реализует этот интерфейс:
package main
import (
"fmt"
"strings"
)
type Ocean struct {
Creatures []string
}
func (o Ocean) String() string {
return strings.Join(o.Creatures, ", ")
}
func log(header string, s fmt.Stringer) {
fmt.Println(header, ":", s)
}
func main() {
o := Ocean{
Creatures: []string{
"sea urchin",
"lobster",
"shark",
},
}
log("ocean contains", o)
}
Когда вы запустите код, вы увидите следующее:
Outputocean contains : sea urchin, lobster, shark
В этом примере определяется новый тип структуры с именемOcean
. Ocean
называетсяimplement интерфейсомfmt.Stringer
, потому чтоOcean
определяет метод с именемString
, который не принимает параметров и возвращаетstring
. Вmain
мы определили новыйOcean
и передали его функцииlog
, которая сначала выводит на печатьstring
, а затем все, что реализуетfmt.Stringer
с. Компилятор Go позволяет нам передавать здесьo
, потому чтоOcean
реализует все методы, запрошенныеfmt.Stringer
. Вlog
мы используемfmt.Println
, который вызывает методString
дляOcean
, когда он встречаетfmt.Stringer
в качестве одного из своих параметров.
ЕслиOcean
не предоставил методString()
, Go выдаст ошибку компиляции, потому что методlog
запрашиваетfmt.Stringer
в качестве аргумента. Ошибка выглядит так:
Outputsrc/e4/main.go:24:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log:
Ocean does not implement fmt.Stringer (missing String method)
Go также будет следить за тем, чтобы предоставленный методString()
в точности совпадает с методом, запрошенным интерфейсомfmt.Stringer
. Если это не так, он выдаст ошибку, которая выглядит следующим образом:
Outputsrc/e4/main.go:26:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log:
Ocean does not implement fmt.Stringer (wrong type for String method)
have String()
want String() string
В приведенных выше примерах мы определили методы для получателя значения. То есть, если мы используем функциональный вызов методов, первым параметром, относящимся к типу, для которого был определен метод, будет значение этого типа, а неpointer. Следовательно, любые изменения, которые мы вносим в экземпляр, предоставленный методу, будут отброшены, когда метод завершит выполнение, потому что полученное значение является копией данных. Также возможно определить методы на приемнике указателя на тип.
Приемник указателя
Синтаксис определения методов в получателе указателя практически идентичен определению методов в получателе значения. Разница заключается в том, что в объявлении получателя перед именем типа ставится звездочка (*
). В следующем примере определяется метод получения указателя на тип:
package main
import "fmt"
type Boat struct {
Name string
occupants []string
}
func (b *Boat) AddOccupant(name string) *Boat {
b.occupants = append(b.occupants, name)
return b
}
func (b Boat) Manifest() {
fmt.Println("The", b.Name, "has the following occupants:")
for _, n := range b.occupants {
fmt.Println("\t", n)
}
}
func main() {
b := &Boat{
Name: "S.S. DigitalOcean",
}
b.AddOccupant("Sammy the Shark")
b.AddOccupant("Larry the Lobster")
b.Manifest()
}
При запуске этого примера вы увидите следующий вывод:
OutputThe S.S. DigitalOcean has the following occupants:
Sammy the Shark
Larry the Lobster
В этом примере определен типBoat
сName
иoccupants
. Мы хотим, чтобы код в других пакетах добавлял агентов только с методомAddOccupant
, поэтому мы сделали полеoccupants
неэкспортированным, указав первую букву имени поля в нижнем регистре. Мы также хотим убедиться, что вызовAddOccupant
приведет к изменению экземпляраBoat
, поэтому мы определилиAddOccupant
на приемнике указателя. Указатели действуют как ссылки на конкретный экземпляр типа, а не как копия этого типа. Зная, чтоAddOccupant
будет вызываться с использованием указателя наBoat
, гарантирует, что любые изменения сохранятся.
Вmain
мы определяем новую переменнуюb
, которая будет содержать указатель наBoat
(*Boat
). Мы дважды вызываем методAddOccupant
для этого экземпляра, чтобы добавить двух пассажиров. МетодManifest
определяется для значенияBoat
, потому что в его определении получатель указан как(b Boat)
. Вmain
мы все еще можем вызватьManifest
, потому что Go может автоматически разыменовать указатель для получения значенияBoat
. b.Manifest()
здесь эквивалентно(*b).Manifest()
.
Независимо от того, определен ли метод на получателе указателя или на получателе значения, это имеет важные последствия при попытке присвоить значения переменным, которые являются типами интерфейса.
Приемники и интерфейсы
Когда вы присваиваете значение переменной с типом интерфейса, компилятор Go проверяет набор методов присваиваемого типа, чтобы убедиться, что он имеет методы, ожидаемые интерфейсом. Наборы методов для получателя указателя и получателя значения различны, потому что методы, которые получают указатель, могут модифицировать свой получатель там, где те, которые получают значение, не могут.
В следующем примере показано определение двух методов: один для получателя указателя типа и для получателя его значения. Однако только приемник указателя сможет удовлетворить интерфейс, также определенный в этом примере:
package main
import "fmt"
type Submersible interface {
Dive()
}
type Shark struct {
Name string
isUnderwater bool
}
func (s Shark) String() string {
if s.isUnderwater {
return fmt.Sprintf("%s is underwater", s.Name)
}
return fmt.Sprintf("%s is on the surface", s.Name)
}
func (s *Shark) Dive() {
s.isUnderwater = true
}
func submerge(s Submersible) {
s.Dive()
}
func main() {
s := &Shark{
Name: "Sammy",
}
fmt.Println(s)
submerge(s)
fmt.Println(s)
}
Когда вы запустите код, вы увидите следующее:
OutputSammy is on the surface
Sammy is underwater
В этом примере определен интерфейс с именемSubmersible
, ожидающий типов, имеющих методDive()
. Затем мы определили типShark
с полемName
и методомisUnderwater
для отслеживания состоянияShark
. Мы определили методDive()
на приемнике указателя наShark
, который изменилisUnderwater
наtrue
. Мы также определили методString()
приемника значения, чтобы он мог чисто распечатать состояниеShark
с помощьюfmt.Println
, используя интерфейсfmt.Stringer
, принятыйfmt.Println
, которые мы рассмотрели ранее. Мы также использовали функциюsubmerge
, которая принимает параметрSubmersible
.
Использование интерфейсаSubmersible
вместо*Shark
позволяет функцииsubmerge
зависеть только от поведения, обеспечиваемого типом. Это делает функциюsubmerge
более пригодной для повторного использования, потому что вам не придется писать новые функцииsubmerge
дляSubmarine
, aWhale
или любых других будущих водных обитателей, которых у нас нет. я еще не думал об этом. Пока они определяют методDive()
, их можно использовать с функциейsubmerge
.
Вmain
мы определили переменнуюs
, которая является указателем наShark
, и сразу напечаталиs
сfmt.Println
. Это показывает первую часть вывода,Sammy is on the surface
. Мы передалиs
вsubmerge
, а затем снова вызвалиfmt.Println
сs
в качестве аргумента, чтобы увидеть вторую часть вывода,Sammy is underwater
.
Если мы изменимs
наShark
, а не на*Shark
, компилятор Go выдаст ошибку:
Outputcannot use s (type Shark) as type Submersible in argument to submerge:
Shark does not implement Submersible (Dive method has pointer receiver)
Компилятор Go любезно сообщает нам, чтоShark
действительно имеет методDive
, он просто определен на приемнике указателя. Когда вы видите это сообщение в своем собственном коде, исправление заключается в передаче указателя на тип интерфейса с помощью оператора&
перед переменной, которой назначен тип значения.
Заключение
Объявление методов в Go в конечном счете ничем не отличается от определения функций, которые получают различные типы переменных. Применяются те же правилаworking with pointers. Go предоставляет некоторые удобства для этого чрезвычайно общего определения функции и собирает их в наборы методов, которые могут быть обоснованы типами интерфейса. Эффективное использование методов позволит вам работать с интерфейсами в вашем коде, чтобы улучшить тестируемость и оставить лучшую организацию для будущих читателей вашего кода.
Если вы хотите узнать больше о языке программирования Go в целом, ознакомьтесь с нашимиHow To Code in Go series.