Go语言Channel深度理解

文章目录

    • Channel 通道的使用
      • 定义通道
      • 初始化通道
      • 通道的操作
        • 1、发送/接收
        • 2、关闭
      • 多返回值模式
      • for range获取通道值
    • 单向通道
    • select
    • goroutine、channel案例

道阻且长,行则将至,行而不辍,未来可期。人生是一条且漫长且充满荆棘的道路,一路上充斥着各种欲望与诱惑,不断学习,不断修炼,不悔昨日,不畏将来!
GO语言也被称为21世纪的C语言,在开发与性能效率上都占据优势(Python+C)。让我们一起来了解这门语言的魅力吧,希望这篇能够带给你们或多或少的帮助!!

在这里插入图片描述

Channel 通道的使用

通常一些语言是可以通过共享内存来实现不同线程间的数据交换,但是容易出现数据不正确的问题,一般需要通过互斥锁来确保数据的安全性,这时候就会出现性能问题

抛出问题

  1. 什么是Channel?
  2. 为什么要用Channel?
  3. 什么时候用Channel?

Go语言采用的是并发模型是(CSP),提倡"通过通信共享内存",而不是"通过共享内存实现通信",如果说goroutine是Go程序并发的执行体,那么channel则是它们之前的连接。channel是可以让一个goroutine发送一个特定的值到另外一个goroutine的通信机制

  • 通过共享内存实现通信
  • 通过通信共享内存共享(Go语言并发通信)

我们也可以理解:channel是G之间的通信的通道

定义通道

通道是chan类型的,并且在定义通道时需要指定通道传递的数据类型

var ch chan int // 定义一个可传递int类型的通道

未初始化的channel,默认值是nil

初始化通道

需要初始化之后才能使用,通道类型需要通过内置函数"make"进行初始化才能使用

make(chan type[缓冲区大小])

缓冲区:通道能存储的元素数量(类似数组),如果缓冲区已经满了,这时候还在往里面发送数据,则会进入阻塞状态,除非此时有其它G把缓冲区内的数据取走了,长时间阻塞则panic

通道的操作

通信共有三种操作:发送(send)、接收(receive)、关闭(close),其中发送和接收都是通过:<-符号实现的

1、发送/接收
ch := make(chan int)
ch <- 10 // 通道在前面表示,发送数据到通道ch内

注意:如果定义的通道没有缓冲区,那么则需要其它goroutine准备好读取通道的值,不然的话会出现 deadlock!异常。所以上面的程序会Panic

发送实例1、带缓冲区的发送

ch := make(chan int, 10)
ch <- 10
t := <- ch
fmt.Println(t)

打印结果

10

发送实例2、无缓存区发送,但是定义一个goroutine准备接收

ch := make(chan int)
	
go func() {
  fmt.Println(<- ch)
}()

ch <- 10
2、关闭
ch := make(chan int, 10)

close(ch)

注意:通常会在所有数据传输、接收完成关闭通道,但需要注意的是通道不是必须关闭的,它可以被垃圾回收,如果是文件的话需要手动关闭,而通道则不是必须的

关闭通道需要注意的问题:

  1. 通道关闭后,发送值会panic
  2. 通道关闭后仍然可以取值(设计主要是为了避免通道内的数据未取完),但如果通道没有值的话会获取对应类型的零值
  3. 关闭一个关闭的通道会panic
ch := make(chan int, 10)

go func() {
  fmt.Println(<-ch)
  // 通道关闭后,只能打印对应类型的零值,这里int类型会打印0
}()

close(ch)

time.Sleep(1000)

多返回值模式

当通道关闭,获取完通道的值后,再次获取也只能获取到零值,所以这时候需要进行判断,如果通道内没有值了,我们则不再进行获取

对一个通道进行接收操作时,支持如下多返回值模式

value, ok := <- ch
  • Value:从通道内获取的数据
  • ok:本次是否从通道内获取数据,true or false
ch := make(chan int)

go func() {
  v1,ok := <- ch
  fmt.Println(v1, ok)

  v2,ok := <- ch
  fmt.Println(v2, ok)
}()

ch <- 10

close(ch)

time.Sleep(3000)

打印结果:

10 true
0 false

for range获取通道值

我们可以通过for range来获取通道内的所有值,而不是通过for {}配合value,ok的方式

