В этом разделе мы поговорим об управляющих конструкциях и функциях в Go.
Контроль потока команд является величайшим изобретением в области программирования. Благодаря этому Вы можете использовать управляющие конструкции, чтобы реализовать сложную логику в своих приложениях. Существуют три категории контроля потока команд: условные конструкции, циклы и безусловные переходы.
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")
}
В Go есть ключевое слово goto
, но, используя его, будьте осторожными. goto
перенаправляет управление потоком команд к заранее определенной метке
внутри блока кода, в котором оно находится.
func myFunc() {
i := 0
Here: // Метка заканчивается на ":"
fmt.Println(i)
i++
goto Here // Переходим к метке "Here"
}
Названия меток чувствительны к регистру.
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)
}
Иногда выходит так, что для того, чтобы реализовать какую-нибудь программную логику, приходится использовать слишком много выражений 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)
, нужно явно передать срез как указатель)
В 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
- дополнительное примечание от сообщества). Поэтому мы можем применять в своих программах сложную логику, поддерживая гибкость нашего кода.
В 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
}
В Go есть две зарезервированные функции - main
и init
, причем init
может быть использована во всех пакетах, а main
- только в пакете main
. У этих функций не может быть аргументов и возвращаемых значений. Даже несмотря на то, что можно использовать несколько функций init
в одном пакете, я настоятельно рекомендую использовать только по одной функции init
для каждого пакета.
Программы на Go вызывают init()
и main()
автоматически, поэтому не нужно запускать их самому. Функция init
может присутствовать в пакете, а может и не присутствовать, но, что касается функции main
, то она обязана быть в package main
, причем только в одном экземпляре.
Программа инициализируется и начинает свое выполнение с пакета main
. Если пакет main
импортирует другие пакеты, они будут импортированы во время компиляции. Если один пакет импортируется несколько раз, он будет скомпилирован лишь единожды. После импорта пакета программа инициализирует переменные и константы в импортированном пакете, а затем выполнит функцию init
, если она присутствует, и т.д. После того, как все пакеты будут проинициализированы, программа инициализирует константы и переменные в пакете main
, а затем выполнит функцию init
внутри него, если она имеется. Весь процесс изображен на следующем рисунке:
Рисунок 2.6 Ход инициализации программы в Go
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
, но не уверены, будем ли мы использовать функции, которые он содержит.
- Содержание
- Предыдущий раздел: Фундамент Go
- Следующий раздел: struct