Хочешь уверенно проходить IT-интервью?

Мы понимаем, как сложно подготовиться: стресс, алгоритмы, вопросы, от которых голова идёт кругом. Но с AI тренажёром всё гораздо проще.
💡 Почему Т1 тренажёр — это мастхэв?
- Получишь настоящую обратную связь: где затык, что подтянуть и как стать лучше
- Научишься не только решать задачи, но и объяснять своё решение так, чтобы интервьюер сказал: "Вау!".
- Освоишь все этапы собеседования, от вопросов по алгоритмам до диалога о твоих целях.
Зачем листать миллион туториалов? Просто зайди в Т1 тренажёр, потренируйся и уверенно удиви интервьюеров. Мы не обещаем лёгкой прогулки, но обещаем, что будешь готов!
Реклама. ООО «Смарт Гико», ИНН 7743264341. Erid 2VtzqwP8vqy
Функции
Функция в программировании является фундаментальным инструментом и представляет собой блок кода, выполняющий определенную задачу и доступный для обращения из другого места программы.
Функции лежат в основе построения большого числа программ, так как позволяют:
- Структурировать код, разбивая его на управляемые фрагменты.
- Предотвратить дублирование кода.
- Скрыть подробности реализации за упрощенным интерфейсом.
- Улучшить читаемость кода.
- Тестировать программу.
В предыдущих циклах самоучителя мы уже познакомились с некоторыми встроенными функциями Go, которые разработчики языка написали для решения часто встречающихся задач. Например, функция Println
из пакета fmt
выводит текст с последующим переводом на новую строку, а Itoa
из пакета strconv
преобразовывает число в строку:
num := 123
str := strconv.Itoa(num)
fmt.Println("Результат преобразования числа в строку: ", str)
Помимо встроенных функций, есть также пользовательские, которые создаются непосредственно при написании программ.
Но прежде чем приступить к написанию собственных функций, давайте разберем общую сигнатуру функции.
Сигнатура функции – это её формальное описание, включающее имена, типы параметров и возвращаемых значений.
Общий вид функции в Go выглядит следующим образом:
// ключевое слово func
// |
func имя_функции (перечисление параметров с типами) типы_возвращаемых_значений {
// тело функции
return возвращаемое_значение // возврат значения
}
Для примера рассмотрим функцию, которая принимает целое число и возвращает сумму его делителей:
func sumDivisors(num int) int {
var sum int
for i := 1; i < num/2+1; i++ {
if num%i == 0 {
sum += i
}
}
sum += num
return sum
}
С сигнатурой разобрались, теперь давайте рассмотрим различные способы записи функций в языке Go.
- Функция может не иметь параметров и возвращаемого значения. Самый простой пример такой функции –
main
:
func main() {
// функция main не принимает параметров и не возвращает значений
}
- Для несколько подряд идущих переменных одного типа можно указать его после последнего параметра:
// функция, возвращающая максимальное из двух чисел
func max(a, b int) int {
if a > b {
return a
}
return b
}
- Функции могут принимать переменное число параметров. Для этого перед типом параметров ставится многоточие:
// Функция sumNums принимает ноль и более целых чисел и возвращает их сумму
func sumNums(nums ...int) int {
sum := 0
for _, num := range nums {
sum += num
}
return sum
}
// тестируем функцию sumNums
func main() {
fmt.Println(sumNums()) // 0
fmt.Println(sumNums(1, 2, 3, 4)) // 10
fmt.Println(sumNums(-1, 1, 100)) // 100
}
- В Go функции часто возвращают два значения – тип и ошибку. К примеру, так выглядит сигнатура знакомой нам функции Atoi из пакета
strconv
, которая преобразует строку в число. Она принимает строку, а возвращает преобразованное число и ошибку в случае её возникновения.
func Atoi(s string) (int, error)
На этом этапе мы научились писать собственные функции, осталось только понять, как передавать в них значения и получать требуемый результат.
В этом нам помогут аргументы – это значения, передаваемые функции при её вызове в программе. Аргументами могут являться базовые и составные типы данных, указатели, функции и интерфейсы.
Для примера вызовем ранее написанную функцию max
и передадим в качестве аргументов два целых числа:
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(max(6, 8)) // 8
}
Обратите внимание, что функция max
возвращает целое число, но для его вывода на экран нужно воспользоваться функцией fmt.Println
. Немного изменим эту функцию, чтобы она сразу печатала максимальное из двух чисел. Это позволит значительно сократить её вызов:
func max(a, b int) {
if a > b {
fmt.Println(a)
} else {
fmt.Println(b)
}
}
func main() {
max(1, 2) // 2
}
Область видимости

Область видимости обозначает тот участок кода, где может использоваться созданная переменная. Во многих языках программирования, включая Go, существует две области видимости: глобальная и локальная.
Переменные, созданные в функции, являются локальными, то есть к ним нельзя обращаться за её пределами. К примеру, переменная res
в примере ниже является локальной и доступна только в функции localVariable
:
func localVariable() {
var res = "local"
fmt.Println("Локальная переменная:", res)
}
func main() {
res = "" // ошибка
localVariable() // Локальная переменная: local
}
Глобальные переменные, в отличие от локальных, доступны из любого места программы. В приведенном ниже примере переменная res
объявлена за пределами функции, поэтому является глобальной и может изменяться в произвольном участке кода:
var res string
func editGlobalVariable() {
res = "global"
fmt.Println("Глобальная переменная:", res)
}
func main() {
editGlobalVariable() // Глобальная переменная: global
res = "changed global"
fmt.Println(res) // changed global
}
Передача по значению и по указателю

