- функция = адрес первой инструкции её скомпилированного кода
- параметры — переменные в объявлении; аргументы — значения при вызове
- аргументы вычисляются слева направо (в C++ порядок не определён)
- сигнатура = типы входных параметров + типы возвращаемых значений (имя, тело, имена параметров — не часть сигнатуры)
- при передаче и возврате всё копируется (указатель, slice, map, interface — копия заголовка)
Функция = адрес
func add(a, b int) int { return a + b }
// add → 0x4A3F10 (адрес первой инструкции)
Вызов функции = переход по этому адресу (инструкция CALL).
Параметры vs аргументы
func greet(name string) { ... } // name — параметр (объявление)
greet("Alice") // "Alice" — аргумент (вызов)
Порядок вычисления аргументов
process(computeX(), computeY(), computeZ())
// 1️⃣ 2️⃣ 3️⃣
// слева направо — можно закладываться на порядок
Сигнатура
func add(a, b int) int // сигнатура: (int, int) int
func sum(x, y int) int // сигнатура: (int, int) int — та же!
// имена a/b, x/y и имя функции — не часть сигнатуры
Всё копируется
func modify(s []int) { s[0] = 99 } // копия заголовка slice → тот же backing array
func replace(m map[string]int) { m["a"] = 1 } // копия указателя → тот же hmap
func bump(x int) { x++ } // копия значения → оригинал не изменён
Возврат — тоже копия.
Нет RVO: объект создаётся внутри, потом копируется наружу.
- функции ведут себя как переменные: можно присваивать, передавать, возвращать
- нельзя сравнивать (как slice) и нельзя взять адрес
&fn - zero value любого функционального типа = nil
- встроенные функции (append, len, cap, make, copy...) нельзя использовать как значения
- через
unsafeвзять адрес функции можно, но стандартный синтаксис — нет
Функция как значение
// присвоить
var fn func(int) int
fn = func(x int) int { return x * 2 }
// передать в функцию
func apply(f func(int) int, x int) int { return f(x) }
// вернуть из функции
func multiplier(n int) func(int) int {
return func(x int) int { return x * n }
}
Нельзя сравнивать и брать адрес
f1 := func() {}
f2 := func() {}
// f1 == f2 // ошибка компиляции
// f1 == nil // ✅ можно только с nil
// _ = &f1 // нельзя взять адрес функции
Zero value = nil
var fn func(int) int
fmt.Println(fn == nil) // true
fn(5) // panic: nil function call
Семантически функция — указатель на первую инструкцию. Nil = никуда не указывает.
Встроенные функции — не значения
// f := append // ошибка: cannot use append as value
// f := len // ошибка: cannot use len as value
Встроенные функции и init нельзя использовать как значения — особенность языка.
- анонимная функция — определяется без имени, в месте использования
- можно присвоить переменной или вызвать сразу
func(x int){ ... }(42) - variadic (
...T) — синтаксический сахар над[]T - variadic: только один, только последний в списке параметров
- nil в variadic = nil slice;
nil...= распаковка nil slice
Анонимные функции
// Способ 1: присвоить переменной
double := func(x int) int { return x * 2 }
fmt.Println(double(5)) // 10
// Способ 2: вызвать сразу (IIFE)
result := func(a, b int) int {
return a + b
}(3, 4)
// result = 7 (функция вызвана сразу после определения)
Способ 1: double — функция. Способ 2: result — уже int (результат вызова).
Variadic параметры
func sum(nums ...int) int { // nums — это []int
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // nums = []int{1, 2, 3}
sum() // nums = []int{} (пустой slice)
Variadic (...T) — синтаксический сахар: внутри функции параметр является обычным []T.
Ограничения variadic
// ❌ variadic не последний
func bad(nums ...int, name string) {}
// ❌ два variadic
func bad2(a ...int, b ...string) {}
// ✅ только один и последний
func ok(prefix string, nums ...int) {}
Variadic может быть только один и только последним параметром.
Variadic и nil
func process(ptrs ...*int) {
fmt.Println(ptrs) // что выведет?
}
process(nil) // [<nil>] — slice из одного nil-указателя
process(nil...) // [] — распаковка nil slice = пустой slice
// Потому что variadic = slice:
// nil → [](*int){nil} один элемент
// nil... → ([](*int))(nil)... → ничего
process(nil) передаёт slice из одного nil-указателя. process(nil...) распаковывает nil slice — результат пустой slice.
- именованные возвращаемые значения — по сути объявление переменных (zero value) в начале функции
- используй когда повышают читаемость (два float64 → lat/lon) или нужно модифицировать в defer
- не мешай явный
return lat, lon, errи голыйreturnв одной функции - если хотя бы один параметр/результат проименован — остальные нужно именовать (или
_)
Когда использовать
// ❌ Непонятно: где широта, где долгота?
func GetLocation(addr string) (float64, float64, error)
// ✅ Понятно сразу
func GetLocation(addr string) (latitude, longitude float64, err error)
Рекомендация Go community: используй именованные возвращаемые, когда это повышает читаемость. Для одного int или error — обычно не нужно.
Это объявление переменных
func divide(a, b float64) (result float64, err error) {
// result = 0.0 (zero value float64)
// err = nil (zero value error)
if b == 0 {
err = errors.New("division by zero")
return // голый return → вернёт result=0, err=ошибка
}
result = a / b
return // голый return → вернёт result, err=nil
}
Именованные переменные инициализируются zero value автоматически: result = 0.0, err = nil. Голый return возвращает текущие значения этих переменных.
Не мешай явный и голый return
// ❌ Плохо: путаница
func process(addr string) (lat, lon float64, err error) {
if addr == "" {
return // голый — неявно возвращает lat, lon, err
}
// ...
return lat, lon, err // явный — а тут другой стиль
}
// ✅ Хорошо: один стиль везде
func process(addr string) (lat, lon float64, err error) {
if addr == "" {
return 0, 0, errors.New("empty") // явный
}
return lat, lon, nil // явный
}
Пропуск имён параметров
// Все пропущены — ок
func Write([]byte) (int, error)
// Если один именован — остальные нужно именовать или _
func Write(_ []byte) (n int, err error)
// ✅ несколько _ допустимо
Правило: всё или ничего. Если хоть одно имя задано — остальные обязаны иметь имя или _.
- замыкание — функция, которая ссылается на переменные из области видимости родительской функции
- замкнутая переменная живёт дольше родительской функции → аллокация в heap (escape analysis)
- генератор/счётчик — классический пример: функция «помнит» состояние между вызовами
- self-referencing anonymous: сначала
var f func(...), потомf = func(...) { ... f(...) ... }
Генератор-счётчик
func generator(start int) func() int {
number := start // переменная родительской функции
return func() int { // замыкание — захватывает number
r := number
number++ // модифицирует НЕ локальную переменную
return r
}
}
gen := generator(100)
gen() // 100
gen() // 101
gen() // 102
// number живёт в heap — не уничтожен с завершением generator()
Почему number не умирает? Родительская функция generator завершилась, но возвращённая функция ссылается на number. Escape analysis → number аллоцируется в heap.
Как это выглядит в памяти
gen (переменная на стеке main)
│
▼
func() int ──ссылка──▶ number: 102 (heap)
Каждый вызов generator() создаёт свой number в heap:
gen1 := generator(0)
gen2 := generator(100)
gen1() // 0 (свой number)
gen2() // 100 (свой number)
gen1() // 1
gen2() // 101
Self-referencing анонимная функция
// ❌ Нельзя — fib ещё не существует
// fib := func(n int) int { return fib(n-1) + fib(n-2) }
// ✅ Сначала объявить переменную, потом присвоить
var fib func(n int) int
fib = func(n int) int {
if n <= 1 { return 1 }
return fib(n-1) + fib(n-2) // fib уже объявлен
}
Почему нельзя fib := func(...) { fib(...) }? В момент правой части := переменная fib ещё не объявлена — компилятор вернёт ошибку. Нужно сначала var fib func(...), чтобы имя было в scope.
Ловушка: замыкание в цикле
// ⚠️ Все горутины замкнут одну переменную i
for i := 0; i < 5; i++ {
go func() { fmt.Println(i) }() // скорее всего напечатает 5 5 5 5 5
}
// ✅ Fix: передать как аргумент (копия)
for i := 0; i < 5; i++ {
go func(n int) { fmt.Println(n) }(i) // копия i
}
Суть ловушки: все анонимные функции замыкают одну и ту же переменную i. К моменту выполнения горутин цикл уже завершился, i == 5. Фикс — передать i как аргумент: создаётся копия значения на момент вызова.
- функция высшего порядка (ФВП) — принимает функцию как аргумент и/или возвращает функцию
- предикат — функция, возвращающая
bool - предикаты удобны для обобщения: одна сортировка + предикат → asc/desc
- ФВП — основа для forEach, map, filter, reduce в Go
Предикат — обобщение поведения
// Сортировка вставками — один код, разное поведение через предикат
func insertionSort(arr []int, less func(a, b int) bool) {
for i := 1; i < len(arr); i++ {
key := arr[i]
j := i - 1
for j >= 0 && less(key, arr[j]) {
arr[j+1] = arr[j]
j--
}
arr[j+1] = key
}
}
// По возрастанию
insertionSort(data, func(a, b int) bool { return a < b })
// По убыванию — тот же код!
insertionSort(data, func(a, b int) bool { return a > b })
Без предиката пришлось бы писать две функции сортировки.
Функция высшего порядка — forEach
func forEach(arr []int, fn func(int) int) []int {
result := make([]int, len(arr)) // копия — чистая функция
for i, v := range arr {
result[i] = fn(v)
}
return result
}
squares := forEach([]int{1, 2, 3, 4}, func(x int) int {
return x * x
})
// [1, 4, 9, 16]
forEach — ФВП: принимает fn как аргумент. fn применяется к каждому элементу.
ФВП: принимает + возвращает
func multiplier(n int) func(int) int {
return func(x int) int { return x * n }
}
double := multiplier(2) // возвращает функцию
triple := multiplier(3)
double(5) // 10
triple(5) // 15
multiplier — ФВП: возвращает функцию. Используется в каррировании и partial application.
- чистая функция = детерминированная (один вход → всегда один выход) + без побочных эффектов
- побочные эффекты: модификация внешних переменных, I/O, рандом, глобальное состояние
- преимущества: проще читать, тестировать, параллелить (нет shared state)
- грязную функцию можно очистить: вынести зависимость в параметр
- не нужно впадать в крайности — бизнес-логику и вычисления делать чистыми, I/O — грязное
Грязная функция
var counter int // глобальная переменная
func dirtySum(a, b *int) int {
counter++ // модифицирует глобальное состояние
*a = *a + *b // модифицирует внешнюю память
fmt.Println(*a) // побочный эффект (I/O)
r := rand.Intn(10) // недетерминированность
secret := os.Getenv("KEY") // зависимость от окружения
return *a + r
}
Чистая функция
func pureSum(a, b int) int {
return a + b
}
// ✅ Детерминированная: pureSum(3, 5) всегда = 8
// ✅ Нет побочных эффектов: ничего не меняет, не печатает, не читает
Как очистить грязную
// Грязная: зависит от rand
func dirty(min, max int) float64 {
factor := rand.Float64()
return float64(max-min) * factor
}
// Чистая: рандом вынесен в параметр
func clean(min, max int, factor float64) float64 {
return float64(max-min) * factor
}
Приём: зависимость, которая делает функцию грязной, выносится в параметр — вызывающий код берёт ответственность за «грязь».
Не впадать в крайности
// Формально «грязная» из-за math.Pi — глобальная константа
func circleArea(r float64) float64 {
return math.Pi * r * r
}
// Принимать Pi параметром ради «чистоты» — бессмысленно
Глобальные константы не делают функцию грязной на практике.
Преимущества чистых функций
Проще разбирать (нет зависимостей от внешнего состояния), проще тестировать (не нужно мокать глобальные переменные), проще параллелить (нет shared state → нет гонок).
- нет перегрузки — нельзя две функции с одним именем и разными параметрами
- нет параметров по умолчанию — все аргументы обязательны
- нет RVO / NRVO — возврат всегда копирует (может появиться в будущих версиях)
- прототипы (функция без тела) — для линковки с ассемблерным кодом
- можно вызывать функции, определённые ниже (прототипы не нужны для Go-кода)
Нет перегрузки
func add(a, b int) int { ... }
func add(a, b float64) float64 { ... } // ❌ ошибка: add redeclared
Исключения: можно объявить несколько init() и несколько _() (функция с подчёркиванием — нельзя вызвать).
Нет параметров по умолчанию
// Так нельзя:
// func connect(host string, port int = 8080) { ... }
// Обходные пути:
// 1. Variadic
// 2. Структура с опциями
// 3. Functional options (паттерн)
Три основных паттерна-обхода отсутствия default params: variadic, struct with options, functional options.
Нет RVO / NRVO
func newPoint() Point {
return Point{X: 1, Y: 2} // объект создаётся внутри, потом КОПИРУЕТСЯ наружу
}
// В C++ компилятор мог бы создать объект сразу на вызывающей стороне
// В Go такой оптимизации нет
RVO — Return Value Optimization (анонимный объект). NRVO — Named RVO (именованная переменная).
Ни RVO, ни NRVO в Go нет — возврат значения из функции всегда копирует. Может появиться в будущих версиях.
Прототипы
func asmFunction(x int) int // нет тела — прототип
// реализация будет в .s файле (ассемблер Go)
// при линковке ассемблерный код подключится к прототипу
Единственный практический кейс прототипов — реализация на ассемблере (крайне редко).
init()
func init() { fmt.Println("first") }\nfunc init() { fmt.Println("second") } // ✅ можно несколько init
// в одном пакете: порядок объявления
// между пакетами: порядок импорта (подробнее — в документации)
Можно объявить несколько init() в одном файле/пакете. В одном пакете порядок — по порядку объявления. Между пакетами — по порядку импорта.
- прямая рекурсия: f → f. Косвенная: f → g → f
- хвостовая рекурсия — последний вызов = сам себя, без дополнительных операндов → можно развернуть в цикл
- в Go нет оптимизации хвостовой рекурсии ("не нужно в языке с циклами")
- мемоизация — кэширование результатов: не вычислять повторно (основа динамического программирования)
Обычный vs хвостовой факториал
// Обычная рекурсия — дополнительный операнд (n *)
func factorial(n int) int {
if n == 0 { return 1 }
return n * factorial(n-1) // n * ... нужно запомнить на стеке
}
// Хвостовая рекурсия — аккумулятор в параметре
func fastFactorial(n int) int {
return tailFact(n, 1)
}
func tailFact(n, acc int) int {
if n == 0 { return acc }
return tailFact(n-1, n*acc) // последний вызов — сам себя, без операндов
}
В языках с TCO (C++, Scheme) tailFact разворачивается в цикл с одним фреймом.
В Go — нет: оба варианта одинаково расходуют стек. Бенчмарк подтверждает: производительность идентичная.
Почему нет TCO в Go
Цитата из обсуждений: "Tail call optimization is not needed in a language with loops. When a programmer writes recursive code, they want to think about a call stack, or they write a loop."
Мемоизация — Fibonacci
func memoFib(n int) int {
cache := make([]int, n+1)
var fib func(int) int // 1. объявить переменную
fib = func(k int) int { // 2. присвоить (self-reference)
if k <= 1 { return 1 }
if cache[k] != 0 { return cache[k] } // уже вычислено
cache[k] = fib(k-1) + fib(k-2) // вычислить и запомнить
return cache[k]
}
return fib(n)
}
Паттерн self-reference: сначала var fib func(int) int, затем присвоение — иначе рекурсия внутри анонимной функции невозможна.
Зачем мемоизация
Дерево рекурсии Fibonacci без кэша:
fib(5) → fib(4) + fib(3)
│ │
fib(3)+fib(2) fib(2)+fib(1) ← fib(3) вычислен дважды
│ │ ← fib(2) вычислен трижды
... ...
С мемоизацией: каждое значение вычисляется один раз → O(n) вместо O(2^n).
- декоратор — обёртка: добавляет поведение (логи, метрики) вокруг бизнес-функции
- композиция — цепочка функций: результат одной → вход следующей (pipeline)
- оба паттерна — ФВП: принимают/возвращают функции
- continuation (callback success/error) — тоже вариант, но осторожно с callback hell
Декоратор
type BinOp func(int, int) int
func calculate(op BinOp, a, b int) int {
fmt.Printf("args: %d, %d\n", a, b) // добавленное поведение (лог)\n result := op(a, b) // вызов оригинальной функции
fmt.Printf("result: %d\n", result) // добавленное поведение (лог)\n return result
}
add := func(a, b int) int { return a + b }
mul := func(a, b int) int { return a * b }
calculate(add, 3, 5) // args: 3, 5 → result: 8
calculate(mul, 3, 5) // args: 3, 5 → result: 15
Декоратор принимает функцию и добавляет поведение (логи, метрики) вокруг неё, не засоряя бизнес-логику.
Композиция
func compose(fns ...func(int) int) func(int) int {
return func(val int) int {
for _, fn := range fns {
val = fn(val) // результат → вход следующей
}
return val
}
}
square := func(x int) int { return x * x }
negate := func(x int) int { return -x }
transform := compose(square, negate, square)
// 4 → 16 → -16 → 256
transform(4) // 256
Композиция — pipeline: каждая функция обрабатывает результат предыдущей. Порядок можно менять (reverse helper).
Декоратор через вложенность
// Альтернативный стиль: декорирование вложением
result := calculate(add, 3, 5)
// эквивалентно:
result := calculate(func(a, b int) int {
return a + b
}, 3, 5)
Декоратор можно применять встроенной анонимной функцией без предварительного присваивания.
Continuation (callbacks)
func divide(a, b int, onSuccess func(int), onError func(string)) {
if b == 0 {
onError("division by zero")
return
}
onSuccess(a / b)
}
divide(10, 2,
func(r int) { fmt.Println("ok:", r) },\n func(e string) { fmt.Println("err:", e) },
)
Continuation (callback) — передача функций success/error вместо возврата значения. Полезен для асинхронных сценариев, но ведёт к callback hell при злоупотреблении.
- каррирование — преобразование f(a, b, c) в f(a)(b)(c): цепочка функций по одному аргументу
- partial application — закрепить часть аргументов, получить специализированную функцию
- ленивые вычисления — инициализация только при первом обращении (через замыкание + флаг)
- используй в узких местах: логгер с секцией, конфиг с prefix, тяжёлая инициализация по требованию
Каррирование
func multiply(x int) func(int) int {
return func(y int) int {
return x * y // x замкнут
}
}
// Полный вызов
multiply(10)(15) // 150
// Partial application — закрепляем первый аргумент
times10 := multiply(10)
times10(5) // 50
times10(15) // 150
Каррирование в Go — это функция, возвращающая функцию. Аргументы фиксируются через замыкание.
Практический пример: логгер
func logger(section string) func(string, string) {
return func(level, message string) {
fmt.Printf("[%s] %s: %s\n", section, level, message)\n }
}
repoLog := logger("repository") // секция закреплена
repoLog("INFO", "query executed") // [repository] INFO: query executed
repoLog("ERROR", "connection lost") // [repository] ERROR: connection lost
bizLog := logger("business")
bizLog("INFO", "order created") // [business] INFO: order created
Partial application позволяет не передавать section каждый раз — он зафиксирован в замыкании.
Ленивые вычисления
type LazyMap func() map[string]string
func MakeLazy(init func() map[string]string) LazyMap {
var data map[string]string
var initialized bool
return func() map[string]string {
if !initialized {
data = init() // тяжёлая инициализация — один раз
initialized = true
init = nil // отпускаем init для GC
}
return data
}
}
config := MakeLazy(func() map[string]string {
// дорогая операция: чтение файла, поход в сеть...
return map[string]string{"host": "localhost", "port": "8080"}
})
// Пока не вызвали config() — память не выделена
// Первый вызов: инициализация
// Второй+ вызов: возврат готовой map
Замыкание хранит data, initialized, init — всё в heap.
Зачем init = nil? После инициализации функция init больше не нужна. Обнуление позволяет GC собрать её и всё что она удерживает (например, конфиги, соединения, буферы).
Практика
Что выведут три вызова gen() после gen := generator(10)?
func generator(start int) func() int {
n := start
return func() int {
r := n
n++
return r
}
}
gen := generator(10)
Что выведет этот код?
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 5; i++ {
go func() { fmt.Println(i) }()
}
time.Sleep(10 * time.Millisecond)
}
Исправь код: замыкание должно запоминать значение переменной в момент вызова, а не захватывать её по ссылке.
Реализуй функцию compose, которая принимает две функции f и g (обе типа func(int) int) и возвращает новую функцию, которая применяет сначала g, потом f: f(g(x)).