Go语言数据结构之信道

383 阅读6分钟

信道定义

信道是Go协程之间通信的管道。通过使用信道,数据可以从一端发送,在另一端接收。

var name chan T

通过name 关键字声明一个新的channel,并且声明时指定channel传输的数据类型T。

创建channel时我们需要借助make函数对channel进行初始化。

ch := make(chan T,size)

创建channel时需指定channel的传输的数据类型T,如果指定size,即指定channel的长度,即信道指定了缓冲区。

channel发送和接收数据

channel作为一个队列,它会保证数据遵循先入先出的原则进行,且同一时刻有且仅有一个goroutine访问channel发送和获取数据。

发送数据格式:

ch <- val  // 将val发送到信道ch

在信道被填满之后再向信道中发送数据会阻塞当前goroutine.

接收数据格式:

val := <- ch //表示从信道ch读取一个值并赋值给val

信道用于协程的一个小例子:

package main

import (  
    "fmt"
)

func hello(done chan bool) {  
    fmt.Println("goroutine 1")
    done <- true
}
func main() {  
    done := make(chan bool)
    go hello(done)
    <-done
    fmt.Println("main function")
}

创建了一个 bool 类型的信道 done,并把 done 作为参数传递给了 hello 协程。我们通过信道 done 接收数据。此时代码发生了阻塞,除非有协程向 done 写入数据,否则程序不会跳到下一行代码。输出如下:

goroutine 1  
main function

如果channel ch中没有数据,将会阻塞读取到goroutine,直到有数据放入channel,也可以在读取channel时判断是否获取到数据。

val,ok := <- ch

检查ok是否为true,用于判断是否读取到了有效数据。从关闭的信道读取到的值会是该信道类型的零值。例如,当信道是一个 int 类型的信道时,那么从关闭的信道读取的值将会是 0

package main

import (
	"fmt"
)

func producer(chnl chan int) {
	for i := 0; i < 10; i++ {
		chnl <- i
	}
	close(chnl)
}
func main() {
	ch := make(chan int)
	go producer(ch)
	for {
		v, ok := <-ch
		if ok == false {
			break
		}
		fmt.Println("Received ", v, ok)
	}
}

输出如下:

Received  0 true
Received  1 true
Received  2 true
Received  3 true
Received  4 true
Received  5 true
Received  6 true
Received  7 true
Received  8 true
Received  9 true

死锁

使用信道需要考虑的一个重点是死锁。当 Go 协程给一个信道发送数据时,照理说会有其他 Go 协程来接收数据。如果没有的话,程序就会在运行时触发 panic,形成死锁。

同理,当有 Go 协程等着从一个信道接收数据时,我们期望其他的 Go 协程会向该信道写入数据,要不然程序就会触发 panic。

package main

func main() {  
    ch := make(chan int)
    ch <- 2
}

输出如下:

fatal error: all goroutines are asleep - deadlock!

程序中创建的信道ch,通过ch <-2把2发送到信道,但没有其他协程从ch接收数据,于是程序触发了panic.

单向信道

只能发送或接收数据的信道称为单向信道。

只写信道定义:

type Sender chan<- T

只读信道定义:

type Reciver <-chan T

T为信道的数据类型。如果向只读信道中接收数据,编译器会报错。同理,向只写信道中读取数据也会报错。

package main

import "fmt"

func sendData(sendch chan<- int) {  
    sendch <- 10
}

func main() {  
    sendch := make(chan<- int)
    go sendData(sendch)
    fmt.Println(<-sendch)
}

输出如下:

invalid operation: <-sendch (receive from send-only type chan<- int)

创建的只写(send-only)信道sendch,<-sendch试图通过send-only信道接收数据,于是编译器报错。下面是正确使用的一个例子:

package main

import "fmt"

func main()  {
	c := make(chan int)
	var readc <- chan int = c
	var writec chan <- int = c
	go SetChan(writec)
	GetChan(readc)
}

func SetChan(writec chan <- int)  {
	for i:=0;i<5;i++ {
		writec <- i
	}
}
func GetChan(readc <- chan int)  {
	for i:=0;i<5;i++ {
		fmt.Printf("我是GetChan函数,从SetChan返回的信息是%d\n",<- readc)
	}
}

输出如下:

我是GetChan函数,从SetChan返回的信息是0
我是GetChan函数,从SetChan返回的信息是1
我是GetChan函数,从SetChan返回的信息是2
我是GetChan函数,从SetChan返回的信息是3
我是GetChan函数,从SetChan返回的信息是4

带缓冲区的channel

无缓冲信道的发送和接收过程是阻塞的。创建数据时若指定了channel的长度那么channel将会拥有缓冲区。只在缓冲已满的情况,才会阻塞向缓冲信道(Buffered Channel)发送数据。同样,只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据。例如:

package main

import (
	"fmt"
)


func main() {
	ch := make(chan int, 2)
	ch <- 0
	ch <- 2
	fmt.Println(<- ch)
	fmt.Println(<- ch)
}

输出如下:

0
2

可以看到上面的程序并未报错,程序未发生阻塞。

下面一个示例来理解当向缓冲信道中写入数据时,什么时候会发生阻塞。

package main

import (  
    "fmt"
    "time"
)

func write(ch chan int) {  
    for i := 0; i < 5; i++ {
        ch <- i
        fmt.Println("successfully wrote", i, "to ch")
    }
    close(ch)
}
func main() {  
    ch := make(chan int, 2)
    go write(ch)
    time.Sleep(2 * time.Second)
    for v := range ch {
        fmt.Println("read value", v,"from ch")
        time.Sleep(1 * time.Second)

    }
}

write 协程里立即会向 ch 写入 0 和 1,接下来发生阻塞,直到 ch 内的值被读取。因此,该程序立即打印出下面两行:

successfully wrote 0 to ch  
successfully wrote 1 to ch

打印上面两行之后,write 协程中向 ch 的写入发生了阻塞,直到 ch 有值被读取到。而 Go 主协程休眠了两秒后,才开始读取该信道,因此在休眠期间程序不会打印任何结果。主协程结束休眠后,在使用 for range 循环,开始读取信道 ch,打印出了读取到的值后又休眠两秒,这个循环一直到 ch 关闭才结束。所以该程序在两秒后会打印下面两行:

read value 0 from ch  
successfully wrote 2 to ch

该过程会一直进行,直到信道读取完所有的值,并在 write 协程中关闭信道。最终输出如下:

successfully wrote 0 to ch  
successfully wrote 1 to ch  
read value 0 from ch  
successfully wrote 2 to ch  
read value 1 from ch  
successfully wrote 3 to ch  
read value 2 from ch  
successfully wrote 4 to ch  
read value 3 from ch  
read value 4 from ch

什么是 select?

select 语句用于在多个发送/接收信道操作中进行选择。select 语句会一直阻塞,直到发送/接收操作准备就绪。如果有多个信道操作准备完毕,select 会随机地选取其中之一执行。该语法与 switch 类似,所不同的是,这里的每个 case 语句都是信道操作。

package main

import "fmt"
func main()  {
	ch1 := make(chan int, 1)
	ch2 := make(chan int, 1)
	ch3 := make(chan int, 1)
	ch1 <- 1
	ch2 <- 1
	ch3 <- 1
	select {
	case <- ch1:
		fmt.Println("ch1")
	case <- ch2:
		fmt.Println("ch2")
	case <- ch3:
		fmt.Println("ch3")
	default:
		fmt.Println("没有满足的ch")
	}

}

输出如下:

ch1

参考链接:studygolang.com/articles/12…