GO学习之 通道(Channel)

GO系列

1、GO学习之Hello World
2、GO学习之入门语法
3、GO学习之切片操作
4、GO学习之 Map 操作
5、GO学习之 结构体 操作
6、GO学习之 通道(Channel)
7、GO学习之 多线程(goroutine)
8、GO学习之 函数(Function)
9、GO学习之 接口(Interface)
10、GO学习之 网络通信(Net/Http)
11、GO学习之 微框架(Gin)
12、GO学习之 数据库(mysql)
13、GO学习之 数据库(Redis)
14、GO学习之 搜索引擎(ElasticSearch)
15、GO学习之 消息队列(Kafka)
16、GO学习之 远程过程调用(RPC)
17、GO学习之 goroutine的调度原理
18、GO学习之 通道(nil Channel妙用)
19、GO学习之 同步操作sync包
20、GO学习之 互斥锁、读写锁该如何取舍
21、GO学习之 条件变量 sync.Cond
22、GO学习之 单例模式 sync.Once
23、GO 面试题总结一【面试官这样问】

前言

按照公司目前的任务,go 学习是必经之路了,虽然行业卷,不过技多不压身,依旧努力!!!
现在的互联网项目中,并发量是衡量一个项目的相当重要的指标,如果一个项目不能支持高并发,就好像一辆超跑用了一缸发动机,中看不中用,一点意义都没有啊。
那怎么支持高并发呢,首当其冲的肯定是多线程了,那多线程就势必会带来一个问题,数据的同步和安全!
当然此篇不说多线程,先说说支持多线程之间数据同步的 Channel。
虽然可以用共享内存进行数据交互,但是共享内存在不同的 goroutine 中很容易发生资源竞争的问题,所以为了保证数据交互的正确性,必须使用锁对内存进行加锁,但是这种做法就会带来性能问题了。
Go 语言的并发模型是 CSP(Communicating Sequential Processes),提倡通过通信共享内存实现线程间的数据交互。

一、Channel 简介

Channel(通道)是Go语言中用于在goroutine之间进行通信和同步的一种数据结构。见名知意,一边进来另一边出去,或者说通道就像一个队列,总是遵循 先入先出(First In Frist Out) 的规则,以便能保证顺序。
每个通道都是一个具体类型的管道,也就是在声明的时候需要指定元素的数据类型一样。

二、初始化通道

Channel 是一种引用类型,声明通道类型的格式如下:

var 变量名 chan 元素类型

package main

import "fmt"

func main() {
	// 声明一个空的channel
	var ch1 chan int
	fmt.Printf("初始化ch1: %+v\n", ch1)
	ch1 <- 1
	ch1 <- 2
	ch1 <- 3
	fmt.Printf("%+v\n", ch1)
}

运行结果:

PS D:\workspaceGo> go run channelTest.go
初始化ch1: <nil>
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send (nil chan)]:
main.main()
        D:/workspaceGo/channelTest.go:9 +0x73
exit status 2

在声明后的 ch1 接受元素是报错了,因为我们声明的 ch1 其实是一个 nil,向一个空的channel发送元素则会报错。
声明的通道需要使用 make() 函数初始化之后才能使用。

2.1 无缓冲通道

无缓冲通道是指在发送数据时,发送方会阻塞直到有其他 goroutine 同时准备好接收这个数据。
所以无缓存通道是同步的,保证了数据的准确传递和顺序,见下图:
无缓存通道

package main

import "fmt"

func main() {
	// 声明并且初始化channel
	ch1 := make(chan int)
	fmt.Printf("初始化ch1: %+v\n", ch1)
	ch1 <- 1
}

运行结果:

PS D:\workspaceGo> go run channelTest.go
初始化ch1: 0xc0000220c0
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        D:/workspaceGo/channelTest.go:9 +0x85
exit status 2

这里为什么会出现 deadlock 错误呢?
答:因为我们创建的是无缓冲通道,无缓冲的通道只有在有其他地方接受值的情况下才能发送值。上面的示例中阻塞在 ch1 <- 1 形成死锁,如何解决呢?我们可以用另一个 goroutine 去接受值,如下:

package main

import "fmt"

func main() {
	// 声明并且初始化channel
	ch1 := make(chan int)
	// 启动一个 gorountine从通道接收值
	go receive(ch1)
	ch1 <- 1
	ch1 <- 2
	ch1 <- 3
	fmt.Println("发送完成!")
}

func receive(c chan int) {
	for i := 0; i < 5; i++ {
		r := <-c
		fmt.Printf("接收到的值为:%+v\n", r)
	}
}

运行结果:

PS D:\workspaceGo> go run channelTest.go
接收到的值为:1
接收到的值为:2
接收到的值为:3
发送完成!

在这个示例中,单独启动一个 goruntine来循环从 ch1 通道中获取值,这样就可以通过 ch1 <- * 不断的往里面放值了。

2.2 有缓冲通道

有缓冲通道是指在创建时指定了一个固定的缓冲区大小,发送操作会在缓冲区未满时立即返回,接收操作会在缓冲区不为空时立即返回。
所以有缓冲通道是异步的,只有当缓冲区满时才会被阻塞,见下图:

