本期分享:
1.循环依赖导致栈溢出
2.无法捕获子协程的panic
循环依赖导致栈溢出
在Go语言开发中,我们经常会遇到结构体之间需要相互引用的情况。当两个结构体直接或间接地相互包含对方作为自己的字段时,就会形成循环依赖。
但是在Go语言中直接进行结构体的相互引用会默认不符合语法,因此我们就使用接口进行引用。
代码示例:
结构体A
type A struct {
Name string
Hi Hi
}
type Say interface {
Say()
}
func (a *A) Say() {
fmt.Println(a.Name, " say Hi")
}
func NewA(name string) *A {
return &A{
Name: name,
Hi: NewB("B"),
}
}
结构体B
type B struct {
Name string
Say Say
}
type Hi interface {
Hi()
}
func (b *B) Hi() {
fmt.Println("Hi ", b.Name)
}
func NewB(name string) *B {
return &B{
Name: name,
Say: NewA("A"),
}
}
当调用NewA(“A”)时,程序会立即崩溃并报错:
runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow
错误原因分析:
无限递归初始化:NewA调用NewB,NewB又调用NewA,从而形成无限循环调用链。
栈空间耗尽:每次函数调用都会占用栈空间,无限递归导致栈空间被耗尽,最终触发栈溢出错误
解决方案
方案1:打破初始化循环
func NewA(name string) *A {
b := &B{Name: "B"} // 先创建B实例
a := &A{
Name: name,
Hi: b, // 直接赋值
}
b.Say = a // 后设置B的Say字段
return a
}
方案2:使用接口+延迟设置
type A struct {
Name string
Hi Hi // 使用接口类型
}
type B struct {
Name string
Say Say // 使用接口类型
}
// 初始化时先创建实例,后设置字段
a := &A{Name: "A"}
b := &B{Name: "B"}
a.Hi = b
b.Say = a
方案3:重新设计结构
考虑是否真的需要双向依赖,可以将共用逻辑提取到第三个结构体。
Go语言中的循环依赖问题看似简单,但可能导致严重的运行时错误。通过本文的分析和解决方案,我们可以更安全地处理对象间的复杂关系
无法捕获子协程的panic
在Go语言中,父协程默认情况下不能直接捕获子协程的panic。这是由Go的并发模型和goroutine的设计决定的:
func Run() {
defer func() {
if r := recover(); r != nil {
log.Printf("ping panic: %v", r) // 这个recover只能捕获当前goroutine的panic
}
}()
go func() {
panic("panic")
}()
time.Sleep(time.Second * 3)
}
原因如下:
独立的执行栈:每个goroutine都有自己的调用栈,panic和recover机制是基于当前goroutine的调用栈的
设计哲学:Go的设计是让每个goroutine自己处理自己的错误,而不是由父goroutine来管理
并发安全:如果允许跨goroutine捕获panic,会导致复杂的并发问题
正确写法:
func Run() {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("ping panic: %v", r)
}
}()
panic("panic")
}()
time.Sleep(time.Second * 3)
}
本篇结束~