引用开源包需要慎重

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

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

func main() {
	str := `
{
  "Be": "",
  "gone": "" 
}
",п"г`
	dst, err := oldrepair.RepairJSON(str)
	fmt.Println(dst, err)
}
➜  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(栈溢出)通常由以下几种情况产生:

无限递归调用

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

package main

func recursiveFunction() {
    recursiveFunction()
}

func main() {
    recursiveFunction()
}

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

深层嵌套调用

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

package main

func func1() {
    func2()
}

func func2() {
    func3()
}

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

func main() {
    func1()
}

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

栈上数据过大

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

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. 合理使用局部变量:避免在函数内部声明过大的局部变量,尽量使用堆内存(通过newmake)来分配较大的数据结构。

资料

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

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:https://shidawuhen.github.io/

往期文章回顾:

  1. 设计模式

  2. 招聘

  3. 思考

  4. 存储

  5. 算法系列

  6. 读书笔记

  7. 小工具

  8. 架构

  9. 网络

  10. Go语言

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员麻辣烫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值