Skip to content

artfaal/Golang-intro

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 

Repository files navigation

Golang-intro

#Это краткое руководство, по основным особенностям языка Go.

##Оглавление:

  • Введение

Введение

Основные преимущества:

  • Простой и понятный синтаксис.
  • Статическая типизация. Позволяет избежать ошибок, допущенных по невнимательности, упрощает чтение и понимание кода, делает код однозначным.
  • Скорость и компиляция. Скорость у Go в десятки раз быстрее, чем у скриптовых языков, при меньшем потреблении памяти. При этом, компиляция практически мгновенна. Весь проект компилируется в один бинарный файл, без зависимостей. Как говорится, «просто добавь воды». И вам не надо заботиться о памяти, есть сборщик мусора.
  • Отход от ООП. В языке нет классов, но есть структуры данных с методами. Наследование заменяется механизмом встраивания. Существуют интерфейсы, которые не нужно явно имплементировать, а лишь достаточно реализовать методы интерфейса.
  • Параллелизм. Параллельные вычисления в языке делаются просто, изящно и без головной боли. Горутины (что-то типа потоков) легковесны, потребляют мало памяти.
  • Богатая стандартная библиотека. В языке есть все необходимое для веб-разработки и не только. Количество сторонних библиотек постоянно растет. Кроме того, есть возможность использовать библиотеки C и C++.
  • Возможность писать в функциональном стиле. В языке есть замыкания (closures) и анонимные функции. Функции являются объектами первого порядка, их можно передавать в качестве аргументов и использовать в качестве типов данных.

Hello World

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

If

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

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]

Карта (Map)

Карта (также известна как ассоциативный массив или словарь) — это неупорядоченная коллекция пар вида ключ-значение. Пример:

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

Другой способ получить указатель - использовать встроенную функцию new.

func one(xPtr *int) {
    *xPtr = 1
}
func main() {
    xPtr := new(int)
    one(xPtr)
    fmt.Println(*xPtr) // x is 1
}

Структуры (struct)

Структура — это тип, содержащий именованные поля. Например, мы можем представить круг таким образом:

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)

Существуют и двунаправленные каналы, которые могут быть переданы в функцию, принимающую только принимающие или отправляющие каналы. Но только отправляющие или принимающие каналы не могут быть переданы в функцию, требующую двунаправленного канала!

Операторы Select

В языке 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 сообщение, то мы не сможем отправить туда ещё одно до тех пор, пока первое не будет получено.

About

Description of GoLang

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published