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 from func(T).
  • func(noescape T) can be assigned to func(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

  1. Parser/type-checker prototype.
  2. Escape analysis integration.
  3. SSA/codegen optimizations.
  4. 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.