func main(){
  ch := make(chan int, 5)
  ch <- 1
  ch <- 2
  ch <- 3

  go f3(ch)

  time.Sleep(time.Second * 3)
}

func f3(c chan int)  {
	for t := range c{
		fmt.Println(t)
	}
}

打印结果:for range会在通道内值获取完毕后结束

1
2
3

我们可以通过len(ch)获取通过内的元素数量

range针对通道属于阻塞式取值,如果没有数据的话就会一直阻塞获取,长时间阻塞的话会被go语言判断成死锁(deadlock)从而panic

只有close掉了通道range才会结束,但是上面并没有close通道,能够正常运行的原因是因为在goroutine内执行的,实际上它已经产生阻塞了,但是由于我们主程序结束了,所以goroutine内的阻塞我们并没有感知到,如下代码示例:

package main

import (
	"fmt"
	"time"
)

func main(){
	ch := make(chan int, 5)
	ch <- 1
	ch <- 2
	ch <- 3

	go f3(ch)

	time.Sleep(time.Second * 3)
}

func f3(c chan int)  {
	for t := range c{
		fmt.Println(t)
	}

	fmt.Println(1111)
}

上面程序打印:

1
2
3

并没有打印1111,这是因为range读取通道的时候已经阻塞了,但是在主程序sleep时间过后整个程序结束了,所以我们没有感觉到for range读取通道数据这种方式有什么问题。

如果想要for range能够正常读取通道数据,我们需要在确认通道数据发送完毕后close

package main

import (
	"fmt"
	"time"
)

func main(){
	ch := make(chan int, 5)
	ch <- 1
	ch <- 2
	ch <- 3
	close(ch) // 关闭通道


	go f3(ch)

	time.Sleep(time.Second * 3)
}

func f3(c chan int)  {
	for t := range c{
		fmt.Println(t)
	}

	fmt.Println(1111)
}

打印结果:

1
2
3
1111

单向通道

一种要么只能从里面读取数据,要么只能往里面写入数据的通道

通常我们会将通道作为参数在函数之间传递,一般情况需要限制通道在不同地方的使用,比如:某些函数只能进行读取数据的操作,或者发送数据的操作。

比较景经典的就是生产者与消费者模型:生产者(Producer)往通道里面写入数据、消费者(Consumer)从通道内读取数据消费。Consumer是通过Producer返回的单向通道进行数据读取。
在其它函数,例如Python内,线程间通信可以通过Queue实现

Go语言层面提供单向通道来限制通道的使用场景

  • <- chan int 只能读取数据的通道,不能发送数据
  • chan <- int 只能写入数据的通道,不能读取数据

我们定义一个Producer用于返回一个单向通道

// Producer 函数返回了一个只能读取数据的单向通道
func Producer() <-chan int {
	ch := make(chan int, 10)

	go func() {
		for i := 0; i < 10; i++ {
			if i%2 == 1 {
				ch <- i
			}
		}
		close(ch)
    // 数据写入完成后关闭通道;虽然关闭掉通道任然可以实现类似单向通道的效果,但是并没有从代码层面去限制通道是否属于单向通道,只会再使用的时候察觉,这种写法并不规范
	}()
  	// 这里关闭通道是为了for range能够从通道内取完数据后不阻塞


	// 上面创建了一个goroutine去往通道内写入值,返回该函数是先返回通道,然后goroutine往里面写入值(goroutine的执行需要时间,不影响后面代码继续执行)

	return ch
}

定义一个Comsumer从只读通道内获取数据

// Consumer 消费者,从通过内获取数据计算并返回结果并
func Consumer(ch <-chan int) int {

	sum := 0
	for {
		sum += <- ch
	}

	return sum
}
func main() {
  // 定义通道类型的变量名最好见名知义(起码知道它是一个通道类型)
	vCh := Producer()

	fmt.Println(Consumer(vCh))
}

执行结果:

1
3
5
7
9 

不同通道状态的对应的操作结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o2DycDtK-1689153452030)(/Users/apple/Library/Application Support/typora-user-images/image-20230530203151606.png)]

注意:对已经关闭的通道close会panic

select

select类似于switch方法,它也有一系列的case分支和default分之。不同的是,select里面的case是通道的通信(接收或者发送)过程。直到某个case的通信操作完成后,才会执行对应case分支的语句

