Compiler Directives and Pragmas

A compiler directive (informally a “pragma”) is a specially formatted comment that instructs the Go compiler to do something it would not do by default — disable an optimization, change a symbol’s name, omit a safety check, embed a file. Directives are comments, not language syntax, precisely so that a Go file carrying them remains a valid Go file for any tool: the [[Go Compiler Architecture|gc compiler]] recognizes them, but gofmt, go vet, and a non-gc compiler simply see comments. The recognized form is “a comment that must be placed on its own line, with only leading spaces and tabs allowed before the comment, and no space between the comment opening and the name of the directive” — //go:noinline, never // go:noinline (cmd/compile directives). Most directives are deliberately restricted: many are honored only in files that import unsafe, or only inside the Go standard library and runtime, because they trade away safety the language otherwise guarantees.

Mental Model

Think of directives as escape hatches at three levels of privilege — though the boundaries are softer than they look, and the canonical compiler-enforced gate is narrower than commonly assumed.

The outer level is open to everyone: //go:build (file selection — see Build Constraints and Conditional Compilation), //go:generate (a go generate command), //go:embed (embed a file), //go:noinline, //go:noescape, //go:nosplit, //go:norace, //go:nocheckptr, //go:uintptrescapes, //go:wasmimport, //go:wasmexport, //go:fix. These compile in any package. The “you should not normally do this” gate for the dangerous ones is cultural (vet warnings, code review), not syntactic — the compiler will accept //go:nosplit on your function without ceremony.

The gated level is //go:linkname alone: per the cmd/compile documentation, “for that reason, it is only enabled in files that have imported unsafe.” This is the one directive with a compiler-enforced “I know what I’m doing” gate, because aliasing object-file symbols can corrupt the type system in ways the other directives cannot. Since Go 1.23 there is also a linker-enforced gate on what //go:linkname may target in third-party code (covered below).

The inner level is reserved for the standard library and runtime themselves — //go:systemstack, //go:nowritebarrier, //go:nowritebarrierrec, //go:yeswritebarrierrec, and the historical //go:notinheap (now spelled as the runtime/internal/sys.NotInHeap type marker). The compiler rejects these in packages outside $GOROOT/std, because they constrain GC interactions in ways that only the runtime is allowed to specify.

The other crucial distinction is who consumes the directive. //go:generate is consumed by the go generate subcommand, not the compiler at all. //go:embed is consumed by the compiler’s frontend, which materializes file bytes into the binary. The optimization pragmas (//go:noinline, //go:noescape) are consumed by the optimizer. //line is consumed by the position-tracking machinery so debuggers and error messages point at the right source. They share a comment syntax but touch entirely different parts of the toolchain.

flowchart TD
  SRC["//go:* comment in source"] --> SCAN[scanner attaches<br/>pragma to next decl]
  SCAN --> RING{privilege level}
  RING -->|"open — any package"| OPEN["//go:build · //go:generate<br/>//go:embed · //go:noinline<br/>//go:noescape · //go:nosplit<br/>//go:norace · //go:nocheckptr<br/>//go:uintptrescapes · //go:wasmimport<br/>//go:wasmexport · //go:fix"]
  RING -->|"requires import _ \"unsafe\""| MID["//go:linkname"]
  RING -->|"std/runtime only<br/>(rejected outside GOROOT)"| INNER["//go:systemstack<br/>//go:nowritebarrier(rec)<br/>//go:yeswritebarrierrec"]
  OPEN --> CONSUME{consumer}
  MID --> CONSUME
  INNER --> CONSUME
  CONSUME -->|frontend| EMBED["embed: bytes baked in"]
  CONSUME -->|optimizer| OPT["inlining / escape decisions"]
  CONSUME -->|linker| LINK["linkname symbol aliasing<br/>(plus -checklinkname gate, 1.23+)"]
  CONSUME -->|go subcommand| GEN["go generate runs command<br/>go fix runs modernizers"]

