go并发协程-通道channel

1.什么是Channel?

channelGo的通道,是协程之间的通信机制。一个channel是一条通信管道,它可以让一个协程通过它给另一个协程发送数据。每个channel都需要指定数据类型,即channel可发送数据的类型。Go语言主张通过数据传递来实现共享内存,而不是通过共享内存来实现数据传递。

2. 创建Channel

2.1 语法

channel是引用类型,需要使用make()进行创建。

// 声明方式1
var cha1 chan 数据类型 
cha1 = make(chan 数据类型)

// 声明方式2
cha1 := make(chan 数据类型)

2.2 使用示例

package main
import (
    "fmt"
)
type People struct {}
func main() {
    // 创建一个整数型chan
    intChan := make(chan int)
    fmt.Printf("intChan类型: %T 值: %v \n",intChan,intChan)
    // 创建一个空接口chan,可以存放任意类型数据
    interfaceChan := make(chan interface{})
    fmt.Printf("interfaceChan类型: %T 值: %v \n",interfaceChan,interfaceChan)
    // 创建一个指针chan
    peopleChan := make(chan *People)
    fmt.Printf("peopleChan类型: %T 值: %v \n",peopleChan,peopleChan)
}
/** 输出
 intChan类型: chan int 值: 0xc000052060 
 interfaceChan类型: chan interface {} 值: 0xc0000520c0 
 peopleChan类型: chan *main.People 值: 0xc000052120 
*/

3.发送数据

3.1 语法

通过channel发送数据需要使用特殊的操作符<-,需要注意的是: channel发送的值的类型必须与channel的元素类型一致。

channel变量名 <- 值

3.2 错误使用示例

package main
import (
    "fmt"
    "time"
)
type People struct {
}
func main() {
    // 创建一个整数型chan
    intChan := make(chan int)
    // 写入
    intChan <- 5
  fmt.Printf("intChan类型: %T 值: %v \n", intChan, intChan)
}

上面示例运行会死锁,报错内容如下:

fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
        /Users/hui/Project/Go/src/go-basic/main.go:12 +0x59
Process finished with exit code 2

报错原因: 如果Goroutine在一个channel上发送数据,其他的Goroutine应该接收得到数据;如果没有接收,那么程序将在运行时出现死锁。

3.3 正确使用示例

package main
import (
    "fmt"
)
func main() {
    // 创建一个整数型chan
    intChan := make(chan int)
    // 写入数据(协程写入)
    go sendMsg(intChan)
    // 接收数据(主线程读取)
    a := <- intChan
    fmt.Printf("接收数据: %v \n", a)
    fmt.Println("运行结束! ")
}
func sendMsg(intChan chan int ){
    // 写入
    intChan <- 5
    fmt.Println("写入数据: 5 ")
}
/** 输出:
  写入数据: 5 
  接收数据: 5 
  运行结束! 
*/

4.普通接收

channel接收同样使用特殊的操作符<-

4.1 阻塞接收语法

// 方式一: ch 指的是通道变量
data := <- ch
//方式二: data 表示接收到的数据。未接收到数据时,data为channel类型的零值,ok(布尔类型)表示是否接收到数据
data,ok := <- ch

执行该语句时channel将会阻塞,直到接收到数据并赋值给data变量。

4.2 忽略接收语法

<- ch

执行该语句时channel将会阻塞。其目的不在于接收channel中数据,而是为了阻塞Goroutine

如果Goroutine正在等待从channel接收数据,而其他Goroutine并没有写入数据时程序将会死锁。

5. 循环接收

循环接收数据,需要配合使用关闭channel,借助普通for循环和for ... range语句循环接收多个元素。遍历channel,遍历的结果就是接收到的数据,数据类型就是channel的数据类型。普通for循环接收channel数据,需要有break循环的条件;for … range会自动判断出channel已关闭,而无须通过判断来终止循环。

5.1 使用普通for接收

方式一: data := <- ch

