Skip to content

Latest commit

 

History

History
762 lines (618 loc) · 52.8 KB

01-basics.adoc

File metadata and controls

762 lines (618 loc) · 52.8 KB

Базовый синтаксис

Начнём с избитой фразы: «Go является статически компилируемым в машинный код языком с сильной статической типизацией». А теперь давайте по порядку разбираться, что же всё это означает. Если вам и так всё понятно, смело переходите параграфу [_hello_world].

Вообще компьютер умеет работать только с так называемым машинным кодом, то есть инструкциями для центрального процессора, закодированных в понятные этому процессору коды. При этом одна инструкция языка, например, print("Hello, World") разворачивается в несколько инструкций для процессора. Программа, на каком бы языке она ни была написана в итоге должна превратиться в инструкции, понятные процессору. Но пути этого превращения различны. Языки могут быть компилируемые и интерпретируемые. У компилируемых языков этап превращения исходного кода в инструкции процессора, называемый компиляцией, отделён от этапа исполнения этих инструкций. В интерпретируемых языках это преобразование делается непосредственно перед исполнением. Стоит отметить, что в Go есть сокращённая форма для компиляции и запуска: go run main.go, выглядящая со стороны как запуск интерпретатора.

С другой стороны даже процесс компиляции может быть разделён на различные этапы. Так есть языки с виртуальными машинами, такие как Java, Erlang, JavaScript и многие другие. Программы, написанные на этих языках компилируются в инструкции виртуального процессора, который эмулируется соответствующей виртуальной машиной, которая на своём уровне преобразует эти инструкции в понятные физическому процессору. То есть вводится промежуточное представление программы, не зависящее от архитектуры конечного компьютера. С другой стороны промежуточное представление программы может быть и без виртуальной машины, например, многие языки используют промежуточное LLVM-представление.

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

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

var x int = 12
var y float64 = 2.2

var res float64 = float64(x) + y

Также как и с компиляцией, преобразование типов операндов производится в любом языке, но в слабо типизированных языках есть набор правил, по которым компилятор или интерпретатор может выполнить часть преобразований без явных инструкций со стороны программиста. Несмотря на то, что это похоже на то, как мы думаем, ведь написанное на бумаге «426» мы без труда интерпретируем и как строку и как число, хоть целое, хоть вещественное, часто такие неявные правила приводят к трудно обнаружимым ошибкам ошибкам.

Динамическая и статическая типизация также различается ограничениями, так при статической типизации тип приписывается переменной один раз и не может быть изменён. Например, следующий код допустим в Python (сильная динамическая типизация), но не допустим в Go:

x = "foo"
x = 5

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

Пара слов о рантайме

Стоит заметить также, что Go имеет так называемый рантайм (runtime), функциональность, которая добавляется в любую программу при компиляции. Поэтому даже просто hello world в скомпилированном виде имеет размер почти 2 МБ (1968 КБ для версии 1.12.10 linux/amd64). Для чего же нужен этот дополнительный багаж?

Во-первых, в рантайм встроен сборщик мусора, освобождающий память, от неиспользуемых переменных. Это значительно облегчает написание кода, в сравнении с низкоуровневыми языками, такими как Си и C++. разработчик не должен сам следить за использованием памяти, вызывая вручную примитивы типа malloc и mfree, хотя для оптимизации высоконагруженных мест Go позволяет переходить к ручному управлению памятью.

Во-вторых, рантайм отвечает также за переключение между go-рутинами, позволяя оптимально распределять ресурсы системы под выполняемые задачи. Более подробно про go-рутины и конкурентную модель выполнения Go вы можете прочитать в соответствующей главе.

Hello, World!

Продолжим следовать избитым клише и первой программой будет «Привет, Мир!». Создайте файл с именем main.go со следующим содержимым:

01_hello_world/main.go
link:examples/01_hello_world/main.go[role=include]

Теперь, если выполнить в командной строке (в директории с файлом main.go) команду

go run main.go

вы должны увидеть вывод нашей программы:

Hello, World!
Note
Все примеры, используемые в данном курсе, можно найти в директории examples в репозитории проекта {github}.

