【Golang】--- Channel

Go语言Channel通道

  • 前言:
  • 一、Channel通道初识
    • [1. Channel是什么?](#1. Channel是什么?)
    • [2. Channel的定义与创建](#2. Channel的定义与创建)
      • [2.1 定义Channel](#2.1 定义Channel)
      • [2.2 创建Channel](#2.2 创建Channel)
      • [2.3 最简单的Channel使用示例](#2.3 最简单的Channel使用示例)
  • 二、Channel的核心操作
    • [1. 发送数据](#1. 发送数据)
    • [2. 接收数据](#2. 接收数据)
      • [2.1 基础接收写法](#2.1 基础接收写法)
      • [2.2 带状态判断的接收写法](#2.2 带状态判断的接收写法)
    • [3. 关闭通道](#3. 关闭通道)
      • [3.1 关闭通道示例](#3.1 关闭通道示例)
      • [3.2 for-range简化读取](#3.2 for-range简化读取)
  • 三、无缓冲Channel与有缓冲Channel的区别
    • [1. 核心特性对比](#1. 核心特性对比)
    • [2. 阻塞规则](#2. 阻塞规则)
      • [2.1 无缓冲Channel](#2.1 无缓冲Channel)
      • [2.2 有缓冲Channel](#2.2 有缓冲Channel)
    • [3. 示例对比](#3. 示例对比)
      • [3.1 无缓冲Channel示例](#3.1 无缓冲Channel示例)
      • [3.2 有缓冲Channel示例](#3.2 有缓冲Channel示例)
  • 四、Channel的死锁问题
    • [1. 死锁的定义](#1. 死锁的定义)
    • [2. 常见的死锁场景](#2. 常见的死锁场景)
      • [2.1 单goroutine操作无缓冲Channel](#2.1 单goroutine操作无缓冲Channel)
      • [2.2 Channel只发不收/只收不发](#2.2 Channel只发不收/只收不发)
      • [2.3 缓冲区满且无接收方](#2.3 缓冲区满且无接收方)
    • [3. 避免死锁的核心原则](#3. 避免死锁的核心原则)
  • 五、定向Channel
    • [1. 定向Channel的定义](#1. 定向Channel的定义)
    • [2. 定向Channel的使用示例](#2. 定向Channel的使用示例)
    • [3. 定向Channel的使用场景](#3. 定向Channel的使用场景)
  • 六、select多路复用
    • [1. select的基本语法](#1. select的基本语法)
    • [2. select的核心规则](#2. select的核心规则)
    • [3. select的使用示例](#3. select的使用示例)
      • [3.1 监听多个Channel](#3.1 监听多个Channel)
      • [3.2 超时控制](#3.2 超时控制)
  • 七、Channel的应用场景
    • [1. goroutine间通信](#1. goroutine间通信)
    • [2. 生产者-消费者模式](#2. 生产者-消费者模式)
    • [3. 工作池模式](#3. 工作池模式)
    • [4. 超时控制](#4. 超时控制)
  • 八、Timer定时器与Channel
    • [1. Timer定时器](#1. Timer定时器)
    • [2. Ticker打点器](#2. Ticker打点器)
    • [3. time.After](#3. time.After)
  • 总结

前言:

上期内容为大家带来了Go语言goroutine协程的知识点学习,这期内容我将为大家带来Go语言中核心的并发通信机制------Channel通道的学习,这部分内容是实现goroutine之间安全通信的关键,也是Go语言"不要通过共享内存来通信,而要通过通信来共享内存"设计理念的核心体现。

一、Channel通道初识

1. Channel是什么?

Channel(通道)是Go语言中各个并发执行体(goroutine)之间的通信机制,我们可以把它理解成goroutine之间通信的"管道",就像生活中水管传递水一样,Channel可以在goroutine之间传递数据。

Channel是类型相关的,也就是说一个Channel只能传递指定类型的数据,比如传递int类型的Channel就不能传递string类型的数据。并且Channel的核心作用是在多个goroutine之间传递数据和同步执行,解决多goroutine并发访问数据的安全问题。

2. Channel的定义与创建

2.1 定义Channel

在Go语言中,定义Channel的语法格式如下:

go 复制代码
var 通道名 chan 数据类型

比如定义一个传递int类型数据的Channel:

go 复制代码
var ch chan int

此时定义的ch是一个Channel类型的变量,默认值为nil,还不能直接使用,需要通过make函数初始化。

2.2 创建Channel

创建Channel必须使用make函数,语法格式:

go 复制代码
通道名 = make(chan 数据类型, [缓冲区大小])

根据是否设置缓冲区,Channel分为两种:

  • 无缓冲Channel:不指定缓冲区大小,缓冲区容量为0
go 复制代码
// 无缓冲Channel
var ch1 chan int
ch1 = make(chan int)
// 简化写法
ch1 := make(chan int)
  • 有缓冲Channel:指定缓冲区大小,缓冲区容量为指定的数值
go 复制代码
// 有缓冲Channel,容量为10
var ch2 chan int
ch2 = make(chan int, 10)
// 简化写法
ch2 := make(chan int, 10)

2.3 最简单的Channel使用示例

我们先通过一个简单示例感受Channel的基本使用,实现子goroutine向主goroutine传递消息:

go 复制代码
package main

import (
   "fmt"
   "time"
)

func main() {
   // 定义并初始化一个bool类型的无缓冲Channel
   var ch chan bool
   ch = make(chan bool)

   // 启动一个子goroutine
   go func() {
      for i := 0; i < 10; i++ {
         fmt.Println("goroutine-", i)
      }
      // 模拟耗时操作
      time.Sleep(time.Second * 3)
      // 向Channel中发送数据,告知主goroutine子goroutine执行完成
      ch <- true
   }()

   // 从Channel中接收数据,主goroutine会阻塞等待,直到有数据传入
   data := <-ch
   fmt.Println("ch data:", data)
}

从结果可以看到,主goroutine会一直等待子goroutine执行完成并向Channel发送数据后,才会继续执行。

二、Channel的核心操作

Channel有三个核心操作:发送数据、接收数据、关闭通道,下面我们分别讲解。

1. 发送数据

向Channel发送数据的语法格式:

go 复制代码
通道名 <- 数据

其中<-是Channel的发送运算符,数据会被发送到Channel中。

go 复制代码
// 向ch1发送int类型数据42
ch1 := make(chan int)
ch1 <- 42

注意:发送数据的类型必须和Channel定义的类型一致,否则会编译报错。

2. 接收数据

从Channel接收数据有两种常见写法:

2.1 基础接收写法

go 复制代码
变量 := <-通道名

<-是Channel的接收运算符,会从Channel中取出数据并赋值给变量。

go 复制代码
ch1 := make(chan int)
// 先启动goroutine发送数据,避免死锁
go func() {
   ch1 <- 42
}()
// 接收数据
value := <-ch1
fmt.Println("接收的数据:", value) // 输出:接收的数据:42

2.2 带状态判断的接收写法

go 复制代码
变量, ok := <-通道名

这种写法可以判断Channel是否已经关闭且没有数据:

  • ok为true:表示成功接收到数据
  • ok为false:表示Channel已经关闭且缓冲区中没有数据
go 复制代码
ch1 := make(chan int)
go func() {
   ch1 <- 42
   close(ch1) // 关闭通道
}()
// 第一次接收
value1, ok1 := <-ch1
fmt.Println("value1:", value1, "ok1:", ok1) 
// 第二次接收(通道已关闭且无数据)
value2, ok2 := <-ch1
fmt.Println("value2:", value2, "ok2:", ok2) 

3. 关闭通道

关闭Channel使用close函数,语法格式:

go 复制代码
close(通道名)

关闭通道后有以下特性:

  1. 不能再向关闭的Channel发送数据,否则会panic
  2. 可以继续从关闭的Channel接收数据,直到缓冲区数据全部取完
  3. 从已关闭且无数据的Channel接收数据,会得到该类型的零值,且ok为false

3.1 关闭通道示例

go 复制代码
package main

import (
   "fmt"
   "time"
)

func test7(ch chan int) {
   for i := 0; i < 10; i++ {
      ch <- i // 向通道发送数据
   }
   close(ch) // 发送完数据后关闭通道
}

func main() {
   ch1 := make(chan int)
   go test7(ch1) // 启动goroutine发送数据

   // 循环读取通道数据
   for {
      time.Sleep(time.Second)
      data, ok := <-ch1
      if !ok {
         fmt.Println("读取完毕", ok)
         break
      }
      fmt.Println("ch1 data:", data)
   }
}

3.2 for-range简化读取

Go语言提供了for-range语法,可以简化Channel的读取操作,它会自动判断Channel是否关闭,当Channel关闭且数据读取完毕后,循环会自动退出:

go 复制代码
package main

import (
   "fmt"
   "time"
)

func test7(ch chan int) {
   for i := 0; i < 10; i++ {
      ch <- i
   }
   close(ch)
}

func main() {
   ch1 := make(chan int)
   go test7(ch1)

   // for-range读取通道数据
   for data := range ch1 {
      time.Sleep(time.Second)
      fmt.Println("ch1 data:",data)
   }
   fmt.Println("读取完毕")
}

运行结果和上面的示例一致,代码更加简洁。

三、无缓冲Channel与有缓冲Channel的区别

这是Channel最核心的知识点之一,我们从特性、阻塞规则、使用场景三个维度详细讲解。

1. 核心特性对比

特性 无缓冲Channel 有缓冲Channel
缓冲区容量 0 指定数值(大于0)
通信模型 同步通信 异步通信
数据存储 无存储,直接传递 先存储在缓冲区,按需读取

2. 阻塞规则

2.1 无缓冲Channel

无缓冲Channel也叫同步Channel,发送和接收操作必须同时准备好:

  • 发送数据时:会阻塞当前goroutine,直到有其他goroutine从该Channel接收数据
  • 接收数据时:会阻塞当前goroutine,直到有其他goroutine向该Channel发送数据

简单理解:无缓冲Channel就像两个人面对面传球,传球的人(发送)必须等接球的人(接收)准备好,否则传球的人只能一直举着球等待。

2.2 有缓冲Channel

有缓冲Channel也叫异步Channel,阻塞规则和缓冲区状态相关:

  • 发送数据时:只有当缓冲区满了,发送操作才会阻塞;缓冲区未满时,发送操作不会阻塞
  • 接收数据时:只有当缓冲区空了,接收操作才会阻塞;缓冲区有数据时,接收操作不会阻塞

简单理解:有缓冲Channel就像快递柜,快递员(发送方)把包裹放进柜子(缓冲区),不用等收件人(接收方)当场取;只有柜子满了,快递员才需要等待;收件人随时可以取,只有柜子空了才需要等待。

3. 示例对比

3.1 无缓冲Channel示例

go 复制代码
package main

import "fmt"

func main() {
   ch := make(chan int) // 无缓冲Channel

   // 启动goroutine接收数据
   go func() {
      fmt.Println("接收方准备接收")
      value := <-ch
      fmt.Println("接收方收到数据:", value)
   }()

   fmt.Println("发送方准备发送")
   ch <- 100 // 无缓冲,会阻塞直到接收方准备好
   fmt.Println("发送方发送完成")
}

3.2 有缓冲Channel示例

go 复制代码
package main

import "fmt"

func main() {
   ch := make(chan int, 3) // 有缓冲Channel,容量3

   fmt.Println("初始状态:cap=", cap(ch), "len=", len(ch)) // cap=3 len=0
   ch <- 1
   fmt.Println("发送1后:cap=", cap(ch), "len=", len(ch)) // cap=3 len=1
   ch <- 2
   ch <- 3
   fmt.Println("发送3后:cap=", cap(ch), "len=", len(ch)) // cap=3 len=3

   // 缓冲区已满,再发送会阻塞
   // ch <- 4 // 取消注释会导致死锁

   // 接收数据
   value := <-ch
   fmt.Println("接收数据:", value) // 接收数据:1
   fmt.Println("接收后:cap=", cap(ch), "len=", len(ch)) // cap=3 len=2
}

四、Channel的死锁问题

死锁是使用Channel时最容易踩的坑,Go程序运行时会直接panic,提示fatal error: all goroutines are asleep - deadlock!

1. 死锁的定义

当所有goroutine都处于阻塞状态,没有任何一个goroutine可以继续执行,程序就会发生死锁。

2. 常见的死锁场景

2.1 单goroutine操作无缓冲Channel

go 复制代码
package main

func main() {
   ch := make(chan int)
   ch <- 10 // 死锁!只有main一个goroutine,发送后阻塞,无接收方
}

2.2 Channel只发不收/只收不发

go 复制代码
package main

func main() {
   ch := make(chan int)
   go func() {
      ch <- 10 // 子goroutine发送数据后阻塞,无接收方
   }()
   // main goroutine直接退出,子goroutine永久阻塞,导致死锁
}

2.3 缓冲区满且无接收方

go 复制代码
package main

func main() {
   ch := make(chan int, 2)
   ch <- 1
   ch <- 2
   ch <- 3 // 缓冲区满,无接收方,死锁
}

3. 避免死锁的核心原则

  • 无缓冲Channel必须保证发送和接收操作在不同goroutine中
  • 有缓冲Channel要控制发送数据量,避免缓冲区满且无接收方
  • 发送方负责关闭Channel,接收方通过for-range或ok判断处理关闭状态
  • 复杂场景可以配合select+超时机制避免永久阻塞

五、定向Channel

默认的Channel是双向的,既可以发送数据也可以接收数据。Go语言还支持定向Channel,即只能发送或只能接收的Channel,主要用于函数参数约束,避免Channel滥用

1. 定向Channel的定义

  • 只写Channel:chan<- 数据类型,只能发送数据,不能接收数据
  • 只读Channel:<-chan 数据类型,只能接收数据,不能发送数据

2. 定向Channel的使用示例

go 复制代码
package main

import (
   "fmt"
   "time"
)

// 只写Channel作为函数参数,限制函数只能发送数据
func writeOnly(ch chan<- int) {
   ch <- 100 // 合法:发送数据
   // value := <-ch // 非法:不能从只写Channel接收数据,编译报错
}

// 只读Channel作为函数参数,限制函数只能接收数据
func readOnly(ch <-chan int) int {
   data := <-ch // 合法:接收数据
   fmt.Println("只读函数接收数据:", data)
   // ch <- 200 // 非法:不能向只读Channel发送数据,编译报错
   return data
}

func main() {
   ch1 := make(chan int) // 双向Channel

   go writeOnly(ch1)    // 双向Channel自动转换为只写Channel
   go readOnly(ch1)     // 双向Channel自动转换为只读Channel

   time.Sleep(time.Second * 3)
}

3. 定向Channel的使用场景

  • 函数参数约束:明确函数对Channel的操作权限,避免误操作
  • 团队协作:规范Channel的使用方式,提高代码可读性和可维护性
  • 大型项目:减少因Channel滥用导致的bug

六、select多路复用

select是Go语言中专门用于处理Channel操作的关键字,可以同时监听多个Channel的发送/接收操作,实现多路复用。

1. select的基本语法

go 复制代码
select {
case 通道操作1:
   // 操作1就绪时执行的逻辑
case 通道操作2:
   // 操作2就绪时执行的逻辑
default:
   // 所有通道操作都未就绪时执行的逻辑
}

2. select的核心规则

  1. 每个case必须是Channel的发送或接收操作
  2. 多个case同时就绪时,随机选择一个执行(不是按顺序)
  3. 没有case就绪且有default时,执行default逻辑,不阻塞
  4. 没有case就绪且无default时,阻塞直到某个case就绪

3. select的使用示例

3.1 监听多个Channel

go 复制代码
package main

import (
   "fmt"
   "time"
)

func main() {
   ch1 := make(chan int)
   ch2 := make(chan int)

   go func() {
      time.Sleep(time.Second * 2)
      ch1 <- 100
   }()

   go func() {
      time.Sleep(time.Second * 1)
      ch2 <- 200
   }()

   // 监听两个Channel
   select {
   case num1 := <-ch1:
      fmt.Println("从ch1接收:", num1)
   case num2 := <-ch2:
      fmt.Println("从ch2接收:", num2)
   // default:
   //    fmt.Println("default")
   }
}

3.2 超时控制

结合time.After实现Channel操作的超时控制,避免永久阻塞:

go 复制代码
package main

import (
   "fmt"
   "time"
)

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

   // 子goroutine 3秒后发送数据
   go func() {
      time.Sleep(time.Second * 3)
      ch <- 100
   }()

   // 只等待1秒,超时则执行default
   select {
   case value := <-ch:
      fmt.Println("成功接收:", value)
   case <-time.After(time.Second * 1):
      fmt.Println("接收超时!")
   }
}

七、Channel的应用场景

Channel是Go语言并发编程的核心,常见的应用场景有:

1. goroutine间通信

最基础的场景,实现goroutine之间的数据传递和同步,比如我们前面的示例。

2. 生产者-消费者模式

这是最经典的并发模式,生产者goroutine生成数据并发送到Channel,消费者goroutine从Channel读取数据并处理。

go 复制代码
// 编程实例:电商订单处理 (生产者---消费者模型)

type Order struct {
	ID        int
	UserID    string
	amount    float64
	status    string
	createdAt time.Time
}

// 产生订单 --- 生产者
func orderProduct(orderChan chan Order, number int) {
	defer close(orderChan)
	for i := 0; i < number; i++ {
		order := Order{
			ID:        i,
			UserID:    fmt.Sprintf("user_%d", rand.Intn(100)),
			amount:    rand.ExpFloat64() * 1000,
			status:    "pending",
			createdAt: time.Now(),
		}
		orderChan <- order
		fmt.Printf("生成订单:ID=%d,用户ID=%s,金额=%.2f\n", order.ID, order.UserID, order.amount)
		time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
	}
}

// 处理订单 --- 消费者
// <-chan 只接收类型为Order的channel
// chan<- 只发送类型为Order的channel
func orderProcessor(orderChan <-chan Order, resultChan chan<- Order) {
	defer close(resultChan)
	for order := range orderChan {
		fmt.Printf("处理订单:ID=%d,用户ID=%s,金额=%.2f\n", order.ID, order.UserID, order.amount)
		time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
		order.status = "completed"
		resultChan <- order
	}

}

// 收集处理结果 --- 消费者
func orderResultCollector(resultChan <-chan Order, done chan<- struct{}) {
	for order := range resultChan {
		fmt.Printf("订单处理完成:ID=%d,用户ID=%s,金额=%.2f,状态=%s\n", order.ID, order.UserID, order.amount, order.status)
	}
	done <- struct{}{}
}
func main() {
	rand.Seed(time.Now().UnixNano())

	// 创建管道
	orderChan := make(chan Order, 10)
	resultChan := make(chan Order, 10)
	done := make(chan struct{})

	// 启动订单生成器
	go orderProduct(orderChan, 20)

	// 启动多个订单处理器(工人)
	for i := 1; i <= 3; i++ {
		go orderProcessor(orderChan, resultChan)
	}

	// 启动结果收集器
	go orderResultCollector(resultChan, done)

	// 等待所有处理完成
	<-done
}

3. 工作池模式

通过Channel控制goroutine的数量,实现任务的批量处理,避免创建过多goroutine导致资源耗尽。

go 复制代码
package main

import (
   "fmt"
   "time"
)

// 工作goroutine
func worker(id int, jobs <-chan int, results chan<- int) {
   for j := range jobs {
      fmt.Printf("Worker %d 处理任务 %d\n", id, j)
      time.Sleep(time.Second)
      results <- j * 2
   }
   close(results)
}

func main() {
   jobs := make(chan int, 100)
   results := make(chan int, 100)

   // 启动4个worker
   for w := 0; w <= 3; w++ {
      go worker(w, jobs, results)
   }

   // 发送100个任务
   for j := 0; j < 100; j++ {
      jobs <- j
   }
   close(jobs)

   // 收集结果
   for value := range results {
	   fmt.Println("result:", value)
   }
}

4. 超时控制

结合select和time.After实现操作的超时控制,比如网络请求、数据库查询等,模板示例:

go 复制代码
package main

import (
	"fmt"
	"time"
)

var done = make(chan struct{})

func event() {
	fmt.Println("event执行开始")
	time.Sleep(4 * time.Second) //4秒
	fmt.Println("event执行结束")
	close(done)
}
func main() {

	go event()

	select {
	case <-done:
		fmt.Println("协程执行完毕")
	case <-time.After(3 * time.Second):
		fmt.Println("超时")
		return
	}
}

八、Timer定时器与Channel

Go语言的time包中的定时器(Timer)和打点器(Ticker)都是基于Channel实现的,是Channel的重要应用场景。

1. Timer定时器

Timer表示一个单次的定时事件,时间到了会向Channel发送当前时间。

go 复制代码
package main

import (
   "fmt"
   "time"
)

func main() {
   // 创建定时器,3秒后触发
   timer := time.NewTimer(time.Second * 3)
   fmt.Println("当前时间:", time.Now())

   // 等待定时器触发
   t := <-timer.C
   fmt.Println("定时器触发:", t)

   // 提前停止定时器(注释上面的<-timer.C才能测试)
   // timer.Stop()
   // fmt.Println("定时器已停止")
}

2. Ticker打点器

Ticker表示一个重复的定时事件,每隔指定时间就会向Channel发送当前时间。

go 复制代码
package main

import (
   "fmt"
   "time"
)

func main() {
   // 创建打点器,每隔500毫秒触发一次
   ticker := time.NewTicker(500 * time.Millisecond)
   done := make(chan bool)

   go func() {
      for {
         select {
         case <-done:
            return
         case t := <-ticker.C:
            fmt.Println("定时触发 at", t.Format("15:04:05"))
         }
      }
   }()

   // 运行2秒后停止
   time.Sleep(2 * time.Second)
   ticker.Stop()
   done <- true
   fmt.Println("Ticker停止")
}

3. time.After

time.After是Timer的简化版,返回一个Channel,指定时间后会向该Channel发送当前时间。

go 复制代码
package main

import (
   "fmt"
   "time"
)

func main() {
   // 3秒后向Channel发送时间
   afterChan := time.After(time.Second * 3)
   fmt.Println("等待3秒...")
   t := <-afterChan
   fmt.Println("3秒到了:", t)

   // 延迟执行函数
   time.AfterFunc(time.Second*2, func() {
      fmt.Println("2秒后执行的函数")
   })
   time.Sleep(time.Second * 3)
}

总结

Channel作为Go语言并发编程的核心组件,完全贯彻了"通过通信来共享内存"的设计思想,让goroutine之间的数据传递和同步变得更加简洁安全。

好了,感谢大家的支持,这期的Channel内容就先到这里了,如果有讲解不到位的地方,欢迎大家在评论区指正!

相关推荐
zlpzpl1 小时前
Java总结进阶之路 (基础二 )
java·开发语言·python
xyq20242 小时前
Chart.js 折线图深入解析与使用指南
开发语言
Evand J2 小时前
【UWB与IMU紧耦合定位,MATLAB例程】UWB的TOA定位方法,与IMU紧耦合,对目标轨迹定位并输出误差统计。适用于二维平面的高精度定位导航
开发语言·matlab·平面·uwb·组合导航
Tony Bai2 小时前
Go 1.26 中值得关注的几个变化:从 new(expr) 真香落地、极致性能到智能工具链
开发语言·后端·golang
焦糖夹心2 小时前
python中,怎么同时输出字典的键和值?
开发语言·python
only-lucky2 小时前
Qt惯性动画效果
开发语言·qt
冬夜戏雪2 小时前
线性池java demo
java·开发语言
强子感冒了2 小时前
JavaScript 零基础入门笔记:核心概念与语法详解
开发语言·javascript·笔记
wuqingshun3141592 小时前
String、StringBuffer、StringBuilder的应用场景
java·开发语言·jvm