在公司的持續發展中,一開始大多是大單體,改造慢了,一個倉庫會有使用十幾年的情況,倉庫的規模基本是不斷增大的過程。
影響之一就是會應用程式打包後的體積越來越大,不知道被用哪裡去了...今天要探討的提案《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 函数,让许多不必要的东西都初始化了。
Go 核心开发团队认为让库作者去修复这些库,而不是让 Go 来 “解决” 这些问题。如果支持惰性初始化,也会为这些低质量库的作者提供继续这样做的借口。
在写这篇文章时,我想起了 Go 的依赖管理(Go modules),其有一个设计是基于语义化版本的规范。
如下图
版本格式为 “主版本号.次版本号.修订号”,版本号的递增规则如下:
Go modules 的原意是软件库都遵守这个规范,因此内部会有最小版本选择的逻辑。
也就是一个模块往往依赖着许多其它许许多多的模块,并且不同的模块在依赖时很有可能会出现依赖同一个模块的不同版本,Go 会把版本清单都整理出来,最终得到一个构建清单。
如下图:
你會發現最終建構出來的依賴版本很有可能是與預期的不一致,從而導致許多業務問題。最經典的就是 grpc-go、protoc-go、etcd 多版本相容問題,讓許多人痛苦不已。
Go 團隊在這一塊的設計是比較理想化的,曹大也將其歸類在 Go modules 的七宗罪之一了。而軟體包的 init 函數亂初始化一堆的問題,也是有些似曾相識了。
這個問題的解決方案(提案)仍然在討論中,顯然Go 團隊更希望軟體庫的作者能夠約束好自己的程式碼,不要亂初始化。
引入惰性初始化的方式如何,你怎麼看?歡迎在留言區留言和討論。
以上是Go 程式太大了,能要個延遲初始化不?的詳細內容。更多資訊請關注PHP中文網其他相關文章!