Давайте разбираться с тем, что происходит в этой короткой программе. Первая не закомментированная строка любого go-файла должна задавать имя пакета, которому этот файл принадлежит. Более подробно о пакетах мы поговорим соответствующей части, пока же упомянем лишь следующее:

  1. Пакетом является директория, то есть все файлы, расположенные в одной директории (за исключением тестов), должны принадлежать одному пакету. Таким образом не нужно явно подключать файл, находящийся рядом, как в том же Python или NodeJS.

  2. Внутри пакета доступны все объявления. То есть нельзя в двух соседних файлах объявлять функции, типы или константы с одинаковыми именами.

Также как в языке Си, исполняемая программа должна содержать функцию main, с которой и начинается исполнение программы. Инструкции за пределами функций, за исключением объявления глобальных переменных, запрещены. Но есть одно ограничение: функция main должна располагаться в пакете main. При этом хорошим тоном считается держать весь код этого пакета в одном файле, так чтобы было удобнее запускать и компилировать программу. На первых порах мы будем оперировать только такими файлами-пакетами, позже мы научимся разбивать код на пакеты и библиотеки.

В строке 3 подключается стандартный пакет fmt. Этот пакет содержит функции по работе с вводом/выводом и другие вспомогательные функции. В данном случае в 6-й строке мы используем функцию Println из этого пакета, позволяющую вывести на экран текст.

Тулинг

С самой первой версии компилятор go содержит специальную команду: go fmt, с помощью которой можно привести код на Go к стандарту оформления. Это оказалось революционным решением, раз и навсегда положившим конец спорам Tab vs Space и исключившее написание условий и циклов без фигурных скобок. Часть сообщества, конечно, восприняли идею общего стиля кода в штыки, но к настоящему моменту большинство одобряет такой подход. Помимо встроенной команды форматирования со временем появились и более продвинутые утилиты, которые не только форматируют код, но и сами добавляют недостающие импорты и даже пропущенные пустые значения в инструкциях выхода из функций, например, goimports и goreturns.

Кроме автоматического форматирования компилятор из коробки умеет запускать тесты и бенчмарки, позволяя без сторонних утилит разрабатывать сложные библиотеки и программы. Про это мы поговорим в соответствующей главе.

Также для языка быстро начал расти арсенал статического анализа кода, направленного как на предотвращение ошибок, так и на ужесточение стиля кода. Первым и классическим линтером для языка стал golint. После этого сообщество создало огромное число анализаторов. О том как лучше запускать этот зоопарк для вашего кода описано в дополнении [_линтеры_и_другие_инструменты].

Но прежде чем двигаться дальше необходимо зафиксировать несколько моментов, способных вызвать проблемы на старте изучения языка.

  • Отступ строк формируется табуляцией;

  • Название переменных, типов и функций принято писать в camelCase;

  • Фигурные скобочки обязательны, и их принято расставлять в египетском стиле:

func declaration, for or if {
    body
}
  • Висящая запятая обязательна:

x := []int{
    1,
    2,
    3,
}

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

Переменные

Переменные можно объявлять следующим способом:

var x int

Это создаст переменную с именем x типа int (то есть целое знаковое число). Эту строку можно прочитать как: «пусть x — переменная типа int». Если необходимо объявить несколько переменных, то можно сгруппировать определения с помощью скобок:

var (
    x int
    s string
)

В случае, когда необходимо определить несколько переменных одного типа, то их можно сгруппировать следующим образом:

var x, y, z int

Переменные будут сразу же инициализирована пустыми значением, соответствующими их типам (в данном случае 0). Можно задать другое значение для инициализации:

var x int = 17

Эта строка будет аналогична строкам:

var x int
x = 17

Более того в данном случае компилятор может сам определить тип по константе справа, так что запись можно сократить до

var x = 17

Для таких случаев в Go предусмотренна короткая запись, эквивалентная написанной выше:

x := 17

Операция := объявляет и инициализирует переменную слева от себя значением правой части. В Go запрещено дважды объявлять переменную, поэтому последующие присвоения необходимо делать с помощью оператора =.

Также как обычное присвоение = операция объявления может сопоставлять кортежи:

x, y, s := 17, 19.5, "hello"

В результате будут объявлены три переменные

  1. x типа int со значением 17;

  2. y типа float64 со значением 19.5;

  3. s типа string со значением "hello";

Особенностью такого объявления является то, что в левой части могут быть уже объявленные переменные (кроме первой):

var s string
x, y, s := 17, 19.5, "hello"

