Golang中Channel实战技巧与一些说明

目录
  • Channel 的一些实战说明
    • 关于 close Channel
      • close Channel 的一些说明
      • v, ok := <-ch 判断是否 close
      • 优雅判断是否 close 的封装
    • for-range 读取 Channel 数据
    • select 读写 Channel 数据
    • Channel 的读写超时机制【select + timeout】
    • TryEnqueue 无阻塞写 Channel 数据
  • Channel 常见错误和根因分析
    • fatal error: all goroutines are asleep - deadlock 问题解决和优化
  • 最后

Channel 的一些实战说明

关于 close Channel

close Channel 的一些说明

channel 不需要通过 close 来释放资源,这个是它与 socket、file 等不一样的地方,对于 channel 而言,唯一需要 close 的就是我们想通过 close 触发 channel 读事件。

  • close chan 对 chan 阻塞无效,写了数据不读,直接 close,还是会阻塞的。
  • 如果 channel 已经被关闭,继续往它发送数据会导致 panic send on closed channel
  • closed 的 channel,再次关闭 close 会 panic
  • close channel 的推荐使用姿势是在发送方来执行,因为 channel 的关闭在接收端能感知到,但是发送端感知不到,因此一般只能在发送端主动关闭。而且大部分时候可以不执行 close,只需要读写即可。
  • 从一个已经 close 的 chan 中读取数据,是可以读取的,读到的数据为 0
  • 读取的 channel 如果被关闭,并不会影响正在读的数据,它会将所有数据读取完毕,在读取完已发送的数据后会返回元素类型的零值(zero value)。

v, ok := <-ch 判断是否 close

比如 v, ok := <-ch 中 ok 是一个 bool 类型,可以通过它来判断 channel 是否已经关闭,如果 channel 关闭该值为 false ,此时 v 接收到的是 channel 类型的零值。比如:channel 是传递的 int, 那么 v 就是 0 ;如果是结构体,那么 v 就是结构体内部对应字段的零值。

_,ok := <-ch对应的函数是 func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool),入参block含义是当前goroutine是否可阻塞,当block为false代表的是select操作,不可阻塞当前goroutine的在channel操作,否则是普通操作(即_, ok不在select中)。返回值selected代表当前操作是否成功,主要为select服务,返回received代表是否从channel读到有效值。它有3种返回值情况:

  • block为false,即执行select时,如果channel为空,返回(false,false),代表select操作失败,没接收到值。
  • 否则,如果channel已经关闭,并且没有数据,ep即接收数据的变量设置为零值,返回(true,false),代表select操作成功,但channel已关闭,没读到有效值。
  • 否则,其他读到有效数据的情况,返回(true,ture)。

优雅判断是否 close 的封装

package main

import "fmt"

type T int

func IsClosed(ch <-chan T) bool {
	select {
	case <-ch:
		return true
	default:
	}

	return false
}

func main() {
	c := make(chan T)
	fmt.Println(IsClosed(c)) // false
	close(c)
	fmt.Println(IsClosed(c)) // true
}

for-range 读取 Channel 数据

不管是有缓冲还是无缓冲,都可以使用 for-range 从 channel 中读取数据,并且这个是一直循环读取的。

for-range 中的 range 产生的迭代值为 Channel 中发送的值,如果已经这个 channel 已经 close 了,那么首先还会继续执行,直到所有值被读取完,然后才会跳出 for 循环,因此,通过 for-range 读取 chann 数据会比较方便,因为我们只需要读取数据就行了,不需管他的退出,在 close 之后如果数据读取完了会自动帮我们退出。如果既没有 close 也没有数据可读,那么就会阻塞到 range 这里,除非有数据产生或者 chan 被关闭了。但是如果 channel 是 nil,读取会被阻塞,也就是会一直阻塞在 range 位置。

一个示例如下:

   ch := make(chan int)

   // 一直循环读取 range 中的迭代值
   for v := range ch {
        // 得到了 v 这个 chann 中的值
        fmt.Println("读取数据:",v)
   }

