Примитивы каналов в Go (Golang)

GolangGolangBeginner
Практиковаться сейчас

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

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

В отличие от других языков, которые достигают параллелизма через разделяемую память, Go реализует параллелизм с использованием каналов (channels) для обмена данными между различными горутинами (goroutines).

Горутины (goroutines) являются единицами параллелизма в Go и можно представить их как легковесные потоки. При переключении между ними они потребляют меньше ресурсов по сравнению с традиционными потоками.

Каналы (channels) и горутины (goroutines) вместе составляют примитивы параллелизма в Go. В этом разделе мы узнаем о каналах (channels) — этой новой структуре данных.

Точки знания:

  • Типы каналов (channels)
  • Создание каналов (channels)
  • Операции с каналами (channels)
  • Блокировка каналов (channels)
  • Однонаправленные каналы (channels)

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL go(("Golang")) -.-> go/ConcurrencyGroup(["Concurrency"]) go(("Golang")) -.-> go/BasicsGroup(["Basics"]) go(("Golang")) -.-> go/DataTypesandStructuresGroup(["Data Types and Structures"]) go/BasicsGroup -.-> go/values("Values") go/BasicsGroup -.-> go/variables("Variables") go/DataTypesandStructuresGroup -.-> go/structs("Structs") go/ConcurrencyGroup -.-> go/goroutines("Goroutines") go/ConcurrencyGroup -.-> go/channels("Channels") go/ConcurrencyGroup -.-> go/select("Select") subgraph Lab Skills go/values -.-> lab-149096{{"Примитивы каналов в Go (Golang)"}} go/variables -.-> lab-149096{{"Примитивы каналов в Go (Golang)"}} go/structs -.-> lab-149096{{"Примитивы каналов в Go (Golang)"}} go/goroutines -.-> lab-149096{{"Примитивы каналов в Go (Golang)"}} go/channels -.-> lab-149096{{"Примитивы каналов в Go (Golang)"}} go/select -.-> lab-149096{{"Примитивы каналов в Go (Golang)"}} end

Обзор каналов (Channels)

Каналы (Channels) - это специальная структура данных, в основном используемая для обмена данными между горутинами (goroutines).

В отличие от других моделей параллелизма, которые используют мьютексы (mutexes) и атомарные функции (atomic functions) для безопасного доступа к ресурсам, каналы (channels) позволяют синхронизировать выполнение путем отправки и приема данных между несколькими горутинами (goroutines).

В Go канал (channel) похож на конвейер или очередь, которая следует правилу "первым пришел - первым ушел" (First-In-First-Out, FIFO).

Типы и объявление каналов (Channels)

Канал (channel) - это ссылочный тип, и его формат объявления выглядит следующим образом:

var channelName chan elementType

Каждый канал имеет определенный тип и может передавать только данные этого же типа. Рассмотрим несколько примеров:

Создайте файл с именем channel.go в директории ~/project:

cd ~/project
touch channel.go

Например, объявим канал типа int:

var ch chan int

В приведенном выше коде объявлен канал целых чисел (int channel) с именем ch.

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

В файле channel.go напишите следующий код:

package main

import "fmt"

func main() {
    var ch1 chan int   // Declare an integer channel
    var ch2 chan bool  // Declare a boolean channel
    var ch3 chan []int // Declare a channel that transports slices of integers
    fmt.Println(ch1, ch2, ch3)
}

В приведенном выше коде просто объявлены три разных типа каналов.

Запустите программу с помощью следующей команды:

go run channel.go

Вывод программы будет следующим:

<nil> <nil> <nil>

Инициализация каналов (Channels)

Как и в случае с картами (maps), каналы (channels) должны быть инициализированы после объявления перед использованием.

Синтаксис инициализации канала выглядит следующим образом:

make(chan elementType)

В файле channel.go напишите следующий код:

package main

import "fmt"

func main() {
    // Channel that stores integer data
    ch1 := make(chan int)

    // Channel that stores boolean data
    ch2 := make(chan bool)

    // Channel that stores []int data
    ch3 := make(chan []int)

    fmt.Println(ch1, ch2, ch3)
}