package main
import "fmt"
func main() {
    // 创建一个整数型chan
    intChan := make(chan int)
    // 写入数据(协程写入)
    go func(cha chan int) {
        // 写入
        for i := 1; i < 5; i++ {
            intChan <- i
            fmt.Printf("写入数据 ->  %v \n", i)
        }
        // 关闭通道
        close(intChan)
    }(intChan)

    // 方式一: data := <- ch
    for {
        // 接收数据
        out := <-intChan
        // 判断通道是否关闭
        //如果通道关闭,则out为通道类型的零值,这里是int型,所以是0
        if out == 0 {
            fmt.Println("通道已关闭")
            break
        }
        fmt.Printf("接收数据 ==> %v \n", out)
    }
    fmt.Println("程序运行结束!")
}
/** 输出:
  写入数据 ->  1 
  接收数据 ==> 1 
  接收数据 ==> 2 
  写入数据 ->  2 
  写入数据 ->  3 
  接收数据 ==> 3 
  接收数据 ==> 4 
  写入数据 ->  4 
  通道已关闭
  程序运行结束!
*/

方式二: data,ok := <- ch

package main
import "fmt"
func main() {
    // 创建一个整数型chan
    intChan := make(chan int)
    // 写入数据(协程写入)
    go func(cha chan int) {
        // 写入
        for i := 1; i < 5; i++ {
            intChan <- i
            fmt.Printf("写入数据 ->  %v \n", i)
        }
        // 关闭通道
        close(intChan)
    }(intChan)

    // 方式二: data,ok := <- ch
    for {
        // 接收数据
        out,ok := <-intChan
        // 判断通道是否关闭,如果通道关闭,则ok为false
        if !ok {
            fmt.Println("通道已关闭")
            break
        }
        fmt.Printf("接收数据 ==> %v \n", out)
    }
    fmt.Println("程序运行结束!")
}
/** 输出:
  写入数据 ->  1 
  接收数据 ==> 1 
  接收数据 ==> 2 
  写入数据 ->  2 
  写入数据 ->  3 
  接收数据 ==> 3 
  接收数据 ==> 4 
  写入数据 ->  4 
  通道已关闭
  程序运行结束!
*/

5.2 使用for...range接收

package main
import "fmt"
func main() {
    // 创建一个整数型chan
    intChan := make(chan int)
    // 写入数据(协程写入)
    go func(cha chan int) {
        // 写入
        for i := 1; i < 5; i++ {
            intChan <- i
            fmt.Printf("写入数据 ->  %v \n", i)
        }
        // 关闭通道
        close(intChan)
    }(intChan)
    // 使用 for...range接收
    for data := range intChan {
        fmt.Printf("接收数据 ==> %v \n", data)
    }
    fmt.Println("程序运行结束!")
}
/** 输出:
  写入数据 ->  1 
  接收数据 ==> 1 
  接收数据 ==> 2 
  写入数据 ->  2 
  写入数据 ->  3 
  接收数据 ==> 3 
  接收数据 ==> 4 
  写入数据 ->  4 
  程序运行结束!
*/

6. Channle的阻塞特性

6.1 特性如下

  • channel默认是阻塞的。
  • 当数据被发送到channel时会发生阻塞,直到有其他Goroutine从该channel中读取数据。
  • 当从channel读取数据时,读取也会被阻塞,直到其他Goroutine将数据写入该channel

6.2 特性使用

package main
import "fmt"
func main() {
    // 创建一个整数型chan
    intChan := make(chan int)
    // 创建一个用于阻塞的chan
    boolChan := make(chan bool)

    // 创建一个写入数据的协程
    go func(cha chan int) {
        // 写入
        intChan <- 50
        fmt.Println("写入数据50")
        // 关闭通道
        close(intChan)
    }(intChan)

    // 创建一个读取数据的协程
    go func(intChan chan int, boolChan chan  bool) {
        data,ok := <- intChan
        if ok {
            fmt.Printf("读取到数据 ->  %v \n", data)
            // 读取到数据后,给boolChan写入值
            boolChan <- true
            // 关闭用于的阻塞的chan
            close(boolChan)
        }
    }(intChan,boolChan)
    // 忽略接收,达到阻塞的效果。(如果不阻塞,则会直接输出: 程序运行结束!,不会等待协程执行)
    <- boolChan
    fmt.Println("程序运行结束!")
}
/** 输出
  写入数据50
  读取到数据 ->  50 
  程序运行结束!
*/