select 读写 Channel 数据

  • select 的 case 分支里面,可以读数据,也可以写数据。最多只允许有一个 default case,它可以放在 case 列表的任何位置,并且没有任何影响。
  • select 可以同时处理多个 channel,如果有同时多个 case 分支可以去处理,比如同时有多个 channel 可以接收数据,那么 Go 会伪随机(pseudo-random)的选择一个 case 处理。如果没有 case 需要处理,则会选择 default 分支去处理。如果没有 default case,则 select 语句会阻塞,直到某个 case 分支可以处理了。
  • 每次 select 语句的执行,是会扫描完所有的 case 后才确定如何执行,而不是说遇到合适的 case 就直接执行了。
  • 对于 nil channel 上的操作会一直被阻塞,如果没有 default case,只有 nil channel 的 select 会一直被阻塞。
  • select 语句和 switch 语句一样,它不是循环,它只会选择一个 case 来处理,如果想一直处理channel,你可以在外面加一个无限的 for 循环
for {
    select {
    case c <- x:
        x, y = y, x+y
    case <-quit:
        fmt.Println("quit")
        return
    }
}

Channel 的读写超时机制【select + timeout】

我们的一般常见场景就是,当我们从 chann 中进行读取数据,或者写入数据的时候,想要快速返回得到是否成功的结果,如果被 chann 阻塞后,需要指定一定的超时时间,然后如果在超时时间内还没有返回,那么就超时退出,不能一直阻塞在读写 chann 的流程中。

Go 的 time 库里面,提供了 time.NewTimer()、time.After()、time.NewTicker() 等方法,最终都可以通过这些方法来返回或者得到一个 channel,然后向这个 channel 中发送数据,就可以实现定时器的功能。

channel 可以通过 select + timeout 来实现阻塞超时的使用姿势,超时读写的姿势如下:

// 通过 select 实现读超时,如果读 chann 阻塞 timeout 的时间后就会返回
func ReadWithSelect(ch chan int) (x int, err error) {
	timeout := time.NewTimer(time.Microsecond * 500)

	select {
	case x = <-ch:
		return x, nil
	case <-timeout.C:
		return 0, errors.New("read time out")
	}
}

// 通过 select 实现写超时,如果写 chann 阻塞 timeout 的时间后就会返回
func WriteChWithSelect(ch chan int) error {
	timeout := time.NewTimer(time.Microsecond * 500)

	select {
	case ch <- 1:
		return nil
	case <-timeout.C:
		return errors.New("write time out")
	}
}

一个简单的实操代码示例

package main

import (
	"fmt"
	"runtime"
	"time"
)

func DoWorker() {
	c := make(chan bool, 1)

	go func() {

		time.Sleep(100 * time.Millisecond) // 等待 100ms 后写入,和后面的读超时配合,看超时判断结果

		c <- true
	}()

	go func() {
		timeout := time.NewTimer(time.Millisecond * 105) // 设置 105 ms 超时,如果超时没有读取到则 timeout
		select {
		case x := <-c:
			fmt.Printf("read chann:%v\n", x)

		case <-timeout.C:
			fmt.Println("read timeout")

		}
		fmt.Printf("over select\n\n")

	}()

}

func main() {
	fmt.Printf("start main num:%v\n", runtime.NumGoroutine())

	go func() {

		for {
			time.Sleep(1 * time.Second)

			fmt.Printf("start go DoWorker\n")

			go DoWorker()
		}

	}()

	for {
		time.Sleep(4 * time.Second)

		fmt.Printf("now main num:%v\n", runtime.NumGoroutine())
	}
}

输出:
start main num:1
start go DoWorker
read chann:true
over select

start go DoWorker
read chann:true
over select

start go DoWorker
read chann:true
over select

TryEnqueue 无阻塞写 Channel 数据

有些场景,我们期望往缓冲队列中写入数据的时候,如果队列已满,那么不要进行写阻塞,而是写完发现队列已满就抛错,那么我们可以通过如下机制的封装来实现,原理是通过一个 select 和 一个 default 语句去实现,有一个 default 就不会阻塞了:

