В этом разделе мы поговорим об управляющих конструкциях и функциях в 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 has a goto
keyword, but be careful when you use it. goto
reroutes the contro to a previously defined label
within the body of same code block.
В 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
в функциях. Зачем использовать указатели? Каковы преимущества?
- Это позволяет многим функциям работать с одной переменной.
- Low cost by passing memory addresses (8 bytes), copy is not an efficient way, both in terms of time and space, to pass variables.
- Низкая стоимость выполнения благодаря тому, что передаются лишь адреса памяти (8 байт); копирование самих переменных не является эффективным как с точки зрения времени, так и объема памяти.
string
,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
(здесь не согласен с оригиналом - прим. переводчика на русский). Поэтому мы можем применять в своих программах сложную логику, подерживая гибкость нашего кода.
В 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
, причем только в одном экземпляре.
Programs initialize and begin execution from the main
package. If the main
package imports other packages, they will be imported in the compile time. If one package is imported many times, it will be only compiled once. After importing packages, programs will initialize the constants and variables within the imported packages, then execute the init
function if it exists, and so on. After all the other packages are initialized, programs will initialize constants and variables in the main
package, then execute the init
function inside the package if it exists. The following figure shows the process.
Программа инициализируется и начинает свое выполнение с пакета 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