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


    提案

    背景

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

    1. package main
    2. import _ "crypto/x509"
    3. func main() {}

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

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

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

    输出结果:

    1. runtime.main -> runtime..inittask
    2. runtime.main -> main..inittask
    3. main..inittask -> crypto/x509..inittask
    4. crypto/x509..inittask -> bytes..inittask
    5. crypto/x509..inittask -> crypto/sha256..inittask
    6. crypto/x509..inittask -> encoding/pem..inittask
    7. crypto/x509..inittask -> errors..inittask
    8. crypto/x509..inittask -> sync..inittask
    9. crypto/x509..inittask -> crypto/aes..inittask
    10. crypto/x509..inittask -> crypto/cipher..inittask
    11. crypto/x509..inittask -> crypto/des..inittask
    12. ...
    13. context..inittask -> context.init.0
    14. vendor/golang.org/x/net/dns/dnsmessage..inittask -> vendor/golang.org/x/net/dns/dnsmessage.init
    15. vendor/golang.org/x/net/route..inittask -> vendor/golang.org/x/net/route.init
    16. vendor/golang.org/x/net/route..inittask -> vendor/golang.org/x/net/route.init.0
    17. ...

    这段程序其实初始化了超级多的软件包(标准库、第三方包等)。使得包的的大小从标准的 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 的例子:

    1. package main
    2. import (
    3.       "crypto/x509" // go:lazyinit
    4.       "fmt"
    5. )
    6. func main() {...}

    2、go:deferred 的例子:

    1. package main
    2. import (
    3.     _ "github.com/eddycjy/core" // go:deferred
    4.     _ "github.com/eddycjy/util" // go:deferred
    5. )
    6. func main() {
    7.     if os.Args[1] != "util" {
    8.         // 现在要使用这个包,开始初始化
    9.         core, err := runtime.InitDeferredImport("github.com/some/module/core")
    10.         ...
    11.     }
    12.     ...
    13. }

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

    讨论

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

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

     

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

    似曾相识的感觉

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

    如下图

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

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

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

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

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

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

    如下图:

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

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

    总结

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

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

  • 相关阅读:
    stm32和电机开发(上位系统)
    M系列 Mac使用Homebrew下载配置git和连接GitHub
    小知识· Zigbee 简介
    2023年计算机毕设选题推荐
    【数据分享】2023年我国省市县三级的独角兽企业数量(Excel/Shp格式)
    PXE高效批量网络装机
    阿里云国际版账户登录不上去什么原因?
    K8S-Pod 进阶
    Arduino + ESP32-C3 + TFT(1.8‘ ST7735S)基础平台(实验三)折腾 TFT_eSPI 库
    云备份项目2
  • 原文地址:https://blog.csdn.net/hebiwen95/article/details/126058487