После изучения данного урока вы сможете:
- Присваивать функции переменным;
- Передавать функции другим функциям;
- Создавать функции для создания других функций.
В Go функции можно присвоить переменным, передать функции другим функциям или даже написать функции для возвращения функций. Функции первого класса работают там же, где типы integer, string и прочие.
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
Содержание статьи
- Присваивание функции переменной
- Передача функции другой функции
- Объявление типов функции
- Замыкания (Closures) и анонимные функции
В данном уроке раскрывается потенциал использования функций первого класса как элементов теоретической программы «Станции экологического мониторинга Ровер (REMS)», что считывает данные температурных сенсоров.
Для сравнения подумаем о примере из реального мира. Мясо будет вкуснее с кетчупом. После приготовления мясного блюда вы можете найти домашний рецепт кетчупа, но зачем все так усложнять — сходите за кетчупом в обычный магазин.
Функции первого класса можно сравнить с мясом, которому нужно добавить кетчупа. Представим код, в котором функции makeMeat нужно вызвать функцию для кетчупа, будь то makeKetchup или openKetchup. Функции для кетчупа также можно использовать отдельно, однако мясо без кетчупа будет не таким вкусным.
Помимо рецептов и сенсоров температуры, какие другие примеры изменения функции с помощью другой функции вы можете привести?
Присваивание функции переменной в Go
Сенсоры станции погоды предоставляют данные о температуре воздуха в диапазоне 150–300° K. У нас есть функции для конвертации градусов Кельвина в другие единицы измерения при наличии данных, однако при отсутствии специального сенсора, встроенного в компьютер (или Raspberry Pi), считывающего информацию, это может стать проблематично.
Пока мы можем использовать фальшивый сенсор, что будет возвращать псевдослучайные числа, однако в таком случае нужно будет найти способ использовать realSensor или fakeSensor взаимозаменяемо. В следующем примере именно это происходит. При создании такой программы различные реальные сенсоры также могут быть подключены, к примеру, для сбора данных как о температуре земли, так и для температуры воздуха.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package main import ( "fmt" "math/rand" ) type kelvin float64 func fakeSensor() kelvin { return kelvin(rand.Intn(151) + 150) } func realSensor() kelvin { return 0 // TO-DO: внедрить реальный сенсор } func main() { sensor := fakeSensor // Присваивает функцию переменной fmt.Println(sensor()) sensor = realSensor fmt.Println(sensor()) } |
В данном примере переменная sensor присваивает функцию fakeSensor, но не сам результат вызова данной функции. Вызовы функции и метода всегда со скобками вроде fakeSensor(), но здесь другой случай.
Теперь вызов sensor() сразу вызовет realSensor или fakeSensor, в зависимости от того, к какой функции присваивается sensor.
Переменная sensor является типом той функции, что не принимает параметров и возвращает результат типа kelvin. Если не полагаться на автоматическое присваивание типа, переменная сенсора может быть объявлена следующим образом:
|
1 |
var sensor func() kelvin |
В Листинге 1 вы можете заново назначить sensor к realSensor из-за совпадения с сигнатурой функции fakeSensor. У обеих функций одинаковое количество параметров с тем же типом, они также возвращают значения.
Вопросы для проверки:
- В чем различие между присваиванием функции переменной и присваиванием результата вызываемой функции?
- Если бы существовала функция
groundSensor, что возвращала бы температуруcelsius, можно было бы ее присвоить кsensorиз Листинга 1?
Ответы
- У вызова функций и методов всегда есть скобки (к примеру,
fn()), но сама функция может присваиваться с определенным названием функции без скобок; - Нет. Для переназначения переменной сенсора параметры и возвращаемые значения должны быть одинакового типа. Компилятор Go сообщит об ошибке: в присваивании нельзя использовать
groundSensor.
Передача функции другой функции в Golang
Переменные могут отсылаться к функциям, а также передаваться функциям. Это значит, что Go допускает передачу одних функций другим.
Для фиксирования данных о температуре каждую секунду в Листинге 2 объявляется новая функция measureTemperature, что принимает функцию сенсора в качестве параметра. Она периодически вызывает функцию сенсора, будь то fakeSensor или realSensor.
Возможность передачи функций представляет собой мощный способ, что позволяет поделить код на части. Без учета функций первого класса вы бы наверняка пришли к функциям measureRealTemperature и measureFakeTemperature, что содержат идентичный код.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package main import ( "fmt" "math/rand" "time" ) type kelvin float64 func measureTemperature(samples int, sensor func() kelvin) { // measureTemperature принимает функцию в качестве второго параметра for i := 0; i < samples; i++ { k := sensor() fmt.Printf("%v° K\n", k) time.Sleep(time.Second) } } func fakeSensor() kelvin { return kelvin(rand.Intn(151) + 150) } func main() { measureTemperature(3, fakeSensor) // Передает название функции другой функции } |
Функция measureTemperature принимает два параметра, второй параметр принадлежит к типу func() kelvin. Объявление выглядит как объявление переменной того же типа:
|
1 |
var sensor func() kelvin |
Функция main может передать название функции к measureTemperature.
Вопрос для проверки:
В чем преимущество передачи функции другой функции?
Ответ
Функции первого класса дают возможность поделить и повторно использовать код.
Объявление типов функции в Golang
В Go есть возможность объявления нового типа для функции, что позволяет сократить и уточнить код, к которому она относится. Мы использовали тип kelvin скорее для передачи единицы измерения температуры, чем для базового представления. То же самое может быть сделано для переданных функций:
|
1 |
type sensor func() kelvin |
В коде речь идет не о функции, что не принимает параметров, возвращая значение kelvin, а о функциях sensor. Данный тип можно использовать для сокращения кода. Таким образом следующее объявление:
|
1 |
func measureTemperature(samples int, s func() kelvin) |
Можно переписать подобным образом:
|
1 |
func measureTemperature(samples int, s sensor) |
В данном примере сложно увидеть улучшение, ведь теперь при просмотре строки кода нужно понимать, что собой представляет sensor. Однако, если бы sensor использовался в нескольких местах, или если бы у типа функции было несколько параметров, использование типа избавило бы от беспорядка.
Задание для проверки:
Перепишите следующую сигнатуру функции для использования типа функции:
|
1 |
func drawTable(rows int, getRow func(row int) (string, string)) |
Решение
|
1 2 |
type getRowFn func(row int) (string, string) func drawTable(rows int, getRow getRowFn) |
Замыкания (Closures) и анонимные функции Golang
В Go анонимными функциями, или функциональными литералами называют функции без названия. В отличие от обычных функций, функциональные литералы являются замыканиями, потому что они сохраняют отсылки к переменным в окружающей области видимости.
Вы можете присвоить анонимную функцию переменной, и затем использовать данную переменную как любую другую функцию, как показано в примере ниже:
|
1 2 3 4 5 6 7 8 9 10 11 |
package main import "fmt" var f = func() { // Присваивает анонимную функцию переменной fmt.Println("Dress up for the masquerade.") } func main() { f() // Выводит: Dress up for the masquerade. } |
Объявленная переменная может быть в области видимости пакета или внутри функции, как показано в листинге ниже:
|
1 2 3 4 5 6 7 8 9 10 |
package main import "fmt" func main() { f := func(message string) { // Присваивает анонимную функцию переменной fmt.Println(message) } f("Go to the party.") // Выводит: Go to the party. } |
Можно объявить и задействовать анонимную функцию как показано ниже:
|
1 2 3 4 5 6 7 8 9 |
package main import "fmt" func main() { func() { // Объявляет анонимную функцию fmt.Println("Functions anonymous") }() // Вызов функции } |
Анонимные функции могут пригодиться, если нужно быстро написать функцию на ходу. Это может быть возвращение одной функции от другой функции. Хотя функция может возвращать существующую именованную функцию, объявлять и возвращать новую анонимную функцию может быть гораздо полезнее.
В Листинге 6 функция calibrate настраивается на ошибки в показаниях температуры воздуха. С помощью функции первого класса calibrate принимает фейковый или реальный сенсор в качестве параметра и возвращает функцию замены. Всякий раз, когда вызывается новая функция sensor, вызывается исходная функция, а чтение корректируется смещением.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package main import "fmt" type kelvin float64 // sensor function type type sensor func() kelvin func realSensor() kelvin { return 0 // TODO: имплементировать реальный сенсор } func calibrate(s sensor, offset kelvin) sensor { return func() kelvin { // Объявляет и возвращает анонимную функцию return s() + offset } } func main() { sensor := calibrate(realSensor, 5) fmt.Println(sensor()) Выводит: 5 } |
Анонимная функция в предыдущем листинге использует замыкания. Это отсылка к переменным s и offset, что функция calibrate принимает в качестве параметров. Даже после возвращения функции calibrate, переменные, зафиксированные замыканием, остаются. Таким образом, у sensor все еще есть доступ к данным переменным. Анонимная функция нужна для размыкания переменных в области видимости, что объясняет термин замыкание.
Из-за того, что замыкание сохраняет ссылку на ближайшие переменные, а не копирует их значения, изменения с этими переменными отражаются в вызовах к анонимной функции:
|
1 2 3 4 5 6 7 8 9 |
var k kelvin = 294.0 sensor := func() kelvin { return k } fmt.Println(sensor()) // Выводит: 294 k++ fmt.Println(sensor()) // Выводит: 295 |
Имейте это в виду, особенно при использовании замыкания внутри циклов for.
Вопросы для проверки:
- Как по-другому называется анонимная функция в Go?
- На что способны замыкания, но не могут обычные функции?
Ответы
- В Go анонимную функцию также называют функциональным литералом.
- Замыкания сохраняют отсылки к переменным в окружающей области видимости.
Заключение
- Функции первого класса открывают новые возможности для разделения и повторного использования кода;
- Для создания функции на ходу используйте анонимные функции с замыканиями.
Итоговое задание для проверки:
Наберите Листинг 6 на Go Playground и посмотрите на программу в действии:
- Вместо передачи 5 в качестве аргумента для
calibrate, объявите и передайте переменную. Измените переменную и убедитесь, что результатом вызовов кsensor()по-прежнему является 5. Все из-за того, что параметрoffsetявляется копией аргумента (переданный через значение); - Используйте
calibrateс функциейfakeSensorиз Листинга 2 для создания новой функцииsensor. Вызовите новую функциюsensorнесколько раз, обратите внимание, что оригинальная функцияfakeSensorпо-прежнему вызывается каждый раз, выдавая в результате случайные значения.
Решение
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package main import ( "fmt" "math/rand" ) type kelvin float64 type sensor func() kelvin func fakeSensor() kelvin { return kelvin(rand.Intn(151) + 150) } func calibrate(s sensor, offset kelvin) sensor { return func() kelvin { return s() + offset } } func main() { var offset kelvin = 5 sensor := calibrate(fakeSensor, offset) for count := 0; count < 10; count++ { fmt.Println(sensor()) } } |

Администрирую данный сайт с целью распространения как можно большего объема обучающего материала для языка программирования Go. В IT с 2008 года, с тех пор изучаю и применяю интересующие меня технологии. Проявляю огромный интерес к машинному обучению и анализу данных.
E-mail: vasile.buldumac@ati.utm.md
Образование
Технический Университет Молдовы (utm.md), Факультет Вычислительной Техники, Информатики и Микроэлектроники
- 2014 — 2018 Universitatea Tehnică a Moldovei, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Universitatea Tehnică a Moldovei, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»