init()函数执行时机完全在冷启动窗口内,其串行执行会直接拖慢首次调用;应避免i/o、网络连接等阻塞操作,改用带context超时和error返回的sync.once懒加载,并精简依赖与二进制体积。

init() 函数执行时机完全在冷启动窗口内
Go 的 init() 函数不是“准备阶段”,而是冷启动耗时的直接组成部分。从二进制 mmap 完成、runtime 初始化结束,到 lambda.Start 或 main() 第一行代码执行前,所有包级 init() 都已串行跑完——AWS 计费和超时计时器此时早已开始。
常见错误现象:go run main.go 启动快,但构建后 ./myapp 或 Lambda 首次调用卡顿 2–5 秒;pprof 火焰图里 runtime.doInit 占比超高;日志里根本没打出 “starting…” 就超时失败。
- 别在
config/包的init()里调os.ReadFile("config.yaml")—— 文件 I/O 直接进冷启动路径 - 别在
db/包里写var db = sql.Open(...); func init() { db.Ping() }—— 网络阻塞 + 失败即进程退出 - 第三方库的隐式初始化也要查:
go tool compile -S main.go | grep "CALL.*init"能快速暴露gopkg.in/yaml.v3、zap、github.com/spf13/viper等是否悄悄执行了重型操作
包依赖图决定 init 执行顺序和范围
Go 不会加载你“没 import 的包”,但只要某个间接依赖 import 了它,整个模块就可能被拉进来并触发其 init()。比如只用了 aws-sdk-go-v2/config 的 LoadDefaultConfig,但 SDK 默认启用 EC2 IMDS 探测逻辑,就会把 net/http、crypto/tls、encoding/json 全部拖进初始化链,哪怕你实际只走环境变量配置。
更隐蔽的是测试/调试相关包:误加 _ "net/http/pprof" 不仅引入 HTTP server,还 embed 大量 HTML/JS 模板,二进制体积涨 1–2MB,冷启动多 100ms+。
- 显式限定子包:
import "github.com/aws/aws-lambda-go/events/apigw",而非"github.com/aws/aws-lambda-go/events" - 禁用 SDK 自动探测:
config.WithClientLogMode(0)+config.WithDefaultRegion("us-east-1")+ 显式传入config.WithCredentialsProvider,避免默认行为拉入 IMDS/SSM 等 client - 检查
go mod graph输出,找那些“只用了一两个函数却带进整个 module”的依赖(如golang.org/x/tools);考虑用go mod edit -droprequire或 fork 精简版
sync.Once 懒加载必须带 context 和 error 返回
sync.Once 是把重操作移出冷启动的正确姿势,但它不是万能胶布——写错照样卡住首请求。
典型陷阱:在 Once.Do() 里写 http.Get("https://api.example.com/health") 或没设 timeout 的 db.Ping(),会导致所有并发首请求排队等待,而不是各自超时降级。
- 必须用
context.WithTimeout控制阻塞上限,例如ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - 必须返回
error,不能log.Fatal或panic—— 上层 handler 才能决定是重试、降级还是返回 503 - 别在
init()里调sync.Once.Do()—— init 本身已是单次执行,套 Once 属冗余且易误导 - 示例安全写法:
var dbOnce sync.Once var db *sql.DB var dbErr error func GetDB() (*sql.DB, error) { dbOnce.Do(func() { d, err := sql.Open("mysql", os.Getenv("DSN")) if err != nil { dbErr = err return } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err = d.PingContext(ctx); err != nil { dbErr = err return } db = d }) return db, dbErr }
构建参数和 vendor 策略直接影响 init 范围
二进制体积大 ≠ init 开销高,但二者强相关:体积越大,mmap 和符号解析越慢;而未裁剪的 embed 资源、调试符号、未用 codec,都会让 init() 加载更多 runtime 数据结构。
CGO_ENABLED=0 是底线,但不够 —— 它只解决 libc 依赖,不解决标准库膨胀(如 crypto/tls 默认链接全部 cipher 实现)。
- 必加构建 flag:
go build -ldflags="-s -w" -gcflags="-l"—— 去符号表 + 关内联,通常减体积 30%–50% - 禁用 embed 资源:
//go:embed要慎用,尤其 JSON Schema、字体、模板文件;Lambda 场景下优先用远程加载或环境变量注入 - vendor 不等于安全:
go mod vendor后必须用go build -mod=vendor,否则仍可能 fallback 到 GOPROXY 加载未 vendor 的间接依赖 - 镜像基础层选
public.ecr.aws/lambda/provided.al2而非golang:alpine—— 前者预装 runtime,省掉 Go stdlib 解压时间,实测对 15MB+ 二进制提升明显
真正难的不是知道该删什么,而是判断“这个 init 是否真被调用”——很多包的初始化逻辑藏在 import _ "xxx" 的空白导入里,不看源码或 go tool nm 输出,根本发现不了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











