Skip to content

Production resource lifecycle

Copy page

govips wraps a C library (libvips), so resource management is more involved than pure-Go programs. Every open image corresponds to libvips-allocated memory — missing one Close() means leaking thousands to millions of pixels worth of VRAM/RAM.

func main() {
// 1. Initialise — must check the error
if err := vips.Startup(nil); err != nil {
log.Fatalf("start libvips: %v", err)
}
defer vips.Shutdown()
// 2. Serve loop
server.Run()
}
  • Startup(nil) is called once per process. It initialises the GLib type system and libvips internal state. As of v2.18.0, Startup may return an error (previously panicked internally).
  • Shutdown() frees all libvips global resources, purges caches, and tears down thread pools.
  • Always defer Shutdown(), even across panics.
func handler(w http.ResponseWriter, r *http.Request) error {
image, err := vips.NewImageFromFile(r.URL.Query().Get("path"))
if err != nil {
return fmt.Errorf("load: %w", err)
}
defer image.Close()
if err := image.Resize(0.5, vips.KernelLanczos3); err != nil {
return fmt.Errorf("resize: %w", err)
}
// image auto-closes when handler returns
return nil
}
  • Every ImageRef allocates libvips memory.
  • defer image.Close() is the idiomatic Go pattern.
  • Never use image after Close — libvips will either segfault or return garbled data.
func process(input string) ([]byte, error) {
image, err := vips.NewImageFromFile(input)
if err != nil {
return nil, err
}
defer image.Close()
if err := image.AutoRotate(); err != nil {
return nil, err // image.Close() runs via defer
}
buf, _, err := image.ExportJpeg(vips.NewJpegExportParams())
return buf, err
}
func process(input string) ([]byte, error) {
image, _ := vips.NewImageFromFile(input) // BAD: ignored error
defer image.Close() // defer is correctly placed
image.Close() // BAD: closed too early — double-close on return
return image.ExportJpeg(nil) // BAD: use after close — segfault
}
func process(input string) ([]byte, error) {
image, err := vips.NewImageFromFile(input)
if err != nil {
return nil, err
}
// Forgot to Close — leaks memory every call
return image.ExportJpeg(nil)
}

govips v2.18.0 introduced leak detection tools:

  • OpenImageRefs() returns the number of currently unclosed ImageRef instances (development/debugging).
  • AssertNoLeaks(t) asserts at the end of a test that nothing is leaking — use with go test.
func TestResize(t *testing.T) {
defer vips.AssertNoLeaks(t) // checks at test end
image, err := vips.NewImageFromFile("input.jpg")
if err != nil {
t.Fatal(err)
}
defer image.Close()
// ...
}

govips panics can occur in these scenarios:

  • ImageRef used after Close (double free).
  • cgo calls made before libvips is initialised (forgot Startup).
  • Go slices passed to C are GC’d mid-call (pre-v2.19.0 SetBlob bug; fixed).

Defence: use recover at goroutine entry points, converting panics into error logs. Do not assume the libvips state is consistent after a panic.

  • Startup(nil) return value is checked.
  • Shutdown() is deferred in main.
  • Every NewImageFromFile/NewImageFromBuffer error is checked.
  • Every ImageRef has defer Close() or explicit close.
  • ImageRef is never accessed after Close.
  • MALLOC_ARENA_MAX=2 is tested under concurrency load.
  • AssertNoLeaks(t) is present in tests.
  • Goroutine panic recovery is set up in the serving process.