• 主页
  • 架构
  • 编程语言
  • 数据存储
  • 网络
  • VMware
  • 服务器
  • 组网
  • AI
  • 算法系列
  • 设计模式
  • 读书笔记
  • 思考
  • 工具
  • 其它技术

  • 主页
  • 架构
  • 编程语言
  • 数据存储
  • 网络
  • VMware
  • 服务器
  • 组网
  • AI
  • 算法系列
  • 设计模式
  • 读书笔记
  • 思考
  • 工具
  • 其它技术

引用开源包需要慎重

2025-05-31

最近需要修复json,查看以前的信息,用的是https://github.com/RealAlexandreAI/json-repair 。

这个包能力很强,大部分json都能修复。但包有个很严重的问题,某种情况下可能触发 stack overflow。

1
2
3
4
5
6
7
8
9
10
func main() {
str := `
{
"Be": "",
"gone": ""
}
",п"г`
dst, err := oldrepair.RepairJSON(str)
fmt.Println(dst, err)
}
1
2
3
4
5
6
7
8
➜  my go run main.go
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0x140200e0390 stack=[0x140200e0000, 0x140400e0000]
fatal error: stack overflow

runtime stack:
runtime.throw({0x104d79071?, 0x100000000?})
exit status 2

后续换了新包https://github.com/kaptinlin/jsonrepair,虽然不会stack overflow,但是修复能力差不多弱了三倍,另外有可能panic,好在panic可以recover。

所以这引出了三个问题:

  1. 选择开源代码需要注意什么?
  2. stack overflow是怎么发生的?
  3. stack overflow发生后有什么补救措施。

选择开源代码的注意事项

我很少引用外部的开源代码,因为结果比较难以把控。印象比较深的一次是为了整数区间计算,这个比较小众,整数区间计算,我这么设计,好在是google家的,我在使用前做了大量的单元测试,上线后效果很好。

在一些相对小众的功能上,在选择 GitHub 上的开源代码时,我觉得可以从以下多个关键方面进行考虑:

项目活跃度

  • 提交频率:查看项目的提交历史,如果在近期有频繁的代码提交,说明项目处于活跃开发状态,开发者在持续对其进行改进、修复问题。比如一个热门的前端框架,每周都有几次提交,这意味着它在不断发展。
  • 问题处理情况:关注项目中 Issues(问题)板块,看新问题的创建频率以及已有问题的解决速度。如果问题能够在短时间内得到回应和处理,表明项目有活跃的维护团队。例如一些知名的开源数据库项目,能在几天内就对用户提出的问题进行解答和修复。
  • 分支合并情况:频繁的分支合并说明项目在不断整合新功能和改进。比如一个大型的开源电商系统,经常有功能分支合并到主分支,代表项目在持续迭代。

社区支持

  • 星星数量:一颗星表示用户对项目的喜爱和关注,星星数量越多,说明项目越受欢迎和受认可。像一些知名的机器学习框架,星星数量能达到十几万甚至更高。
  • Fork 数量:Fork 数量多意味着有很多人基于这个项目进行二次开发,侧面反映了项目的可扩展性和受欢迎程度。例如一些经典的开源博客框架,有大量的 Fork。
  • 参与人数:查看贡献者列表,参与人数多说明项目有一个活跃的社区,不同背景的开发者共同维护和改进项目。例如一些知名的开源操作系统,有来自全球各地的上千名开发者参与。

代码质量

  • 代码结构:良好的代码结构应该是清晰、模块化的,易于理解和维护。可以查看项目的目录结构、文件组织方式以及代码中的注释情况。比如一个遵循 MVC(模型 - 视图 - 控制器)架构的 Web 项目,代码结构层次分明。
  • 测试覆盖率:高测试覆盖率表明代码经过了充分的测试,质量更有保障。可以查看项目中的测试文件和相关测试报告。例如一些严谨的开源金融项目,测试覆盖率能达到 90% 以上。
  • 遵循的规范:查看项目是否遵循行业内的最佳实践和代码规范,这有助于保证代码的一致性和可读性。比如一个 Java 项目遵循阿里巴巴的 Java 开发手册规范。

