21-interface
对于这样一段代码
虽然 dog 和 cat 是structure,但是一样的可以传参 interface {} 因为interface {} 是一个空的接口类型,空接口类型要求至少0个方法,而所有类型都满足零个方法的要求,可以所以传参
空的interface 就可以存储任何类型
定义接口就是
type shape interface {
area() float64
}
这样一个接口名,然后包含方法,空接口就是包含0个方法,里面什么都没有
type circle struct {
radius float64
}
type shape interface {
area() float64
}
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func info(s shape) {
fmt.Printf("函数里面的 的地址为 %p\n", s)
fmt.Printf("函数里面的 的地址为 %p\n", &s)
fmt.Println("area", s.area())
}
func main() {
c := circle{5}
fmt.Printf("c 的地址为 %p\n", c)
fmt.Printf("c 的地址为 %p\n", &c)
fmt.Println("------ 下面是 &c ------")
info(&c)
fmt.Println("------ 下面是 c ------")
info(c)
}
对于下面这个代码,传 c 还是 &c 都是一样的,如果是传c的话,那么传递的是 c的一个副本
type circle struct {
radius float64
}
type shape interface {
area() float64
setRadius(float64)
}
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c *circle) setRadius(val float64) {
c.radius = val
}
func info(s shape) {
fmt.Printf("函数里面的 的地址为 %p\n", s)
fmt.Printf("函数里面的 的地址为 %p\n", &s)
fmt.Println("area", s.area())
s.setRadius(float64(1))
fmt.Println("area", s.area())
//c, ok := s.(*circle)
//if !ok {
// fmt.Println("错误: s 不是一个 *circle 类型")
// return
//}
//c.setRadius(float64(1000)) // 现在可以调用指针方法了
}
func main() {
c := circle{5}
fmt.Printf("c 的地址为 %p\n", c)
fmt.Printf("c 的地址为 %p\n", &c)
fmt.Println("------ 下面是 c ------")
//info(c)
fmt.Println("------ 下面是 &c ------")
info(&c)
fmt.Println("------ 下面是 c ------")
//info(c)
fmt.Println(c.radius)
}
对于这样一段代码,因为info 里面还带了指针类型的setRadius 所以这个时候就强制只能传递了 &c ,传递值就不被允许了
strconv
这个包时用于字符串和基本数据类型之间互相转化的
比如其他的典型用法是这样的
assert
不知道为什么说assert 举例到了interface的type
func main() {
var name interface{} = "Sydney"
str, ok := name.(string)
if ok {
fmt.Printf("%T\n", str)
} else {
fmt.Printf("value is not a string\n")
}
}
因为空接口,可以存储任何类型,所以存储字符串是可以的 同时因为只有接口类型才能使用 .(type) 所以这里必须要使用接口类型
如果想要定义其他的不同类型的集合,那么就要定义接口数组,定义接口数组写起来就比较啰嗦了
func main() {
//var name interface{} = "Sydney"
var name []interface{} = []interface{}{"a", 1, 2.5}
//str, ok := name.(string)
str, ok := name[1].(string)
if ok {
fmt.Printf("%T\n", str)
} else {
fmt.Printf("value is not a string\n")
}
}
写起来就比较长
现在来解释,为什么说到断言反而例子是接口了,对于这样一个代码
var val interface{} = 7
fmt.Printf("%T\n", val)
这样打印出来的是int类型,但是不能
var val interface{} = 7
fmt.Println(val + 6)
这是因为,在go中,当你将一个具体类型赋值给空接口的时候,这个值的具体类型信息将会保留,因此,即使值被存储在空接口中,你仍可以对他执行类型相关的操作
为什么 fmt.Println(val + 6) 不能使用
这是因为在进行算术运算之前,要进行类型断言,以确认执行的具体的数值类型,空接口本身不携带关于任何他所储存值的运行时信息,因此go不知道如何处理 + 运算
因此你需要给他加上类型断言
var val interface{} = 7
//fmt.Println(val + 6)
if v, ok := val.(int); ok {
fmt.Println(v + 6) // 现在可以进行加法运算
} else {
fmt.Println("val does not store an int")
}
在这个例子中,类型断言 val.(int) 尝试将 val 中的值断言为 int 类型。如果断言成功(ok 为 true),则 v 将是 val 中的 int 值,然后你可以使用 v 来执行加法运算。
或者是直接
var val interface{} = 7
fmt.Println(val.(int) + 6)
也是可以的
注意类型断言用的是 .(type) 而不是强制转化的类型()
func main() {
rem := 7.24
fmt.Printf("%T\n", rem)
fmt.Printf("%T\n", int(rem))
var val interface{} = 7
fmt.Printf("%T\n", val)
fmt.Printf("%T\n", int(val))
// fmt.Printf("%T\n", val.(int))
}
比如这样的 fmt.Printf("%T\n", int(val)) 是可以的
goroutines
chan
要深刻理解chan 比如运行这段代码会出现死锁
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 10; i++ {
c <- i
}
}()
//go func() {
// for i := 0; i < 10; i++ {
// c <- i
// }
//}()
for n := range c {
fmt.Println(n)
}
close(c)
}
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
之前不理解的是明明已经有close的操作,而且是读取关闭,读取完毕了就close ,为什么还会报错死锁呢 ?
要这么理解, chan 是一个管道,chan int 代表开辟了一个管道,可以往里面写任意多的数据,所以在前几天,想多写几个数据,还用上了 chan []int 是没必要的,因为作为管道来讲,写多少个同类型的数据都没关系
另外要理解读取管道的操作
for n := range c {
fmt.Println(n)
}
这个操作,在管道打开的时候,会一直卡主读取,有的话就读取,灭有的话,就卡主整个程序不要动,等待新管道有东西进来,相当于一个死循环了,除非管道关闭
当通道关闭后,for 循环会继续接受管道中剩下的值,一旦通道中,所有的值都接受完全了,那么for 循环将会退出,所以你就要这么理解,因为通道没有关闭,还有可能写入值,那么for 循环就会一直不结束
那么如果通道没有关闭,for 循环会一直执行吗 ?
不会,如果所有用户启动的go协程都结束了,还在读取管道,那么就会主动报告死锁错误了
这也就是现在看到的情况,当go都执行完,for 还在不停的读取,go 主动抛出死锁错误
那么我有这么一个想法,让主进程 sleep,然后go开启的协程去写入管道,等协程写入完了,那么我主线程sleep也结束了,这个时候关闭 chan 然后再来for 循环读取
这样也好像实现了效果,代码如下
func main() {
c := make(chan int)
go func() {
for i := 0; i < 10; i++ {
c <- i
fmt.Println(i)
}
}()
time.Sleep(time.Duration(3 * time.Second))
close(c)
for n := range c {
fmt.Println(n)
}
}
但是奇怪的,这个代码输出不了任何结果,完全没有任何输出,sleep之后就直接结束了,给人看起来感觉就好像是,主进程一定要等到最后了,才执行goroutines 一样,主线程sleep,也导致了开启的goroutines 休眠了,或者说,只有等全部遍历完代码,主线程结束之后,goroutines 才启动一样
反正能看到的核心效果就是,根本没有任何的输出,甚至连print i 的输出都灭有,严重怀疑sleep影响到了开启的goroutines
这是因为以下几个原因:
- 首先,sleep作用域只在写sleep的这个部分,不会影响到已经启动的goroutine,肯定不能影响到别的,但是,之所以没有影响,是因为:
- 通道c 是无缓冲的,当发送了第一个数据 0 的时候,他会阻塞住,直到主goroutine 从通道中接收一个值,因为主 goroutine 正在sleep,他不会从通道中接收值,所以子goroutine 写入了一个值就被卡主了,导致发送的操作,一直是在被阻塞的
- 而当主goroutine 结束sleep之后,就关闭了通道,此时所有的发送操作都将会失败,因为向一个已经关闭的通道再发送数据是不允许的,这将会导致 panic
- 随后主goroutine 从通道中尝试取值,但是由于通道已经关闭,且所有发送已经失败,for 循环将立即退出,并不会打印值
给他换一下顺序,就能看到子goroutines 是已经启动的
go func() {
for i := 0; i < 10; i++ {
fmt.Println(i)
c <- i
}
}()
这样可以打印出一个0,然后就一直卡主,然后就直接结束了
那么既然是无缓冲,导致卡主的,我要是加上一个缓冲呢? 会怎么样,所以尝试如下代码(把打印操作放在发送完成之后)
func main() {
c := make(chan int, 10)
go func() {
for i := 0; i < 10; i++ {
c <- i
fmt.Println(i)
}
}()
time.Sleep(time.Duration(3 * time.Second))
close(c)
for n := range c {
fmt.Println(n)
}
}
可以看到,是正常打印出发送了1-9的,但是只打印一次,也就是这个1-9是发送的时候打印出来的,最后接收的时候还是没有1-9,因为要接收的时候,通道已经关闭,无法接收了
func main() {
c := make(chan int, 10)
go func() {
for i := 0; i < 10; i++ {
c <- i
fmt.Printf("send: %d\n", i)
}
}()
time.Sleep(time.Duration(3 * time.Second))
go func() {
for n := range c {
fmt.Printf("rec: %d\n", n)
}
}()
close(c)
for n := range c {
fmt.Printf("main rec: %d\n", n)
}
}
但是这里又非常奇怪了,这里又可以打印了。。。而且打印出来的是 send 和 main, 子的rec 没有打印出来
但是如果加上一个sleep呢,应该就是子的会打印出来
func main() {
c := make(chan int, 10)
go func() {
for i := 0; i < 10; i++ {
c <- i
fmt.Printf("send: %d\n", i)
}
}()
time.Sleep(time.Duration(3 * time.Second))
go func() {
for n := range c {
fmt.Printf("rec: %d\n", n)
}
}()
time.Sleep(time.Duration(3 * time.Second))
close(c)
//time.Sleep(time.Duration(3 * time.Second))
for n := range c {
fmt.Printf("main rec: %d\n", n)
}
}
实测发现,不管是在close前还是close后,都能打印出来子的,main的不会打印出来, 能正常打印,而且不会报错
另外可以感觉到,主的执行的速度要比子的 goroutine要快很多,也就是 新开的goroutine 执行速度是比不上主的,这大概也是为什么,如果不sleep的话,是主的在打印,【而且不会报错】
这是因为主的执行的快,而且主执行完的时候,让整体结束的时候,子的goroutine还存在,没有说,现在不存在任何的goroutines 了,但是chan 还存在的情况
那么为什么明明close 了,你还能读取值呢 ,之前不是说 ,就是因为close了,所以无法读取才会出错的吗?
详细看之前的解释,最开始的版本是因为,通道关闭了,且写入的操作,一直没有成功,这导致出错的。现在有缓冲了,写入成功了,那么通道关闭了,也是可以读取的
在go中,当通道关闭后,你仍可以继续从该管道中继续取值,直到通道所有的数据都读取完毕,一旦通道中所有的值都读完毕,随后的读取操作将会获取不到任何的值,并且循环的bool值变量会变成false,表示通道已经耗尽
注意通道关闭后,发送是不行,但是读取是可以的
- 通道关闭后,对通道的发送操作将会引发 panic。
- 通道关闭后,可以继续从中读取数据,直到通道中所有数据都被读取完毕。
- 通道关闭是一个“单向操作”,一旦关闭,就不能被重新打开
所以彻底理解下通道:
- 通道是因为多个goroutine沟通存在的,所以他才会检测,如果所有的子goroutines 都关闭结束了,你还没有关闭通道的话,就会主动给你报错,因为子的goroutines都没 ,没人写入了,还不关闭肯定是有问题的
- 关闭的通道可以读但是不能写
- 通道可以增加缓冲区,如果通道没有缓冲的话,对通道写的操作,一直没人接受的话,就会一直卡在写的这一步,不会往下执行,这也就是为什么如果把print放在写入后的话,就没结果,不会执行到print
- 这是一个很经典的生产者消费者模式
- 在go语言中,如果通道没有缓冲,那么对通道的发送操作就是一个阻塞操作;如果接收方还没有准备好接收数据,发送方将会阻塞,直到接收方开始从通道中读取数据,这种情况通常称之为“同步”或者“协调”。也就是表示 发送方和接收方的操作被通道缓冲所协调。 无缓冲通道,强制了发送方和接收方之间的同步,确保顺序顺序发送和接受
- 在Go语言中,如果通道(channel)没有缓冲,那么对通道的发送操作(
<-)将是一个阻塞操作:如果接收方(另一个goroutine或线程)还没有准备好接收数据,发送方将会阻塞,直到接收方开始从通道中读取数据 这种情况通常称为“同步”或“协调”,因为发送方和接收方的操作被通道的缓冲区(或缺乏缓冲区)所协调。无缓冲通道强制执行了发送方和接收方之间的同步,确保数据按顺序被发送和接收。
使用无缓冲的好处:
- 确保发送的数据项按照发送的数据被接受,因为通道中不能存储任何数据,发送和接受都必须同步进行。
- 如果发送方和接收方的操作速率不同,或者存在其他接收方导致延迟的情况,发送方可能会长时间阻塞
- 使用带缓冲的通道,可以提高程序的并发性能,因为发送方可以在接收方尚未准备好接收的时候执行其他的任务
- 在设计并发程序的时候,需要考虑通道的使用方式,以确保程序的同步要求得到满足,同时避免不必要的阻塞和性能瓶颈。
同理对于这一段代码也是这样
package main
import (
"fmt"
)
func main() {
c := make(chan int)
done := make(chan bool)
go func() {
for i := 0; i < 10; i++ {
c <- i
}
done <- true
}()
go func() {
for i := 0; i < 10; i++ {
c <- i
}
done <- true
}()
go func() {
<-done
<-done
close(c)
}()
for n := range c {
fmt.Println(n)
}
}
done 发送两个,接受2个,这才表示结束了,不过c 这个没有显示关闭
说错了,c 是关闭了的,但是done没有被关闭
可能是因为done 最后没有写入或者读取操作吧,但是之前的为什么会报错呢 找到问题了,可以引发出来,如果
for n := range c {
fmt.Println(n)
}
for n := range done {
fmt.Println(n)
}
在最后添加上 读取done的,就会跟之前一样
fatal error: all goroutines are asleep - deadlock!
因为所有的goroutine 都结束了,不会再有写入,而还一直卡主循环done,会导致一直不结束,导致死锁的产生
再来看下面这段代码
func main() {
c := make(chan int)
done := make(chan bool)
go func() {
for i := 0; i < 10; i++ {
c <- i
}
done <- true
}()
go func() {
for i := 0; i < 10; i++ {
c <- i
}
done <- true
}()
// we block here until done <- true
<-done
<-done
close(c)
// to unblock above
// we need to take values off of chan c here
// but we never get here, because we're blocked above
for n := range c {
fmt.Println(n)
}
}
这个也会报错
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
GolangTraining/22_go-routines/09_channels/02_n-to-1/04_semaphore_wrong-way/main.go:27 +0xf6
goroutine 6 [chan send]:
main.main.func1()
GolangTraining/22_go-routines/09_channels/02_n-to-1/04_semaphore_wrong-way/main.go:14 +0x3e
created by main.main
GolangTraining/22_go-routines/09_channels/02_n-to-1/04_semaphore_wrong-way/main.go:12 +0x92
goroutine 7 [chan send]:
main.main.func2()
GolangTraining/22_go-routines/09_channels/02_n-to-1/04_semaphore_wrong-way/main.go:21 +0x3e
created by main.main
GolangTraining/22_go-routines/09_channels/02_n-to-1/04_semaphore_wrong-way/main.go:19 +0xea
进程 已完成,退出代码为 2
核心是因为没有缓冲区造成的, 两次接受 <-done 确实因为同步的关系,达到了阻塞的效果,但是由于这个是没有缓冲区的,写入一直写入不了,然后主gorotine 也一直等到,所以导致了死锁
改好也非常简单,就是增加20个缓冲区,这样子就能写入了,写入了之后 ,能正常 done <- true 也就能实现了 <-done 连续两个阻塞,虽然后面关闭了c 但是依然可以读取
也就是变成 c := make(chan int, 20) 即可正常运行
下面再来看一个就更清楚了
func main() {
n := 10
c := make(chan int)
done := make(chan bool)
for i := 0; i < n; i++ {
go func() {
for i := 0; i < 10; i++ {
c <- i
}
fmt.Printf("main run curent goroutines: %d\n", runtime.NumGoroutine())
done <- true
}()
}
go func() {
for i := 0; i < n; i++ {
<-done
}
fmt.Printf("sub go curent goroutines: %d\n", runtime.NumGoroutine())
close(c)
}()
for n := range c {
fmt.Println(n)
}
}
这个是连续开了10个协程,然后所以 done 要连续读取10次才能同步,打印出来的最大的协程数量就是12
0
0
0
0
0
0
0
0
0
0
1
1
1
1
1
1
1
1
1
1
2
2
2
2
2
2
2
2
2
2
3
3
3
3
3
3
3
3
3
4
3
4
4
4
4
4
4
4
4
5
5
4
5
5
5
5
5
5
5
6
6
5
6
6
6
6
6
6
7
6
7
7
6
7
7
7
7
7
8
7
8
8
7
8
8
8
8
9
8
9
8
9
9
9
8
9
9
9
main run curent goroutines: 12
main run curent goroutines: 11
main run curent goroutines: 10
main run curent goroutines: 12
main run curent goroutines: 9
9
main run curent goroutines: 9
main run curent goroutines: 10
main run curent goroutines: 9
main run curent goroutines: 12
main run curent goroutines: 3
sub go curent goroutines: 2
9
同样的写法还有这一步
func main() {
n := 10
c := make(chan int)
done := make(chan bool)
go func() {
for i := 0; i < 100000; i++ {
c <- i
}
close(c)
}()
for i := 0; i < n; i++ {
go func() {
for n := range c {
fmt.Println(n)
}
done <- true
}()
}
for i := 0; i < n; i++ {
<-done
}
}
最后的
for i := 0; i < n; i++ {
<-done
}
就是为了阻塞写的,所以只是n 不需要到最大的 100000
再次来理解这个代码
func main() {
c := incrementor()
cSum := puller(c)
for n := range cSum {
fmt.Println(n)
}
}
func incrementor() chan int {
out := make(chan int)
go func() {
for i := 0; i < 10; i++ {
out <- i
}
close(out)
}()
return out
}
func puller(c chan int) chan int {
out := make(chan int)
go func() {
var sum int
for n := range c {
sum += n
}
out <- sum
close(out)
}()
return out
}
我的疑问是
我认为,在 incrementor 的时候,函数不是立马返回了吗,立马返回,他里面的 incrementor 启动的协程,还能继续正常运行吗 ? 第二个puller 我知道是因为存在循环,所以才让里面的协程运行了,但是第一个没有协程,函数已经退出了,为什么还能正常运行呢
这里面的核心在于理解,在调用函数退出之后,启动的协程,实际上还是会在运行的
即使incrment函数返回了,但是由他创建的协程,依然会在【正常运行】,这是因为goroutine是轻量级的线程,他在go运行时是独立调度,当你启动一个 goroutine 他会成为程序的一部分,并继续执行,即便创建他的函数已经返回
所以incrment 创建的out 管道,会写入1-10 最终的输出结果也就是 sum 1-9等于45
理解关键点:
- goroutine 是独立执行的线程,他们在函数返回后,仍然继续运行
- 调用puller函数,并将out的管道作为一个参数传递给puller, puller 返回另外一个通道cSum
- 使用for循环从 cSum管道中取值,由于cSum 是无缓冲的管道,这个【for 循环将会被阻塞】,直到puller函数启动的goroutine 计算发送完总和,并关闭通道
- 【通过close关闭通道,可以避免接收方在通道关闭后无限期的等待】
- 【在for循环中使用 【range】遍历通道的话,如果通道关闭,循环将结束 】
通道还可以表示方向,比如
package main
import "fmt"
func main() {
c := increment()
cSum := puller(c)
for n := range cSum {
fmt.Println(n)
}
}
func increment() <-chan int {
out := make(chan int)
go func() {
for i := 0; i < 10; i++ {
out <- i
}
close(out)
}()
return out
}
func puller(c <-chan int) <-chan int {
out := make(chan int)
go func() {
var sum int
for n := range c {
sum += n
}
out <- sum
close(out)
}()
return out
}
所有东西都和上面一样,除了函数的定义,就是加上了箭头,然后参数加上了箭头 func increment() <-chan int
表示这是一个只读的通道,包括函数传参也变了 func puller(c <-chan int) <-chan int 变成 c <- chan int 了
如果是只读通道的话,就是 <-chan T 只写通道,那就是 chan <- T
看形象就是 只写就是把T放到chan中,所以是 chan <- T
只读就是 chan T 这个东西读取出来 <- 所以是 <- chan T
这里的T是通道的类型,对应就是 int 这里面就不涉及形参名字的概念 就是 <- chan int 这种写法,上面几个函数都是 <- chan int 的写法(之前经常协程 chan int <- 是不对的,箭头要放在见面
通道的值直接读取出来,由于都是int 类型,还可以使用加法,比如下面这个就是输出40,因为是20 和20的累加
func main() {
c1 := incrementor("Foo:")
c2 := incrementor("Bar:")
c3 := puller(c1)
c4 := puller(c2)
fmt.Println("Final Counter:", <-c3+<-c4)
}
func incrementor(s string) chan int {
out := make(chan int)
go func() {
for i := 0; i < 20; i++ {
out <- 1
fmt.Println(s, i)
}
close(out)
}()
return out
}
func puller(c chan int) chan int {
out := make(chan int)
go func() {
var sum int
for n := range c {
sum += n
}
out <- sum
close(out)
}()
return out
}
闭包
要理解关键词 【捕获变量】
比如对于这么一段代码
func main() {
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v)
done <- true
}()
}
// wait for all goroutines to complete before exiting
for _ = range values {
<-done
}
}
他并没有按照预期的输出 a b c 而是输出的 c c c 全部都是最后一次的值
这是因为闭包捕获变量的问题,这个匿名闭包函数捕获了v,也就是每个协程,都捕获的是同一个变量v的地址,变量v 在循环结束的时候是c 所以最终协程启动后,输出这个变量地址的时候,就变成了c
那么 变量 v 在循环结束时的值是 "c",所有 goroutine 都会打印出 "c" 但是第一个协程启动的时候,不是a吗,如果协程启动的足够快,那么我看到的是不是就是 a ?
回答是:您提出了一个很好的观点,但是这个假设,通常不成立,因为go的编译器和运行时会对代码进行优化,而迭代变量v的行为在go中有着明确的规定
在go中,for的循环变量,在每次迭代过程中都会重新赋值,当你在循环中启动 goroutine 并捕获循环变量时,拿到的实际上是变量v的内存地址,而不是他的当前值
这个我认为是避免循环创建新的值而做的优化,如果循环10000次,不是捕获地址的话,那么就要新增很多很多个变量,基于这个考虑,所以循环捕获的都是地址
反正总之来说,编译器和运行时的优化都会让你不产生这种理想情况
要解决这个问题的办法就是传参,直接传参就好了
但是同样的,如果在循环内,创建了一个新的变量,不走传参的形式,也是可以使用的
没有传参,只是新建了变量
但是这样子就不行了
func main() {
done := make(chan bool)
values := []string{"a", "b", "c"}
var x string
for _, v := range values {
//v := v // create a new 'v'.
x = v
go func() {
fmt.Println(x)
done <- true
}()
}
// wait for all goroutines to complete before exiting
for _ = range values {
<-done
}
}
直接 := 是可以的,创建新变量,但是你要是使用同一个变量的话,那么就不行
说明变量优化,只是优化循环条件内部的,你如果在内部其他的地方新建变量,就不会受到传地址的影响(因为也不是同一个地址了)
在 Go 中,基本类型的变量(如 int、string 等)总是通过值传递的。这意味着当您将一个变量赋给另一个变量时,您得到的是原始值的副本,而不是原始变量的地址。这与指针或引用类型的变量不同,后者可能通过引用传递。
也就是在循环中的时候会特殊一点
关于死锁的理解
如何修改这样一份死锁的代码
func main() {
c := make(chan int)
c <- 1
fmt.Println(<-c)
}
我得改法是
func main() {
c := make(chan int)
go func() {
c <- 1
close(c)
}()
fmt.Println(<-c)
}
因为你必须要一个来发送一个来接受,然后又要卡主主线程,让他等待新启动的协程,所以只能是发送方独立成一个协程
对于下面这2个死锁的情况
func main() {
c := make(chan int)
go func() {
for i := 0; i < 10; i++ {
c <- i
}
}()
fmt.Println(<-c)
}
和
func main() {
c := make(chan int)
go func() {
for i := 0; i < 10; i++ {
c <- i
}
}()
for {
fmt.Println(<-c)
}
}
我的改法是
func main() {
c := make(chan int)
go func() {
for i := 0; i < 10; i++ {
c <- i
}
close(c)
}()
for {
fmt.Println(<-c)
}
}
结果万万没想到。。。
出现了ide里面停止停止不了的情况,就像是主进程停了,但是启动的还没停止的样子
0
0
0
0
0
0
0
^C0
0
进程 已完成,退出代码为 130 (interrupted by signal 2: SIGINT)
不断地在刷出来0,然后ps -ef 又看不到go的进程,想到可能是编译成二进制了,所以又通过二进制的名字来搜,也没搜到(main来搜),又按照 04_02的包名来搜也没搜到....
所以就根本不知道咋结束,又启动了一次,然后出现了这种情况,也不知道咋解决,直到最后操作系统自己把他结束了
所以还是要用 for n:=range 管道的用法啊
func main() {
c := make(chan int)
go func() {
for i := 0; i < 10; i++ {
c <- i
}
close(c)
}()
for n := range c {
fmt.Println(n)
}
}
直接 for {} 真的就是死循环
继续chan
做这样一个功能,计算平方值,但是通过管道的方式,一个管道用于输入需要计算的值,另外一个管道输出计算的结果,最后输出,我的代码是这样的
package main
import "fmt"
// 实际上就是一个求平方的功能 只不过改成了管道的输入和输出
// 返回的是一个管道,这个管道具有求平方的功能,输入一个输出一个
func sq(in chan int) (out chan int) {
go func() {
// 对于输入这个管道的,都写入平方数
for n := range in {
out <- n * n
}
close(out)
}()
return
}
// 这个管道是写入,上面一个管道也是写入
func gen(nums ...int) (out chan int) {
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return
}
func main() {
c := gen(1, 2, 3)
out := sq(c)
fmt.Println(<-out)
fmt.Println(<-out)
}
运行之后就是报错
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]:
第一下没注意看报错,以为是死锁了,后来才发现是 nil chan 管道,原因是因为使用了 命名返回值,即便使用了命名返回值,也是需要 make(chan int) 进行初始化才行
使用命名返回值确实可以自动创建变量,并且初始化,但是这个仅仅限于基本类型,比如 int float string bool
但是 数组,字典,结构体,接口 等这些复杂类型,都只是创建变量,并不会自动初始化为零值
如果数组和字典 命名返回值了,也是一个 nil 需要手工make
所以更改过后的代码是这样的
package main
import "fmt"
func gen(nums ...int) (out chan int) {
out = make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return
}
func sq(in chan int) (out chan int) {
out = make(chan int)
go func() {
for n := range in {
out <- n * n
}
}()
return
}
func main() {
in := gen(2, 3, 4)
out := sq(in)
fmt.Println(<-out)
fmt.Println(<-out)
fmt.Println(<-out)
}
就是增加了make 后面又想到了使用只读或者只写管道,但是发现带上命名返回值的话,不管怎么样都是不行,所以还是不要命名返回值写一个版本
package main
import "fmt"
func gen(nums ...int) chan int {
out := make(chan int)
go func() {
for _, v := range nums {
out <- v
}
}()
return out
}
func sq(in <-chan int) chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
}()
return out
}
func main() {
in := gen(2, 3, 4)
out := sq(in)
fmt.Println(<-out)
fmt.Println(<-out)
fmt.Println(<-out)
}
好像也只能写成这样了。。。注意这里都少了close()、、、、
写一个1-100的求斐波拉契的,但是是并行100个计算的
func fab(num chan int) chan int {
out := make(chan int, 100)
defer func() {
close(out)
}()
for v := range num {
cnt := 0
for i := 1; i <= v; i++ {
cnt += i
}
out <- cnt
}
return out
}
func main() {
in := make(chan int, 100)
for i := 1; i <= 100; i++ {
in <- i
}
close(in)
out := fab(in)
for v := range out {
fmt.Printf(" = %d\n", v)
}
}
第一个版本可以计算,但是读取的时候,就丢失了到底是计算哪一个,看来只能再并发一个,记录管道的值
这个时候就感受到了 同步,无缓冲的管道是多么重要了
真是奇了怪了
func main() {
in := make(chan int, 100)
for i := 1; i <= 100; i++ {
in <- i
}
close(in)
out := fab(in)
for v := range out {
fmt.Printf(" = %d\n", v)
}
}
我这样可以,但是
func main() {
in := make(chan int, 100)
out := fab(in)
for i := 1; i <= 100; i++ {
in <- i
}
close(in)
for v := range out {
fmt.Printf(" = %d\n", v)
}
}
这样就不行了 ??? 大概是因为
func fab(num chan int) chan int {
out := make(chan int, 100)
defer func() {
close(out)
}()
for v := range num {
cnt := 0
for i := 1; i <= v; i++ {
cnt += i
}
out <- cnt
}
return out
}
fab 是这么写的,这个初始化返回完了就关闭了通道
func fab(num chan int) chan int {
out := make(chan int, 100)
go func() {
defer func() {
close(out)
}()
for v := range num {
cnt := 0
for i := 1; i <= v; i++ {
cnt += i
}
out <- cnt
}
}()
return out
}
func main() {
in := make(chan int, 100)
out := fab(in)
for i := 1; i <= 100; i++ {
in <- i
}
close(in)
for v := range out {
fmt.Printf(" = %d\n", v)
}
}
没错,这样就可以了,这样才是正确的,在开启一个新的out,或者不用写的这么麻烦,不用这么嵌套
func fab(out chan int) {
defer func() {
close(out)
}()
for v := range out {
cnt := 0
for i := 1; i <= v; i++ {
cnt += i
}
out <- cnt
}
}
func main() {
//in := make(chan int, 100)
out := make(chan int, 100)
for i := 1; i <= 100; i++ {
out <- i
}
go fab(out)
for v := range out {
fmt.Printf(" = %d\n", v)
}
}
这样子也不行,因为这样子一下子就close了。。。除非在用一个go 嵌套一层,否则一go 就... 不对啊,我这样去go他,进去的时候,就应该是在处理了啊,为什么会死锁呢,说明不是这个问题
func fab(in, out chan int) {
defer func() {
close(out)
}()
for v := range in {
cnt := 0
for i := 1; i <= v; i++ {
cnt += i
}
out <- cnt
}
}
func main() {
in := make(chan int, 100)
out := make(chan int, 100)
for i := 1; i <= 100; i++ {
in <- i
}
close(in)
go fab(in, out)
for v := range out {
fmt.Printf(" = %d\n", v)
}
}
没错这样就可以了,但是能否只用一个管道呢
重新开始写
你知道为什么要用管道吗,因为
func fab(n int) int {
cnt := 0
for i := 1; i <= n; i++ {
cnt += i
}
fmt.Printf("%d = %d\n", n, cnt)
return cnt
}
func main() {
for i := 1; i <= 100; i++ {
go fab(i)
}
}
这么写的话,根本算不到100, 算到20 就退出了,因为主进程退出了,所以必须要用管道卡主他,你用 wg 也可以,但是我们的目的是要 2个协程之间通信
一个协程只管往里面放数字,另外一个协程只管 取数字 来计算
func fab(n int, done chan bool) int {
defer func() {
done <- true
}()
cnt := 0
for i := 1; i <= n; i++ {
cnt += i
}
fmt.Printf("%d = %d\n", n, cnt)
return cnt
}
func main() {
done := make(chan bool, 100)
for i := 1; i <= 100; i++ {
go fab(i, done)
}
for i := 1; i <= 100; i++ {
<-done
}
}
卡主的写法是这样的
现在给出一种新的写法
func fab(in chan int, out chan []int) {
defer func() {
close(out)
}()
for n := range in {
cnt := 0
for i := 0; i <= n; i++ {
cnt += i
}
val := []int{n, cnt}
fmt.Printf("cur goroutine: %d\n", runtime.NumGoroutine())
out <- val
}
}
func main() {
in := make(chan int, 100)
out := make(chan []int, 100)
for i := 1; i <= 100; i++ {
in <- i
}
close(in)
go fab(in, out)
// 开始拿结果
for n := range out {
fmt.Printf("%d = %d\n", n[0], n[1])
}
fmt.Printf("cur goroutine: %d\n", runtime.NumGoroutine())
}
这样能正确输出 多少 等于多少,但是这样的问题是只开启了一个协程来计算,要开启100个协程来计算
夸张点,应该是 200个协程
一个 In 管道,100个协程,同时往里面放 1-100
另外一个out 管道,另外100个协程,同时去提取放进去的值,然后计算结果
这个打印出来很明显,只有2个协程在并发,当计算完后,只有一个主协程在运行了
我这样子的运行什么输出都没有,因为提前结束了
func fab(num int) (rst int) {
for i := 1; i <= num; i++ {
rst += i
}
return
}
func main() {
in := make(chan int, 1024)
out := make(chan int, 1024)
// 并发放入管道
for i := 1; i <= 100; i++ {
go func() {
in <- i
}()
}
// 并发取结果
for i := 1; i <= 100; i++ {
go func() {
num := <-out
rst := fab(num)
fmt.Printf("%d = %d\n", num, rst)
}()
}
}
主进程么有等待子进程,所以计算结果,应该是写入到管道中,再让主进程读取就是
func fab(num int, out chan []int) {
rst := 0
for i := 1; i <= num; i++ {
rst += i
}
val := []int{num, rst}
out <- val
}
func main() {
in := make(chan int, 1024)
out := make(chan []int, 1024)
// 并发放入管道
for i := 1; i <= 100; i++ {
go func() {
in <- i
}()
}
// 并发去计算
for i := 1; i <= 100; i++ {
go func() {
num := <-in
fab(num, out)
}()
}
// 然后再来取结果
for i := 1; i <= 100; i++ {
ans := <-out
fmt.Printf("%d = %d\n", ans[0], ans[1])
}
}
这样子不知道为什么造成了重复计算
而且数字还到了101 ,明明都是100打止了,同时这里还没有加上close , 显示的close
测试后发现,这是因为并发放入管道的时候,for 循环的i 取得是地址,放错了值导致的,所以要新建一个变量,按照变量的方式传入进去,改进了之后的结果是这样的
package main
import (
"fmt"
"sort"
)
func fab(num int, out chan []int) {
rst := 0
for i := 1; i <= num; i++ {
rst += i
}
//fmt.Printf("中间结果: %d = %d\n", num, rst)
val := []int{num, rst}
out <- val
}
func main() {
in := make(chan int, 100)
out := make(chan []int, 100)
// 并发放入管道
for i := 1; i <= 100; i++ {
x := i
go func(j int) {
//fmt.Printf("放入的值是 %d\n", j)
in <- j
}(x)
}
// 并发去计算
for i := 1; i <= 100; i++ {
go func() {
num := <-in
//fmt.Printf("读取到要去计算的值是 %d\n", num)
fab(num, out)
}()
}
// 然后再来取结果, 为了直观能看到值,先组合成字符串,再来字符串排序后输出
output_strings := make([]string, 100)
for i := 1; i <= 100; i++ {
ans := <-out
//fmt.Printf("%d = %d\n", ans[0], ans[1])
o_s := fmt.Sprintf("%03d = %d", ans[0], ans[1])
output_strings = append(output_strings, o_s)
}
sort.Strings(output_strings)
for i, s := range output_strings {
fmt.Printf("%d, %s\n", i, s)
}
fmt.Printf("len: %d", len(output_strings))
}
结果是对了,但是输出了很长的空格,输出了100个空格,第101个才是我要的值 这是因为放入第50个的时候,引发了切片的自动扩容导致的,扩容到了200的长度,然后排序的时候,后面100个空的就排到前面去了
我要是初始化25的长度
output_strings := make([]string, 50)
我初始化1的长度,结果就是101
output_strings := make([]string, 1)
还是想办法用固定长度的数组比较好 结果总长度就是150
想办法还使用上数组,避免扩容,因为容量是已知的
//output_strings := make([]string, 1)
//output_strings := [100] string
var output_strings [101]string
for i := 1; i <= 100; i++ {
ans := <-out
//fmt.Printf("%d = %d\n", ans[0], ans[1])
o_s := fmt.Sprintf("%03d = %d", ans[0], ans[1])
//output_strings = append(output_strings, o_s)
output_strings[i] = o_s
}
//sort.Strings(output_strings)
for i, s := range output_strings {
fmt.Printf("%d, %s\n", i, s)
}
使用数组遇上了几个点的麻烦
- sort不能直接对数组排序,只能对切片排序
- 数组的0是没有使用到的,所以容量要是101才行
特别是容量这点事没想到的,因为我是从1开始的
另外好像不排序,输出的也是直接正确的结果,怀疑是不是没有并发了,需要输出协程数量看看
package main
import (
"fmt"
"runtime"
)
func fab(num int, out chan []int) {
rst := 0
for i := 1; i <= num; i++ {
rst += i
}
//fmt.Printf("中间结果: %d = %d\n", num, rst)
cur_goroutine := runtime.NumGoroutine()
val := []int{num, rst, cur_goroutine}
out <- val
}
func main() {
in := make(chan int, 100)
out := make(chan []int, 100)
// 并发放入管道
for i := 1; i <= 100; i++ {
x := i
go func(j int) {
//fmt.Printf("放入的值是 %d\n", j)
in <- j
}(x)
}
// 并发去计算
for i := 1; i <= 100; i++ {
go func() {
num := <-in
//fmt.Printf("读取到要去计算的值是 %d\n", num)
fab(num, out)
}()
}
// 然后再来取结果, 为了直观能看到值,先组合成字符串,再来字符串排序后输出
//output_strings := make([]string, 1)
//output_strings := [100] string
var output_strings [101]string
for i := 1; i <= 100; i++ {
ans := <-out
//fmt.Printf("%d = %d\n", ans[0], ans[1])
o_s := fmt.Sprintf("%03d = %d, cur goroutines: %d", ans[0], ans[1], ans[2])
//output_strings = append(output_strings, o_s)
output_strings[i] = o_s
}
//sort.Strings(output_strings)
for i, s := range output_strings {
fmt.Printf("%d, %s\n", i, s)
}
fmt.Printf("len: %d", len(output_strings))
}
最终版清晰的代码就是这样
回答之前的几个问题:
- 为什么我 明明是并发放入,并发计算结果的,为什么我 取out 的时候,取出来的结果是顺序的,都不用排序了 ???
回答:哦哦不是,还是需要排序的,是大多数是顺序的,零星极个别不是顺序的
- 既然不能sort不能排序数组,那么怎么处理 ?
回答,转化成切片,排序即可
sort.Strings(output_strings[1:])
for i, s := range output_strings {
fmt.Printf("%d, %s\n", i, s)
}
- 还缺少close,怎么close
为什么一开始不写close,因为我不知道什么时候close,因为我放入,都是协程并发放入的,我不知道什么时候放入完成了,所以不知道怎么close
取的时候,可以知道什么时候取完了
因为没有取完的话,for 循环会一直卡主,不会结束的
取的时候,直接在 取完了close
都是 放的时候不知道close,但是读取的时候,是知道 close的
如果不用 wg 的话,那就要再开个 done 的管道,确认已经放完,done 知道怎么close,因为done 全部读取完了,就能close了,都是 放的时候不知道close,但是读取的时候,是知道 close的
后来一想,其实也不是,可以直接就是
因为我的 out 一定是把 In 处理完
我把 out 读取完,说明in 也完了
那么一起 close就可以
我让gpt 给我改代码,in 也是没有close的,问他为什么没有close(in) 他说:
在go语言中,关闭管道主要有2个目的:
- 确保管道的接收方知道不会再有数据发送到管道中了,允许他们退出循环
- 避免管道的接收方,因为尝试从已经关闭的管道中接收数据而发生阻塞
在你得代码中,管道in 作为一种简单的传输机制,将数组从主协程,传递到工作协程,由于每个数字只发送一次,而且没有协程会无限期的从in管道中取数据,因此在特定的场景中,关闭 in 管道是没必要的,也是不合适的
这里有几个关键点需要注意:
- 发送完毕后关闭:通常,当你知道所有的数据都已经发送到管道里了,你会关闭管道。在你的代码中,所有in管道的值都是事先确定的,并且通过循环和协程来填充,没有额外的发送方会在之后,向管道发送数据
- 接收方行为:in 管道的接收方(在fab函数调用中),在接收到一个值后就会结束,不会等待或期望更多数据,这意味着即使没有显示关闭in管道,接收方也不会遇到问题
- 缓冲区行为:由于in是一个带缓冲区的管道,他可以在没有接收方的情况下暂存数据,但在这个例子中发,发送方和接收方是同时工作的,所以不会超过缓冲区的大小
如果你尝试关闭in管道,你会遇到一个问题:没有接收方的管道不能被关闭,这将导致运行时panic,在你的代码中,每个发送到in的值,都有一个对应的接收方,所以不需要(也不可能)关闭 in
总结来说,在大多数情况下,当且仅当你是管道的最后一个发送者,并且确定不会再发送任何数据时,你才需要关闭管道。在你提供的代码中,in 正确的充当了一个传输工具,并且由于其使用模式,所以不需要被关闭
但是实际上有这么一个问题,有一个致命的问题:
// 并发去计算
for i := 1; i <= 100; i++ {
go func() {
num := <-in
fab(num, out)
}()
}
我这个从管道中取值,并不是取得管道,而是我事先知道是100了,不是从管道中取得,不是循环in的,如果改成了90 那么这个取法就有问题了,in 只放了90个,但是你尝试去100个,就会被卡主,一直取不到,出现了死锁,所以还是要关闭 in 这样才知道什么时候是取值结束
因为你放的时候,是知道要放多少个的,你这个计算的管道,是不需要知道有多个数据的,是来了数据就计算,不断地计算,所以一定是从 读取管道中的循环
另外:
,in 一定要close,为了解决刚才不是读取in 而是循环i的问题, 我刚才想写一个循环,读取 in , 读到多少个,启动多少个
但是这样是不行的,因为 in 没有close,会死锁
in 放完了,协程结束了,但是你还有协程不断地去 读取造成的死锁, 触发了死锁的条件
out 不close也是同样的情况,计算完了,还一直在读取,如果改了100这个值,就会产生变化了
修改之后,发现 不管是 In 还是out 都是需要 wg 计数然后close的
package main
import (
"fmt"
"runtime"
"sort"
"sync"
)
func fab(num int, out chan []int) {
rst := 0
for i := 1; i <= num; i++ {
rst += i
}
cur_goroutine := runtime.NumGoroutine()
val := []int{num, rst, cur_goroutine}
out <- val
}
var wg sync.WaitGroup
func main() {
in := make(chan int, 100)
out := make(chan []int, 100)
// 并发放入管道
for i := 1; i <= 100; i++ {
x := i
wg.Add(1)
go func(j int) {
in <- j
wg.Done()
}(x)
}
// 这里就确保发送方全部发完了
wg.Wait()
close(in)
for num := range in {
wg.Add(1)
x := num
go func(y int) {
fab(y, out)
wg.Done()
}(x)
}
wg.Wait()
close(out)
// 然后再来取结果, 为了直观能看到值,先组合成字符串,再来字符串排序后输出
var output_strings [101]string
for i := 1; i <= 100; i++ {
ans := <-out
o_s := fmt.Sprintf("%03d = %d, cur goroutines: %d", ans[0], ans[1], ans[2])
fmt.Printf("%03d = %d, cur goroutines: %d\n", ans[0], ans[1], ans[2])
output_strings[i] = o_s
}
fmt.Println("-----不排序之前-------")
for i, s := range output_strings {
fmt.Printf("%d, %s\n", i, s)
}
fmt.Println("-----排序之后-------")
sort.Strings(output_strings[1:])
for i, s := range output_strings {
fmt.Printf("%d, %s\n", i, s)
}
fmt.Printf("len: %d", len(output_strings))
}
一个写的比较好的用法就是
package main
import (
"fmt"
"runtime"
"sync"
)
type Out struct {
num int
sum int
thread int
}
var wg sync.WaitGroup
func main() {
var in = make(chan int, 100)
var ou = make(chan Out, 100)
input(in)
out(in, ou)
for o := range ou {
fmt.Printf("num:%v,sum:%v,routine:%d \n", o.num, o.sum, o.thread)
}
}
func input(num chan<- int) {
defer close(num)
for i := 1; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
num <- n
}(i)
}
wg.Wait()
}
func out(num <-chan int, out chan<- Out) {
defer close(out)
for v := range num {
wg.Add(1)
go func(num int) {
wg.Done()
var sum = 0
for j := 1; j <= num; j++ {
sum += j
}
out <- Out{
num: num,
sum: sum,
thread: runtime.NumGoroutine(),
}
}(v)
}
wg.Wait()
}
第一,用了结构体保存,而不需要开数组了,为什么想到结构体,想到类,因为
封装数据,第一个想到的就是类
第二个,这个是真的并发去处理了,读取out 也是并发的
第三,close可以直接写,不需要再包一个函数嵌套了 第四,循环里面传值这个问题也考虑到了
那么就还剩下最后一个问题,为什么打印出来的协程数量,不是200呢,而是只有十几个,20多个,100个去放的,100个去接收的,不应该是200吗 ? 实际上不是的 不足200,说明是有空闲的,跟线程池一样
具体描述是这样的:
在go中,由于底层运行时调度机制,打印出来的协程,不会精确反应你开启的协程的数量,有以下几个因素导致你的协程数量少于200个:
- 协程完成:当你打印当前协程数量时,有一些协程已经完成了
- 协程调度:go 在运行时,会尽量复用现有协程,以减少创建和消费协程的开销,因此一些协程可能结束了他们的执行,并且他们的底层系统线程可能已经被服用
- 协程池:go 在运行时,会创建并维护一个协程池,用于重用已经完成的协程。当一个协程完成时,如果有其他待处理的工作任务,他的底层系统线程可能会被用来执行另一个协程
- 延迟打印:你的打印语句可能在协程启动不久后就开始执行了。但在这个时间点上,不是所有的协程都有机会去运行
- 缓冲区:你使用了一个带缓冲区的管道in和out,这意味着可以暂存一定数量的值,用于减少协程之间的同步等待
- 代码结构:你的代码中,每个协程的工作量都非常的小,仅仅是将一个值发送到管道中,然后退出,运行时,系统可能在打印协程数量的之前,就已经完成了这些协程的执行。
- runtime的原因:runtime 取到的协程,返回的是当前运行时,活跃的协程数量,他并不包括那些已经结束,但是尚未被垃圾回收的协程,因此这个数字应该是一个近似值,而不是确切的计数
为了确保所有的协程都有机会运行,你可以尝试引入一些延迟,比如sleep, 但请注意,这并不是一个推荐的做法,因为他会人为的增加程序的执行时间,并且并不能确保观察到所有的协程数量
如果你确实需要观察所有的协程启动,你可以增加每个协程 工作的复杂性和持续时间来实现,然后,这通常不是必须的。因为go的并发模型,是设计用来搞笑的管理协程的创建和销毁。
最后,还有一点原因:runtime 取到的协程,返回的是当前运行时,活跃的协程数量,他并不包括那些已经结束,但是尚未被垃圾回收的协程,因此这个数字应该是一个近似值,而不是确切的计数