前些日子单机房稳定性下降,找了好一会才找到真正的原因。这里面涉及到不少go语法细节,正好大家一起看一下。
一、仿真代码
这是仿真之后的代码
1 | package main |
执行结果如下:
1 | ➜ my go run main.go |
不知道大家是否能够比较快的看出来问题。
二、相关语法
2.1channel
知识点
在 Go 语言中,channel
是用于在多个goroutine
之间进行通信和同步的重要机制,以下是一些关于channel
的重要知识点:
1. 基本概念
- 定义:
channel
可以被看作是一个类型安全的管道,用于在goroutine
之间传递数据,遵循 CSP(Communicating Sequential Processes)模型,即 “通过通信来共享内存,而不是通过共享内存来通信”,从而避免了传统共享内存并发编程中的数据竞争等问题。 - 声明与创建:使用
make
函数创建,语法为make(chan 数据类型, 缓冲大小)
。缓冲大小是可选参数,省略时创建的是无缓冲channel
;指定大于 0 的缓冲大小时创建的是有缓冲channel
。例如:
1 | unbufferedChan := make(chan int) // 无缓冲channel |
2. 操作方式
- 发送数据:使用
<-
操作符将数据发送到channel
中,语法为channel <- 数据
。例如:
1 | ch := make(chan int) |
- 接收数据:同样使用
<-
操作符从channel
中接收数据,有两种形式。一种是将接收到的数据赋值给变量,如数据 := <-channel
;另一种是只接收数据不赋值,如<-channel
。例如:
1 | ch := make(chan int) |
- **关闭
channel
**:使用内置的close
函数关闭channel
,关闭后不能再向其发送数据,但可以继续接收已发送的数据。接收完所有数据后,再接收将得到该类型的零值。例如:
1 | ch := make(chan int) |
3. 缓冲与非缓冲channel
- **无缓冲
channel
**:也叫同步channel
,数据的发送和接收必须同时准备好,即发送操作和接收操作会互相阻塞,直到对方准备好。只有当有对应的接收者在等待时,发送者才能发送数据;反之,只有当有发送者发送数据时,接收者才能接收数据。这确保了数据的同步传递。 - **有缓冲
channel
**:内部有一个缓冲区,只要缓冲区未满,发送操作就不会阻塞;只要缓冲区不为空,接收操作就不会阻塞。当缓冲区满时,继续发送会阻塞;当缓冲区为空时,继续接收会阻塞。例如:
1 | bufferedChan := make(chan int, 3) |
4. 单向channel
- 单向
channel
只能用于发送或接收数据,分别为只写channel
(chan<- 数据类型
)和只读channel
(<-chan 数据类型
)。单向channel
主要用于函数参数传递,限制channel
的使用方向,增强代码的可读性和安全性。例如:
1 | // 只写channel |
5. select
语句与channel
select
语句用于监听多个channel
的操作,它可以同时等待多个channel
的发送或接收操作。当有多个channel
准备好时,select
会随机选择一个执行。select
语句还可以结合default
分支实现非阻塞操作。例如:
1 | ch1 := make(chan int) |
6. channel
的阻塞与死锁
- 阻塞:发送和接收操作在
channel
未准备好时会阻塞当前goroutine
。无缓冲channel
在没有对应的接收者时发送会阻塞,没有发送者时接收会阻塞;有缓冲channel
在缓冲区满时发送会阻塞,缓冲区空时接收会阻塞。 - 死锁:如果在一个
goroutine
中,channel
的发送和接收操作相互等待,且没有其他goroutine
来打破这种等待,就会发生死锁。例如,一个goroutine
向无缓冲channel
发送数据,但没有其他goroutine
接收;或者一个goroutine
从无缓冲channel
接收数据,但没有其他goroutine
发送数据。运行时系统会检测到死锁并报错。
7. channel
的底层实现
channel
的底层实现基于一个名为hchan
的结构体,它包含了当前队列中元素数量、环形队列大小(缓冲容量)、指向环形队列的指针、元素大小、关闭标志、元素类型信息、发送索引、接收索引、等待接收的协程队列、等待发送的协程队列以及一个互斥锁等字段。- 发送操作时,如果接收队列非空,直接将数据拷贝给第一个等待的接收者并唤醒该
goroutine
;如果缓冲区未满,将数据存入缓冲区;如果缓冲区已满或无缓冲channel
,将当前goroutine
加入发送队列并挂起。接收操作时,如果发送队列非空,直接从发送者获取数据并唤醒发送者;如果缓冲区不为空,从缓冲区取出数据;如果缓冲区为空且无缓冲channel
,将当前goroutine
加入接收队列并挂起。
8. channel
误用导致的问题
在 Go 语言中,操作channel
时可能导致panic
或者死锁等:
1.多次关闭同一个channel
使用内置的close
函数关闭channel
后,如果再次调用close
函数尝试关闭同一个channel
,就会引发panic
。这是因为channel
的关闭状态是一种不可逆的操作,重复关闭没有实际意义,并且可能会导致难以调试的问题。例如:
1 | ch := make(chan int) |
2.向已关闭的channel
发送数据
当一个channel
被关闭后,再向其发送数据会导致panic
。因为关闭channel
意味着不再有数据会被发送到该channel
中,继续发送数据违反了这种约定。示例如下:
1 | ch := make(chan int) |
3.关闭未初始化(nil
)的channel
如果尝试关闭一个值为nil
的channel
,会引发panic
。nil
的channel
没有实际的底层数据结构来支持关闭操作。例如:
1 | var ch chan int |
4.死锁导致的panic
在操作channel
时,如果多个goroutine
之间的通信和同步设计不当,可能会导致死锁。死锁发生时,所有涉及的goroutine
都在互相等待对方,从而导致程序无法继续执行,运行时系统会检测到这种情况。例如:
1 | func main() { |
1 | ➜ my go run main.go |
5.不恰当的select
语句使用
在select
语句中,如果没有default
分支,并且所有的case
对应的channel
操作都无法立即执行(阻塞),那么当前goroutine
会被阻塞。如果在主goroutine
中发生这种情况且没有其他goroutine
可以运行,就会导致死锁。例如:
1 | func main() { |
要避免这些panic
情况,编写代码时需要仔细设计channel
的使用逻辑,合理处理channel
的关闭、数据的发送和接收,以及确保goroutine
之间的同步和通信正确无误。
解析
在NewChannel函数中,send和recv channel被赋值的是同一个ErrorChannel,而send和recv都是单向channel,一个只写,一个只读。
所以当Process里send.ErrorChannel <- fmt.Errorf(“0 Start error \n”)执行的时候,main中的case <-recv.ErrorChannel被立即触发,然后执行recv.Close()函数,该函数执行了close(c.StopChannel),又触发了Process中的case <-send.StopChannel,执行了send.Close()。对于Process退出的时候,有defer,再次执行send.Close(),导致channel被多次关闭。
2.2defer
知识点
以前写过Go defer的一些神奇规则,你了解吗?,这次主要关注
- defer(延迟函数)执行按后进先出顺序执行,即先出现的 defer最后执行。
- Process中的defer的执行顺序与Process中的goroutine里的defer(如果有的话)执行顺序无关。
解析
其实这两个Close位置都有可能panic,主要看谁被先执行到。我是为了演示让Process sleep了1s。
1 | defer func() { |
2.3recover
知识点
在 Go 语言中,recover
只能用于捕获当前goroutine
内的panic
,它的作用范围仅限于当前goroutine
。具体说明如下:
**只能捕获当前goroutine
的panic
**:当一个goroutine
发生panic
时,该goroutine
会沿着调用栈向上展开,执行所有已注册的defer
函数。如果在这些defer
函数中调用recover
,则可以捕获到该goroutine
内的panic
,并恢复正常执行流程。而对于其他goroutine
中发生的panic
,当前goroutine
无法通过recover
捕获。例如:
1 | package main |
在上述代码中,worker
函数中的defer
语句里使用recover
捕获了该goroutine
内的panic
。main
函数中的goroutine
并不会受到影响,继续执行并打印出 “Main goroutine continues”。
解析
当时之所以查的比较困难,主要是发现Process中go func里配置了recover,报了很多错,但感觉没有大问题。加上代码不熟悉,没有发现有概率触发Process的defer中的panic。而且公司的监控没有监控到自建goroutine的panic情况。
三、解决方案
在Process中添加recover
1 | defer func() { |
其实比较建议在涉及channel相关的地方,都加个recover,尤其是不太熟悉的时候。