Diagram: a directive comment is attached to the declaration that follows it, then routed by privilege level and by consumer. The insight: “compiler directive” is an umbrella over distinct mechanisms — some bend optimization, some bend linking, two are not even read by the compiler (//go:generate, //go:fix) — unified only by the //go: comment syntax. The compiler-enforced import "unsafe" gate covers //go:linkname alone; for the others, “needs unsafe” is a folk rule.

Mechanical Walk-through — the directive catalog

Open directives — usable in any package

//go:build — the build constraint deciding whether the whole file is compiled. Covered in its own note; mentioned here only because it shares the directive syntax.

//go:generate <command>not a compiler directive at all. The go generate subcommand scans files for these lines and runs the given command; go build ignores them entirely. Used to invoke code generators (stringer, protoc).

//go:embed <patterns> — embeds files into the binary at compile time. “The directive must immediately precede a variable declaration”; the variable’s type is string, []byte, or embed.FS (embed package). The compiler frontend reads the matched files and bakes their bytes into the binary’s data section. Introduced in Go 1.16; covered more fully below and in Go Executable Layout.

//go:noinline — “specifies that calls to the function should not be inlined, overriding the compiler’s usual optimization rules” (cmd/compile). Honored everywhere. Used to keep a function out-of-line for benchmarking (so the call overhead is real), for stack-frame visibility in profiles, or to defeat an inlining-induced optimization. See Function Inlining.

//go:noescape — “specifies that the function does not allow any of the pointers passed as arguments to escape into the heap or into the values returned from the function. Must be followed by a function declaration without a body” (cmd/compile). It is used for functions implemented in assembly: the escape analyzer cannot see into assembly, so by default it conservatively assumes every pointer argument escapes (forcing heap allocation at call sites). //go:noescape overrides that with the programmer’s promise. A wrong promise causes memory corruption — if the assembly does stash a pointer somewhere, the GC may free memory still in use. The documentation does not require import "unsafe" for this directive; the convention to add the import is folk practice, not a compiler-enforced gate.

//go:uintptrescapes — “the function’s uintptr arguments may be pointer values that have been converted to uintptr and must be on the heap and kept alive for the duration of the call. The conversion from pointer to uintptr must appear in the argument list of any call to this function” (cmd/compile). Used by syscall wrappers, where a pointer is passed to the kernel as an integer; the directive keeps the underlying object alive across the call so the GC does not reclaim it mid-syscall. A sibling UintptrKeepAlive pragma exists in cmd/compile/internal/ir’s PragmaFlag set with weaker semantics (keeps the object alive for the duration of the call without forcing heap residence); the compiler restricts it to standard-library use.

//go:nosplit — “specifies that the function must omit its usual stack-overflow check. Most commonly used by low-level runtime code invoked when it is unsafe for the calling goroutine to be preempted” (cmd/compile). Normally every function prologue checks whether the goroutine stack needs to grow; //go:nosplit removes that check. It is essential for code that runs when stack growth is impossible (the signal handler, the very bottom of the scheduler) but dangerous in user code — a nosplit function that overflows the small reserved “red zone” corrupts memory. The linker tracks a nosplit call-graph budget (the historical 768-byte “red zone”, larger on some arches) and errors if a nosplit call chain could exceed it. No unsafe import is required.

//go:norace — “specifies that the function’s memory accesses must be ignored by the race detector” (cmd/compile). Used by runtime code that legitimately touches memory in ways the race instrumentation would misreport.

//go:nocheckptr — disables the unsafe.Pointer-safety instrumentation that the compiler inserts when building with -race or -asan (NoCheckPtr in ir/node.go); used for unsafe.Pointer arithmetic that is correct but that the checker would flag.

//go:cgo_unsafe_args — tells the compiler to “treat a pointer to one arg as a pointer to them all” — i.e. consider the whole argument frame as a single contiguous block for GC marking purposes. Used by cgo-generated wrappers around C function calls; not intended for hand-written user code.

//go:nointerface — recognized only by gccgo (not gc); marks a method that should not satisfy any interface. Largely obsolete; preserved for gccgo compatibility.

//go:fix <action> — a directive consumed by Go 1.26’s revamped go fix “modernizers” framework (built on the analysis-pass framework that powers go vet), not by the compiler at all. The principal action is //go:fix inline, which marks a function or constant as a redirect target: go fix rewrites call sites of an inline-marked function to inline its body, and references to an inline-marked constant to the constant it refers to (x/tools inline analyzer). The intended use is API migration — deprecate a function by re-implementing it as a thin wrapper around its replacement and tagging it //go:fix inline; downstream code, when run through go fix, is mechanically migrated to the new API.

The gated directive — //go:linkname requires import "unsafe"

//go:linkname localname [importpath.name] — “determines the object-file symbol used for a Go var or func declaration, allowing two Go symbols to alias the same object-file symbol. Only enabled in files that have imported unsafe” (cmd/compile). This is how a package reaches a function the language would not let it call — for example, time uses //go:linkname to call the runtime’s unexported nanotime. There are two forms: with a target (//go:linkname localFn runtime.someFn) the local declaration aliases the named symbol (a “pull” linkname); with one argument (//go:linkname Fn) it exposes the local symbol under its full path so others can link to it (a “push” linkname).

Since Go 1.23 (released August 2024), the linker enforces a second restriction on top of the compiler’s import "unsafe" gate. Per the Go 1.23 release notes — Linker section — “the linker now disallows using a //go:linkname directive to refer to internal symbols in the standard library (including the runtime) that are not marked with //go:linkname on their definitions … For backward compatibility, existing usages of //go:linkname found in a large open-source code corpus remain supported.” Concretely: the linker contains an allow-list of historically-linknamed (importpath, name) pairs harvested from open source, and any new third-party linkname into the standard library or runtime fails at link time. The escape hatch is the linker flag -checklinkname=0 (passed via go build -ldflags="-checklinkname=0"), intended for debugging only. The standard library itself uses //go:linkname on the definition side to opt symbols into being linkname-targetable — that is what “marked with //go:linkname on their definitions” means.

WebAssembly directives

//go:wasmimport importmodule importname — “specifies that the function is provided by a wasm module identified by importmodule and importname. Must be followed by a function declaration with no body” (cmd/compile). This is how a GOOS=wasip1/GOOS=js Go program calls a host-provided function.

//go:wasmexport exportname — “specifies that the function is exported to the wasm host as exportname. Must be followed by a function definition.” The complement: it makes a Go function callable from the wasm host.

The inner ring — runtime and standard library only

//go:systemstack (the function must run on the per-OS-thread system stack), //go:nowritebarrier / //go:nowritebarrierrec / //go:yeswritebarrierrec (control whether GC write barriers may be emitted — critical inside the garbage collector itself), and //go:registerparams-era internal pragmas. The runtime package’s HACKING.md documents these for runtime contributors; the compiler rejects them outside GOROOT (runtime HACKING.md).

//line — position remapping

//line filename:line:col (and the /*line ... */ block form) tells the compiler that “the source position for the character immediately following the comment came from the specified file, line and column” (cmd/compile). It is emitted by code generators so that compiler errors and debugger line numbers point at the original template, not the generated .go file. It is the analog of C’s #line.

The //go:cgo_* family

//go:cgo_import_dynamic, //go:cgo_export_static, //go:cgo_ldflag, and siblings are emitted by the cgo tool into generated Go files (noder.go allowedStdPragmas). User code does not write these by hand; they are an implementation detail of the cgo pipeline.

Code Examples

//go:noinline for honest benchmarks

//go:noinline
func hashOne(x uint64) uint64 {
	x ^= x >> 33
	x *= 0xff51afd7ed558ccd
	return x ^ (x >> 33)
}

The directive on its own line, immediately before the function. Without it, the compiler would inline this tiny function into a benchmark loop, so the benchmark would measure the inlined arithmetic rather than a real call — //go:noinline forces a genuine call so the call cost is part of the measurement.

//go:noescape over an assembly function

//go:noescape
func memhash(p unsafe.Pointer, seed, length uintptr) uintptr
// implemented in memhash_amd64.s

There is no function body — the body is in a .s file. //go:noescape promises the escape analyzer that p does not escape, so callers passing a stack-allocated buffer need not heap-allocate it. The file must import "unsafe". If memhash’s assembly secretly stored p into a global, this promise would be a lie and the GC could free the buffer underneath it.

//go:linkname to reach a runtime symbol

import (
	_ "unsafe" // required to enable //go:linkname
)
 
//go:linkname runtimeNano runtime.nanotime
func runtimeNano() int64

runtimeNano has no body; the directive makes it an alias for the runtime’s unexported nanotime. The blank import _ "unsafe" is mandatory — without it the compiler ignores the directive. This is exactly the pattern the time package uses for the monotonic clock (see time.Time Comparison and the Monotonic Clock).

//go:embed — three target types

import "embed"
 
//go:embed version.txt
var version string            // single file → string
 
//go:embed banner.bin
var banner []byte             // single file → []byte
 
//go:embed templates/* static/*
var assets embed.FS           // many files → read-only filesystem

For string/[]byte the pattern “must match exactly one file” and the variable is initialized with its contents. For embed.FS the patterns may match many files and directories (recursively); files beginning with . or _ are excluded unless the pattern is prefixed all: (embed package). embed.FS “is read-only and safe for concurrent use” and implements fs.FS, so it plugs into http.FileServer and template.ParseFS.

//go:generate

//go:generate stringer -type=Color
 
type Color int
const ( Red Color = iota; Green; Blue )

go build ignores this line entirely. Running go generate ./... finds it and executes stringer -type=Color, which writes a color_string.go with a String() method. The directive records how generated code was produced, in the source, next to the type.

Failure Modes and Common Misunderstandings

The space bug. // go:noinline (space after //) is just an ordinary comment — the compiler silently ignores it and the function is inlined. There is no warning. The directive must be //go: with no space. This is the single most common directive mistake.

Forgetting import "unsafe" for //go:linkname///go:noescape. Without the (often blank) unsafe import, middle-ring directives are silently ignored. The symptom for //go:linkname is a link-time undefined symbol; for //go:noescape it is just slower code (the function’s pointers escape as normal).

A false //go:noescape or wrong //go:linkname target. These do not fail loudly — they corrupt memory. A //go:noescape that lies about a pointer not escaping can lead the GC to free live memory. A //go:linkname to a symbol whose signature differs from the local declaration produces a binary that calls the wrong code with a mismatched ABI. Both can manifest as rare, hard-to-reproduce crashes far from the directive.

Missing function body where one is required (and vice versa). //go:noescape and //go:wasmimport require a bodiless declaration; //go:wasmexport requires a body. Get it wrong and the compiler errors.

//go:embed on the wrong declaration. The directive must “immediately precede a variable declaration” with only blank lines and // comments between — not a const, not a function, not preceded by a doc paragraph. The package must import "embed" (a blank import suffices for string/[]byte targets). A pattern matching no file fails the build.

Assuming directives are portable across compilers. gccgo and TinyGo recognize a different subset. A heavily-pragma’d file may compile under gc and break under another compiler — see gc Compiler vs gccgo vs TinyGo.

Alternatives and When to Choose Them

Most user code should use no directives at all — the optimizer’s defaults are good. Reach for one only with a concrete reason: a benchmark that needs //go:noinline; an assembly routine that needs //go:noescape; a hard requirement to embed an asset (//go:embed) or call a symbol the language hides (//go:linkname).

For embedding, //go:embed superseded a generation of third-party tools (go-bindata, packr, statik) that generated a .go file full of byte literals; //go:embed is now the only correct choice — it is faster to compile, produces smaller source, and is officially supported. For escaping the type system, //go:linkname is a sharper, more dangerous tool than reflection or interfaces — prefer an exported API if one exists, and treat //go:linkname into the standard library as fragile (the target is unexported precisely because it may change).

Production Notes

//go:embed is the directive most user code actually uses in production — single-binary deployment of a web service with its HTML templates, migrations, and static assets baked in is now idiomatic Go, and embed.FS plugs directly into net/http and html/template.

//go:noescape and //go:linkname are the load-bearing directives of the standard library and high-performance libraries. The time, sync, reflect, and internal/* packages use //go:linkname extensively to reach runtime internals; crypto and hashing packages use //go:noescape over hand-written assembly. Reading src/runtime/ shows the inner-ring pragmas in their natural habitat.

A recurring production hazard: a third-party dependency that //go:linknames into an unexported standard-library symbol can break on a Go upgrade if that symbol is renamed or removed. The Go team has progressively restricted third-party //go:linkname pulls for exactly this reason (see the uncertainty flag above) — code relying on this trick should be audited on every Go release.

See Also