Использование каналов в качестве механизма обмена данными
Канал в Go — это механизм обмена данными между Go-подпрограммами. Помните, что подход Go к параллелизму: "Не обменивайтесь памятью, а совместное использование памяти путем обмена данными". Если необходимо отправить значение из одной горутины в другую, вы используете каналы. Давайте посмотрим, как они работают и как можно начать использовать их для написания параллельных программ Go.
Синтаксис канала
Так как канал является механизмом связи, который отправляет и получает данные, одна из его характеристик — тип. Это означает, что по каналу можно отправить данные только того типа, который он поддерживает. В качестве типа данных для канала можно использовать ключевое слово chan
, однако при этом также необходимо указать тип данных, который будет проходить через канал, например тип int
.
Каждый раз при объявлении канала или в случае необходимости указать канал в качестве параметра в функции следует использовать chan <type>
, например chan int
. Чтобы создать канал, используйте встроенную make()
функцию:
ch := make(chan int)
Канал может выполнять две операции: отправлять и получать данные. Чтобы указать тип операции, которую выполняет канал, необходимо использовать оператор канала <-
. Кроме того, отправка и получение данных в канале — это блокирующие операции. И сейчас вы поймете, почему.
Если вы хотите сказать, что канал отправляет данные только, используйте <-
оператор после канала. Если канал будет получать данные, используйте <-
оператор перед каналом, как показано в следующих примерах:
ch <- x // sends (or writes ) x through channel ch
x = <-ch // x receives (or reads) data sent to the channel ch
<-ch // receives data, but the result is discarded
Еще одна операция, которую можно использовать в канале, — это его закрытие. Чтобы закрыть канал, используйте встроенную close()
функцию:
close(ch)
При закрытии канала вы говорите, что данные не будут отправляться в этом канале снова. При попытке отправить данные в закрытый канал программа будет подавать сигнал тревоги. А если вы попытаетесь получить данные из закрытого канала, вы сможете считать все отправленные данные. При каждом последующем "чтении" будет возвращаться нулевое значение.
Давайте вернемся к программе, созданной ранее, и используйте каналы для удаления функциональных возможностей сна. Сначала создадим строковый канал в функции main
, как показано ниже.
ch := make(chan string)
Далее удалим строку спящего режима time.Sleep(3 * time.Second)
.
Теперь мы можем использовать каналы для обмена данными между Go-подпрограммами. Вместо печати результата в checkAPI
функции давайте рефакторингуем наш код и отправим это сообщение по каналу. Чтобы использовать канал из этой функции, необходимо добавить его в качестве параметра. Функция checkAPI
будет выглядеть так:
func checkAPI(api string, ch chan string) {
_, err := http.Get(api)
if err != nil {
ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
return
}
ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}
Обратите внимание, что мы должны использовать fmt.Sprintf
функцию, так как мы не хотим печатать текст, просто отправлять форматированный текст по каналу. Кроме того, обратите внимание, что для отправки данных мы используем после переменной канала оператор <-
.
Теперь необходимо изменить функцию main
, чтобы отправить переменную канала и получить данные для вывода, как показано ниже.
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
fmt.Print(<-ch)
Обратите внимание, как мы используем оператор <-
, прежде чем указывать, что нам необходимо считать данные из канала.
При повторном запуске программы результат может иметь следующий вид:
ERROR: https://api.somewhereintheinternet.com/ is down!
Done! It took 0.007401217 seconds!
По крайней мере теперь он работает без вызова функции спящего режима, верно? Но он по-прежнему не выполняет нужных действий. Мы видим выходные данные только из одной Go-подпрограммы, а создали мы их пять. Давайте в следующем разделе разберемся, почему эта программа работает подобным образом.
Небуферизованные каналы
Канал, созданный с помощью функции make()
, является небуферизованным. Такое поведение предусмотрено по умолчанию. Небуферизованные каналы блокируют операцию отправки до тех пор, пока не появится объект, готовый получить данные. Как уже говорилось ранее, отправка и получение блокируют операции. Эта операция блокировки также объясняется, почему программа из предыдущего раздела остановлена, как только она получила первое сообщение.
Для начала отметим, что функция fmt.Print(<-ch)
блокирует программу, так как она считывает данные из канала и ожидает поступления каких-либо данных. Как только она их получит, выполнение продолжится со следующей строки, и программа завершает работу.
Что происходит с остальными Go-подпрограммами? Они продолжают работать, но прослушивать их некому. А поскольку работа программы уже завершилась, некоторые Go-подпрограммы не смогли отправить данные. Чтобы это проверить, добавим еще одну функцию fmt.Print(<-ch)
, как показано ниже.
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
fmt.Print(<-ch)
fmt.Print(<-ch)
При повторном запуске программы результат может иметь следующий вид:
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
Done! It took 0.263611711 seconds!
Обратите внимание, что теперь вы видите выходные данные для двух API. Если вы продолжите добавлять дополнительные строки fmt.Print(<-ch)
, то сможете считать все данные, отправленные в канал. Но что произойдет, если вы попытаетесь считать дополнительные данные, когда их уже никто не отправляет? Результат будет выглядит примерно так:
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
При повторном запуске программы результат может иметь следующий вид:
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://dev.azure.com is up and running!
Программа работает, но ее выполнение не завершается. Последняя строка вывода блокирует завершение, так как ожидается получение данных. Программу необходимо закрыть с помощью такой команды, как Ctrl+C
.
В предыдущем примере просто показано, что чтение данных и получение данных блокируют операции. Чтобы устранить эту проблему, можно изменить код на for
цикл и получить только те данные, которые вы отправляете, как в следующем примере:
for i := 0; i < len(apis); i++ {
fmt.Print(<-ch)
}
Ниже приведена окончательная версия программы на случай, если с вашей версии возникла какая-либо проблема.
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
start := time.Now()
apis := []string{
"https://management.azure.com",
"https://dev.azure.com",
"https://api.github.com",
"https://outlook.office.com/",
"https://api.somewhereintheinternet.com/",
"https://graph.microsoft.com",
}
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
for i := 0; i < len(apis); i++ {
fmt.Print(<-ch)
}
elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}
func checkAPI(api string, ch chan string) {
_, err := http.Get(api)
if err != nil {
ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
return
}
ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}
При повторном запуске программы результат может иметь следующий вид:
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
Done! It took 0.602099714 seconds!
Программа выполняет то, что она должна. Вы больше не используете функцию сна, вы используете каналы. Обратите внимание также, что теперь он занимает около 600 мс, чтобы закончить вместо почти 2 секунд, когда мы не использовали параллелизм.
Наконец, можно сказать, что небуферизованные каналы синхронизируют операции отправки и получения. Несмотря на использование параллелизма, обмен данными осуществляется синхронно.