有缓冲通道

可以在 make() 函数初始化通道的时候为其指定通道的容量,如下:

package main

import (
	"fmt"
	"time"
)

func main() {
	// 声明并且初始化channel
	ch1 := make(chan int, 5)
	go receive(ch1)
	for i := 0; i < 10; i++ {
		fmt.Printf("向ch1中发送 %d 个元素 \n", i)
		ch1 <- i
	}
	fmt.Println("发送完成!")
	ch1 <- 6
}

func receive(c chan int) {
	for i := 0; i < 10; i++ {
		time.Sleep(time.Second * 3)
		r := <-c
		fmt.Printf("接收到的值为:%+v\n", r)
	}
}

运行结果:

PS D:\workspaceGo> go run channelTest.go
向ch1中发送 0 个元素 
向ch1中发送 1 个元素
向ch1中发送 2 个元素
向ch1中发送 3 个元素
向ch1中发送 4 个元素
向ch1中发送 5 个元素
接收到的值为:0
向ch1中发送 6 个元素
接收到的值为:1
向ch1中发送 7 个元素
接收到的值为:2
向ch1中发送 8 个元素
接收到的值为:3
向ch1中发送 9 个元素
接收到的值为:4
发送完成!

初始化通道是制定缓存数量,可以往通道中放入初始化数量以内个数的元素,但是超过了初始化量则会阻塞,等通道中空闲出来

2.3 单向通道

在日常 coding 过程中,我们会将通道作为参数在多个任务之间进行传递,也会对任务函数中使用的通道进行限制,尤其是在函数中限制发送或者接受,那Go 中呢也提供了单向通道来解决这种问题。

package main

import (
	"fmt"
	"time"
)

func main() {
	// 声明并且初始化channel
	var ch1 = make(chan int, 10)
	var ch2 = make(chan int, 10)
	go initCh1(ch1)
	// 将 ch1 的数字传值给 ch2
	go receive(ch2, ch1)
	go printer(ch2)
	time.Sleep(time.Microsecond + 3)
}

// 向 ch1 通道放入值
func initCh1(ch1 chan<- int) {
	for i := 0; i < 10; i++ {
		ch1 <- i
	}
	close(ch1)
}

// 参数 out 声明为 chan<- int,是一个只发送不接受的通道
// 参数 in 声明为 <-chan int 是一个只接受不发送的通道
func receive(out chan<- int, in <-chan int) {
	for i := range in {
		out <- i
	}
	close(out)
}

// 打印 ch2 通道里面的值
func printer(ch2 <-chan int) {
	for i := range ch2 {
		fmt.Println(i)
	}
}

运行结果:

PS D:\workspaceGo> go run .\channelTest.go
0
1
2
...

小结:

  1. chan<- int是一个只能发送的通道,可以发送但是不能接收
  2. <-chan int是一个只能接收的通道,可以接收但是不能发送

在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。

三、通道操作

通道有三种操作:

  • 发送:上面例子中已包含
  • 接受:上面例子中已包含
  • 关闭

3.1 关闭通道

可以通过 close() 函数来关闭 channel(对通道的发送和接受完毕,记得关闭通道),如下:

package main
import (
	"fmt"
)
func main() {
	// 声明并且初始化channel
	ch1 := make(chan int, 1)
	go receive(ch1)
	for i := 0; i < 5; i++ {
		fmt.Printf("向ch1中发送 %d 个元素 \n", i)
		ch1 <- i
	}
	fmt.Println("发送完成!")
	// 关闭通道
	close(ch1)
}
func receive(c chan int) {
	for i := 0; i < 10; i++ {
		r, ok := <-c
		if !ok {
			fmt.Printf("接收到的值为:%+v\n", r)
		} else {
			fmt.Println("通道已关闭!")
		}
	}
}

运行结果:

PS D:\workspaceGo> go run channelTest.go
向ch1中发送 0 个元素 
向ch1中发送 1 个元素
通道已关闭!
向ch1中发送 2 个元素
通道已关闭!
向ch1中发送 3 个元素
通道已关闭!
向ch1中发送 4 个元素
通道已关闭!
发送完成!

初始化通道,缓冲为 1 ,当向 ch1 通道中发送元素后,就立马关闭 通道,在 receive() 函数中只能接受到部分元素。

四、通道异常情况

操作nil非空没满
接受deadlock接受成功阻塞接受成功接受成功
发送deadlock发送成功发送成功阻塞发送成功
关闭panic关闭成功,关闭后,接受到 0 值关闭成功,接收到 0 值关闭成功,接受到0值关闭成功,接受到0值

五、总结

通道(Channel)基本的初始化和发送、接受、关闭等操作基本在此篇中体现,使用起来还是简洁明了。不像 Java 还需要利用一下锁去控制线程之间的安全问题,虽说是对多线程有较好的支持,但是线程间的数据共享确实实现比较复杂,运用也不简单,需要去深挖。

现阶段还是对 Go 语言的学习阶段,想必有一些地方考虑的不全面,本文示例全部是亲自手敲代码并且执行通过,如有问题,还请指教。


评论去告诉我哦!!!一起学习一起进步!!!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值