При работе с функциями стоит помнить важную деталь – в Go все аргументы передаются по значению, то есть копируются. Это означает, что по умолчанию в функцию передается копия переменной, а не её исходное значение. Чтобы получить доступ к исходному значению, необходимо использовать указатели.
Указатель – это переменная, которая в качестве значений хранит адреса других объектов или функций.
Давайте поймем разницу между этими двумя подходами на конкретном примере. Пусть у нас есть функция incrementValue
, увеличивающая переменную на единицу. Рассмотрим две её реализации, сначала с передачей по значению:
func incrementValue(val int) {
fmt.Println("Значение val до увеличения:", val) // 0
val += 10
fmt.Println("Значение val после увеличения:", val) // 10
}
func main() {
var num int
fmt.Println("Значение num до увеличения:", num) // 0
incrementValue(num)
fmt.Println("Значение num после увеличения:", num) // 0
}
Нетрудно заметить, что переменная num не увеличила своего значения после применения функции incrementValue
, а вот к локальной переменной val
прибавилось 10.
Следующий пример демонстрирует передачу значения по указателю:
func incrementValue(val *int) {
fmt.Println("Значение val до увеличения:", val) // 0x... (адрес переменной num)
*val += 10
fmt.Println("Значение val после увеличения:", val) // 0x... (адрес переменной num)
}
func main() {
var num int
fmt.Println("Значение num до увеличения:", num) // 0
incrementValue(&num) // оператор & используется для взятия адреса
fmt.Println("Значение num после увеличения:", num) // 10
}
В примере выше переменная num была передана по указателю в функцию incrementValue
, поэтому в результате все операции происходили непосредственно с num, а не с val
. В переменной var
, в свою очередь, сохранился адрес переменной num
.
Именование функций
Названия функций в Go рекомендуется писать в стиле camelCase, например: incrementValue
, getUserByID
, fetchConfig
и так далее.
Стоит также учитывать, что заглавная первая буква в названии функции говорит о том, что она экспортируемая, то есть доступная для использования в других пакетах, а маленькая – о том, что используется только в пределах текущего.
Defer или отложенное выполнение

Ключевое слово defer является особенностью Go и используется для отложенного выполнения функций. Defer добавляет вызов новой функции в специальный стек, который обрабатывается в порядке LIFO (Last In First Out – Последним пришел, первым вышел), и после этого продолжает выполнение текущей функции.
На практике defer часто применяется при взаимодействии с файлами и сетевыми соединениями, чтобы гарантировать их корректное закрытие и освобождение ресурсов, что позволяет избежать утечек памяти. Также он используется для досрочного завершения программы в случае ошибки.
Рассмотрим несколько блоков кода, отражающих поведение defer
.
- Пример вызова
defer
для отложенного закрытия файла:
func main() {
f, err := os.Open("filename.txt") // открытие файла
if err != nil {
return ""
}
defer f.Close() // отложенное закрытие файла
}
- Следующий код наглядно показывает важное правило
defer
, которое заключается в том, что его аргументы обрабатываются в момент вызова, а не в момент фактического выполнения:
func incrementNumber() {
num := 0
defer fmt.Println(num)
num++
}
func main() {
incrementNumber() // 0
}
В этом примере переменная num в момент вызова defer fmt.Println(num)
имеет значение 0, которое и передаются в отложенную функцию Println
. Далее num
увеличивается на единицу, но, так как в Println
она была передана со значением 0, в итоге на экран будет выведен именно 0, а не 1.
- При использовании
defer
нужно помнить, что он работает по принципу LIFO. По этой причине код ниже напечатает значения в обратном порядке, то есть 54321:
func main() {
for i := 1; i < 6; i++ {
defer fmt.Print(i)
}
}
Рекурсия

