Go. Объясняем на примерах

Материал из MediaWiki
Перейти к навигации Перейти к поиску

Перевод на русский язык книги "Go by example" за авторством Mark McGranaghan aka mmcgrana. По окончании работ будет подготовлен PDF-файл для оффлайн-чтения. Репозиторий проекта на BitBucket: [1].

Автор перевода: sorrymak (the_electric_hand), используются наработки village_geek (Павел Павленко, [2]). Отдельная благодарность Максиму Полетаеву ([3]), Виктору Розаеву и другим авторам и переводчикам книг о языке программирования Go.

Hello World

Нашей первой практикой в Go будет helloworld — программа, печатающая “hello world” (“привет всем”) в терминале. Ниже приведён её полный исходный код: <syntaxhighlight lang="go"> package main

import "fmt"

func main() {

   fmt.Println("hello world")

} </syntaxhighlight> Чтобы выполнить эту программу, необходимо создать файл hello-world.go и выполнить команду go run:

$ go run hello-world.go
hello world

Если нам необходимо собрать программу в единый бинарный файл, то мы можем использовать команду go build. Полученный файл можно просто запустить, как любой другой исполняемый:

$ go build hello-world.go
$ ls
hello-world	hello-world.go
$ ./hello-world
hello world

Теперь, когда мы научились выполнять и собирать программы на Go, настало время приступить к изучению непосредственно самого языка.

Значения

В языке программирования Go есть значения нескольких типов: строки, целые числа, числа с плавающей точкой (вещественные), логические значения. В коде ниже можно встретить несколько примеров: <syntaxhighlight lang="go"> package main

import "fmt"

