Go 程序太大了,能要个延迟初始化不?

在公司的不断发展中,一开始大多是大单体,改造慢了,一个仓库会有使用十几年的情况,仓库的规模基本是不断增大的过程。

影响之一就是会应用程序打包后的体积越来越大,不知道被用哪里去了...今天要探讨的提案《proposal: language: lazy init imports to possibly import without side effects[1]》,就与此有关。

提案

背景

我们来观察一段很简单的 Go 代码,研究研究。如下代码:

package main

import _ "crypto/x509"

func main() {}

这个 Go 程序只有 3 行代码,看起来就没有任何东西。实际上是这样吗?

我们可以执行以下命令看看初始化过程:

$ go build --ldflags=--dumpdep main.go 2>&1 | grep inittask

输出结果:

runtime.main -> runtime..inittask
runtime.main -> main..inittask
main..inittask -> crypto/x509..inittask
crypto/x509..inittask -> bytes..inittask
crypto/x509..inittask -> crypto/sha256..inittask
crypto/x509..inittask -> encoding/pem..inittask
crypto/x509..inittask -> errors..inittask
crypto/x509..inittask -> sync..inittask
crypto/x509..inittask -> crypto/aes..inittask
crypto/x509..inittask -> crypto/cipher..inittask
crypto/x509..inittask -> crypto/des..inittask
...
context..inittask -> context.init.0
vendor/golang.org/x/net/dns/dnsmessage..inittask -> vendor/golang.org/x/net/dns/dnsmessage.init
vendor/golang.org/x/net/route..inittask -> vendor/golang.org/x/net/route.init
vendor/golang.org/x/net/route..inittask -> vendor/golang.org/x/net/route.init.0
...

这段程序其实初始化了超级多的软件包(标准库、第三方包等)。使得包的的大小从标准的 1.3 MB 变成了 2.3 MB。

在一定规模下,大家认为该影响是非常昂贵的。因为你可以看到只有 3 行的 Go 程序并没有做任何实质性的事情。

对启动性能敏感的程序会比较难受,普通程序也会随着日积月累进入恶性循环,启动会比常规的更慢。

方案

在解决方案上我们结合另外一个提案《proposal: spec: Go 2: allow manual control over imported package initialization[2]》一起来看。

核心思想是:引入惰性初始化(lazy init),业内也常称为延迟加载。也就是必要的时候再真正的导入,不在引入包时就完成初始化。

优化方向上:主要是在导入包路径后增加懒惰初始化的声明,例如在下方即将会提到的:go:lazyinit 或 go:deferred 注解。再等待程序真正使用到时再正式初始化。

1、go:lazyinit 的例子:

package main

import (
      "crypto/x509" // go:lazyinit
      "fmt"
)

func main() {...}

2、go:deferred 的例子:

package main

import (
    _ "github.com/eddycjy/core" // go:deferred
    _ "github.com/eddycjy/util" // go:deferred
)

func main() {
    if os.Args[1] != "util" {
        // 现在要使用这个包,开始初始化
        core, err := runtime.InitDeferredImport("github.com/some/module/core")
        ...
    }
    ...
}

以此来实现,可以大大提高启动性能。

讨论

实际上在大多数的社区讨论中,对这个提案是又爱又恨。因为它似乎又有合理的诉求,但细思似乎又会发现完全不对劲。

这个提案的背景和解决方案,是治标不治本的。因为根本原因是:许多库滥用了 init 函数,让许多不必要的东西都初始化了。

c9422fe596970ef6b9e39e8d7a6bbf13.png

Go 核心开发团队认为让库作者去修复这些库,而不是让 Go 来 “解决” 这些问题。如果支持惰性初始化,也会为这些低质量库的作者提供继续这样做的借口。

似曾相识的感觉

在写这篇文章时,我想起了 Go 的依赖管理(Go modules),其有一个设计是基于语义化版本的规范。

如下图

31811e7e792c7fd8f7872b443c0e35f2.png

版本格式为 “主版本号.次版本号.修订号”,版本号的递增规则如下:

  • 主版本号:当你做了不兼容的 API 修改。

  • 次版本号:当你做了向下兼容的功能性新增。

  • 修订号:当你做了向下兼容的问题修正。

Go modules 的原意是软件库都遵守这个规范,因此内部会有最小版本选择的逻辑。

也就是一个模块往往依赖着许多其它许许多多的模块,并且不同的模块在依赖时很有可能会出现依赖同一个模块的不同版本,Go 会把版本清单都整理出来,最终得到一个构建清单。

如下图:

9c807a5e1414bc905653710bae4edf44.png

你会发现最终构建出来的依赖版本很有可能是与预期的不一致,从而导致许多业务问题。最经典的就是 grpc-go、protoc-go、etcd 多版本兼容问题,让许多人痛苦不已。

Go 团队在这一块的设计是比较理想化的,曹大也将其归类在 Go modules 的七宗罪之一了。而软件包的 init 函数乱初始化一堆的问题,也是有些似曾相识了。

总结

这个问题的解决方案(提案)仍然在讨论中,显然 Go 团队更希望软件库的作者能够约束好自己的代码,不要乱初始化。

引入惰性初始化的方式如何,你怎么看?欢迎在评论区留言和讨论。

推荐阅读

参考资料

[1]

proposal: language: lazy init imports to possibly import without side effects: https://github.com/golang/go/issues/38450

[2]

proposal: spec: Go 2: allow manual control over imported package initialization: https://github.com/golang/go/issues/48174

资料下载

点击下方卡片关注公众号,发送特定关键字获取对应精品资料!

  • 回复「电子书」,获取入门、进阶 Go 语言必看书籍。

  • 回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!

  • 回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。

  • 回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。

  • 回复「后台」,获取后台开发必看 10 本书籍。

对了,看完文章,记得点击下方的卡片。关注我哦~ 👇👇👇

如果您的朋友也在学习 Go 语言,相信这篇文章对 TA 有帮助,欢迎转发分享给 TA,非常感谢!2908c2822ba42356a9e17ef47c13d957.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值