天天看點

Go語言中的通道通道的發送和接收通道阻塞通道關閉select語句與通道總結

通道(channel)是Go 語言中一種特殊的資料類型,通道本身就是并發安全的,可以通過它在多個 goroutine 之間傳遞資料。通道是Go 語言程式設計理念:“Do not communicate by sharing memory; instead, share memory by communicating”(不要通過共享資料來通信,而應該通過通信來共享資料。)的完美實作,在并發程式設計中經常會遇到它。下面來介紹一下通道的使用方法。

目錄

  • 通道的發送和接收
    • 雙向通道
    • 單向通道
  • 通道阻塞
    • 緩沖通道的阻塞
    • 非緩沖通道
  • 通道關閉
  • select語句與通道
  • 總結

通道的發送和接收

通道包括雙向通道和單向通道,這裡雙向通道隻的是支援發送和接收的通道,而單向通道是隻能發送或者隻能接收的通道。

雙向通道

使用make函數聲明并初始化一個通道:

  • chan

    是表示通道類型的關鍵字
  • string

    表示該通道類型的元素類型
  • 3

    表示該通道的容量為3,最多可以緩存3個元素值。

一個通道相當于一個先進先出(FIFO)的隊列,使用操作符

<-

進行元素值的發送和接收:

接收元素值:

首先接收到的元素為先存入通道中的元素值,也就是先進先出:

package main

import "fmt"

func main() {
	str1 := []string{"hello","world", "!"}
	ch1 := make(chan string, len(str1))

	for _, str := range str1 {
		ch1 <- str
	}
	
	for i := 0; i < len(str1); i++ {
		elem := <- ch1
		fmt.Println(elem)
	}
}
           

執行結果:

hello
world
!
           

單向通道

單向通道包括隻能發送的通道和隻能接收的通道:

var WriteChan = make(chan<- interface{}, 1) // 隻能發送不能接收的通道
var ReadChan = make(<-chan interface{}, 1) // 隻能接收不能發送的通道
           

單向通道的這種特性可以用來限制函數的輸入類型或者輸出類型,比如下面的例子限制了隻能從通道中接收元素值:

package main

import (
	"fmt"
)

func OnlyReadChan(num int) <-chan int {
	ch := make(chan int, 1)
	ch <- num
	close(ch)
	return ch
}

func main() {

	Chan1 := OnlyReadChan(6)
	num := <- Chan1
	fmt.Println(num)
}
           

執行結果:

通道阻塞

通道操作是并發安全的,在同一時刻,隻會執行對同一個通道的任意個發送操作中的某一個,直到這個元素值被完全複制進該通道之後,其他針對該通道的發送操作才可能被執行。接收操作也一樣。另外,對于通道中的同一個元素值來說,發送操作和接收操作之間也是互斥的。

發送操作和接收操作是原子操作,也就是說,發送操作絕不會出現隻複制了一部分的情況,要麼還沒有複制,要麼已經複制完畢。接收操作在準備好元素值的副本之後,一定會删除掉通道中的原值,絕不會出現通道中仍有殘留的情況。在進行發送操作和接收操作時,代碼會一直阻塞在那裡,完成操作後才會繼續執行後面的代碼。通道的發送操作和接收操作是很快的,那麼什麼情況下會出現長時間的阻塞呢?下面介紹幾種情況。

緩沖通道的阻塞

緩沖通道是容量大于0的通道,也就是可以緩存資料的通道。

1、發送阻塞

如果緩沖通道已經填滿,如果有goroutine繼續向該通道發送資料就會阻塞。請看下面的例子:

package main

func main() {
	ch1 := make(chan int, 1)
	ch1 <- 1
	ch1 <- 2
}
           

執行結果:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
...........
           

如果通道可以接收資料(有元素被接收),通道會通知最先等待發送操作的 goroutine再次執行發送操作。

2、接收阻塞

類似的,如果通道已空,如果繼續進行接收操作就會被阻塞。

package main

func main() {
	ch1 := make(chan int, 1)
	<- ch1
}
           

執行結果:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
...........
           

非緩沖通道

非緩沖通道是容量為0的通道,不能緩存資料。

非緩沖通道的資料傳遞是同步的,發送操作或者接收操作在執行後就會阻塞,需要對應的接收操作或者發送操作執行才會繼續傳遞。由此可以看出緩沖通道使用的是異步方式進行資料傳遞。

package main

import (
    "fmt"
)

