道阻且长,行则将至,行而不辍,未来可期。人生是一条且漫长且充满荆棘的道路,一路上充斥着各种欲望与诱惑,不断学习,不断修炼,不悔昨日,不畏将来!
GO语言也被称为21世纪的C语言,在开发与性能效率上都占据优势(Python+C)。让我们一起来了解这门语言的魅力吧,希望这篇能够带给你们或多或少的帮助!!
通常一些语言是可以通过共享内存来实现不同线程间的数据交换,但是容易出现数据不正确的问题,一般需要通过互斥锁来确保数据的安全性,这时候就会出现性能问题
抛出问题
Go语言采用的是并发模型是(CSP),提倡"通过通信共享内存",而不是"通过共享内存实现通信",如果说goroutine是Go程序并发的执行体,那么channel则是它们之前的连接。channel是可以让一个goroutine发送一个特定的值到另外一个goroutine的通信机制
我们也可以理解:channel是G之间的通信的通道
通道是chan
类型的,并且在定义通道时需要指定通道传递的数据类型
var ch chan int // 定义一个可传递int类型的通道
未初始化的channel,默认值是nil
需要初始化之后才能使用,通道类型需要通过内置函数"make"进行初始化才能使用
make(chan type,[缓冲区大小])
缓冲区:通道能存储的元素数量(类似数组),如果缓冲区已经满了,这时候还在往里面发送数据,则会进入阻塞状态,除非此时有其它G把缓冲区内的数据取走了,长时间阻塞则panic
通信共有三种操作:发送(send)、接收(receive)、关闭(close),其中发送和接收都是通过:<-符号实现的
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
ch := make(chan int, 10)
close(ch)
注意:通常会在所有数据传输、接收完成关闭通道,但需要注意的是通道不是必须关闭的,它可以被垃圾回收,如果是文件的话需要手动关闭,而通道则不是必须的
关闭通道需要注意的问题:
ch := make(chan int, 10)
go func() {
fmt.Println(<-ch)
// 通道关闭后,只能打印对应类型的零值,这里int类型会打印0
}()
close(ch)
time.Sleep(1000)
当通道关闭,获取完通道的值后,再次获取也只能获取到零值,所以这时候需要进行判断,如果通道内没有值了,我们则不再进行获取
对一个通道进行接收操作时,支持如下多返回值模式
value, ok := <- ch
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 {}
配合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语言层面提供单向通道来限制通道的使用场景
我们定义一个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类似于switch方法,它也有一系列的case分支和default分之。不同的是,select里面的case是通道的通信(接收或者发送)过程。直到某个case的通信操作完成后,才会执行对应case分支的语句
select语句具备以下特点
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 实现一个计算int64随机数各位数和的程序,例如生成随机数61345,计算其每个位数上的数字之和为19。
程序实现
// 随机数生成,写入通道
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