【Go】笔记六 | channel知识

对通道的发送和接收操作都有哪些基本的特性?

  • 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。
  • 发送操作和接收操作中对元素值的处理都是不可分割的。
  • 发送操作在完全完成之前会被阻塞。接收操作也是如此。

第一个基本特性。在同一时刻,Go 语言的运行时系统(以下简称运行时系统)只会执行对同一个通道的任意个发送操作或接受操作中的某一个。
其中注意的一个细节是,元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本。

第二个基本特性。 这里的“不可分割”的意思是,它们处理元素值时都是一气呵成的,绝不会被打断。原子性。

第三个基本特性。 一般情况下,发送操作包括了“复制元素值”和“放置副本到通道内部”这两个步骤。另外,接收操作通常包含了“复制通道内的元素值”“放置副本到接收方”“删掉原值”三个步骤。

发送操作和接收操作在什么时候可能被长时间的阻塞?

  • 向 nil channel 写数据将卡死,一直阻塞
  • 从 nil channel 读数据将卡死,一直阻塞
  • 向已满的缓存 channel 写数据将卡死,一直阻塞
  • 向已空的缓存 channel 读数据将卡死,一直阻塞
  • 向非缓存 channel 读写数据,一开始执行就会被阻塞,直到配对的操作也开始执行

    配对操作
package main

import "sync"

func main() {
    // wg 用来等待程序完成
    // 计数加 2,表示要等待两个goroutine
    var wg sync.WaitGroup
    wg.Add(2)

    c := make(chan int)

    go func(){
        // 在函数退出时调用Done 来通知main 函数工作已经完成
        defer wg.Done()

        c <- 520 // 流入信道,堵塞当前线, 没人取走数据信道不会打开
    }()

    go func(){
        // 在函数退出时调用Done 来通知main 函数工作已经完成
        defer wg.Done()

        println(<-c) // 接受数据,阻塞解除
    }()

    wg.Wait()
}

发送操作和接收操作在什么时候会引发 panic?

  • 向已关闭的 channel 写数据将造成 panic
  • 向已关闭的 channel 再次关闭将会造成panic

通过接收表达式的第二个结果值,来判断通道是否关闭是可能有延时的,应当让发送方来关闭通道。
如果通道关闭时,里面还有元素值未被取出,那么接收表达式的第一个结果,仍会是通道中的某一个元素值,而第二个结果值一定会是true

package main

import "fmt"

func main() {
    ch1 := make(chan int, 2)
    // 发送方。
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Printf("Sender: sending element %v...\n", i)
            ch1 <- i
        }
        fmt.Println("Sender: close the channel...")
        close(ch1)
    }()

    // 接收方。
    for {
        elem, ok := <-ch1
        if !ok {
            fmt.Println("Receiver: closed channel")
            break
        }
        fmt.Printf("Receiver: received an element: %v\n", elem)
    }

    fmt.Println("End.")
}

通道的长度代表着什么?它在什么时候会和通道的容量相同?

长度代表通道当前包含的元素个数,容量就是初始化时你设置的那个数。

什么是单向通道?

我们在说“通道”的时候指的都是双向通道,即:既可以发也可以收的通道。

所谓单向通道就是,只能发不能收,或者只能收不能发的通道。一个通道是双向的,还是单向的是由它的类型字面量体现的。

var uselessChan = make(chan<- int, 1)
//发送通道,操作符<-在关键字chan右边,表示对操作管道来说,只能发而不能收
var uselessChan = make(<-chan int, 1)
//接受通道,操作符<-在关键字chan左边,表示对操作管道来说,只能收而不能发
intChan1 := make(chan int, 3)
SendInt(intChan1)
//双向通道,

从上述变量的名字上你也能猜到,这样的通道是没用的。通道就是为了传递数据而存在的,声明一个只有一端(发送端或者接收端)能用的通道没有任何意义。

单向通道的用途究竟在哪儿呢?