阻塞channel等待匿名函数的Goroutine运行结束,防止主函数的Goroutine退出而导致匿名函数的Goroutine提前退出。

7.关闭Channel

发送方写入完毕后需要主动关闭channel,用于通知接收方数据传递完毕。接收方通过data,ok := <- ch判断channel是否关闭,如果ok=false,则表示channel已经被关闭。

7.1 使用示例

package main
import "fmt"
func main() {
    // 创建一个整数型chan
    intChan := make(chan int)
    // 创建一个写入channel的协程
    go func(intChan chan int) {
        intChan <- 10
        intChan <- 20
        // 关闭通道
        close(intChan)
    }(intChan)

    // 读取数据
    a := <- intChan
    fmt.Printf("接收数据: %v \n",a)
    b := <- intChan
    fmt.Printf("接收数据: %v \n",b)

    // 此时的Chan已经关闭,而且里面的数据也都已经取完
    c := <- intChan
    fmt.Printf("接收数据: %v \n",c)
    fmt.Println("程序运行结束!")
}
/** 输出
  接收数据: 10 
  接收数据: 20 
  接收数据: 0 
  程序运行结束!
*/

又上面示例可以看出: 可以从关闭后的channel中继续读取数据,取到的值为该类型的零值。比如整型是:0; 字符串是:""

7.2 向已关闭的chan写入数据,会崩溃

往关闭的channel中写入数据会报错:panic: send on closed channel。导致程序崩溃。

package main
import "fmt"
func main() {
    // 创建一个整数型chan
    intChan := make(chan int)
    // 创建一个写入channel的协程
    go func(intChan chan int) {
        intChan <- 10
        // 关闭通道
        close(intChan)
    // 向已关闭的chan 继续写入数据,会报错;
        intChan <- 20
    }(intChan)
    // 读取数据
    a := <- intChan
    fmt.Printf("接收数据: %v \n",a)
    b := <- intChan
    fmt.Printf("接收数据: %v \n",b)
    fmt.Println("程序运行结束!")
}
/** 输出:
接收数据: 10 
接收数据: 0 
panic: send on closed channel

goroutine 18 [running]:
main.main.func1(0xc000100060)
        /Users/hui/Project/Go/src/go-basic/main.go:14 +0x5f
created by main.main
        /Users/hui/Project/Go/src/go-basic/main.go:10 +0x6a

Process finished with exit code 2
*/

7.3 重复关闭chan,会崩溃

package main]
import "fmt"
func main() {
    // 创建一个整数型chan
    intChan := make(chan int)

    // 创建一个写入channel的协程
    go func(intChan chan int) {
        intChan <- 10
        // 关闭通道
        close(intChan)
    }(intChan)

    // 读取数据
    a := <- intChan
    fmt.Printf("接收数据: %v \n",a)
    b := <- intChan
    fmt.Printf("接收数据: %v \n",b)
    // 重复关闭(此处会报错)
    close(intChan)
    fmt.Println("程序运行结束!")
}
/** 输出
接收数据: 10 
接收数据: 0 
panic: close of closed channel

goroutine 1 [running]:
main.main()
        /Users/hui/Project/Go/src/go-basic/main.go:22 +0x1cc

Process finished with exit code 2
*/

8.缓冲Channel

默认创建的都是非缓冲channel,读写都是即时阻塞。缓冲channel自带一块缓冲区,可以暂时存储数据,如果缓冲区满了,就会发生阻塞。缓冲通道在发送时无需等待接收方接收即可完成发送过程,并且不会发生阻塞,只有当缓冲区满时才会发生阻塞。同理,如果缓冲通道中有数据,接收时将不会发生阻塞,直到通道中没有数据可读时,通道将会再度阻塞。

