«

Go中的channel怎么声明和使用

时间:2024-3-11 11:43     作者:韩俊     分类: Go语言


这篇文章主要介绍了Go中的channel怎么声明和使用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Go中的channel怎么声明和使用文章都会有所收获,下面我们一起来看看吧。

channel

Go语言中的通道(channel)是一种特殊的类型。

在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。

通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。

(1)channel本身是一个队列,先进先出

(2)线程安全,不需要加锁

(3)本身是有类型的,string, int 等,如果要存多种类型,则定义成 interface类型

(4)channel是引用类型,必须make之后才能使用,一旦 make,它的容量就确定了,不会动态增加!!它和map,slice不一样

特点:

(1)一旦初始化容量,就不会改变了。

(2)当写满时,不可以写,取空时,不可以取。

(3)发送将持续阻塞直到数据被接收

把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞。Go 程序运行时能智能地发现一些永远无法发送成功的语句并做出提示

(4)接收将持续阻塞直到发送方发送数据。

如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。

(5)每次接收一个元素。

通道一次只能接收一个数据元素。

1、关于 channel的声明和使用的代码:

package main
import (
    "fmt"
)

func main() {

    //演示一下管道的使用
    //1. 创建一个可以存放3个int类型的管道
    var intChan chan int
    intChan = make(chan int, 3)

    //2. 看看intChan是什么
    fmt.Printf("intChan 的值=%v intChan本身的地址=%p
", intChan, &intChan)

    //3. 向管道写入数据
    intChan<- 10
    num := 211
    intChan<- num
    intChan<- 50
    // //如果从channel取出数据后,可以继续放入
    <-intChan
    intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量

    //4. 看看管道的长度和cap(容量)
    fmt.Printf("channel len= %v cap=%v 
", len(intChan), cap(intChan)) // 3, 3

    //5. 从管道中读取数据

    var num2 int
    num2 = <-intChan 
    fmt.Println("num2=", num2)
    fmt.Printf("channel len= %v cap=%v 
", len(intChan), cap(intChan))  // 2, 3

    //6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock

    num3 := <-intChan
    num4 := <-intChan

    //num5 := <-intChan

    fmt.Println("num3=", num3, "num4=", num4/*, "num5=", num5*/)
}
fmt.Printf("intChan 的值=%v intChan本身的地址=%p
", intChan, &intChan)

这句代码显示:channel其实和指针一样,本身存放在一个内存单元中,有它的地址,而它的值是一个 int类型的地址。

2、注意空接口类型的 channel

package main
import (
    "fmt"
)

type Cat struct {
    Name string
    Age int
}

func main() {

    //定义一个存放任意数据类型的管道 3个数据
    //var allChan chan interface{}
    allChan := make(chan interface{}, 3)

    allChan<- 10
    allChan<- "tom jack"
    cat := Cat{"小花猫", 4}
    allChan<- cat

    //我们希望获得到管道中的第三个元素,则先将前2个推出
    <-allChan
    <-allChan

    newCat := <-allChan //从管道中取出的Cat是什么?

    fmt.Printf("newCat=%T , newCat=%v
", newCat, newCat)
    //下面的写法是错误的!编译不通过
    //fmt.Printf("newCat.Name=%v", newCat.Name)
    //使用类型断言
    a := newCat.(Cat) 
    fmt.Printf("newCat.Name=%v", a.Name)
}

定义 interface类型的空接口,可以接收任意类型的数据,但是在取出来的时候,必须断言!
a := newCat.(Cat)

3、channel的关闭:close( )

关闭之后,不能再写入,只能读。

只能由发送者执行这句代码

4、channel的遍历: for &hellip; range

通道的数据接收一共有以下 4 种写法。

1.阻塞接收数据

阻塞模式接收数据时,将接收变量作为<-操作符的左值,格式如下:

data := <-ch

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

2.非阻塞接收数据(有问题啊,还是会报错deadlock

使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:

data, ok := <-ch

data:表示接收到的数据。未接收到数据时,data 为通道类型的零值。
ok:表示是否接收到数据。

非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel进行,可以参见后面的内容。

3.接收任意数据,忽略接收的数据

阻塞接收数据后,忽略从通道返回的数据,格式如下:

<-ch

执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。这个方式实际上只是通过通道在 goroutine 间阻塞收发实现并发同步。

使用通道做并发同步的写法,可以参考下面的例子:

package main

import (
    "fmt"
)

func main() {

    // 构建一个通道
    ch := make(chan int)

    // 开启一个并发匿名函数
    go func() {

        fmt.Println("start goroutine")

        // 通过通道通知main的goroutine
        ch <- 0

        fmt.Println("exit goroutine")

    }()

    fmt.Println("wait goroutine")

    // 等待匿名goroutine
    <-ch

    fmt.Println("all done")

}

4.循环接收

通道的数据接收可以借用 for range 语句进行多个元素的接收操作,格式如下:

for data := range ch {
}

通道 ch 是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过 for 遍历获得的变量只有一个,即上面例子中的 data。

package main
import (
    "fmt"
)

func main() {

    intChan := make(chan int, 3)
    intChan<- 100
    intChan<- 200
    close(intChan) // close
    //这时不能够再写入数到channel
    //intChan<- 300
    fmt.Println("okook~")
    //当管道关闭后,读取数据是可以的
    n1 := <-intChan
    fmt.Println("n1=", n1)

    //遍历管道
    intChan2 := make(chan int, 100)
    for i := 0; i < 100; i++ {
        intChan2<- i * 2  //放入100个数据到管道
    }

    //遍历管道不能使用普通的 for 循环
    // for i := 0; i < len(intChan2); i++ {

    // }
    //在遍历时,如果channel没有关闭,则会出现deadlock的错误
    //在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
    close(intChan2)
    for v := range intChan2 { //没有下标
        fmt.Println("v=", v)
    }
}

在遍历管道之前要先关闭管道,不然会出现deadlock的错误

应用1

开两个管道;

当写协程完成工作之后,close数据管道,读协程对数据管道 intChan的数据读完之后,就向退出管道 exitChan 写入一个 true,close掉

主线程循环检测退出管道里是否有数据,如果有,说明读协程完成,主程序就可以退出了。

package main

import (
    "fmt"
)

//write Data
func writeData(intChan chan int) {
    for i := 1; i <= 50; i++ {
        //放入数据
        intChan <- i //
        fmt.Println("writeData ", i)
        //time.Sleep(time.Second)
    }
    close(intChan) //关闭
}

//read data
func readData(intChan chan int, exitChan chan bool) {

    for {
        v, ok := <-intChan
        if !ok {
            break
        }
        // time.Sleep(time.Second)
        fmt.Printf("readData 读到数据=%v
", v)
    }
    //readData 读取完数据后,即任务完成
    exitChan <- true
    close(exitChan)

}

func main() {

    //创建两个管道
    intChan := make(chan int, 10)
    exitChan := make(chan bool, 1)

    go writeData(intChan)
    go readData(intChan, exitChan)

    //time.Sleep(time.Second * 10)
    for {
        _, ok := <-exitChan
        if !ok {
            break
        }
    }

}

应用2

定义三个管道:

  • intChan :放8000个数

  • primeChan:放素数

  • exitChan :4个协程运行完毕的标志

package main

import (
    "fmt"
    "time"
)

//向 intChan放入 1-8000个数
func putNum(intChan chan int) {

    for i := 1; i <= 80000; i++ {
        intChan <- i
    }

    //关闭intChan
    close(intChan)
}

// 从 intChan取出数据,并判断是否为素数,如果是,就
//     //放入到primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {

    //使用for 循环
    // var num int
    var flag bool //
    for {
        //time.Sleep(time.Millisecond * 10)
        num, ok := <-intChan //intChan 取不到..

        if !ok {
            break
        }
        flag = true //假设是素数
        //判断num是不是素数
        for i := 2; i < num; i++ {
            if num%i == 0 { //说明该num不是素数
                flag = false
                break
            }
        }

        if flag {
            //将这个数就放入到primeChan
            primeChan <- num
        }
    }

    fmt.Println("有一个primeNum 协程因为取不到数据,退出")
    //这里我们还不能关闭 primeChan
    //向 exitChan 写入true
    exitChan <- true

}

func main() {

    intChan := make(chan int, 1000)
    primeChan := make(chan int, 20000) //放入结果
    //标识退出的管道
    exitChan := make(chan bool, 4) // 4个

    start := time.Now().Unix()

    //开启一个协程,向 intChan放入 1-8000个数
    go putNum(intChan)
    //开启4个协程,从 intChan取出数据,并判断是否为素数,如果是,就
    //放入到primeChan
    for i := 0; i < 4; i++ {
        go primeNum(intChan, primeChan, exitChan)
    }

    //这里我们主线程,进行处理
    //直接
    go func() {
        for i := 0; i < 4; i++ {
            <-exitChan
        }

        end := time.Now().Unix()
        fmt.Println("使用协程耗时=", end-start)

        //当我们从exitChan 取出了4个结果,就可以放心的关闭 prprimeChan
        close(primeChan)
    }()

    //遍历我们的 primeChan ,把结果取出
    for {
        res, ok := <-primeChan
        if !ok {
            break
        }
        //将结果输出
        fmt.Printf("素数=%d
", res)
    }

    fmt.Println("main线程退出")

}

有一个primeNum 协程因为取不到数据,退出
有一个primeNum 协程因为取不到数据,退出
有一个primeNum 协程因为取不到数据,退出
有一个primeNum 协程因为取不到数据,退出
使用协程耗时= 3
main线程退出

存数字和计算素数比较简单,不提

开启4个协程,运算素数,效率比单个线程高几倍!

go func() {
        for i := 0; i < 4; i++ {
            <-exitChan
        }

        end := time.Now().Unix()
        fmt.Println("使用协程耗时=", end-start)

        //当我们从exitChan 取出了4个结果,就可以放心的关闭 prprimeChan
        close(primeChan)
    }()

这里定义了一个匿名协程,作用是检测4个协程 有没有完成运行,取不出来就会阻塞,等待协程完成。也可以这样:
if len(exitChan) == 4

标签: golang

热门推荐