Каналы в Go

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

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

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

Создание структуры канала Go

Для начала давайте создадим канал в Go с помощью make функции:

// for example if channel created using following : 
ch := make(chan string)

// this is the basic structure of channels 
type hchan struct {
  qcount uint   // total data in the queue
  dataqsiz uint  // size of the circular queue 
  buf  unsafe.Pointer // pointer to an array of dataqsiz elements
  elementSize uint16 
  closed uint32 
  sendx  uint // send index 
  recvx  uint // receive index 
  recvq waitq // list of receive queue 
  sendq  waitq // list of send queue 
  lock mutex // lock protects all fields in hchan, as well as several
}

Использование каналов Go

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

Использование каналов Go в качестве futures и promises

Разработчики часто используют futures и promises в Go для запросов и ответов. Например, если мы хотим реализовать шаблон async/await, мы должны добавить следующее:

package main 

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

func longTimedOperation() <-chan int32 {
  ch := make(chan int32)
  func run(){
    defer close(ch)
    time.Sleep(time.Second * 5)
    ch <- rand.Int31n(300)
  }
  go run()
  return ch
}

func main(){
  ch := longTimedOperation()
  fmt.Println(ch)
}

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

Использование каналов Go для уведомлений

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

Например, реализация индивидуального уведомления с каналом получает значение уведомления:

package main 

import (
  "fmt"
  "time"
) 
type T = struct{}

func main() {
  completed := make(chan T)
  go func() {
    fmt.Println("ping")
    time.Sleep(time.Second * 5) // heavy process simulation
    <- completed // receive a value from completed channel
  }

  completed <- struct{}{} // blocked waiting for a notification 
  fmt.Println("pong")
}

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

Каналы также могут планировать уведомления:

package main

import (
  "fmt"
  "time"
) 

func scheduledNotification(t time.Duration) <- chan struct{} {
  ch := make(chan struct{}, 1) 
  go func() {
    time.Sleep(t)
    ch <- struct{}{}
  }()
  return ch
}

func main() {
    fmt.Println("send first")
    <- scheduledNotification(time.Second)
    fmt.Println("secondly send")
    <- scheduledNotification(time.Second)
    fmt.Println("lastly send")
}

Использование каналов Go для подсчета семафоров

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

Есть два способа получить часть владения семафором канала, аналогично использованию каналов в качестве мьютексов:

  1. Приобретение владения посредством отправки и освобождение посредством получения
  2. Завладение с помощью приема и освобождение с помощью отправки

Однако при использовании семафора канала существуют определенные правила.

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

Во-вторых, чтобы канал работал правильно, кто-то должен получать то, что отправляется по каналу.

Например, мы можем объявить новый канал, используя chan ключевое слово, и закрыть канал, используя close() функцию. Итак, если мы заблокируем код, используя < - синтаксис канала для чтения из канала, после завершения мы сможем закрыть его.

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

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

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

Запись на канал Go

Код в этом подразделе учит нас, как писать в канал в Go. Записать значение x в канал c так же просто, как написать c <- x.

Стрелка показывает направление значения; у нас не будет проблем с этим оператором, если оба xи cимеют один и тот же тип.

В следующем коде chan ключевое слово объявляет, что cпараметр функции является каналом и за ним должен следовать тип канала, то есть int. Затем c <- x оператор позволяет нам записать значение x в канал c, а close() функция закрывает канал:

package main
import (
  "fmt"
  "time"
)

func writeToChannel(c chan int, x int) {
   fmt.Println(x)
   c <- x
   close(c)
   fmt.Println(x)
}

func main() {
    c := make(chan int)
    go writeToChannel(c, 10)
    time.Sleep(1 * time.Second)
}

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

$ go run writeCh.go 
10

Странно то, что writeToChannel() функция печатает заданное значение только один раз, что происходит, когда второй fmt.Println(x) оператор никогда не выполняется.

Причина этого довольно проста: c <- x оператор блокирует выполнение остальной части функции writeToChannel(), поскольку никто не читает то, что было записано в c канал.

Поэтому, когда time.Sleep(1 * time.Second) оператор завершается, программа завершается, не дожидаясь writeToChannel().