Proposal Details
Proposal: Parameter-level noescape modifier for function types
Summary
Add a noescape parameter modifier to Go function parameter declarations and function types that tells the compiler the parameter will not escape the callee or any synchronous callees.
Example:
type Handler func(noescape buf []byte)
func Call(h Handler, data []byte) {
// Call may pass a stack-backed slice to h and the compiler will know h
// (and functions it calls synchronously) will not cause `data` to be
// moved to the heap.
h(data)
}
This extends the existing //go:noescape directive (today only usable on external/assembly-defined functions) into ordinary Go function types.
Motivation
Escape analysis often forces arguments onto the heap if the compiler cannot prove they don’t escape. Programmers often know an argument is only used synchronously, but:
- Must use
//go:noescapewith assembly stubs, or - Pay for heap allocations, or
- Resort to
unsafe.
A noescape modifier on parameters expresses that guarantee in the type system. This unlocks stack allocation, avoids heap moves, and enables high-performance callback APIs without unsafe tricks.
The Go repo already uses a directive //go:noescape to promise non-escaping behavior for external/assembly-declared functions; however that pragma is limited in applicability. A language-level noescape modifier makes the promise usable on ordinary function values and types.
Proposal
Syntax
Extend parameter declarations with an optional noescape modifier:
func Do(f func(noescape b []byte))
type Handler func(noescape buf []byte)
var h = func(noescape b []byte) { /* ... */ }
Multiple parameters may be marked:
func Two(a int, noescape b []byte, noescape c []byte)
Semantics
noescapepromises the parameter will not outlive the current call stack frame.- The callee must not:
- Store the parameter into heap/global memory.
- Capture it in a goroutine.
- Return it.
- Assign it to variables with longer lifetimes.
- The compiler enforces this at compile time.
- Type system:
func(noescape T)is distinct fromfunc(T).func(noescape T)can be assigned tofunc(T), but not vice versa.
Implications
- The compiler must halt as soon as the user tries to assign a function to that type where the implementation violates the noescape property of this parameter (this is checked in escape analysis)
Errors
Any use that violates the promise is a compile-time error, e.g. capturing in a goroutine.
Examples
Valid:
type Writer func(noescape buf []byte)
func Use(w Writer) {
b := make([]byte, 128) // stays on stack
w(b)
}
func Good(noescape buf []byte) {
_ = len(buf)
}
Invalid:
func Bad(noescape buf []byte) {
go func() {
// ERROR: capturing noescape buf in goroutine
_ = buf[0]
}()
}
Implementation
- Parser/Type checker: allow
noescapekeyword on params, enforce type/assignment rules. - Escape analysis: propagate the promise across synchronous calls.
- SSA/Codegen: keep eligible args stack-allocated.
- Tooling: vet /
-mshould report violations and optimizations.
Compatibility
- 100% backward compatible — old code compiles unchanged.
- Only new code using
noescapewill be rejected if it breaks the rules. - Existing
//go:noescapepragmas remain supported.
Alternatives
- Stick with
//go:noescape+ assembly stubs. - Add
unsafeintrinsics. - Rely on escape analysis improvements only.
Non-goals
- Returning stack pointers.
- Storing
noescapevalues into heap data. - Changing GC semantics.
Migration / Use cases
- I/O libraries that want callbacks with stack-backed buffers.
- Cryptographic APIs to avoid heap allocation of sensitive buffers.
- Libraries currently using
//go:noescapeassembly stubs.
Implementation Plan
- Parser/type-checker prototype.
- Escape analysis integration.
- SSA/codegen optimizations.
- Documentation, vet integration, benchmarks.
Open Questions
- Should it ever apply to return values? (Probably not.)
- How best to handle cross-package diagnostics?
- Exact variance rules for type conversion.
Rationale
- Brings
//go:noescapeinto the type system. - Gives programmers safe, explicit control.
- Strong compile-time checks prevent misuse.
Related Work
- Go’s existing
//go:noescapepragma (for assembly stubs). - Swift had
@noescapefor closures. - Other languages/libraries use similar concepts for lifetimes.
Comment From: seankhliao
This falls into a similar category as inlining, see previous discussions. https://github.com/golang/go/issues/21536#issuecomment-482318582
Comment From: zigo101
The idea is interesting. It will make many optimizations possible. Though it will open the Pandora's box of many things.