生产环境里的 govips 生命周期
govips 在 Go 进程里持有 libvips 的原生图像句柄。进程启动时初始化一次,每张图处理完单独关闭。漏掉 Close() 会让 OpenImageRefs() 升高,并保留那张图占用的原生内存。这不等于显存(VRAM)泄漏,也不等于进程 RSS 一定无界增长。
进程级:启动一次
Section titled “进程级:启动一次”Startup 初始化 libvips。从 v2.18.0 起它返回 error,必须检查。每个进程调用一次,不要放进每条请求。
func main() { if err := vips.Startup(nil); err != nil { log.Fatalf("start libvips: %v", err) } defer vips.Shutdown()
if err := serve(); err != nil { log.Fatal(err) }}Shutdown 释放 libvips 的进程级资源。仍有请求在使用 ImageRef 时不要调用它。
每张图:创建成功才关闭
Section titled “每张图:创建成功才关闭”下面的函数从调用方已经校验过的路径读取文件、缩小并导出。创建失败时没有可关闭的引用;创建成功后,defer Close() 覆盖后续的缩放和导出错误。
func resizeJPEG(path string) ([]byte, error) { image, err := vips.NewImageFromFile(path) if err != nil { return nil, fmt.Errorf("load: %w", err) } defer image.Close()
if err := image.Resize(0.5, vips.KernelLanczos3); err != nil { return nil, fmt.Errorf("resize: %w", err) } data, _, err := image.ExportJpeg(vips.NewJpegExportParams()) if err != nil { return nil, fmt.Errorf("export: %w", err) } return data, nil}不要把请求里的原始查询字符串直接交给 NewImageFromFile。路径应来自应用自己解析并允许的存储位置。关闭之后不要再调用该 ImageRef:govips 不能保证对已关闭引用的访问是安全的。
常见生命周期反模式:
- 忽略
NewImageFromFile的错误并直接defer Close(),会在加载失败时对空引用调用关闭。 - 先手动
Close(),函数返回时defer再关一次,属于重复关闭。 - 创建成功但存在提前返回分支未调用
Close(),引用计数将无法回到基线。
引用计数能证明什么
Section titled “引用计数能证明什么”govips v2.18.0 起,OpenImageRefs() 返回当前未关闭的 ImageRef 数量,AssertNoLeaks(t) 可在测试结束时检查。在 v2.19.0 上,对一张已知图片加载并立即关闭、循环 1000 次之后,计数从基线 0 回到 0。该结果说明这条最小路径没有遗留 ImageRef;它不直接测量 RSS,也不排除原生分配器保留的缓存页面。
缓存上限用 vips.Config 的 MaxCacheMem、MaxCacheSize 和 MaxCacheFiles 在启动前设定。ClearCache() 只适合诊断。
和其他入口的边界
Section titled “和其他入口的边界”sharp、pyvips 和 CLI 没有 OpenImageRefs()。它们的对象何时释放取决于各自的运行时,不能把 govips 的计数方式直接套用。流式读取另有 Reader 必须保持可用直到 Close() 的规则,见Reader/Writer 流式 I/O。