var jobChan = make(chan int, 3)

func TryEnqueue(job int) bool {
	select {
	case jobChan <- job:
		fmt.Printf("true\n")  // 队列未满
		return true
	default:
		fmt.Printf("false\n") // 队列已满
		return false
	}
}

Channel 常见错误和根因分析

fatal error: all goroutines are asleep - deadlock 问题解决和优化

参考 go-language-fatal-error-all-goroutines-are-asleep-deadlock,在 main 函数里面,如果要 通过 chann 等待其他子协程的往 chann 中写入数据,但是并没有其他子协程写入或者其他协程没有写入就提前退出或者结束了,此时,main goroutine 协程就会等一个永远不会来的数据,那整个程序就永远等下去了,这个时候就会报上述错误。

fatal error: all goroutines are asleep - deadlock! 异常的示例,在 main 里面,往 chann 中写超过缓冲数量的数据,这个时候,main 是要期望能够从有其他协程读取这些数据的,但是 main 里面并没有,因此就会报错:

package main
import "fmt"
func main() {
    channel := make(chan string, 2)
    fmt.Println("1")
    channel <- "h1"
    fmt.Println("2")
    channel <- "w2"
    fmt.Println("3")
    channel <- "c3"    // 执行到这一步,直接报 error
    fmt.Println("...")
    msg1 := <-channel
    fmt.Println(msg1)
}

优化处理:

package main
import "fmt"
func main() {
    channel := make(chan string, 2)
    fmt.Println("1")
    channel <- "h1"
    fmt.Println("2")
    channel <- "w2"
    fmt.Println("3")
    select {
    case channel <- "c3":
        fmt.Println("ok")
    default:
        fmt.Println("channel is full !")
    }
    fmt.Println("...")
    msg1 := <-channel
    fmt.Println(msg1)
}

最后