В данном случае переменные x и y будут объявлены во второй строке, а переменной s просто будет присвоено значение, то есть этот код эквивалентен следующему:

var s string
x := 17
y := 19.5
s = "hello"

Скалярные типы данных

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

Числа int8, int16, int32, int64, uint8, unit16, uint32, uint64, float32, float64. Как видно все типы имеют в названии размер, занимаемой памяти в битах. Для всех чисел пустое значение — 0. Также любой числовой тип можно привести к любому другому числовому типу, использовав тип как функцию:

var x int32
y := 19.5
x = int32(y)

С числами можно производить следующие бинарные арифметические операции: сложение (+), вычитание (-), умножение (*), деление (/), а для целых чисел также доступна операция деления по модулю (%). Кроме того для знаковых типов можно инвертировать знак с помощью приписывания к числу слева знака -. Для всех бинарных операций есть краткая форма записи в случае, если результат необходимо присвоить переменной, являющейся первым операндом:

x += 2 // эквивалентно x = x + 2
x %= 3 // эквивалентно x = x % 3

Также существуют операции инкрементирования и декрементирования:

x++ // эквивалентно x += 1
x-- // эквивалентно x -= 1
Note
Операция присваивания в Go не имеет собственного результата, поэтому нельзя использовать присваивание как часть другой инструкции. Это же относится и к сокращённым формам бинарных операций.

Есть также типы алиасы к числовым типам. Например, тип byte является тем же типом int8, а runeint32. Но есть и менее предсказуемые типы int и uint, являющиеся алиасами к типам int32 и uint32 или int64 и uint64 соответственно в зависимости от битности операционной системы, на которой выполняется программа.

Булевый тип bool может принимать значение true или false, последнее является пустым значением для этого типа. Для булевых переменных и констант доступны следующие операции: инверсия (!), логическое и (&&) и логическое или (||).

