Функции
Функция в программировании является фундаментальным инструментом и представляет собой блок кода, выполняющий определенную задачу и доступный для обращения из другого места программы.
Функции лежат в основе построения большого числа программ, так как позволяют:
- Структурировать код, разбивая его на управляемые фрагменты.
- Предотвратить дублирование кода.
- Скрыть подробности реализации за упрощенным интерфейсом.
- Улучшить читаемость кода.
- Тестировать программу.
В предыдущих циклах самоучителя мы уже познакомились с некоторыми встроенными функциями Go, которые разработчики языка написали для решения часто встречающихся задач. Например, функция Println
из пакета fmt
выводит текст с последующим переводом на новую строку, а Itoa
из пакета strconv
преобразовывает число в строку:
Помимо встроенных функций, есть также пользовательские, которые создаются непосредственно при написании программ.
Но прежде чем приступить к написанию собственных функций, давайте разберем общую сигнатуру функции.
Сигнатура функции – это её формальное описание, включающее имена, типы параметров и возвращаемых значений.
Общий вид функции в Go выглядит следующим образом:
Для примера рассмотрим функцию, которая принимает целое число и возвращает сумму его делителей:
С сигнатурой разобрались, теперь давайте рассмотрим различные способы записи функций в языке Go.
- Функция может не иметь параметров и возвращаемого значения. Самый простой пример такой функции –
main
:
- Для несколько подряд идущих переменных одного типа можно указать его после последнего параметра:
- Функции могут принимать переменное число параметров. Для этого перед типом параметров ставится многоточие:
- В Go функции часто возвращают два значения – тип и ошибку. К примеру, так выглядит сигнатура знакомой нам функции Atoi из пакета
strconv
, которая преобразует строку в число. Она принимает строку, а возвращает преобразованное число и ошибку в случае её возникновения.
На этом этапе мы научились писать собственные функции, осталось только понять, как передавать в них значения и получать требуемый результат.
В этом нам помогут аргументы – это значения, передаваемые функции при её вызове в программе. Аргументами могут являться базовые и составные типы данных, указатели, функции и интерфейсы.
Для примера вызовем ранее написанную функцию max
и передадим в качестве аргументов два целых числа:
Обратите внимание, что функция max
возвращает целое число, но для его вывода на экран нужно воспользоваться функцией fmt.Println
. Немного изменим эту функцию, чтобы она сразу печатала максимальное из двух чисел. Это позволит значительно сократить её вызов:
Область видимости
Область видимости обозначает тот участок кода, где может использоваться созданная переменная. Во многих языках программирования, включая Go, существует две области видимости: глобальная и локальная.
Переменные, созданные в функции, являются локальными, то есть к ним нельзя обращаться за её пределами. К примеру, переменная res
в примере ниже является локальной и доступна только в функции localVariable
:
Глобальные переменные, в отличие от локальных, доступны из любого места программы. В приведенном ниже примере переменная res
объявлена за пределами функции, поэтому является глобальной и может изменяться в произвольном участке кода:
Передача по значению и по указателю
При работе с функциями стоит помнить важную деталь – в Go все аргументы передаются по значению, то есть копируются. Это означает, что по умолчанию в функцию передается копия переменной, а не её исходное значение. Чтобы получить доступ к исходному значению, необходимо использовать указатели.
Указатель – это переменная, которая в качестве значений хранит адреса других объектов или функций.
Давайте поймем разницу между этими двумя подходами на конкретном примере. Пусть у нас есть функция incrementValue
, увеличивающая переменную на единицу. Рассмотрим две её реализации, сначала с передачей по значению:
Нетрудно заметить, что переменная num не увеличила своего значения после применения функции incrementValue
, а вот к локальной переменной val
прибавилось 10.
Следующий пример демонстрирует передачу значения по указателю:
В примере выше переменная num была передана по указателю в функцию incrementValue
, поэтому в результате все операции происходили непосредственно с num, а не с val
. В переменной var
, в свою очередь, сохранился адрес переменной num
.
Именование функций
Названия функций в Go рекомендуется писать в стиле camelCase, например: incrementValue
, getUserByID
, fetchConfig
и так далее.
Стоит также учитывать, что заглавная первая буква в названии функции говорит о том, что она экспортируемая, то есть доступная для использования в других пакетах, а маленькая – о том, что используется только в пределах текущего.
Defer или отложенное выполнение
Ключевое слово defer является особенностью Go и используется для отложенного выполнения функций. Defer добавляет вызов новой функции в специальный стек, который обрабатывается в порядке LIFO (Last In First Out – Последним пришел, первым вышел), и после этого продолжает выполнение текущей функции.
На практике defer часто применяется при взаимодействии с файлами и сетевыми соединениями, чтобы гарантировать их корректное закрытие и освобождение ресурсов, что позволяет избежать утечек памяти. Также он используется для досрочного завершения программы в случае ошибки.
Рассмотрим несколько блоков кода, отражающих поведение defer
.
- Пример вызова
defer
для отложенного закрытия файла:
- Следующий код наглядно показывает важное правило
defer
, которое заключается в том, что его аргументы обрабатываются в момент вызова, а не в момент фактического выполнения:
В этом примере переменная num в момент вызова defer fmt.Println(num)
имеет значение 0, которое и передаются в отложенную функцию Println
. Далее num
увеличивается на единицу, но, так как в Println
она была передана со значением 0, в итоге на экран будет выведен именно 0, а не 1.
- При использовании
defer
нужно помнить, что он работает по принципу LIFO. По этой причине код ниже напечатает значения в обратном порядке, то есть 54321:
Рекурсия
Рекурсия в программировании – это процесс, при котором функция вызывает саму себя.
Выделяют три вида рекурсии:
- Прямая: функция вызывает саму себя.
- Косвенная: цепочка вызовов включает более одной функции.
- Хвостовая: рекурсивный вызов стоит последней операцией в функции.
Рекурсия широко используется в программировании для решения целого ряда задач:
- Математические расчеты, в которых следующее значение получается на основе предыдущего. Например, вычисление факториала или чисел Фибоначчи.
- Реализация алгоритмов и структур данных, таких как быстрая сортировка, графы, деревья.
- Разбиение сложных задач на простые.
Несмотря на все преимущества, рекурсия не лишена недостатков: неправильное её использование может привести к увеличению времени работы программы и переполнению памяти, что негативно скажется на производительности.
Давайте рассмотрим классический пример рекурсивной функции, вычисляющей факториал неотрицательного целого числа:
Если не указать корректное условия выхода из функции, то можно получить бесконечную рекурсию, которая приведет к некорректному завершению программы. Это распространенная ошибка, которую допускают даже опытные программисты.
Следующий код для вычисления факториала будет выполняться бесконечно, что в определенный момент приведет к досрочному завершению программы:
Для предотвращения бесконечной рекурсии следует внимательно следить за рекурсивными вызовами и условиями выхода из функций, а также пользоваться линтерами – программами для анализа кода на наличие потенциальных ошибок. Например, линтер go-staticcheck при подозрении на бесконечную рекурсию выведет предупреждение infinite recursive call (SA5007)
.
Практическая часть
По традиции решим несколько занимательных задач для закрепления материала.
Гоша считает итоговые оценки
В школе Гоше объяснили, что итоговая оценка за предмет есть не что иное, как среднее арифметическое всех полученных ранее отметок. За последнее время он получил много отметок и совсем не хочет считать итоговые оценки вручную. Ваша задача помочь Гоше, создав функцию averageValue
для подсчета среднего арифметического переданных чисел.
Пример вызовов функции и ожидаемых результатов:
Подсказка: для ввода произвольного числа параметров используйте многоточие и помните, что среднее значение может быть дробным числом, поэтому возьмите тип float64.
Решение:
Просто проверить на простоту
Напишите функцию isPrime
, принимающую целое число и возвращающее true, если оно простое, то есть делится только на себя и на единицу, и false в противном случае.
Пример вызовов функции и ожидаемых результатов:
Подсказка: значения в цикле достаточно перебирать до квадратного корня из числа, для его взятия используйте функцию math.Sqrt
из пакета math
, но не забудьте преобразовать это значение к типу int.
Решение: в цикле проверяем делимость числа на каждое значение от 2 до квадратного корня из числа.
n-е число Фибоначчи
Напишите функцию fibonacci(n int)
, которая по введенному числу n возвращает n-е по номеру число Фибоначчи. Если введено некорректное значение n, то функция должна вернуть -1.
Справка: числа Фибоначчи – это такая последовательность, в которой первые два числа равны 0 и 1, а каждое последующее число получается как сумма двух предыдущих.
Пример вызовов функции и ожидаемых результатов:
Решение: воспользуемся рекурсией, указав корректные условия для значений n
.
Подведем итоги
В этой части самоучителя мы научились использовать функции, работать с отложенными вызовами и рекурсией, узнали про способы передачи параметров и область видимости.
В следующей статье цикла изучим основные структуры данных для хранения элементов – массивы и слайсы. Разберем их внутреннее устройство, познакомимся с основными функциями и рассмотрим пакет slices
стандартной библиотеки.
Содержание самоучителя
- Особенности и сфера применения Go, установка, настройка
- Ресурсы для изучения Go с нуля
- Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
- Переменные. Типы данных и их преобразования. Основные операторы
- Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
- Функции и аргументы. Области видимости. Рекурсия. Defer
- Массивы и слайсы. Append и сopy. Пакет slices
- Строки, руны, байты. Пакет strings. Хеш-таблица (map)
- Структуры и методы. Интерфейсы. Указатели. Основы ООП
- Наследование, абстракция, полиморфизм, инкапсуляция
- Обработка ошибок. Паника. Восстановление. Логирование
- Обобщенное программирование. Дженерики
- Работа с датой и временем. Пакет time
- Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
- Конкурентность. Горутины. Каналы
- Тестирование кода и его виды. Table-driven подход. Параллельные тесты
- Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
Комментарии