Урок курса

Объявление функций, параметры, несколько возвращаемых значений и область видимости

Golang с нуля

В предыдущем уроке вы управляли потоком выполнения через циклы: повторяли блок кода, пока выполняется условие, и досрочно выходили через break. Теперь пора научиться выносить логику в отдельные именованные блоки — функции. Это принципиальный шаг: код перестаёт быть одним длинным main, а отдельные задачи получают собственное имя и границы.

Синтаксис объявления функции: параметры и возвращаемый тип

Функция в Go объявляется ключевым словом func, за ним — имя, список параметров в круглых скобках и, если функция что-то возвращает, тип результата:

func add(a int, b int) int {
    return a + b
}

Здесь a и b — параметры типа int, а int после закрывающей скобки — тип возвращаемого значения. Когда выполнение доходит до return a + b, функция немедленно завершается и передаёт вычисленное значение обратно туда, откуда её вызвали.

Если несколько параметров имеют одинаковый тип, их можно объединить — тип пишется один раз в конце группы:

func add(a, b int) int {
    return a + b
}

Оба варианта полностью эквивалентны; сокращённая форма просто убирает дублирование.

Вызов функции выглядит предсказуемо:

result := add(3, 5)
fmt.Println(result) // 8

add(3, 5) — это выражение, которое вычисляется в 8, и это значение присваивается result.

Если функции нечего возвращать — например, она только печатает что-то — тип после скобок просто не указывается:

func greet(name string) {
    fmt.Println("Hello,", name)
}

В таком случае return можно писать без аргумента или вовсе опустить — функция завершится, когда выполнение дойдёт до закрывающей фигурной скобки.

Один важный момент про return: он останавливает выполнение функции в той точке, где стоит. Весь код ниже return в том же блоке уже не выполнится. Именно это свойство мы используем чуть позже, когда нужно выйти из функции по условию раньше времени.

Несколько возвращаемых значений и пустой идентификатор _

Go позволяет функции возвращать сразу несколько значений. Типы перечисляются в скобках после списка параметров:

func divide(a, b float64) (float64, bool) {
    if b == 0 {
        return 0, false
    }
    return a / b, true
}

Функция возвращает два значения: частное и признак того, что деление прошло успешно. Если делитель равен нулю — возвращаем 0 и false, не пытаясь делить. Никаких исключений — второе значение просто сигнализирует о проблеме.

При вызове оба значения принимаются через множественное присваивание:

result, ok := divide(10, 3)
if ok {
    fmt.Println(result) // 3.3333333333333335
} else {
    fmt.Println("деление на ноль")
}

Переменная ok по соглашению используется в Go для bool-признака успеха — это устойчивый паттерн, который вы будете встречать постоянно.

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

result, _ := divide(10, 3) // принимаем только частное
fmt.Println(result)

_ не создаёт переменную и не требует дальнейшего использования — компилятор просто игнорирует попавшее туда значение в контексте присваивания.

Обратная ситуация — нужен только признак успеха:

_, ok := divide(10, 0)
if !ok {
    fmt.Println("деление невозможно")
}

Есть и ещё один вариант: если результаты функции вообще не нужны, вызов можно написать как самостоятельную инструкцию — тогда все возвращаемые значения молча отбрасываются без какого-либо _:

divide(10, 3) // результат никуда не идёт — допустимо

Правило про _ касается именно присваивания: если вы пишете x, y := f(), позиций слева должно быть ровно столько, сколько значений возвращает f. Пропустить позицию нельзя — только заменить её на _.

Пакетная и локальная область видимости: затенение переменных

Посмотрите на такой код:

package main

import "fmt"

var counter int = 10

func reset() {
    counter := 0  // опасно!
    fmt.Println("внутри reset:", counter) // 0
}

func main() {
    reset()
    fmt.Println("в main:", counter) // 10, а не 0
}

Переменная counter объявлена на уровне пакета через var — это пакетная переменная, доступная из всех функций в том же пакете. Но внутри reset стоит :=, и компилятор воспринимает это как объявление новой локальной переменной с тем же именем — потому что пакетная counter находится во внешней области видимости, а не в том же блоке. Обращения к counter в теле функции идут к локальной копии. После выхода из reset локальная переменная исчезает, а пакетная остаётся нетронутой.

Код компилируется без единого предупреждения — это и делает затенение коварным.

Исправление простое: чтобы изменить пакетную переменную, используйте присваивание =, а не :=:

func reset() {
    counter = 0  // присваивание, не объявление
    fmt.Println("внутри reset:", counter) // 0
}

Теперь reset действительно меняет counter, и main напечатает 0.

Точнее о поведении :=. В примере выше := создаёт новую переменную, потому что counter из другой (более широкой) области. Есть, однако, случай, когда := повторно использует уже объявленную переменную из того же блока: групповое присваивание вида value, err := .... Если err уже объявлена в том же блоке, а value — новая, компилятор переиспользует существующую err и объявит только value. Это не затенение — имя остаётся тем же объектом. Затенение возникает именно тогда, когда := применяется к имени из внешней области.

Общее правило: на уровне пакета объявление всегда через var; внутри функции := удобен, но требует внимания, когда имя совпадает с чем-то из внешней области — тогда вместо изменения существующей переменной вы получите её локальную копию.

Задача 1

