go基础系列:channel入门(代码片段)

f-ck-need-u f-ck-need-u     2023-01-21     563

关键词:

channel基础

channel用于goroutines之间的通信,让它们之间可以进行数据交换。像管道一样,一个goroutine_A向channel_A中放数据,另一个goroutine_B从channel_A取数据

channel是指针类型的数据类型,通过make来分配内存。例如:

ch := make(chan int)

这表示创建一个channel,这个channel中只能保存int类型的数据。也就是说一端只能向此channel中放进int类型的值,另一端只能从此channel中读出int类型的值。

需要注意,chan TYPE才表示channel的类型。所以其作为参数或返回值时,需指定为xxx chan int类似的格式。

向ch这个channel放数据的操作形式为:

ch <- VALUE

从ch这个channel读数据的操作形式为:

<-ch             // 从ch中读取一个值
val = <-ch
val := <-ch      // 从ch中读取一个值并保存到val变量中
val,ok = <-ch    // 从ch读取一个值,判断是否读取成功,如果成功则保存到val变量中

其实很简单,当ch出现在<-的左边表示send,当ch出现在<-的右边表示recv。

例如:

package main

import (
    "fmt"
    "time"
)

func main() 
    ch := make(chan string)
    go sender(ch)         // sender goroutine
    go recver(ch)         // recver goroutine
    time.Sleep(1e9)


func sender(ch chan string) 
    ch <- "malongshuai"
    ch <- "gaoxiaofang"
    ch <- "wugui"
    ch <- "tuner"


func recver(ch chan string) 
    var recv string
    for 
        recv = <-ch
        fmt.Println(recv)
    

输出结果:

malongshuai
gaoxiaofang
wugui
tuner

上面激活了一个goroutine用于执行sender()函数,该函数每次向channel ch中发送一个字符串。同时还激活了另一个goroutine用于执行recver()函数,该函数每次从channel ch中读取一个字符串。

注意上面的recv = <-ch,当channel中没有数据可读时,recver goroutine将会阻塞在此行。由于recver中读取channel的操作放在了无限for循环中,表示recver goroutine将一直阻塞,直到从channel ch中读取到数据,读取到数据后进入下一轮循环由被阻塞在recv = <-ch上。直到main中的time.Sleep()指定的时间到了,main程序终止,所有的goroutine将全部被强制终止。

因为receiver要不断从channel中读取可能存在的数据,所以receiver一般都使用一个无限循环来读取channel,避免sender发送的数据被丢弃。

channel的属性和分类

每个channel都有3种操作:send、receive和close

  • send:表示sender端的goroutine向channel中投放数据
  • receive:表示receiver端的goroutine从channel中读取数据
  • close:表示关闭channel
    • 关闭channel后,send操作将导致painc
    • 关闭channel后,recv操作将返回对应类型的0值以及一个状态码false
    • close并非强制需要使用close(ch)来关闭channel,在某些时候可以自动被关闭
    • 如果使用close(),建议条件允许的情况下加上defer
    • 只在sender端上显式使用close()关闭channel。因为关闭通道意味着没有数据再需要发送

例如,判断channel是否被关闭:

val, ok := <-counter
if ok 
    fmt.Println(val)

channel分为两种:unbuffered channel和buffered channel

  • unbuffered channel:阻塞、同步模式
    • sender端向channel中send一个数据,然后阻塞,直到receiver端将此数据receive
    • receiver端一直阻塞,直到sender端向channel发送了一个数据
  • buffered channel:非阻塞、异步模式
    • sender端可以向channel中send多个数据(只要channel容量未满),容量满之前不会阻塞
    • receiver端按照队列的方式(FIFO,先进先出)从buffered channel中按序receive其中数据

buffered channel有两个属性:容量和长度:和slice的capacity和length的概念是一样的

  • capacity:表示bufffered channel最多可以缓冲多少个数据
  • length:表示buffered channel当前已缓冲多少个数据
  • 创建buffered channel的方式为make(chan TYPE,CAP)

unbuffered channel可以认为是容量为0的buffered channel,所以每发送一个数据就被阻塞。注意,不是容量为1的buffered channel,因为容量为1的channel,是在channel中已有一个数据,并发送第二个数据的时候才被阻塞。

换句话说,send被阻塞的时候,其实是没有发送成功的,只有被另一端读走一个数据之后才算是send成功。对于unbuffered channel来说,这是send/recv的同步模式。

实际上,当向一个channel进行send的时候,先关闭了channel,再读取channel时会发现错误在send,而不是recv。它会提示向已经关闭了的channel发送数据。

