Функции позволяют организовывать логику в повторяющихся процедурах, которые могут использовать различные аргументы при каждом запуске. В ходе определения функций вы часто можете обнаружить, что несколько функций могут работать с одним набором данных каждый раз. Go признает эту схему и позволяет вам определять специальные функции, которые называются методами и используются для работы с экземплярами какого-то определенного типа, называемого получателем. Добавление методов для типов позволяет вам передавать не только данные, но и то, как эти данные следует использовать.
Синтаксис для определения метода аналогичен синтаксису для определения функции. Единственная разница — это добавление дополнительного параметра после ключевого слова 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
для переменной с
, чтобы мы могли обращаться к полям 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!
Этот пример идентичен предыдущему, но в этот раз мы использовали запись через точку для вызова метода 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, этот метод добавляется в набор методов для типа. Набор методов — это коллекция функций, связанных с этим типом как методы и используемых компилятором Go для определения того, может ли определенный тип быть связан с переменной с типом интерфейса. Тип интерфейса — это спецификация методов, используемых компилятором для гарантии того, что тип обеспечивает реализацию этих методов. Любой тип, который имеет методы с тем же именем, теми же параметрами и теми же возвращаемыми значениями, что и методы, которые находятся в определении интерфейса, реализует этот интерфейс и может привязываться к переменным с данным типом интерфейса. Ниже приводится определение интерфейса fmt.Stringer
из стандартной библиотеки:
type Stringer interface {
String() string
}
Чтобы тип реализовывал интерфейс fmt.Stringer
, он должен иметь метод 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
, как сказано, реализует интерфейс fmt.Stringer
, поскольку Ocean
определяет метод под названием String
, который не принимает никаких параметров и возвращает строку
. В main
мы определили новый экземпляр Ocean
и передали его функции log
, которая получает строку
для вывода, после чего следует что-то, реализующее 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
В примерах выше мы определили методы для значения получателя. Это означает, что если мы используем функциональный вызов методов, первый параметр, который указывает тип, для которого был определен метод, будет значением этого типа, а не указателем. Следовательно, любые изменения, которые мы вносим в экземпляр, предоставленный методу, будут отменены, когда метод завершит исполнение, поскольку получаемое значение является копией данных. Также вы можете определить методы для получателя по указателю этого типа.
Синтаксис для определения методов для получателя по указателю практически полностью идентичен определению методов для получателя по значению. Разница состоит в добавлении префикса к имени типа в объявлении получателя со звездочкой (*
). Следующий пример определяет метод для получателя по указателю для типа:
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
, Whale
или любого будущего обитателя моря, которого у нас еще нет. Если они определяют метод 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 не отличается от определения функций, которые получают различные типы переменных. В этом случае применяются те же правила работы с указателями. Go предоставляет некоторые удобства для данного чрезвычайно распространенного определения функции и собирает эти методы в набор, которые могут быть отобраны по типам интерфейса. Использование методов эффективно позволяет вам работать с интерфейсами в вашем коде для облегчения тестирования и позволяет лучше организовать код для будущих читателей.
Если вы хотите узнать больше о языке программирования Go в целом, ознакомьтесь с нашей серией статей о программировании на языке Go.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!