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:noescape
with 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
noescape
promises 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
noescape
keyword on params, enforce type/assignment rules. - Escape analysis: propagate the promise across synchronous calls.
- SSA/Codegen: keep eligible args stack-allocated.
- Tooling: vet /
-m
should report violations and optimizations.
Compatibility
- 100% backward compatible — old code compiles unchanged.
- Only new code using
noescape
will be rejected if it breaks the rules. - Existing
//go:noescape
pragmas remain supported.
Alternatives
- Stick with
//go:noescape
+ assembly stubs. - Add
unsafe
intrinsics. - Rely on escape analysis improvements only.
Non-goals
- Returning stack pointers.
- Storing
noescape
values 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:noescape
assembly 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:noescape
into the type system. - Gives programmers safe, explicit control.
- Strong compile-time checks prevent misuse.
Related Work
- Go’s existing
//go:noescape
pragma (for assembly stubs). - Swift had
@noescape
for 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.