Запустите программу с помощью следующей команды:

go run channel.go

Вывод программы показан ниже:

0xc0000b4000 0xc0000b4040 0xc0000b4080

В приведенном выше коде определены и инициализированы три разных типа каналов.

При рассмотрении вывода можно заметить, что, в отличие от карт (maps), при прямом выводе канала на экран не отображаются его содержимое.

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

Итак, как можно получить доступ к значениям в канале и выполнять операции с ними?

Операции с каналами (Channels)

Отправка данных

Операции с каналами (channels) имеют свои уникальные особенности. Как правило, при работе с другими типами данных мы используем оператор = и индексы для доступа и манипуляции данными.

Но для каналов (channels) мы используем оператор <- как для отправки, так и для приема данных.

В канале запись данных называется отправкой (sending), то есть передача некоторого значения в канал.

Итак, как отправить данные?

В файле channel.go напишите следующий код:

package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 10
    fmt.Println(ch)
}

Запустите программу с помощью следующей команды:

go run channel.go

Вывод программы будет следующим:

0xc0000b4000

Хотя мы поместили данные в канал, мы не можем увидеть их непосредственно из вывода значения канала.

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

Получение данных

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

В случае с каналами (channels) мы можем извлечь только самое раннее значение, которое вошло в канал.

После получения значения оно больше не будет существовать в канале.

Другими словами, получение данных из канала похоже на запрос и удаление данных в других типах данных.

В файле channel.go напишите следующий код:

package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 10
    ch <- 20
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Запустите программу с помощью следующей команды:

go run channel.go

Вывод программы будет следующим:

10
20

Мы видим, что значение 10, которое вошло в канал первым, выводится первым, и после вывода оно больше не существует в канале.

Значение 20, которое вошло позже, выводится позже, что демонстрирует принцип "первым пришел - первым ушел" (FIFO) канала.

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

После того, как канал больше не нужен, его нужно закрыть. Однако при закрытии канала необходимо обратить внимание на следующие моменты:

  • Отправка значения в закрытый канал вызовет ошибку времени выполнения.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
        ch <- 10
    
        fmt.Println(<-ch)
    
        close(ch)
        ch <- 20
    }

    Вывод программы будет следующим:

    10
    panic: send on closed channel
    
    goroutine 1 [running]:
    main.main()
        /home/labex/project/channel.go:10 +0x1a0
    exit status 2
  • Получение данных из закрытого канала будет продолжаться до тех пор, пока канал не опустеет.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
    
        ch <- 10
        ch <- 20
        ch <- 30
    
        close(ch)
    
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
    }

    Запустите программу с помощью следующей команды:

    go run channel.go

    Вывод программы будет следующим:

    10
    20
    30
  • Получение данных из закрытого пустого канала вернет значение по умолчанию соответствующего типа данных.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
    
        ch <- 10
        ch <- 20
        ch <- 30
    
        close(ch)
    
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
    }

    Запустите программу с помощью следующей команды:

    go run channel.go

    Вывод программы будет следующим:

    10
    20
    30
    0
    0
  • Закрытие уже закрытого канала вызовет ошибку.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
        ch <- 10
        close(ch)
        close(ch)
        fmt.Println(<-ch)
    }

    Вывод программы будет следующим:

    panic: close of closed channel
    
    goroutine 1 [running]:
    main.main()
        /home/labex/project/channel.go:9 +0x75
    exit status 2

Блокировка каналов (Channels)

Внимательные студенты, возможно, заметили, что при введении в объявление и инициализацию каналов (channels) мы не указывали емкость канала:

package main

import "fmt"

func main() {
    // Stores integer data in the channel
    ch1 := make(chan int) // Same below

    // Stores boolean data in the channel
    ch2 := make(chan bool)

    // Stores []int data in the channel
    ch3 := make(chan []int)

    fmt.Println(ch1, ch2, ch3)
}

Но при демонстрации операций с каналами мы указывали емкость канала:

package main

import "fmt"

