Здравствуйте. В прошлом уроке вы восстановили базовые конструкции Go: переменные, типы, условия и циклы. Теперь пора вынести часть логики из main в функции. Это делает код короче, проверяемее и ближе к тому, как устроен backend-сервис: одна функция вычисляет результат, другая проверяет правило, третья выполняет переданное ей действие.
Сегодня вы научитесь:
- объявлять и вызывать функции;
- возвращать из функции несколько связанных результатов;
- принимать возвращаемые значения через множественное присваивание;
- передавать функцию в другую функцию как аргумент;
- использовать именованные результаты осознанно, не превращая код в загадку.
Функция как именованный фрагмент логики
Функция объявляется с помощью func. У неё есть имя, параметры, необязательные возвращаемые значения и тело:
func functionName(parameter type) returnType {
// logic
}
Например, вынесем вычисление числа незавершённых задач:
func remainingTasks(completed int, total int) int {
return total - completed
}
В main её можно вызвать так:
remaining := remainingTasks(2, 5)
fmt.Println(remaining) // 3
Параметры — это локальные переменные функции. Значения 2 и 5 передаются в параметры completed и total, а затем функция возвращает результат.
Когда несколько соседних параметров имеют один тип, Go позволяет записать их компактнее:
func remainingTasks(completed, total int) int {
return total - completed
}
Это эквивалентно предыдущей версии.
Функции обычно объявляют на уровне пакета, вне main. Go не поддерживает перегрузку функций по количеству или типам аргументов: в одном пакете нельзя создать две функции с именем remainingTasks, даже если их параметры различаются. Имя функции и её сигнатура должны выражать одну понятную ответственность.
Несколько результатов: нормальная модель Go
В Go функция может вернуть не только одно значение, а несколько. Возвращаемые типы заключаются в круглые скобки:
func taskSummary(completed, total int) (int, bool) {
if completed < 0 || total < 0 || completed > total {
return 0, false
}
return total - completed, true
}
Здесь функция возвращает:
- число незавершённых задач;
- признак, корректны ли исходные данные.
Вызов также использует несколько переменных слева:
remaining, valid := taskSummary(2, 5)
if !valid {
fmt.Println("Invalid task counts")
return
}
fmt.Printf("Remaining tasks: %d\n", remaining)
Порядок значений важен. Вызов ожидает, что первым придёт int, а вторым — bool, потому что именно такой порядок задан в сигнатуре:
func taskSummary(completed, total int) (int, bool)
У функции нет «объекта с двумя полями» или скрытого кортежа: выражение taskSummary(2, 5) производит два отдельных значения. Поэтому следующий код не скомпилируется:
// summary := taskSummary(2, 5)
Слева должна быть подходящая пара переменных:
remaining, valid := taskSummary(2, 5)
Если переменные уже существуют, используйте обычное присваивание:
var remaining int
var valid bool
remaining, valid = taskSummary(2, 5)
Можно смешать старую и новую переменную в коротком объявлении, но хотя бы одна переменная должна быть новой в текущей области видимости:
remaining := 0
remaining, valid := taskSummary(2, 5) // valid — новая переменная
Возвращайте значения с разными ролями
Несколько результатов особенно полезны, когда значения отвечают на разные, но связанные вопросы. Например:
- разобрать вход и вернуть значение вместе с новой позицией чтения;
- вычислить результат и сообщить, валиден ли вход;
- получить значение из хранилища и указать, найдено ли оно;
- выполнить операцию и вернуть результат вместе с ошибкой.
Последний вариант — один из самых важных Go-идиомов:
func loadProject(id int64) (Project, error)
Тему error, создание и обработку ошибок вы разберёте отдельно. Пока важно увидеть форму: функция не обязана прятать проблему в специальном значении вроде -1; она может вернуть основной результат и отдельный сигнал о неуспехе.
Прочитайте раздел “Functions” из официального руководства Effective Go. Он показывает, почему несколько результатов — не экзотика, а обычный способ выразить результат операции и её дополнительное состояние.
В подразделе “Multiple return values” прочитайте основной пример с записью числа и функцией nextInt. Обратите внимание: второе возвращаемое значение позволяет вызывающему коду продолжить обработку с нужной позиции, не меняя аргумент через указатель. Затем в подразделе “Named result parameters” прочитайте объяснение именованных результатов. Сравните безымянную сигнатуру (int, int) и сигнатуру (value, nextPos int): типы те же, но назначение результатов становится очевиднее.
Именованные результаты: документация в сигнатуре
У результатов можно указать имена:
func taskSummary(completed, total int) (remaining int, valid bool) {
if completed < 0 || total < 0 || completed > total {
return 0, false
}
return total - completed, true
}
Для вызывающего кода ничего не меняется:
remaining, valid := taskSummary(2, 5)
Имена remaining и valid существуют внутри функции. Они не навязывают имена переменных снаружи. Их основная задача — сделать контракт функции понятнее.
Именованные результаты также создаются как переменные с нулевыми значениями. Поэтому допустим так называемый «голый» return:
func taskSummary(completed, total int) (remaining int, valid bool) {
if completed < 0 || total < 0 || completed > total {
return
}
remaining = total - completed
valid = true
return
}
Этот вариант корректен, но в прикладном коде его стоит применять осторожно. В короткой функции он читается нормально; в длинной приходится искать глазами, какие именно результаты были изменены перед return.
Для большинства функций в начале пути лучше явный вариант:
func taskSummary(completed, total int) (remaining int, valid bool) {
if completed < 0 || total < 0 || completed > total {
return 0, false
}
return total - completed, true
}
Здесь имена результатов документируют смысл, а явный return total - completed, true показывает итог без необходимости отслеживать состояние переменных.
Функции — это значения
В Go функцию можно сохранить в переменной, присвоить другой переменной или передать как аргумент. У функции есть тип, который определяется её параметрами и результатами.
Например, эта функция принимает int и ничего не возвращает:
func printTaskNumber(number int) {
fmt.Printf("Task %d needs attention\n", number)
}
Её тип можно записать так:
func(int)
То есть «функция, которая принимает один int и не возвращает значений».
Теперь создадим функцию, которая обходит номера незавершённых задач и для каждого номера вызывает переданную функцию:
func forEachPendingTask(completed, total int, visit func(int)) {
for taskNumber := completed + 1; taskNumber <= total; taskNumber++ {
visit(taskNumber)
}
}
Параметр visit имеет тип func(int). Он хранит функцию, которую можно вызвать внутри forEachPendingTask:
visit(taskNumber)
Передадим printTaskNumber как аргумент:
forEachPendingTask(2, 5, printTaskNumber)
Важно различать две записи:
printTaskNumber // передаём саму функцию
printTaskNumber(3) // вызываем функцию прямо сейчас
В первом случае скобок нет: мы передаём поведение, которое другая функция выполнит позже. Во втором случае мы немедленно вызываем функцию.
Такое разделение полезно, когда одна часть кода управляет процессом, а другая определяет конкретное действие. forEachPendingTask знает, когда обработать каждый номер задачи, но не знает, что именно нужно сделать с номером: напечатать, записать в лог, отправить в очередь или сохранить в файл. Конкретное действие передаётся снаружи.
Функциональные литералы: передать действие без отдельного имени
Необязательно заранее объявлять именованную функцию. Можно передать функцию прямо в месте вызова. Это называется функциональным литералом:
forEachPendingTask(2, 5, func(taskNumber int) {
fmt.Printf("Task %d needs attention\n", taskNumber)
})
Фрагмент
func(taskNumber int) {
fmt.Printf("Task %d needs attention\n", taskNumber)
}
создаёт значение-функцию и передаёт его в forEachPendingTask.
Выбирайте именованную функцию, если действие:
- имеет самостоятельный смысл;
- используется в нескольких местах;
- достаточно объёмное;
- удобно проверять отдельно.
Функциональный литерал подходит, если действие короткое и относится только к одному вызову. В production-коде не стоит помещать в такой литерал десятки строк бизнес-логики: отдельная именованная функция будет понятнее.
#85 Golang - Functional Programming: First-Class Functions, Closures & More!
Посмотрите начало видео “Golang - Functional Programming: First-Class Functions, Closures & More!” от codeHeim. Оно наглядно показывает, что функция в Go может быть аргументом и что её сигнатура является контрактом между вызывающим кодом и переданным действием.
Посмотрите функции как значения. Сосредоточьтесь на параметре formatter: это функция, принимающая строку и возвращающая строку. Сравните с нашим visit func(int): оба параметра задают не абстрактную «функцию вообще», а точную ожидаемую сигнатуру.
Собираем программу: проверка данных, несколько результатов и callback
Замените предыдущую учебную программу в main.go этим вариантом:
package main
import "fmt"
const maxTasksPerProject = 100
func taskSummary(completed, total int) (remaining int, valid bool) {
if completed < 0 || total < 0 || completed > total {
return 0, false
}
return total - completed, true
}
func forEachPendingTask(completed, total int, visit func(int)) {
for taskNumber := completed + 1; taskNumber <= total; taskNumber++ {
visit(taskNumber)
}
}
func printTaskNumber(taskNumber int) {
fmt.Printf("Task %d needs attention\n", taskNumber)
}
func main() {
title := "Interview preparation"
ownerID := int64(42)
completed := 2
total := 5
isArchived := false
if title == "" {
fmt.Println("Project title is required")
return
}
if total > maxTasksPerProject {
fmt.Println("Too many tasks in project")
return
}
if isArchived {
fmt.Println("Project is archived")
return
}
remaining, valid := taskSummary(completed, total)
if !valid {
fmt.Println("Invalid task counts")
return
}
fmt.Printf("Project %q, owner %d\n", title, ownerID)
fmt.Printf("Remaining tasks: %d\n", remaining)
if remaining == 0 {
fmt.Println("Project is complete")
return
}
forEachPendingTask(completed, total, printTaskNumber)
}
Запустите привычную проверку из корня модуля:
go fmt ./...
go test ./...
go run .
Ожидаемый вывод:
Project "Interview preparation", owner 42
Remaining tasks: 3
Task 3 needs attention
Task 4 needs attention
Task 5 needs attention
Обратите внимание на границы ответственности:
| Функция | Ответственность |
|---|---|
taskSummary | Проверяет счётчики и вычисляет число оставшихся задач |
forEachPendingTask | Управляет циклом по незавершённым задачам |
printTaskNumber | Определяет конкретное действие для одного номера |
main | Собирает входные значения и определяет общий порядок работы |
Попробуйте сделать две безопасные модификации:
- Передайте в
forEachPendingTaskфункциональный литерал вместоprintTaskNumber, добавив к каждой строке текст"Manual review:". - Измените
completedна5и убедитесь, что функцияforEachPendingTaskне вызывается:mainзавершится после сообщения о полном завершении проекта.
Так вы увидите, что callback не меняет цикл сам по себе: он лишь задаёт действие, которое цикл применяет к каждому подходящему значению.
Как распознавать подходящий момент для callback
Не каждая маленькая функция должна принимать другую функцию. Это полезно, когда есть стабильный алгоритм и изменяемая часть поведения.
В нашем примере стабилен обход номеров задач. Меняться может действие над номером. В будущем похожий приём встретится в стандартной библиотеке:
- HTTP-сервер получает функцию-обработчик запроса;
- сортировка получает функцию сравнения;
- middleware получает следующий HTTP-обработчик;
- тесты нередко передают функцию, которая описывает проверяемое действие.
Но не стоит создавать callback «на всякий случай». Если действие всегда одно и не предполагает вариаций, обычная функция проще:
func printPendingTasks(completed, total int) {
for taskNumber := completed + 1; taskNumber <= total; taskNumber++ {
fmt.Printf("Task %d needs attention\n", taskNumber)
}
}
Вариант с visit func(int) оправдан, когда вызывающий код действительно должен выбирать действие. Хорошая архитектура не максимизирует число абстракций, а оставляет изменяемые решения на границе кода, которому они принадлежат.
Итоги
Теперь вы умеете выделять прикладную логику в функции и формулировать их контракт через параметры и результаты.
Главные идеи урока:
- функция объявляется через
func, принимает параметры и может возвращать результаты; - несколько результатов записываются в сигнатуре в скобках и принимаются множественным присваиванием;
- результаты должны быть связаны по смыслу, например
remainingиvalid; - именованные результаты улучшают читаемость сигнатуры, но «голый»
returnлучше оставлять только для коротких прозрачных функций; - функция в Go является значением: её можно передать другой функции;
- запись
handlerпередаёт функцию, аhandler()вызывает её; - callback полезен, когда алгоритм остаётся стабильным, а действие должно задаваться вызывающим кодом.
В следующем уроке вы перейдёте к массивам, срезам и map. Тогда вместо учебного диапазона номеров появятся реальные коллекции задач, а функции смогут принимать и обрабатывать наборы данных.
Can't find a good explanation? Sign up and we'll make it for you
Sign up