go语言channel 的用法,go语言channel聊天室
终极管理员 知识笔记 80阅读
在本教程中我们将讨论Channel以及 Goroutines 如何使用Channel进行通信。
什么是ChannelChannel可以被认为是 Goroutine 用来进行通信的管道。与水在管道中从一端流向另一端的方式类似可以使用Channel从一端发送数据并从另一端接收数据。

每个Channel都有一个与其关联的类型。该类型是Channel允许传输的数据类型。不允许使用该Channel传输其他类型。
chan T是一个Channel类型是T

Channel的零值为nil
。nil
的Channel没有任何用处因此必须使用make
类似于map和slices来定义Channel。
让我们编写一些声明Channel的代码。
package mainimport fmtfunc main() { var a chan int if a nil { fmt.Println(channel a is nil, going to define it) a make(chan int) fmt.Printf(Type of a is %T, a) }}
Run program in playground
第 1 行中声明的Channel此时的Channel是nil
。因此会执行 if 条件内的语句并定义Channel。该程序将输出
channel a is nil, going to define it Type of a is chan int
与往常一样简写声明也是定义Channel的有效且简洁的方式。
a : make(chan int)
上面的代码行还定义了一个 int Channel变量a
。
下面给出了从Channel发送和接收数据的语法
data : <- a // read from channel a a <- data // write to channel a
箭头相对于Channel的方向指定是发送还是接收数据。
在第一行中箭头指向外部a
因此我们从Channel读取a
并将值存储到变量data
。
在第二行中箭头指向a
因此我们正在向写入通道写入a
。
默认情况下向Channel发送和接收是阻塞的。这是什么意思当数据发送到Channel时控制会在发送语句中被阻塞直到其他 Goroutine 从该Channel读取数据。类似地当从Channel读取数据时读取会被阻塞直到某个 Goroutine 将数据写入该Channel。
Channel的这一属性有助于 Goroutines 有效地进行通信而无需使用其他编程语言中很常见的显式锁或条件变量。
如果现在这没有意义也没关系。接下来的部分将更清楚地说明默认情况下Channel是如何阻塞的。
Channel示例程序让我们编写一个程序来了解 Goroutine 如何使用Channel进行通信。
让我引用上一篇教程中的程序。
package mainimport ( fmt time)func hello() { fmt.Println(Hello world goroutine)}func main() { go hello() time.Sleep(1 * time.Second) fmt.Println(main function)}
Run program in playground
这是上一个教程中的程序。我们在这里使用 sleep 来让主 Goroutine 等待 hello Goroutine 完成。
我们将使用Channel重写上面的程序。
package mainimport ( fmt)func hello(done chan bool) { fmt.Println(Hello world goroutine) done <- true}func main() { done : make(chan bool) go hello(done) <-done fmt.Println(main function)}
Run program in playground
在上面的程序中我们在第 1 行创建了一个布尔Channel。 并将其作为参数传递给hello
Goroutine。在14行号中。 我们正在从done
Channel接收数据。这行代码是阻塞的这意味着在某个 Goroutine 将数据写入Channel之前done
控件不会移动到下一行代码。因此这消除了原始程序中存在的用于防止主 Goroutine 退出的需要time.Sleep
。
该代码行<-done
从完成的Channel接收数据但不使用该数据或将该数据存储在任何变量中。这是完全合法的。
现在我们的main
Goroutine 被阻塞等待完成Channel上的数据。Goroutinehello
接收此Channel作为参数打印Hello world goroutine
然后写入done
。当此写入完成时主 Goroutine 会从 did Channel接收数据解除阻塞然后打印主函数。
该程序输出
Hello world goroutine main function
让我们通过在 Goroutine 中引入 sleep 来修改此程序hello
以更好地理解这种阻塞概念。
package mainimport ( fmt time)func hello(done chan bool) { fmt.Println(hello go routine is going to sleep) time.Sleep(4 * time.Second) fmt.Println(hello go routine awake and going to write to done) done <- true}func main() { done : make(chan bool) fmt.Println(Main going to call hello go goroutine) go hello(done) <-done fmt.Println(Main received data)}
Run program in playground
在上面的程序中我们在第 10 行的函数中引入了 4 秒的睡眠。.
该程序将首先打印Main going to call hello go goroutine
. 然后 hello Goroutine 将启动并打印hello go routine is going to sleep
。打印完此信息后hello
Goroutine 将休眠 4 秒在此期间main
Goroutine 将被阻塞因为它正在等待第 18 行完成Channel的数据<-done
。hello go routine awake and going to write to done
后将打印然后是Main received data
.
Main going to call hello go goroutinehello go routine is going to sleephello go routine awake and going to write to doneMain received data
Channel的另一个例子 让我们再编写一个程序来更好地理解Channel。该程序将打印一个数字的各个数字的平方和和立方之和。
例如如果输入为 123则该程序将计算输出为
正方形 (1 * 1) (2 * 2) (3 * 3)
立方体 (1 * 1 * 1) (2 * 2 * 2) (3 * 3 * 3)
输出 正方形 立方体 50
我们将构建该程序使得平方在单独的 Goroutine 中计算立方体在另一个 Goroutine 中计算最终求和发生在主 Goroutine 中。
package mainimport ( fmt)func calcSquares(number int, squareop chan int) { sum : 0 for number ! 0 { digit : number % 10 sum digit * digit number / 10 } squareop <- sum}func calcCubes(number int, cubeop chan int) { sum : 0 for number ! 0 { digit : number % 10 sum digit * digit * digit number / 10 } cubeop <- sum} func main() { number : 589 sqrch : make(chan int) cubech : make(chan int) go calcSquares(number, sqrch) go calcCubes(number, cubech) squares, cubes : <-sqrch, <-cubech fmt.Println(Final output, squares cubes)}
Run program in playground
第 7行中的函数calcSquares
。 计算该数字各个数字的平方和并将其发送到squareop
。同样第 17 行中的calcCubes
函数。计算数字各个数字的立方和并将其发送到cubeop
。
这两个函数在第31行 和 32行中作为单独的 Goroutine 运行。每个Channel都传递一个要写入的参数作为参数。主 Goroutine 在第 33 行等待来自这两个Channel的数据。 一旦从两个Channel接收到数据它们就会被存储在squares
和cubes
变量中并计算和打印最终输出。该程序将打印
Final output 1536
死锁 使用Channel时要考虑的一个重要因素是死锁。如果一个 Goroutine 正在Channel上发送数据那么预计其他一些 Goroutine 应该正在接收数据。如果这没有发生那么程序将在运行时出现Deadlock
。
类似地如果一个 Goroutine 正在等待从某个Channel接收数据那么其他一些 Goroutine 就应该在该Channel上写入数据否则程序将会出现Deadlock
。
package mainfunc main() { ch : make(chan int) ch <- 5}
Run program in playground
在上面的程序中ch
创建了一个Channel我们在第6 行发送5
到该Channel。在此程序中没有其他 Goroutine 正在从Channel接收数据。因此该程序将因以下运行时错误而发生恐慌。
fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]: main.main() /tmp/sandbox046150166/prog.go:6 0x50
单向Channel 到目前为止我们讨论的所有Channel都是双向Channel即数据可以在它们上发送和接收。还可以创建单向Channel即仅发送或接收数据的Channel。
package mainimport fmtfunc sendData(sendch chan<- int) { sendch <- 10}func main() { sendch : make(chan<- int) go sendData(sendch) fmt.Println(<-sendch)}
Run program in playground
在上面的程序中我们sendch
在第 10 行创建仅发送Channel。chan<- int
箭头指向 时表示仅发送Channel。我们尝试从在的仅发送Channel接收数据。这是不允许的当程序运行时编译器会
./prog.go:12:14无效操作<-sendch从仅发送类型 chan<- int 接收
一切都很好但如果无法读取写入仅发送Channel有什么意义
这就是Channel转换发挥作用的地方。可以将双向Channel转换为仅发送或仅接收Channel但反之则不然。
package mainimport fmtfunc sendData(sendch chan<- int) { sendch <- 10}func main() { chnl : make(chan int) go sendData(chnl) fmt.Println(<-chnl)}
Run program in playground
上面程序的在10行号中。chnl
创建了一个双向Channel。它作为参数传递给sendData
Goroutine。. 该sendData
函数在第 6行将此Channel转换为仅发送Channel。所以现在Channel仅在sendData
Goroutine 内部发送但在主 Goroutine 中是双向的。该程序将打印10
为输出。
发送方能够关闭Channel以通知接收方不再有数据在Channel上发送。
接收者在从Channel接收数据时可以使用附加变量来检查Channel是否已关闭。
v, ok : <- ch
ok
如果通过成功发送到Channel的操作接收到该值则上述语句为 true。如果ok
为 false则意味着我们正在从封闭的Channel中读取数据。从关闭的Channel读取的值将是该Channel类型的零值。例如如果Channel是一个int
Channel那么从关闭的Channel接收到的值将为0
。
package mainimport ( 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) }}
Run program in playground
在上面的程序中producer
Goroutine 将 0 到 9 写入chnl
Channel然后关闭Channel。main 函数for
在第 16 行有一个无限循环它使用第 16 行中的变量检查Channel是否关闭ok
。如果ok
为 false则表示Channel已关闭因此循环已中断。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
for循环的for range形式可用于从Channel接收值直到Channel关闭为止。
让我们使用 for range 循环重写上面的程序。
package mainimport ( 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 : range ch { fmt.Println(Received ,v) }}
Run program in playground
第 16 行中的循环for range
。 从Channel接收数据ch
直至关闭。一旦ch
关闭循环就会自动退出。该程序输出
Received 0 Received 1 Received 2 Received 3 Received 4 Received 5 Received 6 Received 7 Received 8 Received 9
可以使用 for range 循环重写Channel部分的另一个示例中的程序以提高代码可重用性。
如果仔细观察该程序您会发现查找数字的各个数字的代码在calcSquares
函数和calcCubes
函数中都重复出现。我们将该代码移至其自己的函数中并同时调用它。
package mainimport ( fmt)func digits(number int, dchnl chan int) { for number ! 0 { digit : number % 10 dchnl <- digit number / 10 } close(dchnl)}func calcSquares(number int, squareop chan int) { sum : 0 dch : make(chan int) go digits(number, dch) for digit : range dch { sum digit * digit } squareop <- sum}func calcCubes(number int, cubeop chan int) { sum : 0 dch : make(chan int) go digits(number, dch) for digit : range dch { sum digit * digit * digit } cubeop <- sum}func main() { number : 589 sqrch : make(chan int) cubech : make(chan int) go calcSquares(number, sqrch) go calcCubes(number, cubech) squares, cubes : <-sqrch, <-cubech fmt.Println(Final output, squarescubes)}
Run program in playground
上面程序中的函数digits
现在包含从数字中获取各个数字的逻辑并且由 和calcSquares
函数calcCubes
同时调用。一旦号码中不再有数字则第13 行Channel将关闭。.calcSquares
和calcCubes
Goroutine 使用for range
循环监听各自的Channel直到关闭。程序的其余部分是相同的。该程序还将打印
Final output 1536
本教程到此结束。Channel中还有一些概念例如缓冲Channel、工作池和select。我们将在他们自己的单独教程中讨论它们。谢谢阅读。祝你有美好的一天。