И, наконец, скалярным типом в Go является строка (тип string). Вообще говоря строка представляет из себя срез (slice), о которых мы будем говорить позже. Но компилятор не позволяет модифицировать отдельные символы строки, так что она подходит под определение скалярного типа. Пустым значением для переменных типа строка является пустая строка. Для объявления строковых констант можно использовать либо двойные кавычки ("), либо обратные кавычки (`), при этом в первых можно использовать специальные символы, такие как \n, \t и так далее, а в обратных кавычках можно использовать непосредственно переносы строк. То есть следующие две константы равны:

s1 := "foo\nbar"
s2 := `foo
bar`

Строки можно складывать (конкатенировать), используя операцию +. А также можно использовать синтаксис срезов для получения подстроки:

fmt.Println("Hello, World!"[3:5] + "l")

Константы

Для всех скалярных типов можно также определить константу с помощью ключевого слова const. Объявление аналогично определению с ключевым словом var, но полученный объект нельзя использовать в левой части оператора присваивания. Также во время объявления констант доступно ключевое слово iota:

const (
    Foo = iota
    Bar
    Baz
    Qux
)

В результате будет объявлены следующие константы типа int: Foo равная 0, Bar равная 1, Baz равная 2 и Qux равная 3. При этом iota в рамках одного блока определений будет принимать значения от 0 до максимума типа int, увеличиваясь для каждого следующего определения. Для констант можно задать тип, отличный от int, а с iota можно составить выражение, допустимое в определении констант:

const (
    Foo uint32 = iota * iota
    Bar
    Baz
    Qux
)

Такой встроенный итератор можно использовать как некоторую замену отсутствующему в языке enum'у, но в придачу к iota понадобится [_генерация_кода]

Область видимости

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

var x = 17

но нельзя использовать оператор :=.

Более узкую область видимости определяют функции. В теле функции доступны переменные, объявленные в глобальной области, а также переменные, объявленные в этом же теле функции выше. При этом локальные переменные могут перекрывать внешние, то есть следующий код

var s = "foo"

func main() {
    var s = "bar"
    fmt.Println(s)
}

Выведет строку "bar", при этом значение глобальной переменной останется прежним. Некоторые особенности области видимости функций подробнее будут рассмотрены в соответствующем параграфе.

Помимо функций, вложенные области видимости задают все конструкции, управляющие потоком выполнения.

Управление потоком выполнения

Для управления потоком выполнения в Go предусмотренны следующие конструкции:

Условия

if <condition 1> {
    <body 1>
} else if <condition 2> {
    <body 2>
} else {
    <body 3>
}

Если выполнено условие <condition 1>, то будут выполнены инструкции, перечисленные в <body 1>, если не выполнено условие <condition 1>, но выполнено условие <condition 2>, то будут выполнены инструкции из <body 2>, наконец, если ни одно условие не выполнено, то будут выполнены инструкции из блока else (<body 3>). Блоков else if может быть сколько угодно от 0 и до бесконечности. Блок else может быть пропущен.

В качестве условий могут быть использованы любые выражения, результатом которых имеет тип bool, например:

if x < 0 || x > 100 {
    fmt.Println("Неверно задано значение x")
} else if x == 0 {
    fmt.Println("x равен нулю")
} else {
    fmt.Println("x равен", x)
}
Note
Вообще некоторые считают плохим тоном использование ключевого слова else, предпочитая делать ранний выход из функции. Это не особо относится к изучению синтаксиса языка, но может качественно сказаться на читаемости вашего кода. В любом случае рекомендуем прочитать книгу «Чистый код»[cc].

В Go есть особая форма конструкции if, позволяющая выполнить инструкцию прямо внутри условия. Для примера рассмотрим следующую задачу: дана хеш таблица m, необходимо проверить наличие в ней ключа, если ключ есть, то вернуть значение по этому ключу, иначе вернуть defaultValue (аналог метода get словарей в Python). Подробнее о хеш-таблицах мы поговорим в соответствующем разделе, здесь отметим лишь то, что при получении значения по ключу из хеш-таблицы можно использовать синтаксис

val, ok := m[key]

Тогда, если ключ есть в хеш-таблице, то val будет присвоено значение, хранящееся по этому ключу, а в ok будет записано true. Если же ключ не найден, то val будет присвоено пустое значение, соответствующее типу значений хеш-таблицы, а в ok будет записано false.

Таким образом, задачу можно решить следующим способом:

val, ok := m[key]
if ok {
    return val
}
return defaultValue

Но в Go можно объединить первые две строки:

if val, ok := m[key]; ok {
    return val
}
return defaultValue

Это особенно удобно потому, что во втором случае область видимости переменных val и ok будет ограничена условием, в первом же случае они оказываются объявлены за пределами условия.

Switch

Для сопоставления значения выражения возможным значениям можно использовать оператор switch:

switch <expression> {
case <value 1>:
    <body 1>
case <value 2>:
    <body 2>
default:
    <body 3>
}

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

Знакомые с такими языками как Си или JavaScript могут удивиться отсутствию ключевых слов break в конце блоков. В Go поведение по умолчанию противоположно поведению этих языков. По умолчанию выполняется только один блок. Если необходимо выполнить также и следующий блок, необходимо использовать ключевое слово fallthrough. Например,

switch command {
case "save and continue":
    save()
    fallthrough
case "continue":
    next()
default:
    save()
    stop()
}

В данном примере, если значение переменной command равно строке "continue", то будет выполнена только функция next, если значение равно "save and continue", то будет выполнена функция save, после чего будет выполнена следующая ветка, то есть "continue", состоящая из вызова функции next. При любой другой команде будет выполнен блок default.

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

switch command {
case "save and continue", "save and next":
    save()
    fallthrough
case "continue", "next":
    next()
default:
    save()
    stop()
}

Часто конструкцию из множественных блоков else if переписывают в виде:

switch true {
case <condition 1>:
    <body 1>
case <condition 2>:
    <body 2>
default:
    <body 3>
}

В Go в такой конструкции можно опустить выражение true вообще:

switch {
case <condition 1>:
    <body 1>
case <condition 2>:
    <body 2>
default:
    <body 3>
}
Note
В отличии от других языков в Go использование конструкции switch не является анти-паттерном, однако длинных и, тем более, повторяющихся switch-ей стоит избегать, используя полиморфизм или хеш-таблицы.

Циклы

Одной из «фишек» Go является минимизация ключевых слов языка. Именно поэтому все циклы в нём определяются одним ключевым словом for. Но имеются следующие виды циклов:

Классический Си-подобный
for i := 0; i < 10; i++ {
    // body
}

Здесь перед входом в цикл выполняется первая инструкция (i := 0), перед каждой итерацией цикла, в том числе и первой проверяется условие (i < 10) и, если условие выполнено, то выполняется тело цикла, после чего выполняется вторая инструкция (i++), если же условие не выполнено, то цикл завершается и программа переходит к следующим инструкциям. Инструкции и условия могут быть более сложными, например

for i, a, b := 1, 1, 1; i < 100; i, a, b = i+1, b, a+b {
    // body
}
Аналог цикла while
for x < y {
    // body
}

По сути этот цикл можно написать как частный случай предыдущего, где обе инструкции пустые:

for ; x < y; {
    // body
}

Но Go позволяет в этом случае не писать лишние точки с запятыми.

Итерирование
for i, x := range m {
    // body
}

Это особый вид цикла, где после ключевого слова range может стоять выражение, результатом которого является строка, массив, срез, хеш-таблица или канал. Принцип действия для строк, массивов и срезов похож: для всех элементов этих объектов будет выполнено тело цикла, а переменная i будет принимать последовательно значения от 0 до длинны объекта без единицы, а x — значения хранящиеся в данном объекте по индексу i. Более подробно действие циклов такого типа будет рассмотрено в разделах, посвящённых срезам, хеш-таблицам и каналам.

Бесконечный цикл
for {
    // body
}

Наконец, цикл с пустым условием аналогичен циклу с всегда истинным условием. Для выхода из такого цикла необходимо воспользоваться ключевыми словами break или return.

Также как и в других языках, в Go есть ключевые слова continue и break. Первое позволяет преждевременно завершить текущую итерацию цикла и перейти к следующей, например:

sumOfPrimes := 0
for i := 1; i < 100; i++ {
    if !isPrime(i) {
        continue
    }
    sumOfPrimes += i
}

С помощью ключевого слова break можно прекратить выполнение цикла. Однако, надо помнить, что это ключевое слово используется не только для прерывания цикла, но и для прерывания рассмотренной конструкции switch и конструкции из следующей главы select. Например, следующий код выведет все числа от 0 до 9:

for i := 0; i < 10; i++ {
    switch i {
    case 5:
        break
    }
    fmt.Println(i)
}

Функции

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

Определение именованной функции уровня пакета мы с вами уже встречали:

01_hello_world/main.go
link:examples/01_hello_world/main.go[role=include]

В данном примере функция не принимает аргументов и ничего не возвращает. Для более полного описания синтаксиса рассмотрим ещё несколько примеров:

// Вычисление квадрата числа
func square(x float64) float64 {
    return x * x
}

// Сумма чисел
func sum(x, y float64) float64 {
    return x + y
}

// Выравнивание строки по правому краю по заданной длине
func leftPad(s string, n int) string {
    pad := ""
    for i := len(s); i < n; i++ {
        pad += " "
    }
    return pad + s
}

В общем виде определение функции можно описать так:

func <имя>(<описание аргументов>) <описание результата> {
    <тело>
}

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

Кортеж результатов

Функции могут возвращать не одно значение, а кортеж. В качестве примера возьмём функцию, которая принимает два аргумента и возвращает их в обратном порядке:

func swap(x, y float64) (float64, float64) {
    return y, x
}

Типы результатов могут быть разными, а также можно именовать результаты, превращая их в объявление переменных. Это особенно удобно, когда не очевидно назначение результатов. При этом если использовать ключевое слово return без дополнительных аргументов, то будет возвращёны значения именованных результатов:

func stripLeftSpaces(s string) (result string, trimmedSpaceCount int) {
    result := s
    for result != "" && result[0] == ' ' {
        result = result[1:]
        trimmedSpaceCount++
    }
    return
}

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

func stripLeftSpaces(s string) (result string, trimmedSpaceCount int) {
    for ; trimmedSpaceCount < len(s) && s[trimmedSpaceCount] == ' '; trimmedSpaceCount++ {
    }
    return s[trimmedSpaceCount:], trimmedSpaceCount
}
Присваивание в никуда

Бывает ситуации, когда из кортежа результатов, возвращаемых функцией, требуются не все. Тогда можно присвоить часть этого кортежа в никуда, точнее в специальную переменную _, куда можно записать всё что угодно, но ничего нельзя прочитать. Например, если нам необходимо убрать пробелы из начала строки, но не важно сколько их там было, можно написать:

trimmedString, _ := stripLeftSpaces(s)

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

Анонимные функции

В Go функции являются такими же данными, как переменные или константы. То есть функции можно передавать как аргументы в другие функции. Например, напишем функцию, находящую минимальное натуральное число (не включая 0), для которого выполнена некоторая проверка check, которая является функцией, принимающей целое число и возвращающей булево значение:

func findMinimal(check func(x int) bool) int {
    i := 1
    for ; !check(i); i++ {
    }
    return i
}
Note
Функция findMinimal принимает один аргумент check с типом func(x int) bool. Под этот тип подойдут любые функции, принимающие один аргумент с типом int и возвращающие булево значение. Конечно, что то, как аргумент этой функции будет называться нем не важно. Поэтому имя аргумента можно опустить, сократив определение типа до func(int) bool.

Теперь можно использовать эту функцию в комбинации с различными функциями проверки:

02_find_minimal/main.go
link:examples/02_find_minimal/main.go[role=include]

Но иногда не хочется объявлять функции в глобальном пространстве имён. Тогда можно воспользоваться анонимными функциями, функциями без имени:

03_find_minimal_anonymous/main.go
link:examples/03_find_minimal_anonymous/main.go[role=include]

Мы просто объявили функции в месте передачи их как аргументов, не давая им имена. Анонимную функцию можно присвоить переменной и даже вернуть как результат другой функции.

Замыкания

func dividedBy(d int) func(int) bool {
    return func(x int) bool {
        return x % d == 0
    }
}

fmt.Println("Минимальное число, делимое на 11:", findMinimal(dividedBy(11)))

Получившаяся функция также называется замыканием, потому что ссылается на область видимости функции, которая её порождает (аргумент d). При этом в замыкании можно как читать, так и изменять контекст порождающей функции. С помощью замыканий можно реализовывать различные полезные паттерны проектирования. Например, с помощью замыкания можно создать итератор (объект возвращающий по запросу следующее значение из некоторой последовательности). Для примера напишем генератор чисел Фибоначчи:

04_fibonacci/main.go
link:examples/04_fibonacci/main.go[role=include]

Рекурсия

Некоторые задачи требуют рекурсивного вызова функции. Например, вычисление факториала числа:

func factorial(x int) int {
    if x == 1 {
        return 1
    }
    return x * factorial(x - 1)
}
Warning
Никогда не используйте приведённый выше способ вычисления факториала ни в одном языке. Используйте возможности стандартной библиотеки или используйте нерекурсивный вариант.

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

factorial := func(x int) int {
    if x == 1 {
        return 1
    }
    return x * factorial(x - 1)
}

Такой вариант не скомпилируется, потому что компилятор сначала разбирает определение функции, а уже только после этого определение переменной factorial, то есть на момент разбора определения функции переменная factorial ещё не объявлена и мы не можем её использовать. Это можно обойти, разделив определение и присвоение:

var factorial func(int) int
factorial = func(x int) int {
    if x == 1 {
        return 1
    }
    return x * factorial(x - 1)
}

Теперь к моменту разбора тела функции переменная factorial уже объявлена и её можно использовать.

Функции с переменным количеством аргументов

Иногда возникает необходимость определить функцию, которая может принимать переменное число аргументов. Например, функция, суммирующая все переданные в неё аргументы. Вообще говоря мы можем передать в такую функцию один аргумент с типом срез (см. [_срезы]):

func sum(args []float64) float64 {
    res := 1
    for _, x := range args {
        res += x
    }
    return res
}

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

fmt.Println(sum([]float64{4, 5}))

В Go есть синтаксический сахар, позволяющий объявить последний аргумент функции как остаточный (rest):

func sum(args ...float64) float64 {
    res := 1
    for _, x := range args {
        res += x
    }
    return res
}

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

fmt.Println(sum(4, 5))

Этот синтаксис имеет ряд ограничений, но часто позволяет улучшить публичный интерфейс пакета. Старайтесь не злоупотреблять подобным синтаксисом.

Задачи

  1. Что будет результатом --x? Почему?

  2. Предложите минимальное исправление для следующей программы:

package main

import "fmt"

func sum(x, y float64) float64 {
    return x + y
}

func square(x int) int {
    return x * x
}

func main() {
    fmt.Println(square(sum(25, 9)))
}
  1. Чему будет равна константа Qux при следующем определении:

const (
    Foo uint32 = iota * iota
    Bar
    Baz = iota + 2
    Qux
)
  1. Напишите генератор квадратов натуральных чисел.