Каналы (channels) представляют инструменты коммуникации между горутинами. Для определения канала применяется ключевое слово chan:
chan тип_элемента
После слова chan
указывается тип данных, которые будут передаться с помощью канала. Например:
var intCh chan int
Здесь переменная intCh представляет канал, который передает данные типа int.
Для передачи данных в канал или, наоборот, из канала применяется операция <- (направленная влево стрелка). Например, передача данных в канал:
intCh <- 5
В данном случае в канал посылается число 5. Получение данных из канала в переменную:
val := <- intCh
Если ранее в канал было отправлено число 5, то при выполнении операции <- intCh
мы можем получить это число в переменную val.
Стоит учитывать, что мы можем отправить в канал и получить из канала данные только того типа, который представляет канал. Так, в примере с каналом intCh это данные типа int.
Как правило, отправителем данных является одна горутина, а получателем - другая горутина.
При простом определении переменной канала она имеет значение nil, то есть по сути канал неинициализирован. Для инициализации применяется функция make(). В зависимости от определения емкости канала он может быть буферизированным или небуферизированным.
Для создания небуферизированного канала вызывается функция make()
без указания емкости канала:
var intCh chan int = make(chan int) // канал для данных типа int strCh := make(chan string) // канал для данных типа string
Если канал пустой, то горутина-получатель блокируется, пока в канале не окажутся данные. Когда горутина-отправитель посылает данные, горутина-получатель получает эти данные и возобновляет работу.
Горутина-отправитель может отправлять данные только в пустой канал. Горутина-отправитель блокируется до тех пор, пока данные из канала не будут получены. Например:
package main import "fmt" func main() { intCh := make(chan int) go func(){ fmt.Println("Go routine starts") intCh <- 5 // блокировка, пока данные не будут получены функцией main }() fmt.Println(<-intCh) // получение данных из канала fmt.Println("The End") }
Через небуферизированный канал intCh горутина, представленная анонимной функцией, передает число 5:
intCh <- 5
А функция main получает это число:
fmt.Println(<-intCh)
Общий ход выполнения программы выглядит следующим образом:
Запускается функция main. Она создает канал intCh и запускает горутину в виде анонимной функции.
Функция main продолжает выполняться и блокируется на строке fmt.Println(<-intCh)
, пока не будут получены данные.
Параллельно выполняется запущенная горутина в виде анонимной функции. В конце своего выполнения она отправляет даные через канал: intCh <- 5
. Горутина блокируется,
пока функция main не получит данные.
Функция main получает отправленные данные, деблокируется и продолжает свою работу.
В данном случае горутина определена в виде анонимной функции и поэтому она имеет доступ к окружению, в том числе к переменной intCh. Если же мы работаем с обычными функциями, то объекты каналов надо передавать через параметры:
package main import "fmt" func main() { intCh := make(chan int) go factorial(5, intCh) // вызов горутины fmt.Println(<-intCh) // получение данных из канала fmt.Println("The End") } func factorial(n int, ch chan int){ result := 1 for i := 1; i <= n; i++{ result *= i } fmt.Println(n, "-", result) ch <- result // отправка данных в канал }
Обратите внимание, как определяется параметр, который представляет канал данных типа int: ch chan int
. Консольный вывод данной программы:
5 - 120 120 The End
Таким образом, при использовании канала вызывающий поток - функция main ожидает завершения выполнения горутины.
Стоит отметить, что одномоментно одна горутина должеа отправлять данные, а другая - получать. Например, если мы определим отправление и получение данных через канал в самой функции main, то мы столкнемся с взаимоблокировкой:
package main import "fmt" func main() { intCh := make(chan int) intCh <- 10 // функция main блокируется fmt.Println(<-intCh) }
От этой ситуации следует отличать ситуацию, когда две горутины попеременно обмениваются данными, при этом одномоментно опять же одна горутина выступает отправителем, а другая - получателем. Например:
package main import "fmt" func main() { intCh := make(chan int) go square(intCh) // square ожидает получения через канал intCh <- 4 // отправляем в канал число fmt.Println("result := ", <-intCh) // получаем из канала результат fmt.Println("The End") } // функция возведения в квадрат func square(ch chan int){ num := <-ch // получаем из канала число fmt.Println("num := ", num) ch <- num * num // обратно отправляем квадрат числа }
Здесь определена функция square, которая получает через канал число, возводит его в квадрат и возвращает обратно в канал. Функция main запускает функцию square в виде горутины, отправляет в канал некоторое число и ожидает получить в ответ из канала квадрат этого числа.
В итоге сначала запускается горутина square:
go square(intCh)
Горутина square блокируется на строке
num := <-ch
В этот момент получателем является горутина square, а отправителем функция main. И функция main отправляет данные в поток:
intCh <- 4
После этого горутина square получает из канала число, и роли меняются: теперь отправителем становится горутина square, а получателем - функция main, которая в ожидании данных блокируется на строке
fmt.Println("result := ", <-intCh)
Функция square обрабатывает полученное число и отправляет квадрат числа в поток:
ch <- num * num
Функция main получает из канала квадрат числа и завершает свою работу. Консольный вывод программы:
num := 4 result := 16 The End
Буферизированные каналы также создаются с помощью функции make()
, только в качестве второго аргумента в функцию передается емкость канала.
Если канал пуст, то получатель ждет, пока в канале появится хотя бы один элемент.
При отправке данных горутина-отправитель ожидает, пока в канале не освободится место для еще одного элемента и отправляет элемент, только тогда, когда в канале освобождается для него место.
package main import "fmt" func main() { intCh := make(chan int, 3) intCh <- 10 intCh <- 3 intCh <- 24 fmt.Println(<-intCh) // 10 fmt.Println(<-intCh) // 3 fmt.Println(<-intCh) //24 }
В данном случае отправителем и получателем данных является функция main. В ней создается канал из трех элементов, и последовательно отправляются три значения типа int.
В то же время в данном случае должно быть соответствие между количеством отправляемых и получаемых данных. Если в функции main будет одновременно отправлено значений больще, чем вмещает канал, то функция заблокируется:
package main import "fmt" func main() { intCh := make(chan int, 3) intCh <- 10 intCh <- 3 intCh <- 24 intCh <- 15 // блокировка - функция main ждет, когда освободится место в канале fmt.Println(<-intCh) fmt.Println("The End") }
С помощью встроенных функций cap() и len() можно получить соответственно емкость и количество элементов в канале:
package main import "fmt" func main() { intCh := make(chan int, 3) intCh <- 10 fmt.Println(cap(intCh)) // 3 fmt.Println(len(intCh)) // 1 fmt.Println(<-intCh) }
В Go можно определить канал, как доступный только для отправки данных или только для получения данных.
Определение канала только для отправки данных:
var inCh chan<- int
Определение канала только для получения данных:
var outCh <-chan int
Например:
package main import "fmt" func main() { intCh := make(chan int, 2) go factorial(5, intCh) fmt.Println(<-intCh) fmt.Println("The End") } func factorial(n int, ch chan<- int){ result := 1 for i := 1; i <= n; i++{ result *= i } ch <- result }
Здесь второй параметр функции factorial определен как канал, доступный только для отправки данных: ch chan<- int
. Соответственно
внутри функции factorial мы можем только отправлять данные в канал, но не получать их.
Канал может быть возвращаемым значением функции. Однако следует внимательно подходить к операциям записи и чтения в возвращаемом канале. Например:
package main import "fmt" func main() { fmt.Println("Start") // создание канала и получение из него данных fmt.Println(<-createChan(5)) // блокировка fmt.Println("End") } func createChan(n int) chan int{ ch := make(chan int) // создаем канал ch <- n // отправляем данные в канал return ch // возвращаем канал }
Функция createChan возвращает канал. Однако при выполнении операции ch <- n
мы столкнемся с блокировкой, поскольку происходит ожидание
получения данных из канала. Поэтому следующее выражение return ch
не будет выполняться.
И если все таки необходимо определить функцию, которая возвращает канал, то все операции чтения-записи в канал следует вынести в отдельную горутину:
package main import "fmt" func main() { fmt.Println("Start") // создание канала и получение из него данных fmt.Println(<-createChan(5)) // 5 fmt.Println("End") } func createChan(n int) chan int{ ch := make(chan int) // создаем канал go func(){ ch <- n // отправляем данные в канал }() // запускаем горутину return ch // возвращаем канал }