514 lines
29 KiB
Markdown
514 lines
29 KiB
Markdown
# 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` внутри него, если она имеется. Весь процесс изображен на следующем рисунке:
|
||
|
||

|
||
|
||
Рисунок 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)
|