根据以上两条规则我们来看如下测试代码,
func main() {
a := 100000
println(a)
//1. 单独defer输出--猜想下它的输出
defer println("defer :", &a, a)
//2. defer 配合闭包--猜想下它的输出
defer func() { println("defer fun :", &a, a) }()
//3. defer 配合闭包与入参
defer func(a int) { println("defer fun with param :", &a, a) }(a)
a++
println(a)
}
所以输出如下:
100000
100001
defer fun with param : 0xc0000446e8 100000
defer fun : 0xc000044710 100001
defer : 0xc000044710 100000
进程 已完成,退出代码为 0
泄露的原因:
如果routine在运行中被阻塞,或者速度很慢,不会正常关闭routine时就会泄漏。阻塞分为两种情况:
防止泄露:
举个例子
//选择一个合适的缓冲区大小
myChan = make(chan int, 10)
// 非阻塞读一次
select {
case msg := <- myChan:
fmt.Println(msg)
default:
fmt.Println("No Msg")
}
// 阻塞<=1s读
for {
select {
case msg := <- myChan:
fmt.Println(msg)
return msg
case <-time.After(1 * time.Second):
fmt.Println("You're too slow.")
return ""
default:
fmt.Println("No Msg")
time.Sleep(50 * time.Millisecond)
}
}
// 非阻塞写
select {
case myChan <- "message":
fmt.Println("sent the message")
default:
fmt.Println("no message sent")
}
遵循一个约定:谁创建,谁停止(谁创建goroutine,谁负责停止goroutine)
func producer(ch chan int) {
defer close(ch) // defer保证异常退出时自动关闭channel
for i := 0; i < 6; i++ {
time.Sleep(time.Second)
ch <- i
}
}
func consumer(id int, ch <-chan int, wg *sync.WaitGroup) {
defer wg.Done() //使用defer确保执行成功
for v := range ch {
time.Sleep(time.Millisecond)
fmt.Printf("ID:%d,task:%d\n", id, v)
}
fmt.Printf("ID:%d,task done\n", id)
}
func main() {
conNum := 2
var wg sync.WaitGroup
//定义带缓存的管道
ch := make(chan int, conNum)
// 创建生产者
go producer(ch)
for i := 0; i < conNum; i++ {
wg.Add(1) // 避免一次设置所有的数量,
go consumer(i, ch, &wg)
}
wg.Wait()
fmt.Println(runtime.NumGoroutine())
}
输出:
ID:1,task:0
ID:0,task:1
ID:1,task:2
ID:0,task:3
ID:1,task:4
ID:1,task done
ID:0,task:5
ID:0,task done
1
func producer(ch chan int) {
defer close(ch) // defer保证异常退出时自动关闭channel
for i := 0; i < 6; i++ {
time.Sleep(time.Second)
ch <- i
}
}
func consumer(id int, ch <-chan int, done chan bool) {
defer func() { done <- true }() //使用defer确保执行成功
for v := range ch {
time.Sleep(time.Millisecond)
fmt.Printf("ID:%d,task:%d\n", id, v)
}
fmt.Printf("ID:%d,task done\n", id)
}
func main() {
conNum := 2
var wg sync.WaitGroup
//定义带缓存的管道
ch := make(chan int, conNum)
done := make(chan bool, conNum)
// 创建生产者
go producer(ch)
for i := 0; i < conNum; i++ {
wg.Add(1) // 避免一次设置所有的数量,
go consumer(i, ch, done)
}
// 使用channel 阻塞主线程
for i := 0; i < conNum; i++ {
<-done
}
fmt.Println(runtime.NumGoroutine())
}
good case:
func main() {
defer func() {
log.Println("done") // Println executes normally even if there is a panic
if x := recover(); x != nil {
log.Printf("run time panic: %v", x)
}
}()
panic("hello,I'm panic") //panic和defer在同一个协程里面
}
输出:
2022/06/30 14:23:13 done
2022/06/30 14:23:13 run time panic: hello,I'm panic
进程 已完成,退出代码为 0
Bad case:
func main() {
defer func() {
log.Println("done") // Println executes normally even if there is a panic
if x := recover(); x != nil {
log.Printf("run time panic: %v", x)
}
}()
go func() {
panic("hello,I'm Goroutine panic") // Panic在新的go协程
}()
}
输出:
panic: hello,I'm Goroutine panic
goroutine 6 [running]:
main.main.func2()
/Users/bytedance/GolandProjects/awesomeProject/main.go:23 +0x27
created by main.main
/Users/bytedance/GolandProjects/awesomeProject/main.go:22 +0x45
进程 已完成,退出代码为 2
所以在go 的并发中我们启动新的协程时要使用defer捕获新协程的panic,防止拖累主服务。
参考网上的实现,提供了一种安全地启动并发的实现,其实就是类似AOP的思想,在所有goroutine启动之前defer+recover确保安全,同时提供了带err处理的方式(可选),省去之前单独传入errChan来收集错误的烦恼
package main
import (
"context"
"fmt"
"sync/atomic"
"time"
)
type PanicGroup[T int | string] struct {
ctx context.Context //ctx
param chan T //参数 通道
panics chan interface{} // 协程 panic 通知信道
dones chan int // 协程完成通知信道
err chan error // 错误消息通道
jobN int32 // 协程并发数量
}
func (g *PanicGroup[T]) GoWithErrReturn(f func(param chan T) error) *PanicGroup[T] {
atomic.AddInt32(&g.jobN, 1)
go func() {
defer func() {
if r := recover(); r != nil {
g.panics <- r
return
}
g.dones <- 1
}()
g.err <- f(g.param)
}()
return g // 方便链式调用
}
func (g *PanicGroup[T]) Go(f func(param chan T)) *PanicGroup[T] {
atomic.AddInt32(&g.jobN, 1)
go func() {
defer func() {
if r := recover(); r != nil {
g.panics <- r
return
}
g.dones <- 1
}()
f(g.param)
}()
return g // 方便链式调用
}
func (g *PanicGroup[T]) Wait(ctx context.Context) error {
for {
select {
case <-g.dones:
if atomic.AddInt32(&g.jobN, -1) == 0 {
return nil
}
case p := <-g.panics:
fmt.Println(p)
return nil
case e := <-g.err:
return e
case <-ctx.Done():
return ctx.Err()
}
}
}
func NewPanicGroup[T int | string](ctx context.Context, chanCap int) *PanicGroup[T] {
return &PanicGroup[T]{
param: make(chan T, chanCap),
panics: make(chan interface{}, chanCap),
dones: make(chan int, chanCap),
err: make(chan error, chanCap),
ctx: ctx,
}
}
func main() {
ctx := context.Background()
//定义消费者数量
comNum := 3
var g = NewPanicGroup[int](ctx, comNum)
//创建生产者
g.Go(producer)
//创建消费者
for i := 0; i < comNum; i++ {
g.Go(consumer)
}
// 等待所有 goroutines 结束
err := g.Wait(ctx)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("all done")
}
func producer(ch chan int) {
defer close(ch) // defer保证异常退出时自动关闭channel
for i := 0; i < 6; i++ {
time.Sleep(time.Second)
ch <- i
}
}
func consumer(ch chan int) {
for v := range ch {
if v == 4 {
panic("o my god!!! panic when task 4")
}
time.Sleep(time.Millisecond)
fmt.Printf("all:6,task:%d\n", v)
}
fmt.Printf("task done\n")
}
输出:
all:6,task:0
all:6,task:1
all:6,task:2
all:6,task:3
o my god!!! panic when task 4
all done
进程 已完成,退出代码为 0
注意事项