《Go小技巧&易错点100例》第三十五篇

本期分享:

1.循环依赖导致栈溢出

2.无法捕获子协程的panic


循环依赖导致栈溢出

在Go语言开发中,我们经常会遇到结构体之间需要相互引用的情况。当两个结构体直接或间接地相互包含对方作为自己的字段时,就会形成循环依赖。

但是在Go语言中直接进行结构体的相互引用会默认不符合语法,因此我们就使用接口进行引用。

代码示例

结构体A

go 复制代码
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

go 复制代码
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")时,程序会立即崩溃并报错:

go 复制代码
runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow

错误原因分析

  • 无限递归初始化:NewA调用NewB,NewB又调用NewA,从而形成无限循环调用链。

  • 栈空间耗尽:每次函数调用都会占用栈空间,无限递归导致栈空间被耗尽,最终触发栈溢出错误

解决方案

方案1:打破初始化循环

go 复制代码
func NewA(name string) *A {
    b := &B{Name: "B"}  // 先创建B实例
    a := &A{
        Name: name,
        Hi:   b,  // 直接赋值
    }
    b.Say = a  // 后设置B的Say字段
    return a
}

方案2:使用接口+延迟设置

go 复制代码
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的设计决定的:

go 复制代码
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,会导致复杂的并发问题

正确写法

go 复制代码
func Run() {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                log.Printf("ping panic: %v", r)
            }
        }()
        panic("panic")
    }()

    time.Sleep(time.Second * 3)
}

本篇结束~

相关推荐
驕傲的兎孒7 分钟前
基于 SpringBoot + Vue3 + AI 打造企业级售后服务支持平台 | 实战方案分享
人工智能·spring boot·后端
大傻^10 分钟前
Spring AI Alibaba 可观测性实践:AI应用监控与链路追踪
java·人工智能·后端·spring·springaialibaba
Dxy123931021610 分钟前
js如何把字符串转数字
开发语言·前端·javascript
诗人不写诗19 分钟前
spring是如何组织切面的
java·后端·spring
_饭团28 分钟前
字符串函数全解析:12 种核心函数的使用与底层模拟实现
c语言·开发语言·学习·考研·面试·蓝桥杯
Larry_Yanan29 分钟前
Qt网络开发之基于 QWebEngine 实现简易内嵌浏览器
linux·开发语言·网络·c++·笔记·qt·学习
2401_8318249636 分钟前
嵌入式C++驱动开发
开发语言·c++·算法
小杨同学4938 分钟前
STM32 进阶封神之路(二十二):DMA 实战全攻略 ——ADC 采集 + 串口收发 + 内存复制(库函数 + 代码落地)
后端·单片机·嵌入式
qingcyb42 分钟前
重复 id 对应的多个对象
开发语言·python
li星野1 小时前
[特殊字符] 模拟试卷一:C++核心与系统基础(90分钟)答案版
开发语言·c++·算法