channel
channel是goroutine之間的通信機制,它可以讓一個goroutine通過它給另一個goroutine發送數據,每個channel在創建的時候必須指定一個類型,指定的類型是任意的。
使用內置的make函數,可以創建一個channel類型:
ch := make(chan int)
發送和接受
channel主要的操作有發送和接受:
// 發送數據到channel
ch <- 1
// 從channel接受數據
x := <- ch
如向channel發送數據的時候,該goroutine會一直阻塞直到另一個goroutine接受該channel的數據,反之亦然,goroutine接受channel的數據的時候也會一直阻塞直到另一個goroutine向該channel發送數據,如下面操作:
func main() {
ch := make(chan string)
// 在此處阻塞,然后程序會彈出死鎖的報錯
c <- "hello"
fmt.Println("channel has send data")
}
正確的操作:
func main() {
ch := make(chan string)
go func(){
// 在執行到這一步的時候main goroutine才會停止阻塞
str := <- ch
fmt.Println("receive data:" + str)
}()
ch <- "hello"
fmt.Println("channel has send data")
}
說到channel的阻塞,就不得不說到有緩沖的channel。
帶緩沖的channel
帶緩沖的channel的創建和不帶緩沖的channel(也就是上面用的channel)的創建差不多,只是在make函數的第二個參數指定緩沖的大小。
// 創建一個容量為10的channel
ch := make(chan int, 10)
帶緩沖的channel就像一個隊列,遵從先進先從的原則,發送數據向隊列尾部添加數據,從頭部接受數據。
goroutine向channel發送數據的時候如果緩沖還沒滿,那么該goroutine就不會阻塞。
ch := make(chan int, 2)
// 前面兩次發送數據不會阻塞,因為緩沖還沒滿
ch <- 1
ch <- 2
// goroutine會在這里阻塞
ch <- 3
反之如果接受該channel數據的時候,如果緩沖有數據,那么該goroutine就不會阻塞。
channel與goroutine之間的應用可以想象成某個工廠的流水線工作,流水線上面有打磨,上色兩個步驟(兩個goroutine),負責打磨的工人生產完成后會傳給負責上色的工人,上色的生產依賴於打磨,兩個步驟之間的可能存在存放槽(channel),如果存放槽存滿了,打磨工人就不能繼續向存放槽當中存放產品,直到上色工人拿走產品,反之上色工人如果把存放槽中的產品都上色完畢,那么他就只能等待新的產品投放到存放槽中。
備注
其實在實際應用中,帶緩沖的channel用的並不多,繼續拿剛才的流水線來做案例,如果打磨工人生產速度比上色工人工作速度要快,那么即便再多容量的channel,也會遲早被填滿然后打磨工人會被阻塞,反之如果上色工人生產速度大於打磨工人速度,那么有緩沖的channel也是一直處於沒有數據,上色工人很容易長時間處於阻塞的狀態。
因此比較好的解決方法還是針對生產速度較慢的一方多加人手,也就是多開幾個goroutine來進行處理,有緩沖的channel最好用處只是拿來防止goroutine的完成時間有一定的波動,需要把結果緩沖起來,以平衡整體channel通信。
單方向的channel
使用channel來使不同的goroutine去進行通信,很多時候都和消費者生產者模式很相似,一個goroutine生產的結果都用channel傳送給另一個goroutine,一個goroutine的執行依賴與另一個goroutine的結果。
因此很多情況下,channel都是單方向的,在go里面可以把一個無方向的channel轉換為只接受或者只發送的channel,但是卻不能反過來把接受或發送的channel轉換為無方向的channel,適當地把channel改成單方向,可以達到程序強約束的做法,類似於下面例子:
fuc main(){
ch := make(ch chan string)
go func(out chan<- string){
out <- "hello"
}(ch)
go func(in <-chan string){
fmt.Println(in)
}(ch)
time.Sleep(2 * time.Second)
}
select多路復用
在一個goroutine里面,對channel的操作很可能導致我們當前的goroutine阻塞,而我們之后的操作都進行不了。而如果我們又需要在當前channel阻塞進行其他操作,如操作其他channel或直接跳過阻塞,可以通過select來達到多個channel(可同時接受和發送)復用。如下面我們的程序需要同時監聽多個頻道的信息:
broadcaster1 := make(chan string) // 頻道1
broadcaster2 := make(chan string) // 頻道2
select {
case mess1 := <-broadcaster1:
fmt.Println("來自頻道1的消息:" + mess1)
case mess2 := <-broadcaster2:
fmt.Println("來自頻道2的消息:" + mess2)
default:
fmt.Println("暫時沒有任何頻道的消息,請稍后再來~")
time.Sleep(2 * time.Second)
}
select和switch語句有點相似,找到匹配的case執行對應的語句塊,但是如果有兩個或以上匹配的case語句,那么則會隨機選擇一個執行,如果都不匹配就會執行default語句塊(如果含有default的部分的話)。
值得注意的是,select一般配合for循環來達到不斷輪詢管道的效果,可能很多小伙伴想着寫個在某個case里用break來跳出for循環,這是不行的,因為break只會退出當前case,需要使用return來跳出函數或者弄個標志位標記退出
var flag = 0
for {
if flag == 1 {break}
select {
case message := <- user.RecMess :
event := gjson.Get(string(message), "event").String()
if event == "login" {
Login(message, user)
}
break
case <- user.End :
flag = 1
break
}
}
關閉
channel可以接受和發送數據,也可以被關閉。
close(ch)
關閉channel后,所有向channel發送數據的操作都會引起panic,而被close之后的channel仍然可以接受之前已經發送成功的channel數據,如果數據全部接受完畢,那么再從channel里面接受數據只會接收到零值得數據。
channel的關閉可以用來操作其他goroutine退出,在運行機制方面,goroutine只有在自身所在函數運行完畢,或者主函數運行完畢才會打斷,所以我們可以利用channel的關閉作為程序運行入口的一個標志位,如果channel關閉則停止運行。
無法直接讓一個goroutine直接停止另一個goroutine,但可以使用通信的方法讓一個goroutine停止另一個goroutine,如下例子就是程序一邊運行,一邊監聽用戶的輸入,如果用戶回車,則退出程序。
func main() {
shutdown := make(chan struct{})
var n sync.WaitGroup
n.Add(1)
go Running(shutdown, &n)
n.Add(1)
go ListenStop(shutdown, &n)
n.Wait()
}
func Running(shutdown <-chan struct{}, n *sync.WaitGroup) {
defer n.Done()
for {
select {
case <-shutdown:
// 一旦關閉channel,則可以接收到nil。
fmt.Println("shutdown goroutine")
return
default:
fmt.Println("I am running")
time.Sleep(1 * time.Second)
}
}
}
func ListenStop(shutdown chan<- struct{}, n *sync.WaitGroup) {
defer n.Done()
os.Stdin.Read(make([]byte, 1))
// 如果用戶輸入了回車則退出關閉channel
close(shutdown)
}
利用channel關閉時候的傳送的零值信號可以有效地退出其他goroutine,特別是關閉多個goroutine的時候,就不需要向channel傳輸多個信息了。