Каналы

Последнее обновление: 23.02.2024

Каналы (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)

Общий ход выполнения программы выглядит следующим образом:

  1. Запускается функция main. Она создает канал intCh и запускает горутину в виде анонимной функции.

  2. Функция main продолжает выполняться и блокируется на строке fmt.Println(<-intCh), пока не будут получены данные.

  3. Параллельно выполняется запущенная горутина в виде анонимной функции. В конце своего выполнения она отправляет даные через канал: intCh <- 5. Горутина блокируется, пока функция main не получит данные.

  4. Функция 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	// возвращаем канал
}
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850