概括地说,单向通道最主要的用途就是约束其他代码的行为,更大作用于编写模板代码或者可扩展的程序库的时候。

func SendInt(ch chan<- int) {
    //只能接受,不能发送
    ch <- rand.Intn(1000)
}

实际场景中,这种约束一般会出现在接口类型声明中的某个方法定义上

type Notifier interface {
    SendInt(ch chan<- int)
}

调用有以单向通道作为参的函数时,只需要传入元素类型匹配的双向通道,会自动地把双向通道转换为函数所需的单向通道

select语句与通道怎样联用,应该注意些什么?

  1. select语句只能与通道联用,它一般由若干个分支组成。每次执行这种语句的时候,一般只有一个分支中的代码会被运行。
  2. 基本功能与switch差不多,只不过select case 表达式中都只能包含操作通道的表达式,比如接收表达式。

注意

因从已关闭的通道接受只能接受到nil,所以通常通过接受表达式的第二个结果值来判断通道是否已经关闭

select语句只能对其中的每一个case表达式各求值一次。
所以,如果我们想连续或定时地操作其中的通道的话,就往往需要通过在for语句中嵌入select语句的方式实现。

select语句的分支选择规则都有哪些?

  1. 每个case表达式,都至少会包含一个代表操作的可以被赋值表达式。
  2. case表达式都会在该语句执行开始时先被求值,并且求值的顺序是依从代码编写的顺序从上到下的。
  3. 对于每一个case表达式,如果其中的发送表达式或者接收表达式在被求值时,相应的操作正处于阻塞状态,那么对该case表达式的求值就是不成功的。
  4. 仅当select语句中的所有case表达式都被求值完毕后,它才开始选择候选分支。
  5. 如果select语句发现同时有多个候选分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个并执行。
  6. 一条select语句中只能够有一个默认分支。
  7. select语句的每次执行,包括case表达式求值和分支选择,都是独立的。
package main

import "fmt"

var channels = [3]chan int{
    nil,
    make(chan int),
    nil,
}

var numbers = []int{1, 2, 3}

func main() {
    select {
    case getChan(0) <- getNumber(0):
        fmt.Println("The first candidate case is selected.")
    case getChan(1) <- getNumber(1):
        fmt.Println("The second candidate case is selected.")
    case getChan(2) <- getNumber(2):
        fmt.Println("The third candidate case is selected")
    default:
        fmt.Println("No candidate case is selected!")
    }
}

func getNumber(i int) int {
    fmt.Printf("numbers[%d]\n", i)
    return numbers[i]
}

func getChan(i int) chan int {
    fmt.Printf("channels[%d]\n", i)
    return channels[i]
}

如果在select语句中发现某个通道已关闭,那么应该怎样屏蔽掉它所在的分支?

在候选分支中将接收表达式赋值给两个变量,使用第二个变量来判断 channel 是否关闭,如果关闭,可以使用 break 来屏蔽这条候选分支的逻辑代码。

在select语句与for语句联用时,怎样直接退出外层的for语句?

显然使用 break 语句只能推出 select 分支语句,无法退出外层 for 循环,要想在 select 分支语句中直接跳出外层循环,需要借助 标签来完成,在外层 for 循环语句前 定义标签 Loop:(当然也可以起个其他名字)在 select 的 case 语句中使用 goto Loop 或者 break Loop 来直接跳出外层 for 循环。
当然,这两者是有区别的。区别在于「break 标签 」只能用于 for 循环,且标签在 for 循环前面。而「goto 标签」是一个通用的跳转语句。

package main

import "fmt"

func main() {
    c := make(chan int, 2)
    cc := make(chan int, 1)
    c <- 1
    c <- 2
    cc <- 3
    Loop:
    for {
        select {
        case <-c:
            fmt.Print("c 1")
        case <-c:
            fmt.Print("c 2")
            goto end
        case <-cc:
            break Loop
        }
    }
    fmt.Print("out for")
    end:
        fmt.Print("out end")

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值