Управляющие конструкции и функции
В этом разделе мы поговорим об управляющих конструкциях и функциях в Go.
Управляющие конструкции
Контроль потока команд является величайшим изобретением в области программирования. Благодаря этому Вы можете использовать управляющие конструкции, чтобы реализовать сложную логику в своих приложениях. Существуют три категории контроля потока команд: условные конструкции, циклы и безусловные переходы.
if
if
, вероятно, будет часто встречающимся ключевым словом в Ваших программах. Если условие, указанное в нем, удовлетворяется, выполняется блок кода, а если не удовлетворяется, то выполняется что-то другое.
В Go if
не нуждается в скобках.
if x > 10 { fmt.Println("x больше 10") } else { fmt.Println("x меньше или равно 10") }
Наиболее полезной чертой if
в Go является то, что перед выражением условия может находиться выражение присваивания. Область видимости переменных, инициализированных в этом выражении, ограничена блоком, относящимся к if
:
// определяем x, затем проверяем, больше ли x, чем 10 if x := computedValue(); x > 10 { fmt.Println("x больше 10") } else { fmt.Println("x меньше или равно 10") } // А этот код не скомпилируется fmt.Println(x)
Для множественных условий используйте if-else:
if integer == 3 { fmt.Println("Целое число равно 3") } else if integer < 3 { fmt.Println("Целое число меньше 3") } else { fmt.Println("Целое число больше 3") }
goto
В Go есть ключевое слово goto
, но, используя его, будьте осторожными. goto
перенаправляет управление потоком команд к заранее определенной метке
внутри блока кода, в котором оно находится.
func myFunc() { i := 0 Here: // Метка заканчивается на ":" fmt.Println(i) i++ goto Here // Переходим к метке "Here" }
Названия меток чувствительны к регистру.
for
for
- самый мощный способ управления потоком в Go. Он может работать с данными в циклах и итеративных операциях, так же, как while
.
for expression1; expression2; expression3 { //... }
expression1
, expression2
и expression3
- это выражения, где expression1
и expression3
- определения переменных или значений, возвращаемых функциями, а expression2
- условное выражение. expression1
выполняется один раз перед запуском цикла, а expression3
выполняется после каждого шага цикла.
Примеры, однако, полезнее слов:
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
, если в них нет необходимости:
sum := 1 for ; sum < 1000; { sum += sum }
Опускаем также ;
. Знакомо? Да, такая конструкция идентична while
.
sum := 1 for sum < 1000 { sum += sum }
В циклах есть две важные операции break
и continue
. break
прекращает выполнение цикла, а continue
прекращает выполнение текущей итерации цикла и начинает выполнять следующую. Если у Вас есть вложенные циклы, используйте break
вместе с метками.
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
.
for k,v:=range map { fmt.Println("Ключ карты:",k) fmt.Println("Значение карты:",v) }
Так как в Go может возвращаться сразу несколько значений, а если не использовать какое-либо присвоенное значение, возвращается ошибка компиляции, можно использовать _
, чтобы отбросить ненужные возвращаемые значения:
for _, v := range map{ fmt.Println("Значение элемента карты:", v) }
switch
Иногда выходит так, что для того, чтобы реализовать какую-нибудь программную логику, приходится использовать слишком много выражений if-else
, что приводит к тому, что код становится трудно читать и поддерживать в будущем. Самое время воспользоваться ключевым словом switch
, чтобы решить эту проблему!
switch sExpr { case expr1: какие-нибудь инструкции case expr2: другие инструкции case expr3: еще инструкции default: другой код }
Тип sExpr
, expr1
, expr2
, and expr3
должен быть один и тот же. switch
очень гибок. Условия не обязаны быть постоянными, условия проверяются сверху вниз, пока не будет достигнуто условие, которое удовлетворяется. Если после ключевого слова switch
нет выражения, ищется true
.
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
.
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
.
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { // тело функции // возврат множества значений return value1, value2 }
Мы можем сделать вывод из примера выше:
- Нужно использовать ключевое слово
func
для того, чтобы определить функциюfuncName
. - Функции могут не возвращать аргументов или возвращать один или несколько. Тип аргумента следует после его имени, аргументы разделяются запятой
,
. - Функции могут возвращать множество значений.
- В примере есть два значение
output1
иoutput2
, Вы можете опустить их имена и использовать только типы. - Если функция возвращает только одно значение, и имя его не указано, можно объявлять его без скобок.
- Если функция не возвращает значений, вы можете вообще опустить параметры return.
- Если функция возвращает значения, нужно обязательно использовать выражение
return
где-нибудь в теле функции.
Давайте рассмотрим какой-нибудь практический пример: (вычисление максимального значения)
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 могут возвращать несколько значений.
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
; это сделает Ваш код более читаемым.
func SumAndProduct(A, B int) (add int, Multiplied int) { add = A+B Multiplied = A*B return }
Переменные аргументы
Go поддерживает переменные аргументы, что означает, что можно передать неопределенное количество аргументов в функцию.
func myfunc(arg ...int) {}
arg …int
говорит Go о том, что данная функция имеет неопределенное количество аргументов. Заметьте, что в функцию передаются аргументы типа int
. В теле функции arg
становится срезом
элементов типа int
.
for _, n := range arg { fmt.Printf("И число равно: %d\n", n) }
Передача аргументов по значению и указателю
Когда мы передаем в функцию аргументы, на самом деле она получает копию передаваемых переменных, поэтому любое изменение не затронет оригинал переданной переменной.
Чтобы доказать это, рассмотрим следующий пример:
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
. Мы передаем в функцию копию указателя, не копию значения.
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
; они будут выполняться в обратном порядке в тот момент, когда процесс выполнения программы дойдет до конца функции. Рассмотрим случай: когда программа открывает какие-либо файлы, они затем должны быть закрыты перед тем, как функция закончит свою работу с ошибкой. Давайте взглянем на примеры:
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
просто решает эту проблему. Оно не только помогает Вам писать чистый код, но и делает его более читаемым.
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
.
for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) }
Функции как значение и типы
В Go функции также являются переменными, мы можем использовать type
, чтобы их определять. Функции с идентичными подписями являются функциями одного типа:
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
В чем преимущества такого способа? Ответ состоит в том, что это позволяет передавать функции как значения в другие функции.
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
.
var user = os.Getenv("USER") func init() { if user == "" { panic("не присвоено значение переменной $USER") } }
Следующий пример показывает, как проверять panic
.
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
внутри него, если она имеется. Весь процесс изображен на следующем рисунке:
import
import
очень часто используется в Go следующим образом:
import( "fmt" )
Вот так используются функции из импортированного пакета:
fmt.Println("hello world")
fmt
находится в стандартной библиотеке Go, он располагается в $GOROOT/pkg. Go поддерживает сторонние пакеты двумя способами:
- Относительный путь import "./model" // импортирует пакет из той же директории, где находится программа, я не рекомендую этот способ.
- Абсолютный путь import "shorturl/model" // импортирует пакет, находящийся по пути "$GOPATH/pkg/shorturl/model"
Существует несколько специальных операторов, относящихся к импорту пакетов, и новички в них постоянно путаются:
- Оператор "Точка". Иногда мы можем видеть, как пакеты импортируются так: import( . "fmt" ) Оператор "Точка" означает, что можно опускать имя пакета при использовании функций этого пакета. Теперь
fmt.Printf("Hello world")
превращается вPrintf("Hello world")
. - Операция с псевдонимом. Она изменяет имя пакета при использовании функций из него: import( f "fmt" ) Теперь вместо
fmt.Printf("Hello world")
можноf.Printf("Hello world")
. - Оператор
_
. Этот оператор трудно понять без объяснения. import ( "database/sql" _ "github.com/ziutek/mymysql/godrv" ) Оператор_
означает, что мы просто хотим импортировать пакет и выполнить его функциюinit
, но не уверены, будем ли мы использовать функции, которые он содержит.