What does runtime.KeepAlive do and when is it required?
The GC decides liveness from the point of last use, not from lexical scope. An object can therefore become unreachable, and its finalizer or cleanup can run, in the middle of a function that is still using a resource derived from it, such as an fd or C pointer extracted from its fields. runtime.KeepAlive(x) marks x as used up to that point. It compiles to almost nothing.
type FD struct{ fd int }
func OpenFD(path string) (*FD, error) {
fd, err := syscall.Open(path, syscall.O_RDONLY, 0)
if err != nil {
return nil, err
}
f := &FD{fd: fd}
runtime.AddCleanup(f, func(fd int) { syscall.Close(fd) }, fd)
return f, nil
}
func (f *FD) Read(p []byte) (int, error) {
fd := f.fd
// After this line f is no longer used, so without KeepAlive
// the GC could collect f and close fd DURING the syscall.
n, err := syscall.Read(fd, p)
runtime.KeepAlive(f)
return n, err
}
You need it when all three hold: (1) the object has a finalizer or cleanup that releases a resource, (2) you extracted a raw handle or uintptr from it, and (3) you keep using that handle after your last Go-level use of the object. It is also common in cgo code that passes C pointers owned by a Go wrapper.
What the interviewer is looking for: you know that scope does not equal liveness in Go, and that this bug is real. os.File and net use this pattern internally. The syscall calls above assume a Unix-like platform.
More on Memory, GC & Runtime Internals
- Q355What is
runtime.AddCleanupand why is it preferred overSetFinalizer? - Q356What are weak pointers in Go and when would you use them?
- Q358What does this print, and what memory problem does it illustrate?
- Q359Why can removing elements from a slice of pointers leak memory?
- Q360Do Go maps shrink after you delete entries? How do you reclaim the memory?
- Q361How do goroutine leaks happen, and how do you detect them?