func main() 
    counter := make(chan int)
    go func() 
        counter <- 32
    ()
    close(counter)
    fmt.Println(<-counter)

输出报错:

panic: send on closed channel

所以,在Go的内部行为中,send和recv是一个整体行为,数据未读就表示未send成功

死锁(deadlock)

当channel的某一端(sender/receiver)期待另一端的(receiver/sender)操作,另一端正好在期待本端的操作时,也就是说两端都因为对方而使得自己当前处于阻塞状态,这时将会出现死锁问题。

比如,在main函数中,它有一个默认的goroutine,如果在此goroutine中创建一个unbuffered channel,并在main goroutine中向此channel中发送数据并直接receive数据,将会出现死锁:

package main 

import (
    "fmt"
)

func main ()
    goo(32)


func goo(s int) 
    counter := make(chan int)
    counter <- s
    fmt.Println(<-counter)

在上面的示例中,向unbuffered channel中send数据的操作counter <- s是在main goroutine中进行的,从此channel中recv的操作<-counter也是在main goroutine中进行的。send的时候会直接阻塞main goroutine,使得recv操作无法被执行,go将探测到此问题,并报错:

fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:

要修复此问题,只需将send操作放在另一个goroutine中执行即可:

package main

import (
    "fmt"
)

func main() 
    goo(32)


func goo(s int) 
    counter := make(chan int)
    go func() 
        counter <- s
    ()
    fmt.Println(<-counter)

或者,将counter设置为一个容量为1的buffered channel:

counter := make(chan int,1)

这样放完一个数据后send不会阻塞(被recv之前放第二个数据才会阻塞),可以执行到recv操作。

unbuffered channel同步通信示例

下面通过sync.WaitGroup类型来等待程序的结束,分析多个goroutine之间通信时状态的转换。因为创建的channel是unbuffered类型的,所以send和recv都是阻塞的。

package main

import (
    "fmt"
    "sync"
)

// wg用于等待程序执行完成
var wg sync.WaitGroup

