golang中channel通道的实现

 更新时间:2026年03月17日 10:23:15   作者:X_PENG  
本文主要介绍了golang中channel通道的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

简介-是什么、有什么用

channel字面意思是“通道”,用于goroutine之间进行通信、同步

Goroutine 和 channel 是 Go 语言并发编程的两大基石。Goroutine用于执行并发任务,channel用于 goroutine之间的通信、同步。

看个简单demo:

package main

import (
   "log"
   "time"
)

var logger = log.Default()

func main() {
   c1 := make(chan int)

   go func() {
      logger.Println("go sleep start")
      time.Sleep(3 * time.Second)
      logger.Println("go sleep end, send start")
      c1 <- 1
      logger.Println("send end")
   }()

   logger.Println("main receive start")
   i, ok := <-c1
   logger.Printf("main receive end, i:%d, ok:%t\n", i, ok)
}

运行程序可以看到main协程从通道读取值时会阻塞,直到另一个协程往通道发送数据,才会唤醒main协程。

CSP模型

与主流语言通过共享内存来进行并发控制方式不同,Go 语言采用了 CSP 模型。

共享内存存在竞态问题,需要加锁同步,会造成性能问题。

CSP (Communicating Sequential Process ),即通信顺序进程, 是一种并发编程模型,是一个很强大的并发数据模型,是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。

强调通信,有这么一句话:

不要通过共享内存来通信,而要通过通信来实现内存共享。

Go语言通过协程Goroutine和通道Channel实现了 CSP 模型。go语言并没有完全实现了CSP模型的所有理论,仅借用了 process和channel这两个概念(分别对应go语言的goroutine和channel):process是并发执行的实体,每个实体之间通过channel通讯来实现数据共享。

channel详解

channel具有如下特性:

  • 并发安全
  • 先进先出
  • 能阻塞和唤醒goroutine

channel类型

channel是go语言的一种特殊类型,它是一个引用类型,因此未初始化时其默认零值是nil

  1. 定义channel变量:
var 变量名称 chan 元素类型
  • 元素类型,是指channel通道中元素的具体类型。

例子:

var c1 chan int
var c2 chan string
var c3 chan bool
var c4 chan []int
  1. 初始化channel,使用make

未初始化,默认零值是nil,对nil通道发送或接收都会一直阻塞。

make(chan 元素类型, [缓冲区容量])
  • 缓冲区容量:是可选参数,不指定或为0则创建无缓冲通道,指定且大于0则创建有缓冲通道。

例子:

c1 := make(chan int)
c2 := make(chan int, 1)

channel的操作

三种操作:

  • 发送(即写操作):往通道中发送数据
  • 接收(即读操作):从通道中读取数据
  • 关闭

发送和接收使用<-符号。

发送

ch <- 10 // 把10发送到ch中

发送什么时候会阻塞?
当通道未关闭且缓冲区满时,发送数据就会阻塞,直到有其他协程消费数据使缓冲区不满才不再阻塞。

接收

x := <- ch // 接收并赋值
<-ch       // 仅接收

接收什么时候会阻塞?
当通道未关闭且缓冲区空(无数据可读)时,接收数据就会阻塞,直到有其他协程向通道生产数据使缓冲区不空、有数据可读时才不再阻塞。

多返回值模式,接收操作可以使用多返回值模式:

value, ok := <- ch
  • ok:表示是否成功从通道中读取数据。false,即未成功读取,表示通道为空、无数据可读了,且此时通道肯定关闭了,不然接收操作会阻塞。

注意:接收操作不阻塞,只有两种情况:1. 有数据可读、通道不空;2. 若无数据可读,则通道必须关闭了

  • value:从通道中读取的值,若ok为false,则value是通道元素的数据类型的零值。

个人认为,多返回值模式的作用:如果ok返回false,则表示通道空了且已关闭。

nil通道

chan类型的默认零值是nil,对nil通道进行读、写都会阻塞。

关闭通道

使用close函数

close(ch)

注意:

  • 通常由发送方执行关闭操作
  • 关闭通道不是必须的(不像关闭文件是必须的),一般仅在接收方明确需要关闭信号时才执行关闭操作

关闭后的通道有如下特点:

  • 不能再发送,再发送数据会panic
  • 可以接收。通道被关闭,仍然可以读取未读完的数据;当通道为空(即没有数据了),再读取也不会造成阻塞,而是会返回对应数据类型的零值

重要:已关闭通道,一定不会阻塞读取操作

  1. 重复关闭会panic

close最佳实践:

  1. 非必要不要close
  2. 比较常见的是将close作为一种通知机制,通过close告诉消费者我关闭了,此时消费者消费就不会再阻塞了。

