#Это краткое руководство, по основным особенностям языка Go.
##Оглавление:
- Введение
Основные преимущества:
- Простой и понятный синтаксис.
- Статическая типизация. Позволяет избежать ошибок, допущенных по невнимательности, упрощает чтение и понимание кода, делает код однозначным.
- Скорость и компиляция. Скорость у Go в десятки раз быстрее, чем у скриптовых языков, при меньшем потреблении памяти. При этом, компиляция практически мгновенна. Весь проект компилируется в один бинарный файл, без зависимостей. Как говорится, «просто добавь воды». И вам не надо заботиться о памяти, есть сборщик мусора.
- Отход от ООП. В языке нет классов, но есть структуры данных с методами. Наследование заменяется механизмом встраивания. Существуют интерфейсы, которые не нужно явно имплементировать, а лишь достаточно реализовать методы интерфейса.
- Параллелизм. Параллельные вычисления в языке делаются просто, изящно и без головной боли. Горутины (что-то типа потоков) легковесны, потребляют мало памяти.
- Богатая стандартная библиотека. В языке есть все необходимое для веб-разработки и не только. Количество сторонних библиотек постоянно растет. Кроме того, есть возможность использовать библиотеки C и C++.
- Возможность писать в функциональном стиле. В языке есть замыкания (closures) и анонимные функции. Функции являются объектами первого порядка, их можно передавать в качестве аргументов и использовать в качестве типов данных.
package main
import "fmt"
// this is a comment
func main() {
fmt.Println("Hello World")
}
Литерал | Пояснение |
---|---|
&& |
И |
` | |
! |
Не |
package main
import "fmt"
func main() {
var x string
x = "Hello World"
fmt.Println(x)
}
Если мы хотим присвоить значение переменной при её создании, то можем использовать сокращенную запись:
x := "Hello World"
Обратите внимание на то что :
стоит перед =
и на отсутствие типа. Тип в данном случае указывать необязательно, так как компилятор Go способен определить тип по литералу, которым мы инициализируем переменную. (Тут мы присваиваем строку, поэтому x
будет иметь тип string
) Компилятор может определить тип и при использовании var
:
var x = "Hello World"
И так со всеми типами:
x := 5
fmt.Println(x)
В общем, желательно всегда использовать краткий вариант написания.
Функция f
имеет доступ к переменной x
. Теперь предположим, что вместо этого мы написали:
func main() {
var x string = "Hello World"
fmt.Println(x)
}
func f() {
fmt.Println(x)
}
Если вы попробуете выполнить эту программу, то получите ошибку.
Компилятор говорит вам, что переменная x
внутри функции f
не существует. Она существует только внутри функции main
. Места, где может использоваться переменная x называется областью видимости переменной. Согласно спецификации, «В Go область видимости ограничена блоками».
Go также поддерживает константы. Константы — это переменные, чьи значения не могут быть изменены после инициализации. Они создаются таки же образом, как и переменные, только вместо var используется ключевое слово const
:
package main
import "fmt"
func main() {
const x string = "Hello World"
fmt.Println(x)
}
Константы — хороший способ использовать определенные значения в программе, без необходимости писать их каждый раз. Например: константа Pi
из пакета math.
В Go существует еще одно сокращение, на случай, если необходимо определить несколько переменных:
var (
a = 5
b = 10
c = 15
)
Используя ключевые слово var
(или const
), за которым идут круглые скобки с одной переменной в каждой строке.
Оператор for
даёт возможность повторять список инструкций (блок) определённое количество раз. Давайте перепишем предыдущую программу используя оператор for
:
package main
import "fmt"
func main() {
i := 1
for i <= 10 {
fmt.Println(i)
i = i + 1
}
}
Сначала создается переменная i
, хранящая число, которое нужно вывести на экран. Затем, с помощью ключевого слова for
, создается цикл, указывается условное выражение, которое может быть true
или false
, и, наконец, сам блок для выполнения.
В других языках программирования существуют разные виды циклов (while, do, until, foreach, …). У Go вид цикла один, но он может использоваться в разных случаях. Предыдущую программу можно так же записать следующим образом:
func main() {
for i := 1; i <= 10; i++ {
fmt.Println(i)
}
}
Теперь условное значение включает в себя так-же и две других инструкции, разделенные точкой с запятой. Сначала инициализируется переменная, затем выполняется условное выражение, и в завершение, переменная «инкрементируется». (Добавление 1 к значению переменной является настолько распространённым действием, что для этого существует специальный оператор: ++
. Аналогично, вычитание 1 может быть выполнено с помощью --
.)
func main() {
for i := 1; i <= 10; i++ {
if i % 2 == 0 {
fmt.Println(i, "even")
} else {
fmt.Println(i, "odd")
}
}
}
Давайте рассмотрим эту программу:
- Создать переменную i типа int и присвоить ей значение 1;
- i больше или равно 10? Да: перейти в блок;
- остаток от i ÷ 2 равен 0? Нет: переходим к блоку else;
- вывести i вместе с odd;
- инкрементировать i (оператор после условия);
- i больше или равно 10? Да: перейти в блок;
- остаток от i ÷ 2 равен 0? Да: переходим к блоку if;
- вывести i вместе с even;
- …
switch i {
case 0: fmt.Println("Zero")
case 1: fmt.Println("One")
case 2: fmt.Println("Two")
case 3: fmt.Println("Three")
case 4: fmt.Println("Four")
case 5: fmt.Println("Five")
default: fmt.Println("Unknown Number")
}
Массив — это нумерованная последовательность элементов одного типа, с фиксированной длинной. В Go они выглядят так:
var x [5]int
x
— это пример массива, состоящего из пяти элементов типа int. Запустим следующую программу:
package main
import "fmt"
func main() {
var x [5]int
x[4] = 100
fmt.Println(x)
}
Вы должны увидеть следующее:
[0 0 0 0 100]
Пример программы, использующей массивы:
func main() {
var x [5]float64
x[0] = 98
x[1] = 93
x[2] = 77
x[3] = 82
x[4] = 83
var total float64 = 0
for i := 0; i < 5; i++ {
total += x[i]
}
fmt.Println(total / 5)
}
Эта программа работает, но её всё еще можно улучшить. Во-первых, бросается в глаза следующее: i < 5
и total / 5
. Если мы изменим количество оценок с 5 на 6, то придется переписывать код в этих двух местах. Будет лучше использовать длину массива:
var total float64 = 0
for i := 0; i < len(x); i++ {
total += x[i]
}
fmt.Println(total / float64(len(x)))
Другая вещь, которую мы можем изменить в нашей программе это цикл:
var total float64 = 0
for i, value := range x {
total += value
}
fmt.Println(total / float64(len(x)))
В этом цикле i
представляет текущую позицию в массиве, а value
будет тем же самым что и x[i]
. Мы использовали ключевое слово range перед переменной, по которой мы хотим пройтись циклом.
Выполнение этой программы вызовет ошибку. Нельзя указывать переменные, которые не используются. Поэтому нужно написать так:
var total float64 = 0
for _, value := range x {
total += value
}
fmt.Println(total / float64(len(x)))
А еще в Go есть короткая запись для создания массивов:
x := [5]float64{ 98, 93, 77, 82, 83 }
Иногда массивы могут оказаться слишком длинными для записи в одну строку, в этом случае Go позволяет записывать их в несколько строк:
x := [5]float64{
98,
93,
77,
82,
83,
}
Обратите внимание на последнюю ,
после 83
. Она обязательна и позволяет легко удалить элемент из массива просто закомментировав строку:
x := [4]float64{
98,
93,
77,
82,
// 83,
}
Срез это часть массива. Как и массивы срезы индексируются и имеют длину. В отличии от массивов, их длину можно изменить. Вот пример среза:
var x []float64
Единственное отличие объявления среза от объявления массива — отсутствие указания длины в квадратных скобках. В нашем случае x
будет иметь длину 0.
Срез создается встроенной функцией make
:
x := make([]float64, 5)
Этот код создаст срез, который связан с массивом типа float64
и длиной 5
. Срезы всегда связаны с каким-нибудь массивом. они не могут стать больше чем массив, а вот меньше — пожалуйста. Функция make
принимает и третий параметр:
x := make([]float64, 5, 10)
10
— это длина массива, на который указывает срез.
Другой способ создать срез — использовать выражение [low : high]
:
arr := [5]float64{1,2,3,4,5}
x := arr[0:5]
Карта (также известна как ассоциативный массив или словарь) — это неупорядоченная коллекция пар вида ключ-значение. Пример:
var x map[string]int
Карта представляется в связке с ключевым словом map
, следующим за ним типом ключа в скобках и типом значения после скобок. Читается это следующим образом: «x
это карта string
-ов для int
-ов».
Подобно массивам и срезам, к элементам карт можно обратиться с помощью скобок. Запустим следующую программу:
x := make(map[string]int)
x["key"] = 10
fmt.Println(x["key"])
Это выглядит очень похоже на массив, но существует несколько различий. Во-первых длина карты (которую мы можем найти так: len(x)
) может измениться, когда мы добавим новый элемент в него. В самом начале, при создании длина 0
, после x[1] = 10
она станет равна 1
. Во-вторых, карта не являются последовательностью. В нашем примере у нас есть элемент x[1]
, в случае массива должен быть и первый элемент x[0]
, но в картах это не так.
Также мы можем удалить элементы из карты используя встроенную функцию delete
:
delete(x,1)
Давайте посмотрим на пример программы, использующей карты:
package main
import "fmt"
func main() {
elements := make(map[string]string)
elements["H"] = "Hydrogen"
elements["He"] = "Helium"
elements["Li"] = "Lithium"
elements["Be"] = "Beryllium"
elements["B"] = "Boron"
elements["C"] = "Carbon"
elements["N"] = "Nitrogen"
elements["O"] = "Oxygen"
elements["F"] = "Fluorine"
elements["Ne"] = "Neon"
fmt.Println(elements["Li"])
}
В данном примере, elements
это карта, которое представляет 10 первых химических элементов, индексируемых символами. Это очень частый способ использования карт — в качестве словаря, или таблицы. Предположим, мы пытаемся обратимся к не существующему элементу:
fmt.Println(elements["Un"])
Если вы выполните это, то ничего не увидите. Технически, карта вернет нулевое значение хранящегося типа (для строк это пустая строка). Несмотря на то, что мы можем проверить нулевое значение с помощью условия (elements["Un"] == ""
), в Go есть лучший способ сделать это:
name, ok := elements["Un"]
fmt.Println(name, ok)
Доступ к элементу карты может вернуть два значения вместо одного. Первое значение это результат запроса, второе говорит был ли запрос успешен. В Go часто встречается такой код:
if name, ok := elements["Un"]; ok {
fmt.Println(name, ok)
}
Сперва мы пробуем получить значение из карты, а затем, если это удалось, мы выполняем код внутри блока.
Объявления карт можно записывать сокращенно, также как массивы:
elements := map[string]string{
"H": "Hydrogen",
"He": "Helium",
"Li": "Lithium",
"Be": "Beryllium",
"B": "Boron",
"C": "Carbon",
"N": "Nitrogen",
"O": "Oxygen",
"F": "Fluorine",
"Ne": "Neon",
}
Карты часто используются для хранения общей информации. Давайте изменим нашу программу так, чтобы вместо имени элемента хранить какую-нибудь дополнительную информацию о нем. Например, его агрегатное состояние:
func main() {
elements := map[string]map[string]string{
"H": map[string]string{
"name":"Hydrogen",
"state":"gas",
},
"He": map[string]string{
"name":"Helium",
"state":"gas",
},
"Li": map[string]string{
"name":"Lithium",
"state":"solid",
},
"Be": map[string]string{
"name":"Beryllium",
"state":"solid",
},
"B": map[string]string{
"name":"Boron",
"state":"solid",
},
"C": map[string]string{
"name":"Carbon",
"state":"solid",
},
"N": map[string]string{
"name":"Nitrogen",
"state":"gas",
},
"O": map[string]string{
"name":"Oxygen",
"state":"gas",
},
"F": map[string]string{
"name":"Fluorine",
"state":"gas",
},
"Ne": map[string]string{
"name":"Neon",
"state":"gas",
},
}
if el, ok := elements["Li"]; ok {
fmt.Println(el["name"], el["state"])
}
}
Заметим что тип нашей карты теперь map[string]map[string]string
. Мы получили карту строк для карты строк
func average(xs []float64) float64 {
panic("Not Implemented")
}
Функция начинается с ключевого слова func
, за которым следует имя функции. Аргументы (входы) определяются как: имя тип, имя тип, …
. Наша функция имеет один параметр (список оценок), под названием xs
. За параметром следует возвращаемый тип. В совокупности, аргументы и возвращаемое значение так же известны как сигнатура функции.
Go способен возвращать несколько значений из функции:
func f() (int, int) {
return 5, 6
}
func main() {
x, y := f()
}
Для этого необходимы три вещи: указать несколько типов возвращаемых значений, разделенных ,
, изменить выражение после return так, чтобы оно содержало несколько значений, разделенных ,
и, наконец, изменить конструкцию присвоения так, чтобы она содержала несколько значений в левой стороне, перед :=
или =
.
Возврат нескольких значений часто используется для возврата ошибки вместе с результатом (x, err := f())
, или логическое значение, говорящее об успешном выполнении (x, ok := f())
.
Существует особая форма записи последнего аргумента в функции Go:
func add(args ...int) int {
total := 0
for _, v := range args {
total += v
}
return total
}
func main() {
fmt.Println(add(1,2,3))
}
### Замыкания
Возможно создавать функции замыкания внутри функций:
```go
func main() {
add := func(x, y int) int {
return x + y
}
fmt.Println(add(1,1))
}
Функция может вызывать сама себя:
func factorial(x uint) uint {
if x == 0 {
return 1
}
return x * factorial(x-1)
}
factorial
вызывает сам себя, что делает эту функцию рекурсивной. Для того, чтобы лучше понять, как работает эта функция, давайте пройдемся по factorial(2)
:
- x == 0? Нет. (x равен 2)
- Ищем факториал от x - 1. * x == 0? Нет. (x равен 1) * Ищем факториал от 0. * x == 0? Да, возвращаем 1.
- Возвращаем 1 * 1
- Возвращаем 2 * 1
Замыкание и рекурсивный вызов — сильные техники программирования, формирующие основу парадигмы, известной как функциональное программирование. Большинство людей находят функциональное программирование более сложным для понимания, чем подход на основе циклов, логических операторов, переменных и простых функций.
В Go есть специальный оператор defer
, который позволяет отложить вызов указанной функции, до тех пор, пока не завершится текущая. Рассмотрим следующий пример:
package main
import "fmt"
func first() {
fmt.Println("1st")
}
func second() {
fmt.Println("2nd")
}
func main() {
defer second()
first()
}
}
Эта программа выводит 1st
, затем 2nd
. Грубо говоря, defer
перемещает вызов second в конец функции:
func main() {
first()
second()
}
Defer
часто используется в случаях, когда нужно нужно освободить ресурсы после завершения. Например, открывая файл необходимо убедиться, что позже он должен быть закрыт. C defer
это выглядит так:
f, _ := os.Open(filename)
defer f.Close()
package main
import "fmt"
func main() {
defer func() {
str := recover()
fmt.Println(str)
}()
panic("PANIC")
}
Когда мы вызываем функцию с аргументами, аргументы копируются в функцию:
func zero(x int) {
x = 0
}
func main() {
x := 5
zero(x)
fmt.Println(x) // x всё еще равен 5
}
В этой программе функция zero
не изменяет оригинальную переменную x
из функции main
. Но что если мы хотим её изменить? Один из способов сделать это — использовать специальный тип данных — указатель:
func zero(xPtr *int) {
*xPtr = 0
}
func main() {
x := 5
zero(&x)
fmt.Println(x) // x is 0
}
Указатели указывают (прошу прощения за тавтологию) на участок в памяти, где хранится значение. Используя указатель (*int
) в функции zero
мы можем изменить значение оригинальной переменной.
В Go указатели представлены через оператор *
(звёздочка), за которым следует тип хранимого значения. В функции zero xPtr
является указателем на int
.
*
также используется для «разыменовывания» указателей. Когда мы пишем *xPtr = 0
, то читаем это так: «храним int 0
в памяти, на которую указывает xPtr
». Если вместо этого мы попробуем написать xPtr = 0
то получим ошибку компиляции, потому что xPtr
имеет тип не int
, а *int
. Соответственно, ему может быть присвоен только другой *int
.
Также существует оператор &
, который используется для получения адреса переменной. &x
вернет *int
(указатель на int
) потому что x
имеет тип int
. Теперь мы можем изменять оригинальную переменную. &x
в функции main
и xPtr
в функции zero
указывают на один и тот же участок в памяти.
Другой способ получить указатель - использовать встроенную функцию new
.
func one(xPtr *int) {
*xPtr = 1
}
func main() {
xPtr := new(int)
one(xPtr)
fmt.Println(*xPtr) // x is 1
}
Структура — это тип, содержащий именованные поля. Например, мы можем представить круг таким образом:
type Circle struct {
x float64
y float64
r float64
}
Ключевое слово type вводит новый тип. За ним следует имя нового типа (Circle) и ключевое слово struct, которое говорит, что мы определяем структуру и список полей внутри фигурных скобок. Каждое поле имеет имя и тип. Как и с функциями, мы можем объединять поля одного типа:
type Circle struct {
x, y, r float64
}
Мы можем создать экземпляр нового типа Circle
несколькими способами:
var c Circle
Подобно другим типами данных, будет создана локальная переменная типа Circle
, чьи поля по умолчанию будут равны нулю (0
для int
, 0.0
для float
, ""
для string
, nil
для указателей, …). Также, для создания экземпляра можно использовать функцию new
.
c := new (Circle)
Это выделит память для всех полей, присвоит каждому из них нулевое значение и вернет указатель (*Circle
). Часто, при создании структуры мы хотим присвоить полям структуры какие-нибудь значения. Существует два способа сделать это. Первый способ:
c := Circle{x: 0, y: 0, r: 5}
Второй способ - мы можем опустить имена полей, если мы знаем порядок в котором они определены:
c := Circle{0,0,5}
Получить доступ к полям можно с помощью оператора .
(точка):
fmt.Println(c.x, c.y, c.r)
c.x = 10
c.y = 5
Давайте изменим функцию circleArea
так, чтобы она использовала структуру Circle
:
func circleArea(c Circle) float {
return math.Pi * c.r*c.r
}
В функции main
у нас будет:
c := Circle{0,0,5}
fmt.Println(circleArea(c))
Очень важно помнить о том, что аргументы в Go всегда копируются. Если мы попытаемся изменить любое поле в функции circleArea
, оригинальная переменная не изменится. Именно поэтому мы будем писать функции так:
func circleArea(c *Circle) float64 {
return math.Pi * c.r*c.r
}
И изменим main
:
c := Circle{0,0,5}
fmt.Println(circleArea(&c))
Не смотря на то, что программа стала значительно лучше, мы все еще может значительно улучшить, используя метод - функцию особого типа:
func (c *Circle) area() float64 {
return math.Pi * c.r*c.r
}
Между ключевым словом func
и именем функции мы добавили «получателя». Получатель похож на параметр — у него есть имя и тип, но объявление функции таким способом позволяет нам вызывать функцию с помощью оператора .
:
fmt.Println(c.area())
Это проще прочесть, нам не нужно использовать оператор &
(Go автоматически предоставляет доступ к указателю на Circle
для этого метода). и поскольку эта функция может быть использована только для Circle
мы можем назвать её просто area
.
Давайте сделаем то же самое с прямоугольником:
type Rectangle struct {
x1, y1, x2, y2 float64
}
func (r *Rectangle) area() float64 {
l := distance(r.x1, r.y1, r.x1, r.y2)
w := distance(r.x1, r.y2, r.x2, r.y1)
return l * w
}
В main
будет написано:
r := Rectangle{0,0,10,10}
fmt.Println(r.area())
Обычно, поля структур представляют отношения принадлежности (включения). Например, у Circle
(круга) есть radius
(радиус). Предположим, у нас есть структура Person
(личность):
type Person struct {
Name string
}
func (p *Person) Talk() {
fmt.Println("Hi, my name is", p.Name)
И если мы хотим создать структуру Android
, то можем сделать так:
type Android struct {
Person Person
Model string
}
Это будет работать, но мы можем захотеть создать другое отношение. Сейчас у андроида «есть» личность, можем ли мы описать отношение андроид «является» личностью. Go поддерживает подобные отношения с помощью встраиваемых типов, также называемых анонимными полями. Выглядят они так:
type Android struct {
Person
Model string
}
Мы использовали тип (Person
) и не написали его имя. Объявленная таким способом структура доступна через имя типа:
a := new(Android)
a.Person.Talk()
Но мы так же можем вызвать любой метод Person
прямо из Android
:
а := new(Android)
a.Talk()
Это отношение работает достаточно интуитивно: личности могут говорить, андроид это личность, значит андроид может говорить.
Вы могли заметить, что названия методов для вычисления площади круга и прямоугольника совпадают. Это было сделано не случайно. И в реальной жизни и в программировании отношения могут быть очень похожими. В Go есть способ сделать эти случайные сходства явными с помощью типа называемого интерфейсом. Пример интерфейса для фигуры (Shape
):
type Shape interface {
area() float64
}
Как и структуры, интерфейсы создаются с помощью ключевого слова type
, за которым следует имя интерфейса и ключевое слово interface
. Однако, вместо того чтобы определять поля, мы определяем «множество методов» . Множество методов это список методов, которые будут использоваться для «реализации» интерфейса.
В нашем случае у Rectangle
и Circle
есть метод area
, которые возвращает float64
, получается они оба реализуют интерфейс Shape
. Само по себе это не очень полезно, но мы можем использовать интерфейсы как аргументы в функциях:
func totalArea(shapes ...Shape) float64 {
var area float64
for _, s := range shapes {
area += s.area()
}
return area
}
Мы будет вызывать эту функцию так:
fmt.Println(totalArea(&c, &r))
Интерфейсы также могут быть использованы в качестве полей:
type MultiShape struct {
shapes []Shape
}
Мы можем даже хранить в MultiShape
данные Shape
, определив в ней метод area
:
func (m *MultiShape) area() float64 {
var area float64
for _, s := range m.shapes {
area += s.area()
}
return area
}
Теперь MultiShape
может содержать Circle
, Rectangle
и даже другие MultiShape
.
Горутина — это функция, которая может работать параллельно с другими функциями. Для создания горутины используется ключевое слово go
, за которым следует вызов функции.
package main
import "fmt"
func f(n int) {
for i := 0; i < 10; i++ {
fmt.Println(n, ":", i)
}
}
func main() {
go f(0)
var input string
Эта программа состоит из двух горутин. Функция main
, сама по себе, является горутиной. Вторая горутина создаётся, когда мы вызываем go f(0)
. Обычно, при вызове функции, программа выполнит все конструкции внутри вызываемой функции, а только потом перейдет к, следующей после вызова, строке. С горутиной программа немедленно прейдет к следующей строке, не дожидаясь, пока вызываемая функция завершится. Вот почему здесь присутствует вызов Scanln
, без него программа завершится еще перед тем, как ей удастся вывести числа.
Горутины очень легкие, мы можем создавать их тысячами. Давайте изменим программу так, чтобы она запускала 10 горутин:
func main() {
for i := 0; i < 10; i++ {
go f(i)
}
var input string
fmt.Scanln(&input)
}
При запуске вы наверное заметили, что все горутины выполняются последовательно, а не одновременно, как вы того ожидали. Давайте добавим небольшую задержку функции с помощью функции time.Sleep
и rand.Inin
:
package main
import (
"fmt"
"time"
"math/rand"
)
func f(n int) {
for i := 0; i < 10; i++ {
fmt.Println(n, ":", i)
amt := time.Duration(rand.Intn(250))
time.Sleep(time.Millisecond * amt)
}
}
func main() {
for i := 0; i < 10; i++ {
go f(i)
}
var input string
fmt.Scanln(&input)
}
f
выводит числа от 0 до 10, ожидая от 0 до 250 мс после каждой операции вывода. Теперь горутины должны выполняться одновременно.
Каналы обеспечивают возможность общения нескольких горутин друг с другом, чтобы синхронизировать их выполнение. Вот пример программы с использованием каналов:
package main
import (
"fmt"
"time"
)
func pinger(c chan string) {
for i := 0; ; i++ {
c <- "ping"
}
}
func printer(c chan string) {
for {
msg := <- c
fmt.Println(msg)
time.Sleep(time.Second * 1)
}
}
func main() {
var c chan string = make(chan string)
go pinger(c)
go printer(c)
var input string
fmt.Scanln(&input)
}
Программа будет постоянно выводить «ping» (нажмите enter, чтобы её остановить). Тип канала представлен ключевым словом chan
, за которым следует тип, который будет передаваться по каналу (в данном случае мы передаем строки). Оператор <-
(стрелка влево) используется для отправки и получения сообщений по каналу. Конструкция c <- "ping"
означает отправку "ping"
, а msg := <- c
— его получение и сохранение в переменную msg
. Строка с fmt
может быть записана другим способом: fmt.Println(<-c)
, тогда можно было бы удалить предыдущую строку.
Данное использование каналов позволяет синхронизировать две горутины. Когда pinger
пытается послать сообщение в канал, он ожидает, пока printer
будет готов получить сообщение. Такое поведение называется блокирующим. Давайте добавим ещё одного отправителя сообщений в программу и посмотрим, что будет. Добавим эту функцию:
func ponger(c chan string) {
for i := 0; ; i++ {
c <- "pong"
}
}
и изменим функцию main
:
func main() {
var c chan string = make(chan string)
go pinger(c)
go ponger(c)
go printer(c)
var input string
fmt.Scanln(&input)
}
Теперь программа будет выводить на экран то ping, то pong по очереди.
Мы можем задать направление передачи сообщений в канале, сделав его только отправляющим или принимающим. Например, мы можем изменить функцию pinger
:
func pinger(c chan<- string)
и канал c
будет только отправлять сообщение. Попытка получить сообщение из канала c
вызовет ошибку компилирования. Также мы можем изменить функцию printer
:
func printer(c <-chan string)
Существуют и двунаправленные каналы, которые могут быть переданы в функцию, принимающую только принимающие или отправляющие каналы. Но только отправляющие или принимающие каналы не могут быть переданы в функцию, требующую двунаправленного канала!
В языке Go есть специальный оператор select
который работает как switch
, но для каналов:
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
for {
c1 <- "from 1"
time.Sleep(time.Second * 2)
}
}()
go func() {
for {
c2 <- "from 2"
time.Sleep(time.Second * 3)
}
}()
go func() {
for {
select {
case msg1 := <- c1:
fmt.Println(msg1)
case msg2 := <- c2:
fmt.Println(msg2)
}
}
}()
var input string fmt.Scanln(&input) } ``` Эта программа выводит «from 1» каждые 2 секунды и «from 2» каждые 3 секунды. Оператор `select` выбирает первый готовый канал, и получает сообщение из него, или же передает сообщение через него. Когда готовы несколько каналов, получение сообщения происходит из случайно выбранного готового канала. Если же ни один из каналов не готов, оператор блокирует ход программы до тех пор, пока какой-либо из каналов будет готов к отправке или получению. Обычно `select` используется для таймеров: ```go
select {
case msg1 := <- c1:
fmt.Println("Message 1", msg1)
case msg2 := <- c2:
fmt.Println("Message 2", msg2)
case <- time.After(time.Second):
fmt.Println("timeout")
}
time After
создаёт канал, по которому посылаем метки времени с заданным интервалом. В данном случае мы не заинтересованы в значениях временных меток, поэтому мы не сохраняем его в переменные. Также мы можем задать команды, которые выполняются по умолчанию, используя конструкцию default
:
select {
case msg1 := <- c1:
fmt.Println("Message 1", msg1)
case msg2 := <- c2:
fmt.Println("Message 2", msg2)
case <- time.After(time.Second):
fmt.Println("timeout")
default:
fmt.Println("nothing ready")
}
Выполняемые по умолчанию команды исполняются сразу же, если все каналы заняты.
При инициализации канала можно использовать второй параметр:
c := make(chan int, 1)
и мы получим буферизированный канал с ёмкостью 1. Обычно каналы работают синхронно - каждая из сторон ждёт, когда другая сможет получить или передать сообщение. Но буферизованный канал работает асинхронно — получение или отправка сообщения не заставляют стороны останавливаться. Но канал теряет пропускную способность, когда он занят, в данном случае, если мы отправим в канал 1 сообщение, то мы не сможем отправить туда ещё одно до тех пор, пока первое не будет получено.