func main() { //Строки. Соединяются (конкатенируются) оператором + fmt.Println("go" + "lang")

//Целые числа и числа с плавающей точкой fmt.Println("1+1 =", 1+1) fmt.Println("7.0/3.0 =", 7.0/3.0)

//Логические значения и логические операторы fmt.Println(true && false) fmt.Println(true || false) fmt.Println(!true) } </syntaxhighlight>

$ go run values.go
golang
1+1 = 2
7.0/3.0 = 2.3333333333333335
false
true
false

Переменные

Все переменные в Go необходимо объявлять явным образом. Это используется компилятором, в частности, для проверки вызовов функций на корректность. <syntaxhighlight lang="go"> package main

import "fmt"

func main() {

var a string = "initial" fmt.Println(a)

var b, c int = 1, 2 fmt.Println(b, c)

var d = true fmt.Println(d)

var e int fmt.Println(e)

f := "short" fmt.Println(f) } </syntaxhighlight>

$ go run variables.go
initial
1 2
true
0
short

С помощью ключевого слова var можно объявить одну или сразу несколько переменных, при этом при использовании Go самостоятельно определит их тип. Переменные, которые были объявлены, но которые ещё не были инициализированы, имеют нулевое значение (zero-valued). Как видно на примере выше, переменная e имеет значение 0.

Оператор := может служить заменой ключевому слову var. Так, выражение f := "short" эквивалентно выражению var f string = "short".

Константы

В Go поддерживаются константы нескольких видов: символы, строки, логические и численные значения. <syntaxhighlight lang="go"> package main

import "fmt" import "math"

const s string = "constant"

func main() { fmt.Println(s)

const n = 500000000 const d = 3e20 / n fmt.Println(d)

fmt.Println(int64(d))

fmt.Println(math.Sin(n)) } </syntaxhighlight>

$ go run constant.go 
constant
6e+11
600000000000
-0.28470407323754404

Константы объявляются с помощью ключевого слова const, и, как видно на примере выше, выражения с этим ключевым словом могут находиться в любой части программы — так же, как и выражения с var.

Операции с константами — это так называемая арифметика произвольной точности, т.е. длина чисел ограничена только объёмом оперативной памяти.

У численной константы нет типа до тех пор, пока он не присвоен явным образом, например, так: fmt.Println(int64(d)). Также числу может быть присвоен тип в том случае, если этого требует контекст, в частности, в случае присвоения переменной или вызова функции. Так, в примере выше, math.Sin имеет тип float64.

For

for — это конструкция для построения циклов. В отличие от других языков, в Go все циклы создаются только с помощью for, конструкции while в Go нет. В примере ниже представлены все три основных типа циклов. <syntaxhighlight lang="go"> package main

import "fmt"

func main() {

//Основной тип -- for с одним условием i := 1 for i <= 3 { fmt.Println(i) i = i + 1 }

//Классический тип -- начальное значение, условие и приращение for j := 7; j <= 9; j++ { fmt.Println(j) }

//for без условий будет продолжать цикл бесконечно; выйти из него //можно с помощью ключевого слова break for { fmt.Println("loop") break } } </syntaxhighlight>

$ go run for.go
1
2
3
7
8
9
loop

Мы увидим прочие разновидности конструкции for в следующих разделах, когда будем рассматривать диапазоны (ключевое слово range), каналы и прочие структуры данных.

If/else

Ветвление в Go осуществляется с помощью операторов if и else. <syntaxhighlight lang="go"> package main

import "fmt"

func main() {

//Базовый пример if 7%2 == 0 { fmt.Println("7 is even") } else { fmt.Println("7 is odd") }

//else -- необязательная часть выражения if 8%4 == 0 { fmt.Println("8 is divisible by 4") }

//Условию может предшествовать объявление, в таком случае //эта переменная будет доступна во всех ответвлениях if num := 9; num < 0 { fmt.Println(num, "is negative") } else if num < 10 { fmt.Println(num, "has 1 digit") } else { fmt.Println(num, "has multiple digits") } } </syntaxhighlight>

$ go run if-else.go 
7 is odd
8 is divisible by 4
9 has 1 digit

Заметьте, что в Go, в отличие от других языков, условия не окружаются круглыми скобками, но при этом в конструкции всё равно необходимо использовать скобки фигурные.

В языке программирования Go нет так называемого тернарного оператора, вместо него необходимо использовать описанную в этом разделе конструкцию if/else.

Switch

Оператор switch предназначен для множественного ветвления. <syntaxhighlight lang="go"> package main

import "fmt" import "time"

func main() {

//Базовый switch i := 2 fmt.Print("write ", i, " as ") switch i { case 1: fmt.Println("one") case 2: fmt.Println("two") case 3: fmt.Println("three") }

//В одном и том же выражении case может быть одновременно //несколько условий, которые должны быть разделены запятой. //Также в этот пример мы включили необязательный раздел default, //т.е. значение по умолчанию switch time.Now().Weekday() { case time.Saturday, time.Sunday: fmt.Println("it's the weekend") default: fmt.Println("it's a weekday") }

//switch без следующего за ним выражения можно использовать как //замену операторам if/else. Из этого примера также видно, что //условия case не обязательно должны быть константами t := time.Now() switch { case t.Hour() < 12: fmt.Println("it's before noon") default: fmt.Println("it's after noon") } } </syntaxhighlight>

$ go run switch.go 
write 2 as two
it's the weekend
it's before noon

Массивы

Массивы в Go — это нумерованная последовательность элементов, для которой задана определённая длина. <syntaxhighlight lang="go"> package main

import "fmt"

func main() {

//Здесь мы создаём массив из 5 целочисленных элементов. Тип //элементов массива и их количество вместе образуют тип массива. //По умолчанию массив заполнен нулевыми значениями (zero-valued) var a [5]int fmt.Println("emp:", a)

//Мы можем присвоить определённому элементу массива значение //и в дальнейшем использовать его таким образом, как на примере ниже a[4] = 100 fmt.Println("set:", a) fmt.Println("get:", a[4])

//Функция len возвращает длину массива fmt.Println("len:", len(a))

//Таким образом можно объявить массив и присвоить ему значения //в одну строчку b := [5]int{1, 2, 3, 4, 5} fmt.Println("dcl:", b)

//Массивы могут быть многомерными var twoD [2][3]int for i := 0; i < 2; i++ { for j := 0; j < 3; j++ { twoD[i][j] = i + j } } fmt.Println("2d: ", twoD) } </syntaxhighlight>

$ go run arrays.go
emp: [0 0 0 0 0]
set: [0 0 0 0 100]
get: 100
len: 5
dcl: [1 2 3 4 5]
2d: [[0 1 2] [1 2 3]]

Стоит отметить, что при выводе массивов с помощью функции fmt.Println они появляются в форме [знач1 знач2 знач3 ...].

В следующем разделе мы рассмотрим срезы, которые как правило используются чаще, чем массивы.

Срезы

Срезы — это одна из ключевых структур данных в Go, которая даёт бо́льшую гибкость в сравнении с массивами. <syntaxhighlight lang="go"> package main

import "fmt"

func main() {

//В отличие от массивов, тип срезов определяется элементами, //которые они содержат, а не их количеством. Чтобы создать //срез с ненулевой длиной, используется встроенная функция make. //В примере ниже мы создаём срез строк (strings) длиной в 3 элемента //(которым изначально присвоены нулевые значения (zero-valued)) s := make([]string, 3) fmt.Println("emp:", s)

//Мы можем присваивать и получать значения так же как и с массивами s[0] = "a" s[1] = "b" s[2] = "c" fmt.Println("set:", s) fmt.Println("get:", s[2])

//Функция len возвращает длину среза fmt.Println("len:", len(s))

//В дополнение к этому, срезы позволяют выполнять операции, недоступные //при работе с массивами. К примеру, функция append возвращает срез с //новыми элементами. Заметьте, что необходимо сохранить значение, возвращённое //append, например, в той же переменной s = append(s, "d") s = append(s, "e", "f") fmt.Println("apd:", s)

//Срезы можно копировать. В этом примере мы создаём пустой срез той же длины, //что и s, и копируем s в c c := make([]string, len(s)) copy(c, s) fmt.Println("cpy:", c)

//Срезы поддерживают оператор с синтаксисом срез[нижн:верхн]. В примере ниже //переменной l присваивается срез с элементами s[2], s[3] и s[4] l := s[2:5] fmt.Println("sl1:", l)

//Этот срез содержит элементы, предшествующие s[5] (но не включает непосредственно s[5]) l = s[:5] fmt.Println("sl2:", l)

//Этот срез содержит элементы, следующие за s[2] (и включает в себя непосредственно s[2]) l = s[2:] fmt.Println("sl3:", l)

//Мы можем инициализировать срез и присвоить ему значения в одну строчку t := []string{"g", "h", "i"} fmt.Println("dcl:", t)

//Из срезов можно составить многомерные структуры данных. Длина срезов нижнего уровня //может варьироваться, в отличие от случаев с многомерными массивами twoD := make([][]int, 3) for i := 0; i < 3; i++ { innerLen := i + 1 twoD[i] = make([]int, innerLen) for j := 0; j < innerLen; j++ { twoD[i][j] = i + j } } fmt.Println("2d: ", twoD) } </syntaxhighlight>

$ go run slices.go
emp: [ ]
set: [a b c]
get: c
len: 3
apd: [a b c d e f]
cpy: [a b c d e f]
sl1: [c d e]
sl2: [a b c d e]
sl3: [c d e f]
dcl: [g h i]
2d: [[0] [1 2] [2 3 4]]

Несмотря на то, что срезы серьёзно отличаются от массивов, при их выводе с помощью функции fmt.Println они появляются в той же форме — [знач1 знач2 знач3 ...].

Если вы хотите узнать больше о внутреннем устройстве срезов, обратите внимание на этот пост в блоге разработчиков языка программирования Go: [4] (англ.).

В следующем разделе мы рассмотрим ещё одну структуру данных, а именно — хеши.

Хеши

Хеши — это ассоциативная структура данных ([5]), иногда называемая картами или словарями. <syntaxhighlight lang="go"> package main

import "fmt"

func main() {

//Чтобы создать пустой хеш, используется функция make. Синтаксис: //make(map[тип-ключей]тип-значений) m := make(map[string]int)

//Задавать пары ключ/значение можно с помощью выражения имя-хеша[ключ] = значение m["k1"] = 7 m["k2"] = 13

//При выводе хеша с помощью функции fmt.Println будут напечатаны все пары ключ/значение fmt.Println("map:", m)

//Получить значение, соответствующее ключу, можно с помощью выражения имя-хеша[ключ] v1 := m["k1"] fmt.Println("v1: ", v1)

//Функция len возвращает количество пар ключ/значение fmt.Println("len:", len(m))

//Функция delete удаляет пару ключ/значение из хеша delete(m, "k2") fmt.Println("map:", m)

//При получении значения, соответствующего ключу, возвращается также ещё одно значение, //которое показывает, присутствует ли этот ключ в хеше. Эта возможность может быть //использована для того, чтобы отделить отсутствующие ключи от ключей с нулевым значением. //В примере ниже нам не нужно непосредственно основное значение, поэтому мы используем пустой //идентификатор _ _, prs := m["k2"] fmt.Println("prs:", prs)

//Инициализировать хеш и присвоить ему значения можно в одну строчку n := map[string]int{"foo": 1, "bar": 2} fmt.Println("map:", n) } </syntaxhighlight>

$ go run maps.go 
map: map[k1:7 k2:13]
v1: 7
len: 2
map: map[k1:7]
prs: false
map: map[foo:1 bar:2]

При выводе хешей с помощью функции fmt.Println они появляются в форме [ключ:знач ключ:знач].

Range

С помощью ключевого слова range можно обойти (iterate) все элементы какой-либо структуры данных. На примерах ниже мы покажем, как использовать range со срезами, массивами, хешами и строками. <syntaxhighlight lang="go"> package main

import "fmt"

func main() {

//Здесь мы используем range, чтобы сложить все числа в срезе. //Работать с массивами можно аналогичным образом nums := []int{2, 3, 4} sum := 0 for _, num := range nums { sum += num } fmt.Println("sum:", sum)

//При использовании с массивами и срезами, range возваращает как //индекс, так и значение элемента. В примере выше индекс нам был //не нужен, поэтому мы использовали пустой идентификатор _, но //иногда нам необходимо использовать и индекс тоже for i, num := range nums { if num == 3 { fmt.Println("index:", i) } }

//При использовании с хешами range обходит (iterate) пары ключ/значение kvs := map[string]string{"a": "apple", "b": "banana"} for k, v := range kvs { fmt.Printf("%s -> %s\n", k, v) }

//При использовании со строками range обходит кодовые точки (code //points). Первое возвращаемое значение это стартовый байтовый //индекс "руны"(прим. пер.: "руна" в Go -- это целочисленное //значение, присвоенное кодовой точке), второе -- непосредственно //"руна" for i, c := range "go" { fmt.Println(i, c) } } </syntaxhighlight>

$ go run range.go 
sum: 9
index: 1
a -> apple
b -> banana
0 103
1 111

Функции

Функции — один из главных элементов в языке программирования Go. Как и ранее в этой книге, мы рассмотрим функции на нескольких примерах. <syntaxhighlight lang="go"> package main

import "fmt"

//Функция, которая принимает в качестве аргументов два целых числа (int) //и возвращает их сумму, которая так же является целым числом (int) func plus(a int, b int) int { //В отличие от некоторых других языков, Go не возвращает автоматически //результат последнего выражения — это приходится делать явно return a + b }

//Когда у функции есть несколько параметров одинакового типа, можно указать их тип //один раз, после последнего параметра этого типа func plusPlus(a, b, c int) int { return a + b + c }

func main() { //Функция вызывается следующим образом: имя-функции(аргументы) res := plus(1, 2) fmt.Println("1+2 =", res) res = plusPlus(1, 2, 3) fmt.Println("1+2+3 =", res) } </syntaxhighlight>

$ go run functions.go 
1+2 = 3
1+2+3 = 6

Функции в Go поддерживают ещё несколько возможностей, одна из которых — возможность возвращения нескольких значений, которую мы будем рассматривать в следующем разделе.

Возврат нескольких значений

Go поддерживает возможность возврата нескольких значений, которая часто используется, чтобы, например, возвратить из функции как непосредственно результат её работы, так и ошибку. <syntaxhighlight lang="go"> package main

import "fmt"

//(int, int) означает, что эта функция возвращает 2 целых числа func vals() (int, int) { return 3, 7 }

func main() { //В этой части программы мы используем множественное присваивание, //чтобы использовать значения из функции a, b := vals() fmt.Println(a) fmt.Println(b)

//Если нам нужны не все возвращаемые значения, то можно //использовать пустой идентификатор _ _, c := vals() fmt.Println(c) } </syntaxhighlight>

$ go run multiple-return-values.go
3
7
7

Другая полезная возможность в Go — это то, что функции могут принимать произвольное количество аргументов. Об этом мы поговорим в следующем разделе.

Вариативные функции

Вариативные функции ([6]) — это функции, которые принимают произвольное число аргументов. Примером вариативной функции является fmt.Println. <syntaxhighlight lang="go"> package main

import "fmt"

//Функция sum принимает в качестве аргументов произвольное число целых чисел func sum(nums ...int) { fmt.Print(nums, " ") total := 0 for _, num := range nums { total += num } fmt.Println(total) } func main() { //Вариативные функции вызываются так же, как и любые другие sum(1, 2) sum(1, 2, 3)

//Вариативные функции могут принимать в качестве аругмента срезы, в данном //случае -- срез с целыми числами. Делается это так: функ(срез...) nums := []int{1, 2, 3, 4} sum(nums...) } </syntaxhighlight>

$ go run variadic-functions.go 
[1 2] 3
[1 2 3] 6
[1 2 3 4] 10

Ещё одна примечательная возможность функций в Go — возможность формировать замыкания, которые будут рассмотрены в следующем разделе.

Замыкания

Язык программирования Go поддерживает анонимные функции ([7]), которые могут использоваться для формирования замыканий ([8]). Анонимные функции бывают полезны, когда нужно “завернуть” код в функцию, не присваивая ей имени. <syntaxhighlight lang="go"> package main

import "fmt"

//Функция intSeq возвращает другую функцию, которую мы анонимно объявляем //в теле функции intSeq. Возвращаемая функция замыкает переменную i, образуя //замыкание func intSeq() func() int { i := 0 return func() int { i += 1 return i } }

func main() { //Мы присваиваем значение (возвращаемую функцию) intSeq переменной nextInt. //Эта функция захватывает переменную i, которая будет обновляться каждый раз, //когда мы вызываем nextInt nextInt := intSeq()

//Увидеть эффект от замыкания можно, вызвав nextInt несколько раз fmt.Println(nextInt()) fmt.Println(nextInt()) fmt.Println(nextInt())

//Чтобы убедиться, что значение переменной i уникально для каждой функции, //создадим ещё одну newInts := intSeq() fmt.Println(newInts()) } </syntaxhighlight>

$ go run closures.go
1
2
3
1

В следующем разделе мы рассмотрим ещё одну возможность функций — рекурсию.

Рекурсия

Go поддерживает рекурсивные функции ([9]). Ниже мы приводим классический пример с факториалом. <syntaxhighlight lang="go"> package main

import "fmt"

func fact(n int) int { if n == 0 { return 1 } return n * fact(n-1) }

func main() { fmt.Println(fact(7)) } </syntaxhighlight>

$ go run recursion.go 
5040

Функция fact вызывает саму себя до тех пор, пока наконец не достигнет значения fact(0).

Указатели

Go поддерживает указатели ([10]), позволяющие передавать адреса ячеек памяти. <syntaxhighlight lang="go"> package main

import "fmt"

func zeroval(ival int) { ival = 0 }

func zeroptr(iptr *int) { *iptr = 0 }

func main() { i := 1 fmt.Println("initial:", i)

zeroval(i) fmt.Println("zeroval:", i)

zeroptr(&i) fmt.Println("zeroptr:", i)

fmt.Println("pointer:", &i) } </syntaxhighlight>

$ go run pointers.go
initial: 1
zeroval: 1
zeroptr: 0
pointer: 0x42131100

Мы объясним принципы работы указателей на примере двух функций, zeroval и zeroptr. У функции zeroval есть целочисленный (int) параметр, поэтому аргумент будет передан по значению (по умолчанию). zeroval получит копию значения, переданного через вызов функции.

У zeroptr же, в отличие от zeroval, есть параметр *int, что означает, что эта функция принимает в качестве аргумента целочисленный указатель. Далее в функции происходит так называемое разыменование указателя, после чего ему присваивается новое значение, что приводит к изменению значения в ячейке памяти.

С помощью синтаксиса &i можно получить адрес ячейки памяти i, т.е. указатель на i.

Указатели тоже можно вывести на печать, как видно на примере выше.

zeroval не меняет значение i в функции main, но zeroptr меняет, потому что обращается к значения в памяти для этой переменной.

Структуры

Структуры — это группы полей, каждое из которых имеет свой тип. Они бывают полезны, когда необходимо сгруппировать данные в удобной форме. <syntaxhighlight lang="go"> package main

import "fmt"

//Тип структуры person состоит из двух полей, name и age type person struct { name string age int }

func main() { //Так создаётся новая структура fmt.Println(person{"Bob", 20})

//При инициализации структуры можно указать названия полей fmt.Println(person{name: "Alice", age: 30})

//Пропущенные при инициализации структуры поля будут иметь //нулевое значение (zero-valued) fmt.Println(person{name: "Fred"})

//Для передачи указателя используется & fmt.Println(&person{name: "Ann", age: 40})

//Для получения доступа к полям используется следующий синтаксис s := person{name: "Sean", age: 50} fmt.Println(s.name)

//Тот же синтаксис используется и для работы с указателями в структуре sp := &s fmt.Println(sp.age)

//Структуры мутабельны sp.age = 51 fmt.Println(sp.age) } </syntaxhighlight>

$ go run structs.go
{Bob 20}
{Alice 30}
{Fred 0}
&{Ann 40}
Sean
50
51

Методы

Методы — это особые функции, связанные со структурами (и не только). <syntaxhighlight lang="go"> package main

import "fmt"

type rect struct { width, height int }

//У метода area есть так называемый "ресивер" с типом *rect func (r *rect) area() int { return r.width * r.height }

//Тип ресивера может быть либо указателем, либо простым значением. //Ниже — пример ресивера с простым значением (value receiver) func (r rect) perim() int { return 2*r.width + 2*r.height }

func main() { r := rect{width: 10, height: 5}

//Здесь мы вызываем два метода нашей структуры fmt.Println("area: ", r.area()) fmt.Println("perim:", r.perim())

//При вызове методов, в случае необходимости Go автоматически //урегулирует вопрос использования указателей или простых //значений. Использование указателей //может быть полезно, если вы хотите избежать копирования //значения при вызове метода, или для того, чтобы позволить //методам изменять значения структуры rp := &r fmt.Println("area: ", rp.area()) fmt.Println("perim:", rp.perim()) } </syntaxhighlight>

$ go run methods.go 
area: 50
perim: 30
area: 50
perim: 30

В следующем примере мы рассмотрим механизм, предназначенный для группирования связанных методов — интерфейсы.

Интерфейсы

Интерфейсы — это группы сигнатур методов, имеющие своё имя. <syntaxhighlight lang="go"> package main

import "fmt" import "math"

//Ниже представлен базовый интерфейс для геометрических фигур type geometry interface { area() float64 perim() float64 }

//В качестве примера мы создадим интерфейсы для типов rect //(rectangle, прямоугольник) и circle (круг) type rect struct { width, height float64 } type circle struct { radius float64 }

//Чтобы создать интерфейс, необходимо создать все методы этого //интерфейса. Ниже мы реализуем интерфейс geometry для типа rect func (r rect) area() float64 { return r.width * r.height } func (r rect) perim() float64 { return 2*r.width + 2*r.height }

//Теперь -- для типа circle func (c circle) area() float64 { return math.Pi * c.radius * c.radius } func (c circle) perim() float64 { return 2 * math.Pi * c.radius }

//Если переменная имеет тип "интерфейс", то мы можем вызывать методы //из указанного интерфейса. Здесь мы видим функцию measure, которая //работает как с circle, так и с rect func measure(g geometry) { fmt.Println(g) fmt.Println(g.area()) fmt.Println(g.perim()) }

func main() { r := rect{width: 3, height: 4} c := circle{radius: 5}

//У типа circle и типа rect есть общий интерфейс geometry, что //позволяет нам использовать переменные этих типов в качестве //аргументов к функции measure measure(r) measure(c) } </syntaxhighlight>

$ go run interfaces.go
{3 4}
12
14
{5}
78.53981633974483
31.41592653589793

Если вы хотите узнать больше об интерфейсах в Go, обратите внимание на эту статью: [11] (англ.).

Ошибки

Для обработки ошибок в Go принято использовать отдельное возвращаемое значение, в отличие от, к примеру, Java или Ruby, в которых принято использовать механизм исключений, или C, в котором иногда для этого используется единственное значение. Используемые в Go методы позволяют легко увидеть, какие функции возвратили ошибки, и обработать их стандартными средствами языка. <syntaxhighlight lang="go"> package main

import "errors" import "fmt"

//Ошибки возвращаются последними и имеют встроенный тип error func f1(arg int) (int, error) { if arg == 42 { //errors.New создаёт значение, указывающее на ошибку, //с определённым сообщением return -1, errors.New("can't work with 42") } //nil в качестве последнего возвращаемого сообщения указывает //на то, что ошибки не было return arg + 3, nil }

//Возможно использовать собственный тип вместо типа error, для этого //нужно реализовать метод Error() для этого типа. На примере ниже //мы создаём свой тип, для того, чтобы показать, что произошла ошибка //при работе с аргументом функции type argError struct { arg int prob string }

func (e *argError) Error() string { return fmt.Sprintf("%d - %s", e.arg, e.prob) }

func f2(arg int) (int, error) { if arg == 42 { //В этом случае мы используем &argError для того, чтобы //создать новую структуру с полями arg и prob return -1, &argError{arg, "can't work with it"}

   }
   return arg + 3, nil

func main() { //Эти два цикла тестируют приведённые выше функции. Стоит //отметить, что в этом примере используется короткая форма //оператора if for _, i := range []int{7, 42} { if r, e := f1(i); e != nil { fmt.Println("f1 failed:", e) } else { fmt.Println("f1 worked:", r) } } for _, i := range []int{7, 42} { if r, e := f2(i); e != nil { fmt.Println("f2 failed:", e) } else { fmt.Println("f2 worked:", r) } }

//Если вам нужно использовать данные из возвращаемой структуры, //то необходимо использовать так называемый "type assertion" _, e := f2(42) if ae, ok := e.(*argError); ok { fmt.Println(ae.arg) fmt.Println(ae.prob) } } </syntaxhighlight>

$ go run errors.go
f1 worked: 10
f1 failed: can't work with 42
f2 worked: 10
f2 failed: 42 - can't work with it
42
can't work with it

Чтобы узнать больше об обработке ошибок в Go, стоит обратить внимание на этот пост: [12] (англ.).

Горутины

Горутина — это легковесный поток выполнения (тред). <syntaxhighlight lang="go"> package main

import "fmt"

func f(from string) { for i := 0; i < 3; i++ { fmt.Println(from, ":", i) } }

func main() { //Здесь мы вызываем функцию f() обычным образом, она //выполняется синхронно f("direct")

//Чтобы запустить эту функцию в горутине, необходимо //использовать ключевое слово go. Эта горутина будет //выполняться параллельно с остальными go f("goroutine")

//Горутины могут работать и с анонимными функциями go func(msg string) { fmt.Println(msg) }("going")

//Два наших вызова функции выполняются асинхронно в //разных горутинах, поэтому здесь нужно использовать //fmt.Scanln, чтобы программа не завершилась досрочно //(в нашем случае программа завершится только тогда, //когда пользователь нажёмт клавишу Enter) var input string fmt.Scanln(&input) fmt.Println("done") } </syntaxhighlight>

$ go run goroutines.go
direct : 0
direct : 1
direct : 2
goroutine : 0
going
goroutine : 1
goroutine : 2
<enter>
done

Когда мы запускаем эту программу, в начале мы видим вывод первого (блокирующего) вызова, затем чередующийся вывод двух горутин. Чередование означает, что эти горутины выполняются одновременно.

В следующем разделе мы рассмотрим каналы, которые являются дополнением к горутинам.

Каналы

Каналы — это связующие звенья между одновременно выполняющимися горутинами. В каналы можно отправлять сообщения из одной горутины и получать их в другой. <syntaxhighlight lang="go"> package main

import "fmt"

func main() { //Новый канал создаётся так: make(chan тип-значения). //Тип каналов определяется типом значения, который //они передают messages := make(chan string)

//Чтобы отправить какое-либо значение в канал, //необходимо использовать оператор <-. В примере //ниже мы отправляем из новой горутины //строку "ping" в канал messages, который мы //создали выше go func() { messages <- "ping" }()

//С помощью синтаксиса <-имя-канала можно получить //значение из канала. В этом примере мы получаем //строку "ping" (которую мы отправили выше) и выводим //её на экран msg := <-messages fmt.Println(msg) } </syntaxhighlight>

$ go run channels.go 
ping

Во время выполнения программы сообщение “ping” успешно передаётся из одной горутины в другую с помощью нашего канала.

По умолчанию сообщения отправляются и принимаются только в том случае, когда горутины готовы к отправке/приёму сообщения. Благодаря этому нам не нужно было использовать fmt.Scanln как в прошлом примере.

Буферизация каналов

По умолчанию каналы не буферизованы (unbuffered), что означает, что отправить в них сообщение (имя-канала <-) можно лишь в том случае, когда принимающая горутина будет готова его принять (<-имя-канала). Буферизованные каналы же, в свою очередь, могут принимать ограниченное количество сообщений даже в том случае, когда получателя ещё нет. <syntaxhighlight lang="go"> package main

import "fmt"

func main() { //В этом примере мы создаём канал с буфером, вмещающим //до 2 значений messages := make(chan string, 2)

//Поскольку этот канал буферизован, мы можем отправить //сообщения, не принимая их одновременно с этим messages <- "buffered" messages <- "channel"

//В дальнейшем мы можем принять эти сообщения как обычно fmt.Println(<-messages) fmt.Println(<-messages) } </syntaxhighlight>

$ go run channel-buffering.go 
buffered
channel

Синхронизация каналов

Мы можем использовать каналы, чтобы синхронизировать выполнение горутин. В примере ниже мы увидим, как с помощью блокирующего приёма сообщения можно дождаться завершения горутины перед завершением программы. <syntaxhighlight lang="go"> package main

import "fmt" import "time"

//Это функция, которую мы запустим в горутине. //Канал done будет использован для того, чтобы //оповестить другую горутину о том, что работа //этой функции выполнена func worker(done chan bool) { fmt.Print("working...") time.Sleep(time.Second) fmt.Println("done")

//Отправляем сообщение о завершении работы done <- true }

func main() { //Запускаем горутину worker и передаём ей канал, //который будет использован для оповещения done := make(chan bool, 1) go worker(done)

//Здесь мы блокируем выполнение программы до тех пор, //пока мы не получим сообщение от worker <-done } </syntaxhighlight>

$ go run channel-synchronization.go 
working...done

Если мы уберём <-done из программы, то программа завершится ещё до того, как запустится worker.

Направления каналов

При использовании каналов в качестве параметров функции можно указать, будет ли канал предназначен только для отправки или только для получения значений. Это повышает типобезопасность программы. <syntaxhighlight lang="go"> package main

import "fmt"

//Функция ping принимает канал только для отправки //значений. Попытка приёма в этом канале вызовет //ошибку компиляции func ping(pings chan<- string, msg string) { pings <- msg }

//Функция pong принимает один канал для приёма (pings) //и второй для отправки (pongs) func pong(pings <-chan string, pongs chan<- string) { msg := <-pings pongs <- msg }

func main() { pings := make(chan string, 1) pongs := make(chan string, 1) ping(pings, "passed message") pong(pings, pongs) fmt.Println(<-pongs) } </syntaxhighlight>

$ go run channel-directions.go
passed message

Select

select позволяет ожидать выполнения многоканальных операций. Совместив горутины, каналы и оператор select, мы можем получить мощный и полезный инструмент. <syntaxhighlight lang="go"> package main

import "time" import "fmt"

func main() { //В нашем примере мы будем производить выбор из //двух каналов c1 := make(chan string) c2 := make(chan string)

//Каждый канал будет получать значение после //некоторого промежутка времени, например, //для имитации блокирования RPC операций в //одновременно выполняющихся горутинах go func() { time.Sleep(time.Second * 1) c1 <- "one" }() go func() { time.Sleep(time.Second * 2) c2 <- "two" }()

//Мы будем использовать select для того, чтобы //одновременно дождаться получения обоих значений //и вывести их на экран по мере получения for i := 0; i < 2; i++ { select { case msg1 := <-c1: fmt.Println("received", msg1) case msg2 := <-c2: fmt.Println("received", msg2) } } } </syntaxhighlight>

$ time go run select.go 
received one
received two
real 0m2.245s

Как и ожидалось, мы получили сообщения “one” (“один”) и “two” (“два”).

Стоит отметить, что общее время выполнения составило всего около двух секунд, так как команды Sleep выполнялись одновременно.

Таймауты

Таймауты используются в программах, которые подключаются ко внешним ресурсам, или в программах, которые должны выполняться определённое время. Благодаря каналам и оператору select, реализация таймаутов в Go проста и элегантна. <syntaxhighlight lang="go"> package main

import "time" import "fmt"

func main() { //Для нашего примера предположим, что мы выполняем внешний //вызов, который возвращает результат в канал c1 после 2 //секунд c1 := make(chan string, 1) go func() { time.Sleep(time.Second * 2) c1 <- "result 1"

   }()

//В примере ниже представлена конструкция select, реализующая //таймаут. res := <-c1 ожидает результата, а <-Time.After ожидает, //когда сообщение будет отправлено после таймаута в одун секунду. //Поскольку select будет продолжаться после первого успешного //приёма сообщения, то, если операция будет продолжаться дольше //одной секунды, будет выбран вариант с таймаутом select { case res := <-c1: fmt.Println(res) case <-time.After(time.Second * 1): fmt.Println("timeout 1")

   }

//Если мы зададим таймаут в 3 секунды, то мы успеем получить //сообщение от c2 и на печать будет выведен результат c2 := make(chan string, 1) go func() { time.Sleep(time.Second * 2) c2 <- "result 2" }() select { case res := <-c2: fmt.Println(res) case <-time.After(time.Second * 3): fmt.Println("timeout 2") } } </syntaxhighlight>

$ go run timeouts.go 
timeout 1
result 2

Выполняя эту программу, мы увидим, что время первой операции истекло (это и был таймаут), тогда как вторая успешно выполнилась.

Многие важные возможности Go (как и паттерн select-таймаут, использование которого требует обмен результатами через каналы) основываются на использовании каналов и оператора select, следующие из них — таймеры и счётчики тиков — мы рассмотрим позднее.

Неблокирующие операции с каналами

Обычно операции приёма или отправки сообщений в каналах блокируют эти каналы. Но с помощью оператора select с блоком default мы можем реализовать неблокирующую отправку, приём сообщений и даже неблокирующий многовариантный выбор (select). <syntaxhighlight lang="go"> package main

import "fmt"

func main() { messages := make(chan string) signals := make(chan bool)

//В этом примере показан неблокирующий приём. Если сообщение //в канале messages доступно, то с этим сообщение //будет исполнен первый блок кода (<-messages), тогда как //в обратном случае будет выбран блок default select { case msg := <-messages: fmt.Println("received message", msg) default: fmt.Println("no message received") }

//Неблокирующая отправка сообщений работает так же msg := "hi" select { case messages <- msg: fmt.Println("sent message", msg) default: fmt.Println("no message sent") }

//Мы можем поместить несколько блоков case перед //блоком default, чтобы реализовать многовариантный //неблокирующий select. В этом примере мы пытаемся //произвести неблокирующий приём сообщений //одновременно в messages и signals select { case msg := <-messages: fmt.Println("received message", msg) case sig := <-signals: fmt.Println("received signal", sig) default: fmt.Println("no activity") } } </syntaxhighlight>

$ go run non-blocking-channel-operations.go 
no message received
no message sent
no activity

Закрытие каналов

Закрытие канала означает, что в него больше нельзя будет отправить сообщение. Эта возможность может быть полезна в том случае, когда требуется сообщить принимающим горутинам о завершении приёма. <syntaxhighlight lang="go"> package main

import "fmt"

func main() { //В этом примере мы используем канал jobs для сообщения //о выполнении работы. Сообщение будет отправлено из //горутины main() в горутину, выполняющую работу. Когда //вся работа будет выполнена, мы закроем канал jobs jobs := make(chan int, 5) done := make(chan bool)

//Это горутина, выполняющая работу. Она получает сообщения //из jobs (j, more := <-jobs). В этот раз мы принимаем //два значения: значение more будет ложным (false), если //канал jobs будет закрыт и все задания будут //приняты. Мы используем эту возможность для того, чтобы //отправить сообщение в канал done в том случае, когда //вся работа будет выполнена go func() { for { j, more := <-jobs if more { fmt.Println("received job", j) } else { fmt.Println("received all jobs") done <- true return } } }()

//Здесь мы посылаем три задания в канал jobs, //а затем закрываем его for j := 1; j <= 3; j++ { jobs <- j fmt.Println("sent job", j) } close(jobs) fmt.Println("sent all jobs")

//Чтобы программа не завершилась досрочно, мы используем //синхронизацию, которая была описана в прошлых разделах <-done } </syntaxhighlight>

$ go run closing-channels.go 
sent job 1
received job 1
sent job 2
received job 2
sent job 3
received job 3
sent all jobs
received all jobs

Закрытие каналов мы будем использовать и в следующем разделе, посвящённом перебору значений в каналах.

Перебор значений в каналах

В предыдущих примерах мы видели, как с помощью for и range мы можем обойти (iterate) все значения в основных структурах данных, но мы так же можем использовать эти операторы для обхода значений в каналах. <syntaxhighlight lang="go"> package main

import "fmt"

func main() { //Мы обходим 2 значения в канале queue queue := make(chan string, 2) queue <- "one" queue <- "two" close(queue)

//range обходит все элементы в том порядке, в каком //они были приняты в queue. После обхода двух элементов //итерация прерывается, т.к. мы закрыли канал queue выше. //Если бы мы не сделали этого, то мы бы остановились на //третьем элементе for elem := range queue { fmt.Println(elem) } } </syntaxhighlight>

$ go run range-over-channels.go
one
two

Этот пример также показывает, что мы можем закрыть непустой канал, при этом сохранив сообщения в нём.

Таймеры

Зачастую нужно выполнить какой-то код в какое-то время в будущем, или выполнять его многократно через определённый интервал. В Go для этих целей служат таймеры (которые мы разберём в первую очередь) и тикеры (рассмотрим их в следующем разделе). <syntaxhighlight lang="go"> package main

import "time" import "fmt"

func main() { //Таймеры представляют какое-то определённое событие в //будущем. Таймеру нужно указать, сколько времени ему //нужно ждать, и он создаст канал, в который будет //отправлено сообщение по истечении срока. Таймер в //этмо примере будет ждать две секунды timer1 := time.NewTimer(time.Second * 2)

//<-timer1.C блокирует канал до тех пор, пока не будет //послано сообщение о том, что время истекло <-timer1.C fmt.Println("Timer 1 expired")

//Как показано на примере ниже, таймеры бывают полезны, //например, тем, что их можно отменить до истечения //времени (если же нужно просто выждать какое-то время, //то можно использовать time.Sleep) timer2 := time.NewTimer(time.Second) go func() { <-timer2.C fmt.Println("Timer 2 expired") }() stop2 := timer2.Stop() if stop2 { fmt.Println("Timer 2 stopped") } } </syntaxhighlight>

$ go run timers.go
Timer 1 expired
Timer 2 stopped

Время первого таймера истекло спустя 2 секунды после запуска программы, но второй был остановлен ещё до того, как успел истечь.

Тикеры

Таймеры предназначены для того, чтобы выполнить какое-то действие в будущем один раз. Тикеры же, в свою очередь, нужны, чтобы выполнять какое-то действие многократно через определённый интервал. В этом разделе мы увидим тикер, который “тикает” (по факту в нашем примере — печатает сообщение) до тех пор, пока не будет остановлен. <syntaxhighlight lang="go"> package main

import "time" import "fmt"

func main() { //Тикеры работают примерно схожим образом с таймерами. Так //же как и в случае с последними, в созданный тикером канал //отправляются сообщения. В этом примере мы используем //range, чтобы обойти все сообщения по мере их прибытия //(в нашем примере -- каждые 500 миллисекунд) ticker := time.NewTicker(time.Millisecond * 500) go func() { for t := range ticker.C { fmt.Println("Tick at", t) } }()

//Тикеры, как и таймеры, могут быть остановлены. После того //как это будет сделано, в канале тикера больше не будут //приниматься сообщения. В нашем примере мы остановим //тикер по истечении 1600 миллисекунд time.Sleep(time.Millisecond * 1600) ticker.Stop() fmt.Println("Ticker stopped") } </syntaxhighlight>

$ go run tickers.go
Tick at 2012-09-23 11:29:56.487625 -0700 PDT
Tick at 2012-09-23 11:29:56.988063 -0700 PDT
Tick at 2012-09-23 11:29:57.488076 -0700 PDT
Ticker stopped

Во время выполнения этой программы тикер должен сработать 3 раза, перед тем как мы его остановим.

Наборы обработчиков

В этом примере мы увидим, как реализовать набор обработчиков (worker pool), используя горутины и каналы. <syntaxhighlight lang="go"> package main

import "fmt" import "time"

//В функции ниже представлен наш обработчик (worker). Мы запустим //одновременно несколько параллельно работающих обработчиков, которые //будут получать задания из канала jobs и отправлять соответствующие //результаты в results. Чтобы симулировать работу, мы будем использовать //time.Sleep(time.Second) func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Println("worker", id, "processing job", j) time.Sleep(time.Second) results <- j * 2 } }

func main() { //Чтобы использовать наш набор обработчиков, необходимо отправлять //им задания и собирать результаты. Для этого мы создадим 2 канала jobs := make(chan int, 100) results := make(chan int, 100)

//Этот блок кода запускает 3 обработчика, которые изначально //блокированы, т.к. мы ещё не послали им задания for w := 1; w <= 3; w++ { go worker(w, jobs, results) }

//Здесь мы отправляем 9 заданий и затем закрываем канал, //чтобы сообщить о том, что больше заданий нет for j := 1; j <= 9; j++ { jobs <- j } close(jobs)

//И в конце концов мы собираем все результаты for a := 1; a <= 9; a++ { <-results } } </syntaxhighlight>

$ time go run worker-pools.go 
worker 1 processing job 1
worker 2 processing job 2
worker 3 processing job 3
worker 1 processing job 4
worker 2 processing job 5
worker 3 processing job 6
worker 1 processing job 7
worker 2 processing job 8
worker 3 processing job 9
real 0m3.149s

Во время выполнения программы мы видим, как 9 заданий выполняются разными обработчиками. Несмотря на то, что суммарно обработчики выполнили 9 секунд работы, на выполнение программы ушло всего около 3 секунд, т.к. 3 обработчика работали одновременно.

Ограничение скорости

Ограничение скорости — это важный механизм, который используется для контроля использования ресурсов. Go элегантно поддерживает ограничение скорости с помощью горутин, каналов и тикеров. <syntaxhighlight lang="go"> package main

import "time" import "fmt"

func main() { //Для начала мы рассмотрим базовое ограничение скорости. //Допустим, мы хотим ограничить объём обрабатываемых //входящих запросов. Мы будем работать с ними через канал //requests requests := make(chan int, 5) for i := 1; i <= 5; i++ { requests <- i } close(requests)

//Канал limiter будет принимать сообщение каждые 200 //миллисекунд. В нашем случае он будет выступать регулятором limiter := time.Tick(time.Millisecond * 200)

//Блокируя получение сообщения перед обслуживанием каждого //запроса, мы ограничиваем себя до 1 запроса каждые 200 мс for req := range requests { <-limiter fmt.Println("request", req, time.Now()) }

//Однако, мы можем хотеть, чтобы на короткие периоды времени //количество обрабатываемых запросов могло возрастать, но //чтобы при этом в целом ограничения сохранялись. Мы можем //добиться этого, буферизовав ограничивающий (limiter) канал. //В данном примере мы видим канал burstyLimiter, который //позволит нам увеличить скорость приёма для 3 сообщений burstyLimiter := make(chan time.Time, 3)

//Заполним канал, чтобы указать допустимое увеличение //скорости for i := 0; i < 3; i++ { burstyLimiter <- time.Now() }

//Каждые 200 миллисекунд мы пытается отправить новое //сообщение в burstyLimiter, вплоть до ограничения в //3 сообщения go func() { for t := range time.Tick(time.Millisecond * 200) { burstyLimiter <- t } }()

//Теперь же мы имитируем ещё 5 входящих запросов. Первые //3 из них получат преимущество благодая возможности //увеличения скорости, которую мы реализовали для //burstyLimiter burstyRequests := make(chan int, 5) for i := 1; i <= 5; i++ { burstyRequests <- i } close(burstyRequests) for req := range burstyRequests { <-burstyLimiter fmt.Println("request", req, time.Now()) } } </syntaxhighlight>

$ go run rate-limiting.go
request 1 2012-10-19 00:38:18.687438 +0000 UTC
request 2 2012-10-19 00:38:18.887471 +0000 UTC
request 3 2012-10-19 00:38:19.087238 +0000 UTC
request 4 2012-10-19 00:38:19.287338 +0000 UTC
request 5 2012-10-19 00:38:19.487331 +0000 UTC
request 1 2012-10-19 00:38:20.487578 +0000 UTC
request 2 2012-10-19 00:38:20.487645 +0000 UTC
request 3 2012-10-19 00:38:20.487676 +0000 UTC
request 4 2012-10-19 00:38:20.687483 +0000 UTC
request 5 2012-10-19 00:38:20.887542 +0000 UTC

Выполняя нашу программу, мы видим, что первая группа запросов обслуживается по одному каждые ~200 миллисекунд, как и планировалось.

Но во второй группе первые 3 запроса были обслужены немедленно, благодаря возможности временного увеличения скорости. Оставшиеся же 2 запрсоа были обработаны как обычно, с задержкой в ~200 мс.

Атомарные счётчики

В основном для управления состоянием в Go используются каналы и передача сообщений через них (пример мы видели в разделе про наборы обработчиков). Но помимо этого есть и ещё несколько механизмов. В этом разделе мы рассмотрим пакет sync/atomic, который реализует так называемые атомарные счётчики, которые доступны сразу нескольким горутинам. <syntaxhighlight lang="go"> package main

import "fmt" import "time" import "sync/atomic" import "runtime"

func main() { //Для представления счётчика мы будем использовать //беззнаковое (всегда позитивное) целое var ops uint64 = 0

//Чтобы симулировать одновременные обновления, мы //запустим 50 горутин, каждая из которых увеличивает //счётчик примерно раз в миллисекунду for i := 0; i < 50; i++ { go func() { for { //Чтобы атомарно обновлять счётчик, //используем AddUint64, в качестве //аргумента передавая адрес в памяти //для нашего счётчика ops с помощью & atomic.AddUint64(&ops, 1) //Позволяем другим горутинам продолжить //выполнение runtime.Gosched() } }() } //Подождём секунду, чтобы накопить результаты работы в ops time.Sleep(time.Second)

//Чтобы безопасно использовать счётчик, в то время, как он //обновляется горутинами, извлекаем копию текущего значения //в переменную opsFinal с помощью LoadUint64. Как и в примере //выше, мы должны передать в качестве аргумента адрес &ops, //чтобы получить значение opsFinal := atomic.LoadUint64(&ops) fmt.Println("ops:", opsFinal) } </syntaxhighlight>

$ go run atomic-counters.go
ops: 40200

Запустив эту программу, мы увидим, что было выполнено примерно 40 тысяч операций.

В следующем разделе мы рассмотрим мьютексы, ещё один инструмент для управления состоянием.

Мьютексы

В предыдущем разделе мы видели, как можно управлять простым счётчиком с помощью атомарных операций. Для более комплексного управления состоянием же мы можем использовать мьютексы ([13]), которые позволяют обеспечить безопасный доступ к данным более чем одной горутинам. <syntaxhighlight lang="go"> package main

import (

   "fmt"
   "math/rand"
   "runtime"
   "sync"
   "sync/atomic"
   "time"

)

func main() { //В нашем примере переменная, отвечающая за состояние (state), //будет хешом var state = make(map[int]int)

//mutex будет синхронизировать доступ к state var mutex = &sync.Mutex{}

//Чтобы сравнить мьютексы с другими инструментами управления //состоянием, которые мы рассмотрим позже, ops будет считать //количество операций над состоянием var ops int64 = 0

//Запускаем 100 горутин, которые будут выполнять чтение //состояния снова и снова for r := 0; r < 100; r++ { go func() { total := 0 for { //Каждый раз при чтении сначала мы выбираем ключ, //блокируем (Lock()) мьютекс, чтобы убедиться, //что только эта горутина имеет доступ //к состоянию, читаем значение с выбранным //ключом, разблокируем (Unlock()) //мьютекс и увеличиваем счётчик ops key := rand.Intn(5) mutex.Lock() total += state[key] mutex.Unlock() atomic.AddInt64(&ops, 1)

//Для того, чтобы убедиться в том, что горутина //не остановит планировщик, явно выполняем //runtime.Gosched() после каждой операции. //runtime.Gosched() выполняется автоматически //после операций с каналами или блокирующих //вызовов (таких, как time.Sleep), но в данном //случае нам необходимо сделать это вручную runtime.Gosched()

           }
       }()

}

//Мы также запустим 10 горутин, которые будут симулировать //запись, используя тот же подход, который мы применили //для симуляции чтения for w := 0; w < 10; w++ { go func() { for { key := rand.Intn(5) val := rand.Intn(100) mutex.Lock() state[key] = val mutex.Unlock() atomic.AddInt64(&ops, 1) runtime.Gosched() } }() }

//Позволим 10 горутинам работать над state и mutex в //течение 1 секунды time.Sleep(time.Second)

//Окончательное число операций opsFinal := atomic.LoadInt64(&ops) fmt.Println("ops:", opsFinal)

//Блокируем (фиксируем) окончательное состояние //state и выводим его на экран mutex.Lock() fmt.Println("state:", state) mutex.Unlock() } </syntaxhighlight>

$ go run mutexes.go
ops: 3598302
state: map[1:38 4:98 2:23 3:85 0:44]

Выполняя программу, видим, что мы выполнили примерно 3,5 миллиона операций над state, синхронизировав доступ к нему с помощью мьютексов.

В следующем разделе мы выполним эту же задачу с помощью одних только горутин и каналов.

Горутины, сохраняющие состояние

В предыдущем разделе мы, для того, чтобы синхронизировать доступ к состоянию между горутинами, использовали блокирование с помощью мьютексов. Но это можно сделать и другими путями, например с помощью встроенных в Go возможностей по синхронизации горутин и каналов. Этот подход, основанный на использовании каналов, сочетается с распространёнными в Go идеями, в частности, идее о том, что разделять память следует, используя обмен данными, и о том, что каждой единицей данных должна обладать только 1 горутина. <syntaxhighlight lang="go"> package main

import ( "fmt" "math/rand" "sync/atomic" "time" )

//В нашем примере состояние будет принадлежать одной горутине. //Это даёт нам гарантию того, что данные не будут испорчены при //совместном доступе. Чтобы записать или считать состояние, //остальные горутины будут посылать этой горутине сообщения и //получать соответствующие ответы. Структуры readOp и writeOp //инкапсулируют эти запросы и то, как горутина будет отвечать //на них type readOp struct { key int resp chan int } type writeOp struct { key int val int resp chan bool }

func main() { //Как и в предыдущих примерах, мы будем считать, сколько //операций мы выполнили var ops int64 = 0

//Каналы reads и writes будут использоваться другими //горутинами для того, чтобы запросить чтение или запись reads := make(chan *readOp) writes := make(chan *writeOp)

//Это горутина, владеющая состоянием. Состояние в данном //случае -- это хеш, как и в прошлом примере. Но в этот раз //хеш доступен только этой горутине. Эта горутина постоянно //выполняет select над каналами reads и writes, отвечая на //запросы по мере их прибытия. Запрос выполняется так: //сначала выполняется запрошенная операция (чтение или запись), //затем отправляется сообщение в канал resp, сообщающее о том, //что операция прошла успешно (в случае с запросом на чтение //отправляется также запрошенное значение) go func() { var state = make(map[int]int) for { select { case read := <-reads: read.resp <- state[read.key] case write := <-writes: state[write.key] = write.val write.resp <- true } } }()

//Здесь мы запускаем 100 горутин, которые запрашивают чтение у //горутины, владеющей состоянием, с помощью канала reads. Во //время каждого запроса на чтение конструируется структура readOp, //которая отправляется в канал reads. Затем принимается результат //с помощью канала resp for r := 0; r < 100; r++ { go func() { for { read := &readOp{ key: rand.Intn(5), resp: make(chan int)} reads <- read <-read.resp atomic.AddInt64(&ops, 1) } }() }

//Тем же подходом мы запускаем 10 запросов на запись for w := 0; w < 10; w++ { go func() { for { write := &writeOp{ key: rand.Intn(5), val: rand.Intn(100), resp: make(chan bool)} writes <- write <-write.resp atomic.AddInt64(&ops, 1) } }() }

//Позволим горутинам поработать секунду time.Sleep(time.Second)

//Фиксируем и выводим на экран счётчик ops opsFinal := atomic.LoadInt64(&ops) fmt.Println("ops:", opsFinal) } </syntaxhighlight>

$ go run stateful-goroutines.go
ops: 807434

Выполнив нашу программу, мы можем увидеть, что основанное на горутинах управление состоянием позволило добиться выполнения около 800 тысяч операций в секунду.

В этом конкретном случае подход, основанный на горутинах, был чуть более сложным, чем использование мьютексов. Но в некоторых случаях он всё-таки может быть полезным, например, тогда, когда используются ещё и другие каналы, или тогда, когда организация такого большого количества мьютексов может ненароком привести к ошибке. Использовать тот или иной подход следует в зависимости от того, каккой из них будет более естественным в том или ином случае, главное — иметь возможность разобраться в конечном результате.

Сортировка

В пакете sort представлены методы для сортировки типов данных, как встроенных, так и определённых пользователем. В этом примере мы рассмотрим сортировку данных встроенных типов. <syntaxhighlight lang="go"> package main

import "fmt" import "sort"

func main() { //Методы сортировки зависят от того, данные какого типа //требуется сортировать. Ниже приведён пример для строк. //Стоит отметить, что в результате сортировки модифицируется //уже имеющийся срез, а не возвращается новый strs := []string{"c", "a", "b"} sort.Strings(strs) fmt.Println("Strings:", strs)

//Сортировка среза из целых чисел ints := []int{7, 2, 4} sort.Ints(ints) fmt.Println("Ints: ", ints)

//sort также можно использовать для того, чтобы узнать, //отсортирован ли уже срез s := sort.IntsAreSorted(ints) fmt.Println("Sorted: ", s) } </syntaxhighlight>

$ go run sorting.go
Strings: [a b c]
Ints: [2 4 7]
Sorted: true

Наша программа выводит на экран отсортированные срезы строк и целых чисел, а также положительный результат (true) для нашего теста на то, был ли отсортирован срез.

Произвольная сортировка

Иногда нам необходимо отсортировать срез не в обыкновенном порядке, а определённым специфическим образом. Например, отсортировать строки по их длине, а не по алфавитному порядку. Произвольная сортировка — тема этого раздела. <syntaxhighlight lang="go"> package main

import "sort" import "fmt"

//В Go, для того, чтобы выполнить произвольную сортировку, //необходимо создать соответствующий тип. В строке ниже мы //создаём тип ByLength ("по длине"), который является алиасом //для встроенного типа []string type ByLength []string

//Создаём sort.Interface (интерфейс для сортировки) с методами //Len (сокращённо от "длина"), Less ("меньше") и Swap ("перемена //мест") для нашего типа, благодаря чему мы получаем возможность //использовать оригинальную функцию Sort из одноимённого пакета. //Методы Len и Swap обычно похожи вне зависимости от типа, тогда //как непосредственно сам принцип сортировки будет заложен в Less. //В нашем случае мы реализуем сортировку по длине (от меньшей //к большей), поэтому используем len(s[i]) и len(s[j]) func (s ByLength) Len() int { return len(s) } func (s ByLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s ByLength) Less(i, j int) bool { return len(s[i]) < len(s[j]) }

//Реализовав все необходимые методы, мы можем наконец использовать //нашу сортировку. Для этого мы приводим срез fruits к типу ByLength, //и затем вызываем sort.Sort func main() { fruits := []string{"peach", "banana", "kiwi"} sort.Sort(ByLength(fruits)) fmt.Println(fruits) } </syntaxhighlight>

$ go run sorting-by-functions.go 
[kiwi peach banana]

Выполнив нашу программу, мы видим, что была выполнена сортировка по длине, как мы и планировали.

Используя этот подход — создать произвольный тип, реализовать три метода интерфейса для него, и затем выполнить sort.Sort с набором данных этого типа — мы можем выполнять любую другую произвольную сортировку в Go.

Паника

Паника означает, что какая-либо операция была проведена неправильно. Паника обычно используется в тех случаях, когда необходимо завершить работу при ошибках, которые обычно не должны возникать, или тогда, когда мы не можем правильно обработать ошибку. <syntaxhighlight lang="go"> package main

import "os"

func main() { //В этом учебнике мы используем панику в случае непредвиденных //ошибок. В этом примере же мы намерено используем панику, для //того, чтобы показать, как с ней работать panic("a problem")

//Часто паника используется для того, чтобы прервать выполнение //программы тогда, когда функция возвращает неизвестный код //ошибки, который мы не можем (или не желаем) обработать. В этом //примере мы вызываем панику, когда получаем непредвиденную ошибку //при создании файла _, err := os.Create("/tmp/file") if err != nil { panic(err) } } </syntaxhighlight>

$ go run panic.go
panic: a problem

goroutine 1 [running]:
main.main()
/.../panic.go:12 +0x47

...
exit status 2

Выполнение этой программы приведёт её к панике, после чего будет выведено сообщение об ошибке и трейсы горутины, и в результате программа завершится с ненулевым статусом.

Следует отметить, что, в отличие от многих других языков, в которых используются исключения для обработки ошибок, в Go для этого предлагается возвращать значения, сообщающие об ошибке.

Defer

defer используется для того, чтобы отложить вызов указанной функции до тех пор, пока не завершится текущая функция — он находит применение, например, в случае необходимости высвободить ресурсы. Аналогами defer в других языках являются ensure и finally. <syntaxhighlight lang="go"> package main

import "fmt" import "os"

//Допустим, требуется создать какой-либо файл, записать в него //информацию, и закрыть по завершении работы. Ниже показано, как //сделать это с использованием defer func main() { //После создания файла с помощью createFile, откладываем //вызов функции closeFile ("закрыть файл") с помощью defer. //Эта функция выполнится в конце программы, после выполнения //функции writeFile ("записать в файл") f := createFile("/tmp/defer.txt") defer closeFile(f) writeFile(f) }

func createFile(p string) *os.File { fmt.Println("creating") f, err := os.Create(p) if err != nil { panic(err) } return f }

func writeFile(f *os.File) { fmt.Println("writing") fmt.Fprintln(f, "data") }

func closeFile(f *os.File) { fmt.Println("closing") f.Close() } </syntaxhighlight>

$ go run defer.go
creating
writing
closing

Выполнив эту программу, мы увидим, что закрытие файла действительно было выполнено уже после того, как была произведена запись.

Функции для работы с коллекциями

Зачастую нам необходимо выполнять какие-либо действия над целыми группами данных — например, нам может понадобиться выбрать из группы все элементы, соответствующие какому-либо условию, или перераспределить элементы из одной группы в другую.

В некоторых языках для этого принято использовать технологии обобщённого программирования ([14]), так называемые дженерики. Но Go их не поддерживает, вместо них Go-программисты, когда возникает такая потребность, предпочитают использовать функции для работы с коллекциями.

Ниже приведены несколько примеров таких функций для срезов из строк. Вы можете использовать эти примеры для создания собственных. Стоит отметить, что в некоторых случаях легче просто напрямую встроить код, чем вызывать для этого вспомогательную функцию. <syntaxhighlight lang="go"> package main

import "strings" import "fmt"

//Возвращает индекс первого совпадения строки t в срезе; //в случае, если строка в срезе не обнаружена, //возвращается -1 func Index(vs []string, t string) int { for i, v := range vs { if v == t { return i } } return -1 }

//Возвращает true в том случае, если строка t присутствует //в срезе func Include(vs []string, t string) bool { return Index(vs, t) >= 0 }

//Возвращает true, если какая-либо строка в срезе //соответствует условию f func Any(vs []string, f func(string) bool) bool { for _, v := range vs { if f(v) { return true } } return false }

//Возвращает true, если все строки в срезе соответствуют //условию f func All(vs []string, f func(string) bool) bool { for _, v := range vs { if !f(v) { return false } } return true }

//Возвращает новый срез, в котором будут содержаться все //строки, соответствующие условию f func Filter(vs []string, f func(string) bool) []string { vsf := make([]string, 0) for _, v := range vs { if f(v) { vsf = append(vsf, v) } } return vsf }

//Возвращает новый срез, в котором будут содержаться строки //из оригинального среза, обработанные функцией f func Map(vs []string, f func(string) string) []string { vsm := make([]string, len(vs)) for i, v := range vs { vsm[i] = f(v) } return vsm }

func main() { //Ниже мы пробуем наши функции для работы с коллекциями var strs = []string{"peach", "apple", "pear", "plum"}

fmt.Println(Index(strs, "pear"))

fmt.Println(Include(strs, "grape"))

fmt.Println(Any(strs, func(v string) bool { return strings.HasPrefix(v, "p") }))

fmt.Println(All(strs, func(v string) bool { return strings.HasPrefix(v, "p") }))

fmt.Println(Filter(strs, func(v string) bool { return strings.Contains(v, "e") }))

//В примерах выше применялись анонимные функции, //но мы можем использовать и именованные функции //корректного типа fmt.Println(Map(strs, strings.ToUpper)) } </syntaxhighlight>

$ go run collection-functions.go 
2
false
true
false
[peach apple pear]
[PEACH APPLE PEAR PLUM]

Функции для работы со строками

Пакет strings из стандартной библиотеки содержит множество функций, отвечающих за обработку строк. Ниже приведены примеры использования некоторых из них. <syntaxhighlight lang="go"> package main

import s "strings" import "fmt"

//Мы создали короткий алиас для fmt.Println, который //и будем использовать дальше var p = fmt.Println

//Ниже вы можете увидеть примеры функций из пакета //strings. Следует отметить, что все они именно функции, //а не методы для объекта строки. Это значит, что мы //должны передать функции в качестве первого аргумента //строку func main() { //Содержит ли строка подстроку p("Contains: ", s.Contains("test", "es")) //Сколько раз встречается подстрока p("Count: ", s.Count("test", "t")) //Начинается ли строка с подстроки p("HasPrefix: ", s.HasPrefix("test", "te")) //Заканчивается ли строка подстрокой p("HasSuffix: ", s.HasSuffix("test", "st")) //Индекс подстроки в строке p("Index: ", s.Index("test", "e")) //Объединение среза в строку p("Join: ", s.Join([]string{"a", "b"}, "-")) //Повторение строки p("Repeat: ", s.Repeat("a", 5)) //Замена частей строки p("Replace: ", s.Replace("foo", "o", "0", -1)) p("Replace: ", s.Replace("foo", "o", "0", 1)) //Разбивка строки на срез p("Split: ", s.Split("a-b-c-d-e", "-")) //Изменение регистра p("ToLower: ", s.ToLower("TEST")) p("ToUpper: ", s.ToUpper("test")) p()

//Эти функции не являются частью пакета strings, //но о них стоит упомянуть. Узнаём длину строки и //получаем символ по его индексу p("Len: ", len("hello")) p("Char:", "hello"[1]) } </syntaxhighlight>

$ go run string-functions.go
Contains: true
Count: 2
HasPrefix: true
HasSuffix: true
Index: 1
Join: a-b
Repeat: aaaaa
Replace: f00
Replace: f0o
Split: [a b c d e]
toLower: test
ToUpper: TEST

Len: 5
Char: 101

Больше информации о функциях для работы со строками можно найти в документации к пакету strings: [15] (англ.).

Форматирование строк

В Go содержится большое количество возможностей для форматирования строк в традиционном стиле printf. Ниже приведены несколько примеров решения подобных задач. <syntaxhighlight lang="go"> package main

import "fmt" import "os"

type point struct { x, y int }

func main() { //В Go есть специальные "глаголы", которые специфицируют //форматирование основных значений данных. Пример ниже //выводит на печать образец структуры point p := point{1, 2} fmt.Printf("%v\n", p)

//Используя %+v, можно вывести на печать также имена //полей структуры fmt.Printf("%+v\n", p)

//%#v же позволит напечатать синтаксическое //представление этого значения, т.е. создавший //его кусок кода fmt.Printf("%#v\n", p)

//Чтобы вывести тип данного значения, используйте //%T fmt.Printf("%T\n", p)

//С форматированием логических значений тоже нет //ничего сложного fmt.Printf("%t\n", true)

//Существует множество способов форматировать целые //числа. Чтобы вывести представление числа в десятичной //системе счисления, используйте %d fmt.Printf("%d\n", 123)

//Представление в двоичной системе счисления fmt.Printf("%b\n", 14)

//Вывод символа Unicode, соответствующего данному числу fmt.Printf("%c\n", 33)

//Шестнадцатеричная система счисления fmt.Printf("%x\n", 456)

//Существует множество способов и для форматирования //чисел с плавающей точкой. %f используется для //десятичной формы fmt.Printf("%f\n", 78.9)

//%e и %E с небольшими различиями форматируют числа //в экспоненциальной записи fmt.Printf("%e\n", 123400000.0) fmt.Printf("%E\n", 123400000.0)

//%s предназначается для того, чтобы "просто вывести" //строку fmt.Printf("%s\n", "\"string\"")

//Строка с двойными кавычками fmt.Printf("%q\n", "\"string\"")

//Как и в случае с целыми числами, %x используется для //вывода строки в шестнадцатеричной форме, по два символа //на каждый байт fmt.Printf("%x\n", "hex this")

//Вывод указателя fmt.Printf("%p\n", &p)

//При форматировании чисел нам зачастую требуется //контролировать ширину и точность конечного //результата. Указать ширину вывода целого числа //можно, вставив значение ширины после %. По //умолчанию будет использоваться выравнивание по //правой стороне. Пустоты будут заполнены пробелами fmt.Printf("|%6d|%6d|\n", 12, 345)

//Таким же образом можно указать ширину вывода чисел //с плавающей точкой. Точность вывода можно указать в форме //"ширина.количество-знаков-после-запятой" fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)

//Для выравнивания по левой стороне используется - fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)

//Ширину строк также можно контролировать (это может быть //полезно в том случае, когда вы хотите вывести на печать //таблицу). Ниже представлен пример с выравниванием по //правой стороне fmt.Printf("|%6s|%6s|\n", "foo", "b")

//Как и в случае с числами, - используется для выравнивания //по левой стороне fmt.Printf("|%-6s|%-6s|\n", "foo", "b")

//Printf применяется для вывода в os.Stdout. Sprintf же //форматирует и возвращает строку, не печатая её нигде s := fmt.Sprintf("a %s", "string") fmt.Println(s)

//Чтобы форматировать и вывести на печать в другие потоки //(io.Writers), используйте Fprintf fmt.Fprintf(os.Stderr, "an %s\n", "error") } </syntaxhighlight>

$ go run string-formatting.go
{1 2}
{x:1 y:2}
main.point{x:1, y:2}
main.point
true
123
1110
!
1c8
78.900000
1.234000e+08
1.234000E+08
"string"
"\"string\""
6865782074686973
0x42135100
| 12| 345|
| 1.20| 3.45|
|1.20 |3.45 |
| foo| b|
|foo |b |
a string
an error