for range接收通道数据

for range可不断从通道中接收数据,当通道为空且已关闭了,会退出循环(因为为空且已关闭的通道是不可能再有数据了);当通道为空但未关闭,会阻塞直到通道有数据。

示例:

func main() {
   c1 := make(chan int, 3)

   go func() {
      for i := 0; i < 10; i++ {
         <-time.After(time.Second)
         c1 <- i
      }

      <-time.After(5 * time.Second)

      for i := 90; i < 100; i++ {
         <-time.After(time.Second)
         c1 <- i
      }

      <-time.After(5 * time.Second)
      log.Default().Println("---close chan---")
      close(c1)
   }()

   go func() {
      for i := range c1 {
         log.Default().Println(i)
      }
      log.Default().Println("---chan closed---")
   }()

   for {

   }
}

单向通道

是什么?

只能发送或只能接收的通道就是单向通道。

为什么要单向通道?

某些场景下我们想限制使用者对通道的操作:只允许发送或接收,为了表明这种意图并防止被滥用,所以go语言提供了单向channel。

如何使用?

定义单向通道:

箭头<-和关键字chan的相对位置表明了当前通道允许的操作,这种限制将在编译阶段进行检测。

<- chan int // 只接收通道,只能接收不能发送
chan <- int // 只发送通道,只能发送不能接收

「只接收channel」不允许close,因为一般都是发送方来close通道。

通道类型转换:

双向通道可以转成单向通道(是隐式转换),但反之不行。

示例:

生产者只能向channel发送数据,消费者只能从channel接收数据,所以使用单向channel

func main() {
   c1 := make(chan int, 1)
   go producer(c1)
   go consumer(c1)
   for {
   }
}
func producer(ch chan<- int) {
   for i := 0; i < 10; i++ {
      <-time.After(time.Second)
      ch <- i
   }
   log.Default().Println("producer. close chan")
   // 发送通道可close
   close(ch)
}

func consumer(ch <-chan int) {
   for i := range ch {
      log.Default().Println("consume:", i)
      // 接收通过不能close
      //close(ch)
   }
   log.Default().Println("consumer. chan closed")
}

select多路复用

作用:

可以同时监听多个channel的发送或接收操作。

如何使用?

  • 类似switch语句,select也有一系列case分支和一个default分支,每个case分支可以监听一个channel的发送或接收操作,若可接收或可发送,则匹配该case分支;没有任何case匹配,则走default分支。
  • 若没有default分支,select会一直阻塞直到某个case匹配。
  • 若没有case也没有default分支,select会一直阻塞。
  • 若同时有多个case满足,select会随机选择一个case执行。

使用方式如下:

select {
case <-ch1:
    //...
case data := <-ch2:
    //...
case ch3 <- 10:    
    //...
default:
    //默认操作
}

示例:

func main() {
   ch := make(chan int, 1)
   for i := 0; i < 10; i++ {
      select {
      case ch <- i:
         log.Default().Println("send")
      case x := <-ch:
         log.Default().Println("receive, x:", x)
      default:
         log.Default().Println("default")
      }

      // 会panic panic: send on closed channel
      //if i == 0 {
      // close(ch)
      //}
   }
}

无缓冲channel和有缓冲channel

make(chan 元素类型, [缓冲区容量])

创建channel时:

  • 未指定缓冲区容量或为0,则创建的是无缓冲channel
  • 指定了缓冲区容量且大于0,则创建的是有缓冲channel

无缓冲channel

没有缓冲区,channel中不能缓存数据

  • 发送操作一定会阻塞,直到有人接收
  • 接收操作一定会阻塞,直到有人发送

有缓冲channel

有缓冲区,channel中可以缓存数据

  • 仅当缓冲区满时,发送操作才阻塞,直到有人接收
  • 仅当缓冲区空时,接收操作才阻塞,直到有人发送

len和cap方法

  • len方法可以返回通道中元素的个数
  • cap方法可以返回通道的缓冲区容量

对于「无缓冲channel」,len和cap都是0

问题

channel有一个发送方,多个接收方怎么办?

多个接收方共同消费channel中的数据。

示例:

func main() {
   ch1 := make(chan int, 1)
   go consumer(ch1)
   go consumer2(ch1)
   go producer(ch1)
   for {

   }
}
func producer(ch chan<- int) {
   for i := 0; i < 10; i++ {
      <-time.After(time.Second)
      ch <- i
   }
   log.Default().Println("producer. close chan")
   // 发送通道可close
   close(ch)
   log.Default().Println("producer. end")
}

