Статьи

Пристальный взгляд на функции в Go

Функции являются одним из основных строительных блоков любого языка программирования. Однако набор функций и роль, которую они играют, сильно различаются от языка к языку. В этой статье мы рассмотрим, что может предложить Go, когда дело доходит до функций. Мы обнаружим, что функции Go являются первоклассными гражданами и предоставят разработчику очень богатый набор функций — с некоторыми интересными деталями, которые сначала не были очевидны. Давайте начнем, охватывая основы.

аргументы

Функция в Go может принимать любое количество типизированных аргументов. Все аргументы являются обязательными, так как нет понятия необязательных аргументов. Также важно знать, что аргументы не передаются по ссылке, а копируются в функцию (с несколькими заметными исключениями, которые мы вскоре увидим). Это означает, что если аргумент изменяется внутри функции, это не повлияет на исходное значение. Единственный способ обойти это — передать указатель на функцию. Если вы используете динамические языки, такие как PHP, Ruby или JavaScript, то концепция указателей может быть вам не знакома. Однако вы бы неявно использовали указатели, поскольку эти языки передают (некоторые) аргументы по ссылке. Основная концепция указателя проста: когда вы объявляете переменную в вашей программе, она находится в некотором месте в памяти. Указатели просто содержат адрес этого местоположения (они «указывают» на переменную). Это означает, что с помощью указателя вы можете получить фактическое значение и изменить его.

Теперь вернемся к функциям. Как упоминалось ранее, большинство аргументов копируются в функции. Если указатель скопирован, он все еще указывает на тот же адрес в памяти, так что вы можете изменить значение. Простой пример:

[Golang]
var counter int
приращение func (i int) {
я = я + 1
}
counter = 0
прирост (счетчик)
fmt.Printf («% v», counter) // печатает 0
[/ Golang]

против

[Golang]
var counter int
приращение func (i * int) {
* я = * я + 1
}
counter = 0
приращение (& счетчик)
fmt.Printf («% v», counter) // Печатает 1
[/ Golang]

& создает указатель и * разыменовывает его (возвращает значение, на которое указывает указатель).

Помните, я упоминал, что почти все передается по стоимости? Важно знать, что карты, срезы и каналы разные, что поначалу может быть довольно запутанным. В Golang FAQ есть подробное объяснение причин, лежащих в основе этого проектного решения. Другое дело, что получателям или аргументам, имеющим тип интерфейса, не предшествует * , но они все же могут быть указателями!

Возвращаемые значения

В Go функции также могут иметь возвращаемые значения. Если ничего не указано, функция ничего не возвращает (в отличие от других языков, где вы можете получить nil или undefined что на самом деле является чем-то ). Фактически, попытка использовать результат функции, которая не имеет возвращаемого значения, является ошибкой компиляции.

Более интересной характеристикой является то, что Go допускает множественные возвращаемые значения. Это то, чего не хватает большинству популярных языков веб-программирования, и оно оказывается очень удобным. Внутри стандартной библиотеки Go он обычно используется для обработки ошибок. Рассмотрим интерфейс Writer :

[Golang]
type Writer interface {
Запись (p [] byte) (n int, ошибка err)
}
[/ Golang]

Если Write успешна, n будет числом записанных байтов, а err будет nil . Если запись не удалась, n будет 0 а err будет содержать произошедшую ошибку. Потребительский код может использовать это следующим образом:

[Golang]
если n, err: = file.Write (p); err! = nil {
fmt.Println («Не могу написать.»)
}
[/ Golang]

Интерфейс Writer показывает еще один интересный бит о возвращаемых значениях: они могут быть названы заранее. Это устраняет необходимость инициализации переменных внутри функции. Эти именованные переменные автоматически инициализируются до их нулевого значения и могут быть возвращены в любое время в функции простым оператором return .

Приемники

До этого мы обсуждали обычные функции. Если вы знакомы с объектно-ориентированными языками, вы будете знать, что у них есть концепция, похожая на функцию, называемую методом, которая в основном является функцией, но действует в контексте объекта.

Go предоставляет эту функциональность через приемники. Однако есть небольшая разница, потому что все может быть получателем. Это может быть структура, но может быть и целым числом. Кроме того, получатель может быть значением или указателем.

Возьмем, к примеру, этот метод Name определенный для значения типа User (которое является структурой):