func main() 
    count := make(chan int)

    // 增加两个待等待的goroutines
    wg.Add(2)
    fmt.Println("Start Goroutines")

    // 激活一个goroutine,label:"Goroutine-1"
    go printCounts("Goroutine-1", count)
    // 激活另一个goroutine,label:"Goroutine-2"
    go printCounts("Goroutine-2", count)

    fmt.Println("Communication of channel begins")
    // 向channel中发送初始数据
    count <- 1

    // 等待goroutines都执行完成
    fmt.Println("Waiting To Finish")
    wg.Wait()
    fmt.Println("
Terminating the Program")

func printCounts(label string, count chan int) 
    // goroutine执行完成时,wg的计数器减1
    defer wg.Done()
    for 
        // 从channel中接收数据
        // 如果无数据可recv,则goroutine阻塞在此
        val, ok := <-count
        if !ok 
            fmt.Println("Channel was closed:",label)
            return
        
        fmt.Printf("Count: %d received from %s 
", val, label)
        if val == 10 
            fmt.Printf("Channel Closed from %s 
", label)
            // Close the channel
            close(count)
            return
        
        // 输出接收到的数据后,加1,并重新将其send到channel中
        val++
        count <- val
    

上面的程序中,激活了两个goroutine,激活这两个goroutine后,向channel中发送一个初始数据值1,然后main goroutine将因为wg.Wait()等待2个goroutine都执行完成而被阻塞。

再看这两个goroutine,这两个goroutine执行完全一样的函数代码,它们都接收count这个channel的数据,但可能是goroutine1先接收到channel中的初始值1,也可能是goroutine2先接收到初始值1。接收到数据后输出值,并在输出后对数据加1,然后将加1后的数据再次send到channel,每次send都会将自己这个goroutine阻塞(因为unbuffered channel),此时另一个goroutine因为等待recv而执行。当加1后发送给channel的数据为10之后,某goroutine将关闭count channel,该goroutine将退出,wg的计数器减1,另一个goroutine因等待recv而阻塞的状态将因为channel的关闭而失败,ok状态码将让该goroutine退出,于是wg的计数器减为0,main goroutine因为wg.Wait()而继续执行后面的代码。

使用for range迭代channel

前面都是在for无限循环中读取channel中的数据,但也可以使用range来迭代channel,它会返回每次迭代过程中所读取的数据,直到channel被关闭。

例如,将上面示例中的printCounts()改为for-range的循环形式。

func printCounts(label string, count chan int) 
    defer wg.Done()
    for val := range count 
        fmt.Printf("Count: %d received from %s 
", val, label)
        if val == 10 
            fmt.Printf("Channel Closed from %s 
", label)
            close(count)
            return
        
        val++
        count <- val
    

多个"管道":输出作为输入

channel是goroutine与goroutine之间通信的基础,一边产生数据放进channel,另一边从channel读取放进来的数据。可以借此实现多个goroutine之间的数据交换,例如goroutine_1->goroutine_2->goroutine_3,就像bash的管道一样,上一个命令的输出可以不断传递给下一个命令的输入,只不过golang借助channel可以在多个goroutine(如函数的执行)之间传,而bash是在命令之间传。

以下是一个示例,第一个函数getRandNum()用于生成随机整数,并将生成的整数放进第一个channel ch1中,第二个函数addRandNum()用于接收ch1中的数据(来自第一个函数),将其输出,然后对接收的值加1后放进第二个channel ch2中,第三个函数printRes接收ch2中的数据并将其输出。

如果将函数认为是Linux的命令,则类似于下面的命令行:ch1相当于第一个管道,ch2相当于第二个管道

getRandNum | addRandNum | printRes

以下是代码部分:

package main

import (
    "fmt"
    "math/rand"
    "sync"
)

var wg sync.WaitGroup

func main() 
    wg.Add(3)
    // 创建两个channel
    ch1 := make(chan int)
    ch2 := make(chan int)

    // 3个goroutine并行
    go getRandNum(ch1)
    go addRandNum(ch1, ch2)
    go printRes(ch2)

    wg.Wait()


func getRandNum(out chan int) 
    // defer the wg.Done()
    defer wg.Done()

    var random int
    // 总共生成10个随机数
    for i := 0; i < 10; i++ 
        // 生成[0,30)之间的随机整数并放进channel out
        random = rand.Intn(30)
        out <- random
    
    close(out)


func addRandNum(in,out chan int) 
    defer wg.Done()
    for v := range in 
        // 输出从第一个channel中读取到的数据
        // 并将值+1后放进第二个channel中
        fmt.Println("before +1:",v)
        out <- (v + 1)
    
    close(out)


func printRes(in chan int)
    defer wg.Done()
    for v := range in 
        fmt.Println("after +1:",v)
    

指定channel的方向

上面通过两个channel将3个goroutine连接起来,其中起连接作用的是第二个函数addRandNum()。在这个函数中使用了两个channel作为参数:一个channel用于接收、一个channel用于发送。

其实channel类的参数变量可以指定数据流向:

  • in <-chan int:表示channel in通道只用于接收数据
  • out chan<- int:表示channel out通道只用于发送数据

技术分享图片

只用于接收数据的通道<-chan不可被关闭,因为关闭通道是针对发送数据而言的,表示无数据再需发送。对于recv来说,关闭通道是没有意义的。

所以,上面示例中三个函数可改写为:

func getRandNum(out chan<- int) 
    ...


func addRandNum(in <-chan int, out chan<- int) 
    ...


func printRes(in <-chan int)
    ...

buffered channel异步队列请求示例

下面是使用buffered channel实现异步处理请求的示例。

在此示例中:

  • 有(最多)3个worker,每个worker是一个goroutine,它们有worker ID。
  • 每个worker都从一个buffered channel中取出待执行的任务,每个任务是一个struct结构,包含了任务id(JobID),当前任务的队列号(ID)以及任务的状态(worker是否执行完成该任务)。
  • 在main goroutine中将每个任务struct发送到buffered channel中,这个buffered channel的容量为10,也就是最多只允许10个任务进行排队。
  • worker每次取出任务后,输出任务号,然后执行任务(run),最后输出任务id已完成。
  • 每个worker执行任务的方式很简单:随机睡眠0-1秒钟,并将任务标记为完成。

以下是代码部分:

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

type Task struct 
    ID         int
    JobID      int
    Status     string
    CreateTime time.Time


func (t *Task) run() 
    sleep := rand.Intn(1000)
    time.Sleep(time.Duration(sleep) * time.Millisecond)
    t.Status = "Completed"


var wg sync.WaitGroup

// worker的数量,即使用多少goroutine执行任务
const workerNum = 3

func main() 
    wg.Add(workerNum)

    // 创建容量为10的buffered channel
    taskQueue := make(chan *Task, 10)

    // 激活goroutine,执行任务
    for workID := 0; workID <= workerNum; workID++ 
        go worker(taskQueue, workID)
    
    // 将待执行任务放进buffered channel,共15个任务
    for i := 1; i <= 15; i++ 
        taskQueue <- &Task
            ID:         i,
            JobID:      100 + i,
            CreateTime: time.Now(),
        
    
    close(taskQueue)
    wg.Wait()


// 从buffered channel中读取任务,并执行任务
func worker(in <-chan *Task, workID int) 
    defer wg.Done()
    for v := range in 
        fmt.Printf("Worker%d: recv a request: TaskID:%d, JobID:%d
", workID, v.ID, v.JobID)
        v.run()
        fmt.Printf("Worker%d: Completed for TaskID:%d, JobID:%d
", workID, v.ID, v.JobID)
    

select多路监听

很多时候想要同时操作多个channel,比如从ch1、ch2读数据。Go提供了一个select语句块,它像switch一样工作,里面放一些case语句块,用来轮询每个case语句块的send或recv情况。

select

用法格式示例:

select 
    // ch1有数据时,读取到v1变量中
    case v1 := <-ch1:
        ...
    // ch2有数据时,读取到v2变量中
    case v2 := <-ch2:
        ...
    // 所有case都不满足条件时,执行default
    default:
        ...

defalut语句是可选的,不允许fall through行为,但允许case语句块为空块。select会被return、break关键字中断。

select的行为模式主要是对channel是否可读进行轮询,但也可以用来向channel发送数据。它的行为如下:

  • 如果所有的case语句块都被阻塞,则阻塞直到某个语句块可以被处理
  • 如果多个case同时满足条件,则随机选择一个进行处理
  • 如果存在default且其它case都不满足条件,则执行default。所以default必须要可执行而不能阻塞

需要注意的是,如果在select中执行send操作,则可能会永远被send阻塞。所以,在使用send的时候,应该也使用defalut语句块,保证send不会被阻塞

一般来说,select会放在一个无限循环语句中,一直轮询channel的可读事件。

下面是一个示例,pump1()和pump2()都用于产生数据(一个产生偶数,一个产生奇数),并将数据分别放进ch1和ch2两个通道,suck()则从ch1和ch2中读取数据。然后在无限循环中使用select轮询这两个通道是否可读,最后main goroutine在1秒后强制中断所有goroutine。

package main

import (
    "fmt"
    "time"
)

func main() 
    ch1 := make(chan int)
    ch2 := make(chan int)
    go pump1(ch1)
    go pump2(ch2)
    go suck(ch1, ch2)
    time.Sleep(1e9)

func pump1(ch chan int) 
    for i := 0; i <= 30; i++ 
        if i%2 == 0 
            ch <- i
        
    

func pump2(ch chan int) 
    for i := 0; i <= 30; i++ 
        if i%2 == 1 
            ch <- i
        
    

func suck(ch1 chan int, ch2 chan int) 
    for 
        select 
        case v := <-ch1:
            fmt.Printf("Recv on ch1: %d
", v)
        case v := <-ch2:
            fmt.Printf("Recv on ch2: %d
", v)
        
    

go急速入门系列(代码片段)

...本文档为本人的学习笔记,里面的内容并不适合纯零基础的朋友学习,建议至少掌握了python,java,至少会C 查看详情

go语言自学系列|golang并发编程之channel的遍历(代码片段)

视频来源:B站《golang入门到项目实战[2021最新Go语言教程,没有废话,纯干货!持续更新中...]》一边学习一边整理老师的课程内容及试验笔记,并与大家分享,请移步至知乎网站,谢谢支持!附上... 查看详情

go并发编程基础-channel(代码片段)

协程(Goroutine)Go语言中没有线程的概念,只有协程,也称为goroutine。相比线程来说,协程更加轻量,一个程序可以随意启动成千上万个goroutine。goroutine被Goruntime所调度,这一点和线程不一样。也就是说,Go语言的并发是由Go自己... 查看详情

[易学易懂系列|golang语言|零基础|快速入门|](代码片段)

golang编程语言,是google推出的一门语言。主要应用在系统编程和高性能服务器编程,有广大的市场前景,目前整个生态也越来越强大,未来可能在企业应用和人工智能等领域占有越来越重要的地位。本文章是【易学易懂系列|编... 查看详情

go基础:入门学习资料(代码片段)

Go入门文档Google文档Documentation-TheGoProgrammingLanguageGo语言圣经(中文版)前言·Go语言圣经https://book.itsfun.top/gopl-zh/《Go入门指南》|Go技术论坛《Go入门指南》|Go技术论坛我们致力于为Golang/Go语言开发者提供一个分享创造、结... 查看详情

二.netty入门到超神系列-javanio三大核心(selector,channel,buffer)(代码片段)

...章节我们详细了解一下NIO的工作原理以及三大核心Selector,Channel,Buffer并尝试来做一些小案例。JavaNIO模型JavaNIO有三个核心的组件:selector选择器,channel通道,buffer缓冲区,模型如下:Selector多路复用器 查看详情

go语言入门goroutine和channel(代码片段)

goroutine和channelgoroutine多线程funchello()//fmt.Printf("HelloGoroutine!!")fori:=0;i<100;i++fmt.Printf("hello:%d",i)time.Sleep(time.Millisecond)funcmain()gohello()//启动了一个独立的线程,使其与下面的代码交替执行,使之成为一个多线程//fmt.Printf("mainfunction")fo... 查看详情

三.netty入门到超神系列-javanio三大核心(selector,channel,buffer)(代码片段)

...点讲解了Buffer的原理和几个使用场景,其中也用到了channel。这一章我们来理解一下selector,结合channel来做一个c/s通信。理解Selector和ChannelSelector选择器,也叫多路复用器,可以同时处理多个客户端连接,多路复用器... 查看详情

go语言基础之并发goroutinechan(代码片段)

...uot;fmt""sync")varwgsync.WaitGroupfuncmain()wg.Add(2)gonobufChannel()//不带缓冲区的初始化gobufChannel()//有缓冲区的通道wg.Wait()//不带缓冲区的初始化funcnobufChannel()deferwg.Done()channel1:=make(chanint)gofunc()x:=<-channel1fmt.Println("channel里取... 查看详情

go基础并发编程(代码片段)

...发编程并发编程Go并发的设计相关概念启动协程同步通道channel创建channelchannel的读写单方向channel定时器相关资料Go并发的设计  Go语言最大的特色是并发,而且Go的并发并不像线程或进程那样,受CPU核心数的限制,只... 查看详情

go基础系列:数组(代码片段)

...章会详细解释它。Go中的数组是slice和map两种数据类型的基础,这两种数据类型的底层都是通过数组实现的。数组的存储方式当在Go中声 查看详情

go语言入门指南零基础入门go语言|golang入门指南(代码片段)

...`https://github.com/CocaineCong/Golang-Learning`1.【第一轮】基础部分1.1教程1.2练习2.【第二轮】网络爬虫2.1教程2.2mod管理第三方包2.3git机制3.【第三轮】备忘录4.【第四轮】商城or视频网站5.【第五轮】IM即时通信6.【第六轮】微服务7.... 查看详情

go语言入门指南零基础入门go语言|golang入门指南(代码片段)

...`https://github.com/CocaineCong/Golang-Learning`1.【第一轮】基础部分1.1教程1.2练习2.【第二轮】网络爬虫2.1教程2.2mod管理第三方包2.3git机制3.【第三轮】备忘录4.【第四轮】商城or视频网站5.【第五轮】IM即时通信6.【第六轮】微服务7.... 查看详情

go基础系列:go接口(代码片段)

接口用法简介接口(interface)是一种类型,用来定义行为(方法)。typeNamerinterfacemy_method1()my_method2(para)my_method3(para)return_type...但这些行为不会在接口上直接实现,而是需要用户自定义的方法来实现。所以,在上面的Namer接口类型中的... 查看详情

go语言基础入门(代码片段)

文章目录前言安装代理设置开发环境程序编写与编译包和模块包(package)模块(module)模块编写与使用多模块工作区总结前言Go是由谷歌支持的开源编程语言,属于编译型语言,对并发编程有较好的支持。... 查看详情

go基础系列:流程控制结构(代码片段)

条件判断结构:ifelse分支选择结构:switchcase循环结构:forbreak:退出for或switch结构(以及select)continue:进入下一次for迭代虽然Go是类C的语言,但Go在这些流程控制语句中的条件表达式部分不使用括号。甚至有些时候使用括号会报错... 查看详情

go语言自学系列|golang并发编程之selectswitch(代码片段)

...itch语句,用于处理异步IO操作。select会监听case语句中channel的读写操作,当case中channel读写操作为非阻塞状态(即能读写)时,将会触发相应的动作。select的case语句必须是一个channel的操作select的default子句总是... 查看详情

go基础系列:常量和变量(代码片段)

常量(Constants)和iota常量包含不会发生更改的数据。常量的数据类型只能是boolean、number(int/float/complex)或string。定义方式:constNAME[TYPE]=VALUETYPE基本可以省略,因为常量都是简单数据类型,编译器可以根据值推断出它的数据类型。例... 查看详情