Skip to content

Latest commit

 

History

History
519 lines (375 loc) · 29.8 KB

File metadata and controls

519 lines (375 loc) · 29.8 KB

2.3 Управляющие конструкции и функции

В этом разделе мы поговорим об управляющих конструкциях и функциях в 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 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

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 в функциях. Зачем использовать указатели? Каковы преимущества?

  • Это позволяет многим функциям работать с одной переменной.
  • 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), нужно явно передать срез как указатель)

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 (здесь не согласен с оригиналом - прим. переводчика на русский). Поэтому мы можем применять в своих программах сложную логику, подерживая гибкость нашего кода.

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, причем только в одном экземпляре.

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

import очень часто используется в Go следующим образом:

import(
	"fmt"
)

Вот так используются функции из импортированного пакета:

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, но не уверены, будем ли мы использовать фунцкии, которые он содержит.

Ссылки