[Golang]
Тип Пользовательская структура {
строка имени
фамилия строка
}
func (user User) Имя () строка {
вернуть user.firstname + «» + user.lastname
}
peter: = Пользователь {имя: «Питер», фамилия: «Пан»}
fmt.Println (peter.Name ()) // печатает «Питер Пэн»
[/ Golang]

Здесь всякий раз, когда вызывается Name() , структура User копируется. Часто лучше использовать указатель приемника. Кроме того, это необходимо сделать, если вы хотите изменить приемник, например, так:

[Golang]
func (пользователь * пользователь) SwapName () {
имя: = user.firstname
user.firstname = user.lastname
user.lastname = имя
}
Пол: = & Пользователь {имя: «Пол», фамилия: «Пантера»}
paul.SwapName ()
fmt.Println (paul.Name ()) // печатает «Пантера Пол»
[/ Golang]

Если бы этот метод был определен для user User получателя user User , имя поменялось бы только внутри функции, а fmt.Println напечатало бы Paul Panther .

Что произойдет, если мы попытаемся вызвать использование peter.SwapName ? peter имеет тип User (значение), однако получатель имеет тип *User (указатель). Это работает? Интересно, что это так. Go достаточно умен, чтобы создать и передать указатель на значение User , а затем функция оперирует этим указателем, изменяя переменную peter .

Страшный нулевой указатель

Еще один вопрос, который вы могли бы задать себе, читая о приемниках, — это что произойдет, если получатель будет nil ? Это распространенная проблема во многих языках, и я уверен, что вы пытались вызвать метод для объекта, который был nil в какой-то момент вашей жизни, как программист, и результат не был хорошим вообще. Затем вы пошли дальше и добавили меры предосторожности к своему коду везде, где могут возникнуть эти нулевые указатели, и в результате получили очень уродливый код…

Иди на помощь! Допустим, у нас есть FirstUser() который возвращает нашего первого пользователя. Однако у нас пока нет пользователей, поэтому начальная реализация будет:

[Golang]
func FirstUser () * Пользователь {
вернуть ноль
}
[/ Golang]

Теперь, если мы хотим получить имя первого пользователя в нашем коде, мы напишем что-то вроде этого:

[Golang]
name: = FirstUser (). Name ()
[/ Golang]

С таким кодом и потому что FirstUser() возвращает nil , Go будет паниковать. Самый простой способ исправить это — добавить защиту, как упоминалось выше, но она выглядит не очень красиво:

[Golang]
пользователь: = FirstUser ()
если пользователь! = ноль {
name: = user.Name ()
}
[/ Golang]

В Go мы можем справиться с этим лучше: внутри функций. Мы можем сделать это, потому что в Go можно ввести nil s:

[Golang]
func (u * User) Name () строка {
если ты == ноль {
возвращение «»
}
вернуть user.firstname + «» + user.lastname
}
[/ Golang]

В отличие от других языков, где вы не можете вызвать какой-либо метод для nil объектов, Go позволяет вам вызывать функцию на приемнике указателя. Поэтому, если может быть сценарий, в котором получатель равен nil , убедитесь, что он обрабатывается внутри функции, чтобы вам не требовалась защита снаружи, что делает потребительский код намного менее загроможденным.

Затворы

Последняя особенность функций Go заключается в том, что они могут быть анонимными и использоваться как замыкания, очень похожие на те, что вы знаете из JavaScript.

Например:

[Golang]
func counter () func () int {
с: = 0
return func () int {
с + = 1
возврат с
}
}
[/ Golang]

Функция counter имеет тип возврата func() int , что означает, что она возвращает функцию с типом возврата int . Если мы вызываем функцию счетчика и сохраняем ее возвращаемое значение в переменной, мы можем вызвать эту переменную (которая содержит значение функции) несколько раз, и счетчик будет увеличен (поскольку внутренняя функция имеет доступ к той же самой переменной c ):

[Golang]
count: = counter ()
fmt.Println (count ()) // Печатает 1
fmt.Println (count ()) // Печать 2
fmt.Println (count ()) // Печать 3
[/ Golang]

Вывод

Функции являются основным строительным блоком языка Go. У них есть некоторые необычные особенности, такие как множественные возвращаемые значения и передача-копирование, которые сильно влияют на способ написания программ. Реализация «методов» через приемники довольно гибкая и допускает не только обычные методы, такие как хорошая обработка nil . Однако функции Go также требуют от программиста глубокого понимания значений и указателей, а также того, когда и когда их использовать.