许可证

  • 开源协议类型:不同的开源协议对使用、修改和分发代码有不同的规定。例如,MIT 协议非常宽松,允许自由使用、修改和分发代码;而 GPL 协议则要求基于该协议开源的代码所衍生的项目也必须开源。如果是商业项目使用开源代码,需要特别注意许可证是否允许商业使用。比如一个商业软件想使用某个开源库,就需要确保该开源库的许可证允许商业用途。

其它

  • 大厂背书:看看是个人开发者还是大厂推出的,一般而言大厂的质量会更好一点
  • 充足测试:在使用前,对自己要用的功能,做充足的单元测试,防止意外发生

stack overflow出现原因

Go 语言运行时会为每个 goroutine 分配一定大小的栈空间(初始栈大小一般较小,随着需求会动态增长),但当上述情况使得栈空间的使用超出了限制时,就会出现stack overflow错误 。我们可以通过debug.SetMaxStack()控制栈的大小。

在 Go 语言中,stack overflow(栈溢出)通常由以下几种情况产生:

无限递归调用

递归函数在没有正确的终止条件时,会不断地调用自身,导致栈上的函数调用信息不断增加,最终耗尽栈空间。例如:

1
2
3
4
5
6
7
8
9
package main

func recursiveFunction() {
recursiveFunction()
}

func main() {
recursiveFunction()
}

在上述代码中,recursiveFunction函数没有终止条件,会一直递归调用自己,很快就会引发栈溢出错误。

深层嵌套调用

即使不是无限递归,如果函数调用的层次过深,也可能导致栈溢出。例如,一系列函数层层调用,每层调用都在栈上增加新的帧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

func func1() {
func2()
}

func func2() {
func3()
}

// 假设这里有很多类似层层调用的函数
func funcN() {
// 没有实际操作,仅作为深层调用示例
}

func main() {
func1()
}

如果这种嵌套调用的层次足够深,超过了 Go 运行时分配给栈的空间大小,就会发生栈溢出。

栈上数据过大

如果在函数调用过程中,函数的局部变量占用了大量的栈空间,并且同时有较多的函数调用在栈上,也可能导致栈溢出。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func largeStackFunction() {
// 创建一个非常大的数组,占用大量栈空间
bigArray := make([]int, 10000000000)
// 这里可以有更多的操作
fmt.Println(bigArray[0], bigArray[500])
}

func main() {
debug.SetMaxStack(1)
for i := 0; i < 10; i++ {
go largeStackFunction()
}
time.Sleep(50 * time.Second)
}

在这个例子中,largeStackFunction函数内创建了一个非常大的数组,每次调用该函数都会在栈上占用较多空间,多次调用后可能会引发栈溢出。

stack overflow解决方案

在 Go 语言中,stack overflow(堆栈溢出)无法通过常规的recover机制捕获。这是因为堆栈溢出是一种底层的、严重的运行时错误,它会破坏调用栈的完整性,导致recover无法正常工作。

虽然无法捕获堆栈溢出错误,但可以采取一些措施来预防它:

  1. 设置递归终止条件:在递归函数中,确保有明确的终止条件,避免无限递归。
  2. 优化递归算法:对于深度递归的算法,可以考虑使用迭代或尾递归优化来减少栈空间的使用。
  3. 合理使用局部变量:避免在函数内部声明过大的局部变量,尽量使用堆内存(通过new或make)来分配较大的数据结构。

资料

  1. Goroutine堆栈大小使用调研
  2. Golang高效编程技巧:如何设置初始栈大小优化性能与内存管理

扫一扫,分享到微信

微信分享二维码
模型应用层商业化建设思路
国际化机房部署常用方案
© 2025 John Doe
Hexo Theme Yilia by Litten