func main() {
    str1 := []string{"hello","world", "!"}
	ch1 := make(chan string, 0)

    go func() {
        for _, str := range str1 {
            ch1 <- str
        }
    }()

	for i := 0; i < len(str1); i++ {
		elem := <- ch1
		fmt.Println(elem)
	}

}
           

執行結果:

hello
world
!  
           

上面的代碼中3個goroutine向通道寫了三次資料,必須有三次接收,不然會阻塞。

對值為nil的通道進行發送操作和接收操作也會發生阻塞:

var ch1 chan int
ch1 <- 1 // 阻塞
<-ch1 // 阻塞
           

通道關閉

可以使用close()方法來關閉通道,通道關閉後,不能再對通道進行發送操作,可以進行接收操作。

package main

import "fmt"

func main() {
	ch1 := make(chan int, 1)
	ch1 <- 1
	close(ch1)
    
    ele := <-ch1
	fmt.Println(ele)  
    
	ch1 <- 2
}
           

執行結果:

1
panic: send on closed channel

goroutine 1 [running]:
.....
           

如果通道關閉時,裡面還有元素,進行接收操作時,傳回的通道關閉标志仍然為true:

package main

import "fmt"

func main() {
	ch1 := make(chan int, 1)
	ch1 <- 1
	close(ch1)

	ele1, statu1 := <-ch1
	fmt.Println(ele1, statu1)
	ele2, statu2 := <-ch1
	fmt.Println(ele2, statu2)
}
           

執行結果:

1 true
0 false
           

由于通道的這種特性,可以讓發送方來關閉通道。前面的例子可以這樣寫:

package main

import (
    "fmt"
)

func main() {
    str1 := []string{"hello","world", "!"}
	ch1 := make(chan string, 0)

    go func() {
        for _, str := range str1 {
            ch1 <- str
        }
        close(ch1)
    }()

	for i := 0; i < len(str1); i++ {
		elem := <- ch1
		fmt.Println(elem)
	}

}
           

另外,不能對關閉的通道再次關閉:

package main

// import "fmt"

func main() {
	ch1 := make(chan int, 1)
	ch1 <- 1
	close(ch1)
	close(ch1)

}
           

執行結果:

panic: close of closed channel
           

select語句與通道

select語句通常與通道聯用,它是專為通道而設計的。select語句執行時,一般隻有一個case表達式或者default語句會被運作。

package main

import "fmt"

func main() {
    ch1 := make(chan int, 1)
	num := 2
	
	select {
		case data := <-ch1:
			fmt.Println("Read data: ", data)
		case ch1 <- num:
			fmt.Println("Write data: ", num)	
		default:
			fmt.Println("No candidate case is selected!")
	}
}
           

執行結果:

需要注意的是,如果沒有default預設分支,case表達式都沒有滿足條件,那麼select語句就會被阻塞,直到至少有一個case表達式滿足條件為止。

如果同時有多個分支滿足條件,會随機選擇一個分支執行

for語句與select語句聯用時,分支中的break語句隻能結束目前select語句的執行,而不會退出for循環。下面的代碼永遠不會退出循環:

package main

import "fmt"

func main() {
    ch1 := make(chan int, 1)
	for {
		select {
		case ch1 <- 6:
			fmt.Println("Write data: 6")
		case data := <-ch1:
			fmt.Println(data)
			break
		}
	}
}
           

解決方案是使用goto語句和标簽。

方法1:

package main

import "fmt"

func main() {
    ch1 := make(chan int, 1)
	num := 6
	for {
		select {
		case ch1 <- num:
			fmt.Println("Write data: ", num)
		case data := <-ch1:
			fmt.Println("Read data: ", data)
			goto loop
		}
	}	
	loop:
	fmt.Println(ch1)
}
           

執行結果:

Write data:  6
Read data:  6
0xc00000e0e0
           

方法2:

package main

import "fmt"

func main() {
	ch1 := make(chan int, 1)
	num := 6
	loop:
	for {
		select {
		case ch1 <- num:
			fmt.Println("Write data: ", num)
		case data := <-ch1:
			fmt.Println("Read data: ", data)
			break loop
		}
	}
	fmt.Println(ch1)
}
           

執行結果:

Write data:  6
Read data:  6
0xc0000e4000
           

總結

本文主要介紹了通道的基本操作:初始化、發送、接收和關閉,要注意在什麼情況下會引起通道阻塞。select語句通常與通道聯用,介紹了分支的選擇規則以及for語句與select語句聯用時如何退出循環。

通道是 Go 語言并發程式設計的重要實作基礎,還是有必要掌握的。

--THE END--

繼續閱讀