1. Синтаксис функции в Go
Если вы пишете программу и ловите себя на мысли «я уже делал это где-то выше» — ваш мозг только что попросил функцию. Функция в Go — это именованный кусок кода, который можно вызвать по имени, передав ему входные значения (параметры), и получить результат (или выполнить действие без результата). Ключевая ценность функций в том, что они превращают «простыню кода» в набор коротких шагов: каждый шаг имеет имя и понятный контракт.
Представьте, что main — это рецепт блюда, а функции — отдельные подрецепты: «нарезать овощи», «разогреть духовку», «смешать соус». Вы же не хотите читать рецепт, где всё написано одним абзацем на страницу? Компьютер, кстати, тоже не хочет, но терпит молча — до первой ошибки.
Скелет объявления: func, имя, параметры, результат, тело
Когда вы впервые видите объявление функции, оно кажется длинным, но на самом деле это очень строгий и повторяемый шаблон. В Go функция объявляется ключевым словом func, затем идёт имя, затем круглые скобки с параметрами, после них (иногда) возвращаемый тип, и потом тело в фигурных скобках.
Простейший пример: функция без параметров и без результата может выглядеть так же, как hello() в демонстрационном коде — просто func hello() { ... }.
А функция с параметром и результатом выглядит похожим образом: func name(param Type) ResultType { ... }.
Чтобы легче «разбирать глазами» объявления функций, полезно держать перед собой такую табличку:
| Часть | Пример | Зачем нужна |
|---|---|---|
| Ключевое слово | |
Сообщает компилятору: «сейчас будет функция» |
| Имя | |
По нему вы вызываете функцию |
| Параметры | |
Входные данные для вычисления/действия |
| Результат | (или ничего) |
Что функция возвращает вызывающему коду |
| Тело | |
Команды, которые выполняются при вызове |
В этой лекции мы не лезем в множественные результаты и ошибки как часть сигнатуры (это отдельная большая тема). Пока работаем с функциями, которые либо ничего не возвращают, либо возвращают одно значение.
Функция без результата: «сделай действие и вернись»
Очень частый тип функций — «процедурные»: они что-то делают (например, печатают текст), но ничего не возвращают. В Go это выглядит просто: после параметров не пишется никакой возвращаемый тип.
Важно понимать: «не возвращает значение» не значит «не заканчивается». Такая функция всё равно завершается, просто результатом является сам факт выполнения.
package main
import "fmt"
func printHeader() {
fmt.Println("=== MiniCalc v1 ===") // === MiniCalc v1 ===
}
func main() {
printHeader()
}
Здесь printHeader — функция без параметров и без результата. Она делает одно действие: печатает заголовок. И это уже маленькая победа: main стал чуть «чище», и вы можете переиспользовать printHeader() где угодно.
Отдельный полезный нюанс: return можно использовать и в функции без результата — просто чтобы закончить её раньше. Но делать так стоит только когда это реально делает код проще.
package main
import "fmt"
func printIfPositive(x int) {
if x <= 0 {
return // ничего не печатаем и выходим
}
fmt.Println("positive:", x) // positive: 3
}
func main() {
printIfPositive(3)
}
Параметры: входные данные, которые живут только внутри
Когда вы передаёте значения в функцию, вы как будто кладёте их в «почтовый ящик» функции. Внутри функции параметры становятся локальными переменными, и их область видимости ограничена телом функции. Снаружи они не существуют.
Это удобно сразу по двум причинам. Во-первых, вы не засоряете main кучей временных переменных. Во-вторых, вы не можете случайно «сломать» что-то снаружи, меняя параметры (пока мы не обсуждаем указатели — это будет позже, и там появятся новые нюансы).
Пример с параметрами:
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func main() {
fmt.Println(add(2, 3)) // 5
}
Обратите внимание на две вещи. Во-первых, тип параметра пишется после имени: a int. Во-вторых, параметры — это не «магия»: a и b обычные переменные, просто созданные при вызове.
Ещё один маленький трюк читаемости: если подряд идут параметры одного типа, в Go разрешено писать тип один раз.
package main
import "fmt"
func rectArea(width, height int) int {
return width * height
}
func main() {
fmt.Println(rectArea(3, 4)) // 12
}
Такую запись вы будете встречать постоянно: она короче и при этом не хуже читается.
Один возвращаемый результат и return
Функции с результатом отличаются от «процедурных» тем, что в их объявлении после параметров указан тип результата, а внутри тела должен быть return со значением этого типа.
Возврат — это не просто «отдать значение». Это ещё и команда «закончить выполнение функции прямо сейчас». Поэтому в функциях с ветвлениями важно следить, чтобы любой путь приводил к корректному return.
Мини-пример с простым условием:
package main
import "fmt"
func max2(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(max2(10, 3)) // 10
}
Здесь есть два пути. Если a > b, возвращаем a. Иначе возвращаем b. И что приятно: код читается линейно, без else — вы уже встречали такой стиль в if, и в функциях он особенно хорошо заходит.
Аккуратность с типами: return должен совпадать с объявлением
Одна из самых частых причин «почему оно не компилируется» у новичков — несоответствие типов в return. Компилятор Go достаточно строгий: если вы объявили func something(...) int, то вернуть вы обязаны именно int.
Пример: хотим вернуть строку — нужно объявить результат строкой.
package main
import "fmt"
func greet(name string) string {
return "Hello, " + name
}
func main() {
fmt.Println(greet("Gopher")) // Hello, Gopher
}
Здесь всё честно: результат string, и return возвращает строку. Если бы мы написали func greet(name string) int, а внутри вернули строку — компилятор бы справедливо возмутился.
3. Контракт и читаемость
Пока вы учитесь писать функции, есть одна идея, которая экономит вам много нервов: у каждой функции должен быть понятный контракт. Даже если вы пока не пишете документацию комментариями, вы всё равно должны уметь ответить на вопросы: «что эта функция ждёт на вход?» и «что она обещает вернуть?»
Например, rectArea(width, height int) int — контракт понятен: берёт ширину и высоту, возвращает площадь. А вот функция calc(a, b int) int без уточнения — подозрительная: «что именно она считает?». Имя функции — часть контракта. Поэтому мы стараемся называть функции глаголами или «действиями»: add, printHeader, max2, rectArea.
И ещё важный момент: не бойтесь маленьких функций. Новички часто думают, что «много функций = сложно». На практике обычно наоборот: «одна функция на 200 строк = сложно», а «10 функций по 10 строк = жить можно».
У функций есть ещё одна «тихая» ловушка: аргументы передаются по позиции. То есть sum(a, b) — это не «положи числа как-нибудь», а «первое значение идёт в первый параметр, второе — во второй».
В функциях, где параметры однотипные, особенно важно следить за смыслом и названием параметров. Например, rectArea(width, height int) гораздо понятнее, чем rectArea(a, b int), хотя типы одинаковые.
Мини-демо:
package main
import "fmt"
func rectArea(width, height int) int {
return width * height
}
func main() {
fmt.Println(rectArea(2, 10)) // 20
// rectArea(10, 2) тоже будет 20, но смысл "ширина/высота" вы уже потеряли :)
}
Да, математика в этом примере не страдает, но в реальной задаче (например, «минимум/максимум», «начало/конец», «x/y») порядок аргументов может полностью поменять смысл.
4. Пример: MiniCalc v1
Сейчас мы начнём собирать мини-приложение, которое будет развиваться дальше по темам. Это будет простой консольный калькулятор: он читает два числа и печатает сумму и произведение. Звучит скромно, зато идеально для тренировки функций: вычисления можно вынести, а main оставить «дирижёром».
Сначала посмотрим на «наивный» вариант (всё внутри main). Он рабочий, но как только логики станет больше — начнёт распухать.
package main
import (
"fmt"
)
func main() {
var a, b int
fmt.Scan(&a, &b)
fmt.Println("sum =", a+b)
fmt.Println("mul =", a*b)
}
Теперь сделаем то же самое, но с функциями для вычислений. Важно: мы пока не усложняем ввод/вывод, просто выносим арифметику, чтобы увидеть механику.
package main
import "fmt"
func sum(a, b int) int {
return a + b
}
func mul(a, b int) int {
return a * b
}
func main() {
var a, b int
fmt.Scan(&a, &b)
fmt.Println("sum =", sum(a, b))
fmt.Println("mul =", mul(a, b))
}
С точки зрения результата программа делает то же самое. Но с точки зрения структуры произошло важное: main теперь читается как сценарий «прочитать → вызвать функции → вывести», а детали вычислений из него ушли.
main как сценарий, функции как шаги
Когда вы начинаете писать функции, полезно иногда рисовать в голове (или на бумаге) цепочку вызовов. Это помогает не потеряться: «кто кого вызывает» и «где что считается».
Например, для нашего MiniCalc v1 цепочка такая:
flowchart TD
A[main: читаем a и b] --> B["sum(a, b)"]
A --> C["mul(a, b)"]
B --> D[main: печатаем sum]
C --> E[main: печатаем mul]
Смысл диаграммы простой: main отвечает за «организацию», а sum и mul — за «вычисления». Это хорошая привычка, которая позже сильно поможет, когда вычисления станут сложнее и появятся ошибки.
5. Типичные ошибки
Ошибка №1: забыли указать тип параметра (или указали только для первого).
Новички часто пишут что-то вроде func sum(a, b int) int и удивляются, что это работает, а потом пишут func sum(a, b) int и получают ошибку. В Go тип обязателен, но его можно «разделить» на несколько имён только когда тип одинаковый, как в a, b int. Если типы разные — каждый параметр должен явно иметь свой тип.
Ошибка №2: перепутали местами части сигнатуры.
Иногда пытаются написать «по-человечески»: func int sum(a int, b int). Но Go любит порядок: func → имя → параметры → результат. Это как очередь в поликлинике: можно спорить, но проще один раз запомнить и жить дальше.
Ошибка №3: функция с результатом не возвращает значение во всех ветках.
Вы пишете if, в одной ветке делаете return, а в другой забываете, и компилятор ругается. Это не вредность компилятора, а его забота о вашей психике: лучше получить ошибку сразу, чем «странное поведение» на проде. Лечится просто: проверяйте, что любой путь заканчивается return нужного типа.
Ошибка №4: слишком «абстрактные» имена функций и параметров.
f(x, y) работает, но читается как «что-то где-то». Через неделю вы забудете, что такое f, а через месяц начнёте подозревать, что это вообще не ваш код (хотя ваш). Нормальные имена вроде rectArea(width, height) или max2(a, b) делают половину работы за комментарии.
Ошибка №5: превращение main в свалку всего на свете — даже после появления функций.
Бывает так: функции вы создали, но main всё равно остался огромным, потому что вы вынесли только «крошечные» вещи, а большие логические шаги оставили внутри. Хороший ориентир: если main перестал помещаться на экран без прокрутки — пора снова спросить себя: «какой здесь повторяющийся шаг можно назвать одним словом и вынести в функцию?»
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