func consumer(ch <-chan int) {
   for i := range ch {
      log.Default().Println("consumer consume:", i)
   }
   log.Default().Println("consumer. chan closed")
}
func consumer2(ch <-chan int) {
   for i := range ch {
      log.Default().Println("consumer2 consume:", i)
   }
   log.Default().Println("consumer2. chan closed")
}

运行结果:

2022/10/30 01:46:46 consumer consume: 0
2022/10/30 01:46:47 consumer2 consume: 1
2022/10/30 01:46:48 consumer consume: 2
2022/10/30 01:46:49 consumer2 consume: 3
2022/10/30 01:46:50 consumer consume: 4
2022/10/30 01:46:51 consumer2 consume: 5
2022/10/30 01:46:52 consumer consume: 6
2022/10/30 01:46:53 consumer2 consume: 7
2022/10/30 01:46:54 consumer consume: 8
2022/10/30 01:46:55 consumer2 consume: 9
2022/10/30 01:46:55 producer. close chan
2022/10/30 01:46:55 producer. end
2022/10/30 01:46:55 consumer. chan closed
2022/10/30 01:46:55 consumer2. chan closed

goroutine泄漏

goroutine泄漏是什么?

是指goroutine一直阻塞而无法被回收。

不正确的使用channel可能导致goroutine泄漏,如下:

func main() {
   ch := make(chan int)

   go test(ch)
   select {
   case <-ch:
      log.Default().Println("receive")
   case <-time.After(time.Second):
      log.Default().Println("time")
   }

   for {

   }
}

func test(ch chan int) {
   log.Default().Println("test run")
   <-time.After(3 * time.Second)
   ch <- 1
   log.Default().Println("test end")
}

运行结果:

2022/10/30 01:55:17 test run
2022/10/30 01:55:18 time

发生了goroutine泄漏,test方法的goroutine一直阻塞了,因为main gorutine无法接收通道

到此这篇关于golang中channel通道的实现的文章就介绍到这了,更多相关golang channel通道内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Golang设计模式之生成器模式讲解和代码示例

    Golang设计模式之生成器模式讲解和代码示例

    生成器是一种创建型设计模式,使你能够分步骤创建复杂对象,与其他创建型模式不同,生成器不要求产品拥有通用接口,这使得用相同的创建过程生成不同的产品成为可能,本文就通过代码示例为大家详细介绍Golang生成器模式,感兴趣的同学可以参考下
    2023-06-06
  • go中make用法及常见的一些坑

    go中make用法及常见的一些坑

    golang分配内存主要有内置函数new和make,下面这篇文章主要给大家介绍了关于go中make用法及常见的一些坑,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2022-12-12
  • 一文总结Go语言切片核心知识点和坑

    一文总结Go语言切片核心知识点和坑

    都说Go的切片用起来丝滑得很,Java中的List怎么用,切片就怎么用,作为曾经的Java选手,因为切片的使用不得当,喜提缺陷若干,本文就给大家总结一下Go语言切片核心知识点和坑,需要的朋友可以参考下
    2023-06-06
  • Go语言实现热更新具体步骤

    Go语言实现热更新具体步骤

    这篇文章主要为大家介绍了Go语言实现热更新具体步骤详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-01-01
  • Golang的继承模拟实例

    Golang的继承模拟实例

    这篇文章主要介绍了Go语言使用组合的方式实现多继承的方法,实例分析了多继承的原理与使用组合方式来实现多继承的技巧,需要的朋友可以参考下,希望可以帮助到你
    2021-06-06
  • golang使用泛型结构体实现封装切片

    golang使用泛型结构体实现封装切片

    这篇文章主要为大家详细介绍了golang使用泛型结构体实现封装切片,即封装切片的增、删、改、查、长度大小、ForEach(遍历切片),感兴趣的小伙伴可以学习一下
    2023-10-10
  • Go语言实战之实现均衡器功能

    Go语言实战之实现均衡器功能

    这篇文章主要为大家详细介绍了如何利用Golang 实现一个简单的流浪均衡器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2023-04-04
  • go 原子操作的方式及实现原理全面深入解析

    go 原子操作的方式及实现原理全面深入解析

    这篇文章主要为大家介绍了go 原子操作的方式及实现原理深入解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04
  • golang高并发限流操作 ping / telnet

    golang高并发限流操作 ping / telnet

    这篇文章主要介绍了golang高并发限流操作 ping / telnet,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • 基于Golang实现统一加载资源的入口

    基于Golang实现统一加载资源的入口

    当我们需要在 main 函数中做一些初始化的工作,比如初始化日志,初始化配置文件,都需要统一初始化入口函数,所以本文就来编写一个统一加载资源的入口吧
    2023-05-05

最新评论