Golang 1.11.5版本中的巨型异常处理案例
Golang 1.11.5版本中的巨型异常处理案例 如果这个问题之前有人提过请见谅,但"我的二进制文件太大了!"的原因似乎有很多种,不太确定这是否是新问题。
我有一个中等规模(19千行代码,700KB)的Go项目,其中大约1千行代码和180KB是密集的初始化代码,看起来像这样:
var objList = []Obj{
fg('.', "White", gen(22, 15, desc("Blah blah blah blah. ", grow("thingy", speed(10, hp(150, visualrange(3, ac(14, dozy(10, blow("hit", "hurt", "4d8", blow("hit", "hurt", "4d8", isHidden(isMimic(isInvisible(isCold(hook("all", "none", resist("fear confusion sleep", intel(1, 0, invisithing("blabla")))))))))))))))))),
<跳过1000个类似条目>
}
这段代码中间有一个返回Obj的函数,然后通过每个外围函数调用按值传递回来。Obj结构体有几百字节。虽然不指望它超级高效,但考虑到它只在初始化时运行一次,用来替代外部配置文件,这样已经足够好了。如预期的那样,它花费的时间小于0.1秒——所以我宁愿不用更优化但更丑陋的方案来替换它……
然而它似乎会让二进制文件变得非常庞大——323MB,其中318MB是.rodata段,包含高度重复的二进制数据,类似于Obj结构的副本。通过验证这一点,向Obj结构添加128字节的数组会使大小增加到444MB,而使用lz4压缩后二进制文件可以缩小到约4.5MB。 (如果将添加的数组设置得更大,比如1KB,会出现如下错误: :1: prepwrite: bad off=1073741824 siz=1 s= 这表明它试图生成超过1GB的可执行文件,但碰到了某些内部限制。)
所以UPX是一个有效的解决方法(直到可执行文件超过1GB为止),gccgo是另一个方案(完全静态编译带调试信息时为18MB,动态发布版本小于4MB)。但是这两种方法都很慢(gccgo大约需要105秒,而gc只需要35秒),所以这并不理想。
strip对大小没有显著影响,因为所有内容都在.rodata中。更改编译器参数也没有帮助。我使用nm和readelf进行了检查,但它们没有显示异常大的符号——只有一个庞大且无特征的.rodata段。
我使用的是Linux AMD64的Go 1.11.5。
有什么建议吗?
更多关于Golang 1.11.5版本中的巨型异常处理案例的实战教程也可以访问 https://www.itying.com/category-94-b0.html
我在同一个项目上尝试了 Go 1.12rc1 版本。 这是一个巨大的改进——从 370MB 降至 6.5MB,并且速度也快了很多(重建时间从 13 秒缩短至 3 倍!)。 如果你遇到类似问题,强烈推荐使用此版本。
更多关于Golang 1.11.5版本中的巨型异常处理案例的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
这是一个典型的Go编译器在处理大型静态初始化数据时的问题,特别是当这些数据通过函数调用链生成时。编译器会将所有中间计算值都嵌入到.rodata段中,导致二进制文件异常膨胀。以下是几种可行的解决方案:
方案1:使用代码生成替代静态初始化
创建一个代码生成工具,在编译前将静态数据预计算为序列化格式:
// build_data.go - 代码生成工具
package main
import (
"encoding/gob"
"os"
)
func generateData() []Obj {
// 原有的初始化逻辑
return []Obj{
fg('.', "White", gen(22, 15, desc("Blah blah blah"))),
// ... 更多条目
}
}
func main() {
data := generateData()
file, _ := os.Create("data.gob")
defer file.Close()
encoder := gob.NewEncoder(file)
encoder.Encode(data)
}
然后在主程序中加载:
// main.go
var objList []Obj
func init() {
file, _ := os.Open("data.gob")
defer file.Close()
decoder := gob.NewDecoder(file)
decoder.Decode(&objList)
}
编译时先运行 go run build_data.go 生成数据文件,再编译主程序。
方案2:使用外部配置文件
将数据移到JSON或类似的配置文件中:
// data.json
[
{
"char": ".",
"color": "White",
"properties": {
"x": 22,
"y": 15,
"description": "Blah blah blah"
}
}
// ...
]
加载代码:
import "encoding/json"
func loadObjects() []Obj {
data, _ := os.ReadFile("data.json")
var objList []Obj
json.Unmarshal(data, &objList)
return objList
}
var objList = loadObjects()
方案3:运行时动态构建
如果数据结构允许,考虑在运行时按需构建:
type ObjBuilder struct {
char rune
color string
// 其他字段
}
func (b *ObjBuilder) WithChar(c rune) *ObjBuilder {
b.char = c
return b
}
func (b *ObjBuilder) WithColor(color string) *ObjBuilder {
b.color = color
return b
}
func (b *ObjBuilder) Build() Obj {
return Obj{
Char: b.char,
Color: b.color,
// 其他字段初始化
}
}
func initObjects() []Obj {
var objects []Obj
builder := &ObjBuilder{}
objects = append(objects,
builder.WithChar('.').WithColor("White").Build(),
// 更多构建调用
)
return objects
}
var objList = initObjects()
方案4:使用字符串压缩技术
如果必须保留静态初始化,可以考虑对重复字符串进行压缩:
var stringPool = map[string]string{
"white": "White",
"blah": "Blah blah blah",
// 更多字符串映射
}
func getString(key string) string {
return stringPool[key]
}
var objList = []Obj{
fg('.', getString("white"), gen(22, 15, desc(getString("blah")))),
// ...
}
第一种代码生成方案通常是最有效的,它完全避免了编译器处理大量静态数据的问题,同时保持了代码的可读性和编译速度。