到此这篇关于Golang中Channel实战技巧与一些说明的文章就介绍到这了,更多相关Golang Channel实战技巧内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Golang中channel使用的一些小技巧

    关闭2次 复制代码 代码如下: ch := make(chan bool) close(ch) close(ch)  // 这样会panic的,channel不能close两次 读取的时候channel提前关闭了 复制代码 代码如下: ch := make(chan string) close(ch) i := <- ch // 不会panic, i读取到的值是空 "",  如果channel是bool的,那么读取到的是false 向已经关闭的channel写数据 复制代码 代码

  • Golang中channel的原理解读(推荐)

    数据结构 channel的数据结构在$GOROOT/src/runtime/chan.go文件下: type hchan struct { qcount uint // 当前队列中剩余元素个数 dataqsiz uint // 环形队列长度,即可以存放的元素个数 buf unsafe.Pointer // 环形队列指针 elemsize uint16 // 每个元素的大小 closed uint32 // 标记是否关闭 elemtype *_type // 元素类型 sendx uint //

  • Golang优雅关闭channel的方法示例

    前言 最近使用go开发后端服务,服务关闭需要保证channel中的数据都被读取完,理由很简单,在收到系统的中断信号后,系统需要做收尾工作,保证channel的数据都要被处理掉,然后才可以关闭系统.但实现起来没那么简单,下面来一起看看详细的介绍吧. 关于Go channel设计和规范的批评: 在不能更改channel状态的情况下,没有简单普遍的方式来检查channel是否已经关闭了 关闭已经关闭的channel会导致panic,所以在closer(关闭者)不知道channel是否已经关闭的情况下去

  • golang中单向channel的语法介绍

    本文主要给大家介绍的是关于golang单向channel语法的相关内容,分享出来供大家参考学习,下面话不多说,来一起看看详细的介绍: 今天闲来无事补充一下golang的语法知识,想起来看看context的用法,结果碰到了一个没见过的channel语法: // A Context carries a deadline, cancelation signal, and request-scoped values // across API boundaries. Its methods are sa

  • 详解Golang中Channel的用法

    如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制.一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息. 1 创建channel 每个channel都有一个特殊的类型,也就是channels可发送数据的类型.一个可以发送int类型数据 的channel一般写为chan int.使用内置的make函数,如果第二个参数大于0,则表示创建一个带缓存的channel. ch := make(chan in

  • golang channel管道使用示例解析

    目录 定义channel管道 channel管道塞值和取值 通过channel管道实现同步,和数据交互 无缓冲的channel 有缓冲的channel管道 关闭channel管道 单向channel管道,读写分离 管道消费者生产者模型 定义channel管道 定义一个channel时,也需要定义发送到管道的值类型.channel可以使用内置的make()函数来创建: var ch = make(chan int) //等价于:make(chan Type,0) var ch = make(cha

  • Golang中Channel实战技巧与一些说明

    目录 Channel 的一些实战说明 关于 close Channel close Channel 的一些说明 v, ok := <-ch 判断是否 close 优雅判断是否 close 的封装 for-range 读取 Channel 数据 select 读写 Channel 数据 Channel 的读写超时机制[select + timeout] TryEnqueue 无阻塞写 Channel 数据 Channel 常见错误和根因分析 fatal error: all goroutines

  • golang 中 channel 的详细使用、使用注意事项及死锁问题解析

    目录 什么是channel管道 channel的基本使用 定义和声明 操作channel的3种方式 单向channel 带缓冲和不带缓冲的channel 不带缓冲区channel 带缓冲区channel 判断channel是否关闭 rangeandclose for读取channel select使用 channel的一些使用场景 1.作为goroutine的数据传输管道 2.同步的channel 3.异步的channel 4.channel超时处理 使用channel的注意事项及死锁分析 未初

  • 详解Golang中Context的原理和使用技巧

    目录 Context 背景 和 适用场景 Context 的背景 Context 的功能和目的 Context 的基本使用 Context 的同步控制设计 Context 的定义和实现 Context interface 接口定义 parent Context 的具体实现 Context 的继承和各种 With 系列函数 Context 的常用方法实例 1. 调用 Context Done方法取消 2. 通过 context.WithValue 来传值 3. 超时取消 context.WithT

  • golang开发中channel使用

    channel[通道]是golang的一种重要特性,正是因为channel的存在才使得golang不同于其它语言.channel使得并发编程变得简单容易有趣. channel的概念和语法 一个channel可以理解为一个先进先出的消息队列.channel用来在协程[goroutine]之前传递数据,准确的说,是用来传递数据的所有权.一个设计良好的程序应该确保同一时刻channel里面的数据只会被同一个协程拥有,这样就可以避免并发带来的数据不安全问题[data races]. 正文 channel

  • 5个可以在Golang中优化代码以提高性能的技巧分享

    作为一名软件工程师,确保你的代码高效且性能良好是非常重要的.在Golang中,有几个最佳实践和技术可以用来优化你的代码,以获得更好的性能.这里有五个技巧可以帮助你开始工作: 1.明智地使用指针.Golang使用指针来引用内存位置.虽然指针在某些情况下很有用,但如果过度或不正确地使用,它们也会导致性能下降.例如,使用指针向函数传递大的结构或 slice 会导致不必要的内存分配和复制.相反,可以考虑通过值传递这些类型. // Bad: Passing a large slice by pointer

  • Golang中goroutine和channel使用介绍深入分析

    目录 1.goroutine-看一个需求 2.进程和线程介绍 3.并发和并行 4.Go协程和Go主线程 5.设置Golang运行的CPU数 6.channel(管道)看需求 1.goroutine-看一个需求 需求:要求统计1-900000000的数字中,那些是素数? 分析: 传统方法,就是使用一个循环,循环的判断各个数是不是素数. 使用并发或并行的方式,将统计素数的任务分配给多个goroutine去完成,这时就会使用到goroutine. 2.进程和线程介绍 进程就是程序在操作系统中的一次执行

随机推荐