select语句具备以下特点

  1. 可以处理一个或多个channel的发送/接收操作
  2. 如果多个case满足,会随机执行一个case的分支语句(真的随机)
  3. 对于没有case的select会一直阻塞,可用于阻塞main函数,防止退出(目的是为了让程序内的其它goroutine持续运行)
ch1 := make(chan int, 10)
ch2 := make(chan int, 10)

ch1 <- 10
ch1 <- 20

select {
case data := <-ch1:
  // 从ch1取值成功后执行
  fmt.Println("第一个case,ch1的数据", data)
case data := <-ch1:
  // 从ch1取值成功后执行
  fmt.Println("第二个case,ch1的数据", data)
case ch2 <- 10:
  // 往ch2发送值成功后执行
  fmt.Println("ch2发送了数据")
}

上面属于3个case都满足的情况,那么会随机执行一个case的代码块

1、猜测下面程序打印结果

ch := make(chan int, 1)
for i := 1; i < 10; i++ {
		select {
		case data := <-ch:
			fmt.Println(data)
		case ch <- i:
		}
	}

程序解析

注意观察ch缓冲区大小,第一次i等于1的时候,缓冲区没有值,所以第一个case不行执行,第二个case执行了,往里面存放了数据1
第二次i=2,如果缓冲区再大一些,那么两个case都能满足则会随机执行一个,但是由于缓冲区已经满了,所以第二个case执行不了,那么则执行第一个case打印:1
第三次i=3:重复第一次的行为,缓冲区无数据,执行第二个case
第四次i=4:重复第二次的行为,缓冲区有无数据,无法执行第二个case,所以执行第一个读取

所以打印结果是:1、3、5、7

2、猜测下面程序打印结果

ch2 := make(chan string)

go func() {
  time.Sleep(3 * time.Second)
  ch2 <- "123"
}()

select {
case result := <-ch2:
  fmt.Println(result)
case <-time.After(time.Second):
  return
}

程序解析:

解析:上面第一个case是接收ch2通道的数据,但是由于ch2通道在一个goroutine里面,3s后才会写入数据,而第二个case属于一个定时时间,我们设置的定时时间为1s,所以,第二个会先执行,第一个case只有通道写入数据才会执行。
上面程序不会打印任何结果就结束了,并且会操作goroutine内存泄露,因为select执行完后,goroutine内写入到通道的数据由于没有接收,并且没有定义缓冲区,所以会一直阻塞导致goroutine无法结束(如果我们这个程序一直属于运行状态,当前main函数测试不会出现这种情况)

select也可以用于定义超时时间,我们可以设置在一定时间内没有获取到通道内的值,则退出

goroutine、channel案例

使用 goroutine和channel 实现一个计算int64随机数各位数和的程序,例如生成随机数61345,计算其每个位数上的数字之和为19。

  1. 开启一个 goroutine 循环生成int64类型的随机数,发送到jogChan
  2. 开启24个 goroutine 从jobChan中取出随机数计算各位数的和,将结果发送到resultChan
  3. 主 goroutine 从resultChan取出结果并打印到终端输出

程序实现

// 随机数生成,写入通道
func generateNumber(c chan<- int64) {

	for {
		rand.Seed(time.Now().UnixNano())

		select {
		case c <- rand.Int63n(1000000):
		default:
			time.Sleep(time.Microsecond * 100)

		}
	}
}

func resultCount(jogChan <-chan int64, resultChan chan<- int64) {
	// 开启24个goroutine
	for i := 0; i < 24; i++ {
		go countNum19(jogChan, resultChan)
	}
}

// 计算每个单数相加之和为19发送到通道
func countNum19(jogChan <-chan int64, resultChan chan<- int64) {
	for {
		select {
		case num := <-jogChan:
			if res := sum(num); res > 0 {
				resultChan <- res
			}
		default:
			time.Sleep(time.Microsecond)
		}
	}
}

func sum(num int64) int64 {
	var res int64

	result := num
	
  // 计算每个单数的结果
	for result > 0 {
		res += result % 10
		result /= 10
	}
  
	if res == 19 {
		fmt.Println(num)
		return num
	}

	return 0
}

执行结果:每个数字的个数加起来==19

2166031
3202057
4105045
1041715
510094
2060164
2504413
370108
3124360
1511515
3324043
332236
5331106
9310105
4813201
3220093
4202902
2421703
2130382
5007160
3074221
1022338
1321714
1373005
1413730
2138113
6371200
5114035

你可能感兴趣的:(Golang精进之路,golang,后端)