8.1 语法

// 声明 n:代表缓冲区大小
cha1 := make(chan T,n)

8.2 使用示例

package main
import (
    "fmt"
    "time"
)
func main() {
    fmt.Printf("开始时间: %v \n",time.Now().Unix())
    // 创建一个缓冲区为2的整数型chan
    intChan2 := make(chan int,2)
    // 不会发生阻塞,因为缓冲区未满
    intChan2 <- 100
    fmt.Printf("结束时间: %v \n",time.Now().Unix())
    fmt.Printf("intChan2  类型: %T 缓冲大小: %v \n",intChan2,cap(intChan2))
    fmt.Println("程序运行结束!")
}
/**输出:
  开始时间: 1607496281 
  结束时间: 1607496281 
  intChan2  类型: chan int 缓冲大小: 2 
  程序运行结束!
*/

9.单向Channel

channel默认都是双向的,即可读可写。定向channel也叫单向channel,只读或只写。直接创建单向channel没有任何意义。通常的做法是创建双向channel,然后以单向channel的方式进行函数传递。

9.1 介绍

// 只读
ch <- chan T
// 只写
ch chan <- T

9.1 使用

package main
import (
    "fmt"
    "time"
)
func main() {
    // 创建一个整数型chan
    intChan := make(chan int)
    go writeChan(intChan)
    go readChan(intChan)
    time.Sleep(50 * time.Millisecond)
    fmt.Println("运行结束")

}

// 定义只读通道的函数
func readChan( ch <- chan int)  {
    for data := range ch {
        fmt.Printf("读出数据: %v \n",data)
    }
}
// 定义只写通道的函数
func writeChan( ch chan <- int){
    for i:= 1; i< 5 ; i++ {
        ch <- i
        fmt.Printf("写入数据: %v \n",i)
    }
    close(ch)
}

10.计时器与channel

计时器类型表示单个事件。当计时器过期时,当前时间将被发送到c上(c是一个只读channel <-chan time.Time,该channel中放入的是Timer结构体),除非计时器是After()创建的。计时器必须使用NewTimer()After()创建。

10.1 NewTimer

NewTimer()创建一个新的计时器,它会在至少持续时间d之后将当前时间发送到其channel上。

使用示例:

package main
import (
    "fmt"
    "time"
)
func main() {
    // 创建一个计时器
    timer := time.NewTimer(5 * time.Second)
    fmt.Printf("开始时间 %v \n",time.Now())
    // 此处会阻塞5秒
    out := <- timer.C
    fmt.Printf("变量out->  类型: %T 值:%v  \n",out,out)
    fmt.Printf("开始时间 %v \n",time.Now())
}
/** 输出:
  开始时间 2020-12-10 10:53:22.979673 +0800 CST m=+0.000174275 
  变量out->  类型: time.Time 值:2020-12-10 10:53:27.980079 +0800 CST m=+5.000489969  
  开始时间 2020-12-10 10:53:27.980264 +0800 CST m=+5.000674880 
*/

10.2 After

After()函数相当于NewTimer(d). C,如下源码:

func After(d Duration) <-chan Time {
    return NewTimer(d).C
}

使用示例

package main
import (
    "fmt"
    "time"
)
func main() {
    // 创建一个计时器,返回的是chan
    ch := time.After(5 * time.Second)
    fmt.Printf("开始时间 %v \n",time.Now())
    // 此处会阻塞5秒
    out := <- ch
    fmt.Printf("变量out->  类型: %T 值:%v  \n",out,out)
    fmt.Printf("开始时间 %v \n",time.Now())
}
/** 输出
  开始时间 2020-12-10 11:01:07.272154 +0800 CST m=+0.000153152 
  变量out->  类型: time.Time 值:2020-12-10 11:01:12.273034 +0800 CST m=+5.000956630  
  开始时间 2020-12-10 11:01:12.273153 +0800 CST m=+5.001076196 
*/
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容