func main() {
    // Specify the channel capacity as 3
    ch := make(chan int, 3)
    ch <- 10
    ch <- 20
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Для каналов, у которых емкость не указана, они называются небуферизованными каналами (unbuffered channels), у которых нет буферного пространства.

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

chan1 := make(chan int) // Unbuffered channel of type int

Для каналов, у которых указана емкость и есть буферное пространство, они называются буферизованными каналами (buffered channels).

chan2 := make(chan int, 5) // Buffered channel of type int with a capacity of 5

Они работают как очередь, следуя правилу "первым пришел - первым ушел" (First In First Out, FIFO).

Для буферизованных каналов мы можем использовать операции отправки для добавления элементов в конец очереди и операции приема для удаления элементов из начала очереди.

Что произойдет, если мы поместим в буферизованный канал больше данных, чем его емкость? Давайте проверим. Напишите следующий код в файл channel1.go:

cd ~/project
touch channel1.go
package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    ch <- 10
    ch <- 20
    fmt.Println("succeed")
}

Запустите программу с помощью следующей команды:

go run channel1.go

Вывод программы будет следующим:

fatal error: all goroutines are asleep - deadlock!

Мы видим, что программа попадает в состояние блокировки (deadlock), потому что канал уже заполнен.

Чтобы решить эту проблему, мы можем сначала извлечь данные из канала. Напишите следующий код в файл channel.go:

package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    ch <- 10
    fmt.Println("Data extracted:", <-ch)

    ch <- 20
    fmt.Println("succeed")
}

Запустите программу с помощью следующей команды:

go run channel.go

Вывод программы будет следующим:

Data extracted: 10
succeed

Мы можем постоянно использовать канал с ограниченной емкостью, следуя стратегии "взять - использовать - взять - использовать снова".

Однонаправленные каналы (Unidirectional Channels)

По умолчанию канал (channel) является двунаправленным и может использоваться для чтения и записи. Иногда нам нужно ограничить использование канала, например, мы хотим, чтобы в рамках функции канал можно было только записывать или только читать. Как это можно реализовать?

Мы можем явно объявить канал с ограниченным использованием перед его инициализацией.

Канал только для записи

Канал только для записи означает, что в рамках определенной функции в канал можно только записывать данные, но нельзя читать из него. Давайте объявим и используем канал только для записи типа int:

package main

import "fmt"

func writeOnly(ch chan<- int) {
    ch <- 10
    fmt.Println("Data written to channel")
}

func main() {
    ch := make(chan int, 1)
    writeOnly(ch)
    fmt.Println("Data received:", <-ch)
}

Запустите программу с помощью следующей команды:

go run channel.go

Вывод программы будет следующим:

Data written to channel
Data received: 10

В этом примере функция writeOnly ограничивает канал только для записи.

Канал только для чтения

Канал только для чтения означает, что в рамках определенной функции из канала можно только читать данные, но нельзя записывать в него. Давайте объявим и используем канал только для чтения типа int:

package main

import "fmt"

func readOnly(ch <-chan int) {
    fmt.Println("Data received from channel:", <-ch)
}

func main() {
    ch := make(chan int, 1)
    ch <- 20
    readOnly(ch)
}

Запустите программу с помощью следующей команды:

go run channel.go

Вывод программы будет следующим:

Data received from channel: 20

В этом примере функция readOnly ограничивает канал только для чтения.

Комбинирование каналов только для записи и только для чтения

Вы можете комбинировать каналы только для записи и только для чтения, чтобы показать, как данные передаются через ограниченные каналы:

package main

import "fmt"

func writeOnly(ch chan<- int) {
    ch <- 30
    fmt.Println("Data written to channel")
}

func readOnly(ch <-chan int) {
    fmt.Println("Data received from channel:", <-ch)
}

func main() {
    ch := make(chan int, 1)
    writeOnly(ch)
    readOnly(ch)
}

Запустите программу с помощью следующей команды:

go run channel.go

Вывод программы будет следующим:

Data written to channel
Data received from channel: 30

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

Итоги

В этом практическом занятии (lab) мы изучили основы работы с каналами (channels), в том числе:

  • Типы каналов и их объявление
  • Методы инициализации каналов
  • Операции с каналами
  • Концепцию блокировки каналов
  • Объявление однонаправленных каналов (unidirectional channels)