Что выведет следующая программа?

package main

import "fmt"

func double(n int) int {
    return n * 2
}

func main() {
    x := double(5)
    fmt.Println(x)
}
Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Функция объявлена как func swap(a, b int) (int, int) { return b, a }. Какой код объявляет две новые переменные x и y, принимает в них оба возвращаемых значения и затем выводит их?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Что выведет следующая программа? Введите ровно те строки, которые появятся в stdout (каждая строка с новой строки).

package main

import "fmt"

var n int = 100

func show() {
    n := 7
    fmt.Println(n)
}

func main() {
    show()
    fmt.Println(n)
}
Без регистрации · результат не сохраняется

Задача 4

Средняя скорость на участке

Велосипедист ведёт журнал своих поездок: после каждого участка маршрута он записывает пройденное расстояние в километрах и время, которое на него ушло, в часах. Из этих двух чисел можно получить среднюю скорость на участке — ключевой показатель, который помогает сравнивать разные отрезки пути между собой.

Расчёт прост: расстояние делится на время. Однако среди записей иногда встречаются ошибочные строки, в которых время указано как ноль, — такое деление лишено смысла и не должно выполняться. Программа обязана отличать корректную запись от ошибочной и сообщать о них по-разному.

Чтобы это различие было явным, результат работы программы включает два элемента: само числовое значение средней скорости и признак того, удалось ли рассчитать его корректно. Если время ненулевое, пользователь видит среднюю скорость в километрах в час; если время равно нулю — понятное сообщение о том, что расчёт невозможен.

Что нужно сделать

Напишите функцию averageSpeed(distance, hours float64) (float64, bool). При hours == 0 она должна вернуть 0, false; иначе — среднюю скорость и true. В main считайте входные данные, вызовите функцию, примите оба результата и выберите нужный вывод по значению bool.

Формат ввода

Два числа float64: расстояние в километрах и время в часах.

Формат вывода

Если время ненулевое, выведите скорость ровно с двумя знаками после точки. Если время равно нулю, выведите расчёт невозможен.

Пример

Ввод:
30 2

Вывод:
15.00
РешениеGo

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 5

Перевод температуры в Кельвины

Лабораторные журналы давно привыкли к путанице со шкалами: один прибор фиксирует показания в градусах Цельсия, другой — в Фаренгейтах, а все дальнейшие расчёты требуют единой точки отсчёта. Чтобы сравнивать результаты без ошибок, группа условилась переводить любое поступающее значение в Кельвины — универсальную термодинамическую шкалу, принятую в научном сообществе.

Когда сотрудник передаёт числовое значение вместе с буквенным кодом шкалы, система должна однозначно определить, как именно выполнить пересчёт. Если код распознан, на выходе появляется корректная температура в Кельвинах и подтверждение того, что перевод прошёл успешно. Эти два элемента — само число и признак успеха — позволяют вышестоящему контролю убедиться, что данные пригодны для дальнейшей работы.

Иногда, однако, в систему попадает код, который не соответствует ни одной известной шкале: опечатка, нестандартное обозначение или случайный символ. В таком случае никакого числового результата не существует, и вместо него пользователь должен увидеть понятное сообщение — не пустую строку и не бессмысленный ноль, а явный сигнал о том, что шкала не опознана и перевод не выполнен.

Что нужно сделать

Реализуйте две функции:

  • convertToKelvin(value float64, scale string) (float64, bool) возвращает число и true для шкал C и F, а для неизвестного кода — 0, false;
  • KelvinReport(value float64, scale string) string вызывает convertToKelvin. Для корректного результата она возвращает строку <значение> K, форматируя число через %g; для неизвестного кода — строку шкала не распознана.

Формулы перевода: для C — value + 273.15; для F — (value - 32) * 5 / 9 + 273.15. Входная точка проверки — KelvinReport; она не должна читать ввод или печатать результат.

РешениеGo

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 6

Доля голосов на участке

На каждом избирательном участке после закрытия голосования подводится первичный итог: сколько человек пришли к урне и сколько из них отдали свой голос конкретному кандидату. Эти два числа — основа любого протокола, и именно из них складывается первое представление о настроениях избирателей ещё до сводки по всему округу.

Чтобы цифры приобрели смысл, их нужно привести к единой шкале. Доля голосов, выраженная в процентах, позволяет сразу оценить поддержку кандидата вне зависимости от того, сколько человек зарегистрировано на участке: двести или двадцать тысяч. Даже когда кандидат не получил ни одного голоса, это тоже осмысленный результат — нулевой процент честно отражает реальное положение дел.

Программа берёт число голосов за кандидата и общее число проголосовавших на участке, после чего вычисляет и показывает процентную долю поддержки.

Что нужно сделать

На уровне пакета уже задана константа percentBase = 100.0. Напишите функцию voteShare(votes, total int) float64: она преобразует оба аргумента к float64, вычисляет долю и умножает её на percentBase.

В main считайте votes и total, вызовите voteShare и выведите возвращённый процент через %g, сразу после числа поставив знак %. Функция возвращает одно значение; отдельный признак успеха не нужен.

Формат ввода

Два неотрицательных целых числа: голоса за кандидата и положительное общее число проголосовавших.

Формат вывода

Процентная доля и знак % в одной строке.

РешениеGo

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку