Каналы в 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 для подсчета семафоров
Чтобы установить максимальное количество одновременных запросов, разработчики часто используют счетные семафоры для блокировки и разблокировки параллельных процессов для управления ресурсами и применения взаимных исключений. Например, разработчики могут контролировать операции чтения и записи в базе данных.
Есть два способа получить часть владения семафором канала, аналогично использованию каналов в качестве мьютексов:
- Приобретение владения посредством отправки и освобождение посредством получения
- Завладение с помощью приема и освобождение с помощью отправки
Однако при использовании семафора канала существуют определенные правила.
Во-первых, каждый канал допускает обмен данными определенного типа, который также называется типом элемента канала.
Во-вторых, чтобы канал работал правильно, кто-то должен получать то, что отправляется по каналу.
Например, мы можем объявить новый канал, используя 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()
.