Рекурсия в программировании – это процесс, при котором функция вызывает саму себя.
Выделяют три вида рекурсии:
- Прямая: функция вызывает саму себя.
- Косвенная: цепочка вызовов включает более одной функции.
- Хвостовая: рекурсивный вызов стоит последней операцией в функции.
Рекурсия широко используется в программировании для решения целого ряда задач:
- Математические расчеты, в которых следующее значение получается на основе предыдущего. Например, вычисление факториала или чисел Фибоначчи.
- Реализация алгоритмов и структур данных, таких как быстрая сортировка, графы, деревья.
- Разбиение сложных задач на простые.
Несмотря на все преимущества, рекурсия не лишена недостатков: неправильное её использование может привести к увеличению времени работы программы и переполнению памяти, что негативно скажется на производительности.
Давайте рассмотрим классический пример рекурсивной функции, вычисляющей факториал неотрицательного целого числа:
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
func main() {
fmt.Println(factorial(5)) // 120
}
Если не указать корректное условия выхода из функции, то можно получить бесконечную рекурсию, которая приведет к некорректному завершению программы. Это распространенная ошибка, которую допускают даже опытные программисты.
Следующий код для вычисления факториала будет выполняться бесконечно, что в определенный момент приведет к досрочному завершению программы:
func factorial(n int) int {
return n * factorial(n-1) // бесконечная рекурсия
}
func main() {
fmt.Println(factorial(5))
}
Для предотвращения бесконечной рекурсии следует внимательно следить за рекурсивными вызовами и условиями выхода из функций, а также пользоваться линтерами – программами для анализа кода на наличие потенциальных ошибок. Например, линтер go-staticcheck при подозрении на бесконечную рекурсию выведет предупреждение infinite recursive call (SA5007)
.
Практическая часть
По традиции решим несколько занимательных задач для закрепления материала.
Гоша считает итоговые оценки
В школе Гоше объяснили, что итоговая оценка за предмет есть не что иное, как среднее арифметическое всех полученных ранее отметок. За последнее время он получил много отметок и совсем не хочет считать итоговые оценки вручную. Ваша задача помочь Гоше, создав функцию averageValue
для подсчета среднего арифметического переданных чисел.
Пример вызовов функции и ожидаемых результатов:
func main() {
fmt.Println(averageValue(1, 2, 3, 5)) // 2.5
fmt.Println(averageValue(1, -1)) // 0
fmt.Println(averageValue(1, -1, -1)) // -0.3333333333333333
fmt.Println(averageValue()) // NaN
}
Подсказка: для ввода произвольного числа параметров используйте многоточие и помните, что среднее значение может быть дробным числом, поэтому возьмите тип float64.
Решение:
func averageValue(nums ...float64) float64 {
var sum float64
for _, v := range nums {
sum += float64(v)
}
return sum / float64(len(nums))
}
func main() {
fmt.Println(averageValue(1, 2, 3, 4)) // 2.5
}
Просто проверить на простоту
Напишите функцию isPrime
, принимающую целое число и возвращающее true, если оно простое, то есть делится только на себя и на единицу, и false в противном случае.
Пример вызовов функции и ожидаемых результатов:
func main() {
fmt.Println(isPrime(100)) // false
fmt.Println(isPrime(1)) // false
fmt.Println(isPrime(23)) // true
fmt.Println(isPrime(17)) // true
}
Подсказка: значения в цикле достаточно перебирать до квадратного корня из числа, для его взятия используйте функцию math.Sqrt
из пакета math
, но не забудьте преобразовать это значение к типу int.
Решение: в цикле проверяем делимость числа на каждое значение от 2 до квадратного корня из числа.
func isPrime(num int) bool {
if num <= 1 {
return false
}
sqrt := int(math.Sqrt(float64(num)))
for i := 2; i < sqrt; i++ {
if num%i == 0 {
return false
}
}
return true
}
n-е число Фибоначчи
Напишите функцию fibonacci(n int)
, которая по введенному числу n возвращает n-е по номеру число Фибоначчи. Если введено некорректное значение n, то функция должна вернуть -1.
Справка: числа Фибоначчи – это такая последовательность, в которой первые два числа равны 0 и 1, а каждое последующее число получается как сумма двух предыдущих.
Пример вызовов функции и ожидаемых результатов:
func main() {
fmt.Println(fibonacci(1)) // 0
fmt.Println(fibonacci(2)) // 1
fmt.Println(fibonacci(10)) // 34
fmt.Println(fibonacci(-2)) // -1
}
Решение: воспользуемся рекурсией, указав корректные условия для значений n
.
func fibonacci(n int) int {
if n <= 0 {
return -1
} else if n > 2 {
return fibonacci(n-2) + fibonacci(n-1)
}
return n - 1
}
Подведем итоги
В этой части самоучителя мы научились использовать функции, работать с отложенными вызовами и рекурсией, узнали про способы передачи параметров и область видимости.
В следующей статье цикла изучим основные структуры данных для хранения элементов – массивы и слайсы. Разберем их внутреннее устройство, познакомимся с основными функциями и рассмотрим пакет slices
стандартной библиотеки.
Содержание самоучителя
- Особенности и сфера применения Go, установка, настройка
- Ресурсы для изучения Go с нуля
- Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
- Переменные. Типы данных и их преобразования. Основные операторы
- Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
- Функции и аргументы. Области видимости. Рекурсия. Defer
- Массивы и слайсы. Append и сopy. Пакет slices
- Строки, руны, байты. Пакет strings. Хеш-таблица (map)
- Структуры и методы. Интерфейсы. Указатели. Основы ООП
- Наследование, абстракция, полиморфизм, инкапсуляция
- Обработка ошибок. Паника. Восстановление. Логирование
- Обобщенное программирование. Дженерики
- Работа с датой и временем. Пакет time
- Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
- Конкурентность. Горутины. Каналы
- Тестирование кода и его виды. Table-driven подход. Параллельные тесты
- Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
- Протокол HTTP. Создание HTTP-сервера и клиента. Пакет net/http
Комментарии