# 2.3 Управляющие конструкции и функции В этом разделе мы поговорим об управляющих конструкциях и функциях в Go. ## Управляющие конструкции Контроль потока команд является величайшим изобретением в области программирования. Благодаря этому Вы можете использовать управляющие конструкции, чтобы реализовать сложную логику в своих приложениях. Существуют три категории контроля потока команд: условные конструкции, циклы и безусловные переходы. ### if `if`, вероятно, будет часто встречающимся ключевым словом в Ваших программах. Если условие, указанное в нем, удовлетворяется, выполняется блок кода, а если не удовлетворяется, то выполняется что-то другое. В Go `if` не нуждается в скобках. ```Go if x > 10 { fmt.Println("x больше 10") } else { fmt.Println("x меньше или равно 10") } ``` Наиболее полезной чертой `if` в Go является то, что перед выражением условия может находиться выражение присваивания. Область видимости переменных, инициализированных в этом выражении, ограничена блоком, относящимся к `if`: ```Go // определяем x, затем проверяем, больше ли x, чем 10 if x := computedValue(); x > 10 { fmt.Println("x больше 10") } else { fmt.Println("x меньше или равно 10") } // А этот код не скомпилируется fmt.Println(x) ``` Для множественных условий используйте if-else: ```Go if integer == 3 { fmt.Println("Целое число равно 3") } else if integer < 3 { fmt.Println("Целое число меньше 3") } else { fmt.Println("Целое число больше 3") } ``` ### goto В Go есть ключевое слово `goto`, но, используя его, будьте осторожными. `goto` перенаправляет управление потоком команд к заранее определенной `метке` внутри блока кода, в котором оно находится. ```Go func myFunc() { i := 0 Here: // Метка заканчивается на ":" fmt.Println(i) i++ goto Here // Переходим к метке "Here" } ``` Названия меток чувствительны к регистру. ### for `for` - самый мощный способ управления потоком в Go. Он может работать с данными в циклах и итеративных операциях, так же, как `while`. ```Go for expression1; expression2; expression3 { //... } ``` `expression1`, `expression2` и `expression3` - это выражения, где `expression1` и `expression3` - определения переменных или значений, возвращаемых функциями, а `expression2` - условное выражение. `expression1` выполняется один раз перед запуском цикла, а `expression3` выполняется после каждого шага цикла. Примеры, однако, полезнее слов: ```Go package main import "fmt" func main(){ sum := 0; for index:=0; index < 10 ; index++ { sum += index } fmt.Println("sum равно ", sum) } // Print:sum равно 45 ``` Иногда нам могут понадобиться множественные присваивания, но в Go нет оператора `,`, поэтому можно использовать параллельное присваивание типа `i, j = i + 1, j - 1`. Можно опускать `expression1` и `expression3`, если в них нет необходимости: ```Go sum := 1 for ; sum < 1000; { sum += sum } ``` Опускаем также `;`. Знакомо? Да, такая конструкция идентична `while`. ```Go sum := 1 for sum < 1000 { sum += sum } ``` В циклах есть две важные операции `break` и `continue`. `break` прекращает выполнение цикла, а `continue` прекращает выполнение текущей итерации цикла и начинает выполнять следующую. Если у Вас есть вложенные циклы, используйте `break` вместе с метками. ```Go for index := 10; index>0; index-- { if index == 5{ break // или continue } fmt.Println(index) } // break печатает 10、9、8、7、6 // continue печатает 10、9、8、7、6、4、3、2、1 ``` `for` может читать данные из `срезов` и `карт` при помощи ключевого слова `range`. ```Go for k,v:=range map { fmt.Println("Ключ карты:",k) fmt.Println("Значение карты:",v) } ``` Так как в Go может возвращаться сразу несколько значений, а если не использовать какое-либо присвоенное значение, возвращается ошибка компиляции, можно использовать `_`, чтобы отбросить ненужные возвращаемые значения: ```Go for _, v := range map{ fmt.Println("Значение элемента карты:", v) } ``` ### switch Иногда выходит так, что для того, чтобы реализовать какую-нибудь программную логику, приходится использовать слишком много выражений `if-else`, что приводит к тому, что код становится трудно читать и поддерживать в будущем. Самое время воспользоваться ключевым словом `switch`, чтобы решить эту проблему! ```Go switch sExpr { case expr1: какие-нибудь инструкции case expr2: другие инструкции case expr3: еще инструкции default: другой код } ``` Тип `sExpr`, `expr1`, `expr2`, and `expr3` должен быть один и тот же. `switch` очень гибок. Условия не обязаны быть постоянными, условия проверяются сверху вниз, пока не будет достигнуто условие, которое удовлетворяется. Если после ключевого слова `switch` нет выражения, ищется `true`. ```Go i := 10 switch i { case 1: fmt.Println("i равно 1") case 2, 3, 4: fmt.Println("i равно 2, 3 или 4") case 10: fmt.Println("i равно 10") default: fmt.Println("Все, что я знаю - это то, что i - целое число") } ``` В пятой строке мы поместили несколько значений в один `case`; нам также не надо писать ключевое слово `break` в конце тела `case`. При выполнении какого-либо условия цикл прекратится автоматически. Если Вы хотите продолжать проверку, нужно использовать выражение `fallthrough`. ```Go integer := 6 switch integer { case 4: fmt.Println("integer <= 4") fallthrough case 5: fmt.Println("integer <= 5") fallthrough case 6: fmt.Println("integer <= 6") fallthrough case 7: fmt.Println("integer <= 7") fallthrough case 8: fmt.Println("integer <= 8") fallthrough default: fmt.Println("default case") } ``` Эта программа выведет следующее: ``` integer <= 6 integer <= 7 integer <= 8 default case ``` ## Функции Чтобы определить функцию, используйте ключевое слово `func`. ```Go func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { // тело функции // возврат множества значений return value1, value2 } ``` Мы можем сделать вывод из примера выше: - Нужно использовать ключевое слово `func` для того, чтобы определить функцию `funcName`. - Функции могут не возвращать аргументов или возвращать один или несколько. Тип аргумента следует после его имени, аргументы разделяются запятой `,`. - Функции могут возвращать множество значений. - В примере есть два значение `output1` и `output2`, Вы можете опустить их имена и использовать только типы. - Если функция возвращает только одно значение, и имя его не указано, можно объявлять его без скобок. - Если функция не возвращает значений, вы можете вообще опустить параметры return. - Если функция возвращает значения, нужно обязательно использовать выражение `return` где-нибудь в теле функции. Давайте рассмотрим какой-нибудь практический пример: (вычисление максимального значения) ```Go package main import "fmt" // возвращаем наибольшее значение из a и b func max(a, b int) int { if a > b { return a } return b } func main() { x := 3 y := 4 z := 5 max_xy := max(x, y) // вызываем функцию max(x, y) max_xz := max(x, z) // вызываем функцию max(x, z) fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy) fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz) fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // вызываем функцию непосредственно отсюда } ``` В приведенном примере функция `max` имеет 2 аргумента, оба аргумента имеют тип `int`, поэтому для первого аргумента указание типа может быть опущено. Например, `a, b int` вместо `a int, b int`. Те же правили применимы для дополнительных аргументов. Имейте в виду, что `max` возвращает только одно значение, поэтому нам нужно указать только тип возвращаемого значения - такова краткая форма записи для таких случаев. ### Возврат множества значений Одна из вещей, в которых Go лучше, чем C - это то, что функции в Go могут возвращать несколько значений. Приведем следующий пример: ```Go package main import "fmt" // возвращаем результаты A + B и A * B func SumAndProduct(A, B int) (int, int) { return A+B, A*B } func main() { x := 3 y := 4 xPLUSy, xTIMESy := SumAndProduct(x, y) fmt.Printf("%d + %d = %d\n", x, y, xPLUSy) fmt.Printf("%d * %d = %d\n", x, y, xTIMESy) } ``` В вышеприведенном примере два значения возвращаются без имен; также можно и дать им имена. Если мы именуем переменные, которые будут возвращаться, нам нужно лишь написать `return`, чтобы возвратить значения, так как то, что надо возвращать, уже определено в функции автоматически. Имейте в виду, что если Вы собираетесь использовать функцию вне пакета (что означает, что Вы должны именовать эту функцию с заглавной буквы), лучше указывайте полную форму `return`; это сделает Ваш код более читаемым. ```Go func SumAndProduct(A, B int) (add int, Multiplied int) { add = A+B Multiplied = A*B return } ``` ### Переменные аргументы Go поддерживает переменные аргументы, что означает, что можно передать неопределенное количество аргументов в функцию. ```Go func myfunc(arg ...int) {} ``` `arg …int` говорит Go о том, что данная функция имеет неопределенное количество аргументов. Заметьте, что в функцию передаются аргументы типа `int`. В теле функции `arg` становится `срезом` элементов типа `int`. ```Go for _, n := range arg { fmt.Printf("И число равно: %d\n", n) } ``` ### Передача аргументов по значению и указателю Когда мы передаем в функцию аргументы, на самом деле она получает копию передаваемых переменных, поэтому любое изменение не затронет оригинал переданной переменной. Чтобы доказать это, рассмотрим следующий пример: ```Go package main import "fmt" // простая функция, прибавляющая 1 к a func add1(a int) int { a = a+1 // мы изменяем значение a return a // возвращаем новое значение a } func main() { x := 3 fmt.Println("x = ", x) // должно печатать "x = 3" x1 := add1(x) // вызываем add1(x) fmt.Println("x+1 = ", x1) // должно печатать "x+1 = 4" fmt.Println("x = ", x) // должно печатать "x = 3" } ``` Видите? Несмотря на то, что мы вызвали функцию `add1` с `x`, изначальное значение `x` не изменилось. Причина очень проста: когда мы вызвали `add1`, мы передали в нее копию `x`, а не сам `x`. Теперь Вы можете спросить, как передать в функцию сам `x`? В этом случае нам нужно использовать указатели. Мы знаем, что переменные хранятся в памяти и у них есть адреса. Итак, если мы хотим изменить значение переменной, мы меняем значение, находящееся в памяти по соответствующему ей адресу. Поэтому, для того, чтобы изменить значение `x`, `add1` должна знать адрес `x` в памяти. Здесь мы передаем `&x` в функцию и меняем тип аргумента на тип указателя `*int`. Мы передаем в функцию копию указателя, не копию значения. ```Go package main import "fmt" // простая функция, которая прибавляет 1 к a func add1(a *int) int { *a = *a+1 // мы изменяем a return *a // мы возвращаем значение a } func main() { x := 3 fmt.Println("x = ", x) // должно печатать "x = 3" x1 := add1(&x) // вызываем add1(&x), передаем значение адреса памяти для x fmt.Println("x+1 = ", x1) // должно печатать "x+1 = 4" fmt.Println("x = ", x) // должно печатать "x = 4" } ``` Зная все это, можно изменять значение `x` в функциях. Зачем использовать указатели? Каковы преимущества? - Это позволяет многим функциям работать с одной переменной. - Низкая стоимость выполнения благодаря тому, что передаются лишь адреса памяти (8 байт); копирование самих переменных не является эффективным как с точки зрения времени, так и объема памяти. - `channel`, `slice`, `map` - это ссылочные типы, поэтому они передаются в функцию как указатели по умолчанию. (Внимание: Если Вам нужно изменить длину `среза(slice)`, нужно явно передать срез как указатель) ### defer В Go есть хорошо спроектированное ключевое слово `defer`. В одной функции может быть много выражений `defer`; они будут выполняться в обратном порядке в тот момент, когда процесс выполнения программы дойдет до конца функции. Рассмотрим случай: когда программа открывает какие-либо файлы, они затем должны быть закрыты перед тем, как функция закончит свою работу с ошибкой. Давайте взглянем на примеры: ```Go func ReadWrite() bool { file.Open("file") // Что-нибудь делаем (failureX и failureY - условия, свидетельствующие о том, что произошли ошибки - прим. переводчика на русский) if failureX { file.Close() return false } if failureY { file.Close() return false } file.Close() return true } ``` Мы видим, что один и тот же код повторился несколько раз. `defer` просто решает эту проблему. Оно не только помогает Вам писать чистый код, но и делает его более читаемым. ```Go func ReadWrite() bool { file.Open("file") defer file.Close() if failureX { return false } if failureY { return false } return true } ``` Если присутствует больше одного `defer`, они будут выполняться в обратном порядке. Следующий пример выведет `4 3 2 1 0`. ```Go for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) } ``` ### Функции как значение и типы В Go функции также являются переменными, мы можем использовать `type`, чтобы их определять. Функции с идентичными подписями являются функциями одного типа: ```Go type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...]) ``` В чем преимущества такого способа? Ответ состоит в том, что это позволяет передавать функции как значения в другие функции. ```Go package main import "fmt" type testInt func(int) bool // определяем тип переменной "функция" func isOdd(integer int) bool { if integer%2 == 0 { return false } return true } func isEven(integer int) bool { if integer%2 == 0 { return true } return false } // передаем функцию `f` как аргумент в другую функцию func filter(slice []int, f testInt) []int { var result []int for _, value := range slice { if f(value) { result = append(result, value) } } return result } func main(){ slice := []int {1, 2, 3, 4, 5, 7} fmt.Println("Срез = ", slice) odd := filter(slice, isOdd) // используем функции как значения fmt.Println("Нечетные элементы среза: ", odd) even := filter(slice, isEven) fmt.Println("Четные элементы среза: ", even) } ``` Это свойство очень полезно, когда мы используем интерфейсы. Как мы можем видеть, `testInt` - это переменная, имеющая тип "функция", аргументы и возвращаемые значение `filter` те же самые, что и `testInt` (здесь не согласен с оригиналом - прим. переводчика на русский)(имелось ввиду не `filter`, а `isOdd` и `isEven` - дополнительное примечание от сообщества). Поэтому мы можем применять в своих программах сложную логику, поддерживая гибкость нашего кода. ### Panic и Recover В Go, в отличии от Java, нет структуры `try-catch`. Вместо того, чтобы "кидать" исключения, для работы с ошибками Go использует `panic` и `recover`. Однако, не стоит использовать `panic` слишком много, несмотря на его мощность. Panic - это встроенная функция, которая прерывает ход программы и включает статус "паники". Когда функция `F` вызывает `panic`, `F` не продолжит после этого свое исполнение, но функции `defer` выполняться. Затем `F` возвращается к той точке своего выполнения, где была вызвана panic. Пока все функции не вернут panic функциям уровнем выше, которые их вызвали, программа не прервет своего выполнения. `panic` может произойти в результате вызова `panic` в программе, также некоторые ошибки вызывают `panic` как, например, при попытке доступа к массиву за его пределами. Recover - это встроенная функция для восстановления `горутин` из состояния panic. Нормально будет вызывать `recover` в функциях `defer`, так как обычные функции не буду выполняться, если программа находится в состоянии panic. Эта функция получает значение `panic`, если программа находится в состоянии panic, и `nil`, если не находится. Следующий пример показывает, как использовать `panic`. ```Go var user = os.Getenv("USER") func init() { if user == "" { panic("не присвоено значение переменной $USER") } } ``` Следующий пример показывает, как проверять `panic`. ```Go func throwsPanic(f func()) (b bool) { defer func() { if x := recover(); x != nil { b = true } }() f() // если f вызывает panic, включается recover return } ``` ### функции `main` и `init` В Go есть две зарезервированные функции - `main` и `init`, причем `init` может быть использована во всех пакетах, а `main` - только в пакете `main`. У этих функций не может быть аргументов и возвращаемых значений. Даже несмотря на то, что можно использовать несколько функций `init` в одном пакете, я настоятельно рекомендую использовать только по одной функции `init` для каждого пакета. Программы на Go вызывают `init()` и `main()` автоматически, поэтому не нужно запускать их самому. Функция `init` может присутствовать в пакете, а может и не присутствовать, но, что касается функции `main`, то она обязана быть в `package main`, причем только в одном экземпляре. Программа инициализируется и начинает свое выполнение с пакета `main`. Если пакет `main` импортирует другие пакеты, они будут импортированы во время компиляции. Если один пакет импортируется несколько раз, он будет скомпилирован лишь единожды. После импорта пакета программа инициализирует переменные и константы в импортированном пакете, а затем выполнит функцию `init`, если она присутствует, и т.д. После того, как все пакеты будут проинициализированы, программа инициализирует константы и переменные в пакете `main`, а затем выполнит функцию `init` внутри него, если она имеется. Весь процесс изображен на следующем рисунке: ![](images/2.3.init.png?raw=true) Рисунок 2.6 Ход инициализации программы в Go ### import `import` очень часто используется в Go следующим образом: ```Go import( "fmt" ) ``` Вот так используются функции из импортированного пакета: ```Go fmt.Println("hello world") ``` `fmt` находится в стандартной библиотеке Go, он располагается в $GOROOT/pkg. Go поддерживает сторонние пакеты двумя способами: 1. Относительный путь import "./model" // импортирует пакет из той же директории, где находится программа, я не рекомендую этот способ. 2. Абсолютный путь import "shorturl/model" // импортирует пакет, находящийся по пути "$GOPATH/pkg/shorturl/model" Существует несколько специальных операторов, относящихся к импорту пакетов, и новички в них постоянно путаются: 1. Оператор "Точка". Иногда мы можем видеть, как пакеты импортируются так: import( . "fmt" ) Оператор "Точка" означает, что можно опускать имя пакета при использовании функций этого пакета. Теперь `fmt.Printf("Hello world")` превращается в `Printf("Hello world")`. 2. Операция с псевдонимом. Она изменяет имя пакета при использовании функций из него: import( f "fmt" ) Теперь вместо `fmt.Printf("Hello world")` можно `f.Printf("Hello world")`. 3. Оператор `_`. Этот оператор трудно понять без объяснения. import ( "database/sql" _ "github.com/ziutek/mymysql/godrv" ) Оператор `_` означает, что мы просто хотим импортировать пакет и выполнить его функцию `init`, но не уверены, будем ли мы использовать функции, которые он содержит. ## Ссылки - [Содержание](preface.md) - Предыдущий раздел: [Фундамент Go](02.2.md) - Следующий раздел: [struct](02.4.md)