Directives, Code Generation and Embedding

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Go 1.27 (go1.27.2) and was checked on 2026-10-18. A directive is a comment that a Go tool treats as an instruction: //go:generate tells go generate what to run, //go:embed tells the compiler to store files in the binary, //go:debug pins runtime behaviour, //go:fix inline tells go fix how to migrate callers, and a handful of other //go: comments tune the compiler and linker. They share one syntax, but each is read by a different tool, at a different moment, with different failure modes. For //go:build constraints in depth see The go Command, Builds and Cross-Compilation.

The directive syntax

A directive is a line comment of the form //go:name arguments with no space between // and go:. A comment such as // go:generate x is an ordinary comment and is ignored silently. Tools that do not know a directive treat it like any other comment, which is why directives are safe in code read by older toolchains. Only //line is a historical exception that does not start with go:. The language specification only says that comments are comments (spec: Comments); the meaning of each directive belongs to the tool that reads it.

Directive Read by Where it may appear Purpose

//go:generate

go generate only

any line of a .go file

run an external command that writes source files

//go:embed

compiler, via the go command

directly above a package-level var

include files in the binary

//go:build

go command

before the package clause

select files for a build

//go:debug

go command

before the package clause of a main package

set a default GODEBUG value

//go:fix inline

go fix, gopls

doc comment of a function, alias or constant

mark a declaration as safe to inline into callers

//go:noinline, //go:nosplit, //go:norace, //go:noescape, //go:uintptrescapes

compiler

directly above a function declaration

override optimisation or stack-check rules

//go:linkname

compiler and linker

any position, in a file that imports unsafe

alias an object-file symbol

//go:wasmimport, //go:wasmexport

compiler (wasm targets)

above a bodyless function or a function definition

call into, or be called by, a WebAssembly host

Since Go 1.26 go/ast.ParseDirective parses one comment line into a tool name, a directive name and its arguments, so custom tools follow the same convention. Parsing the line //go:generate go run ./internal/enumgen -type=LoanState yields tool go, name generate and arguments go run ./internal/enumgen -type=LoanState; // An ordinary comment. is reported as not a directive.

Source: go/ast: ParseDirective (added in Go 1.26)

flowchart LR src["Go source with directive comments"] gen["go generate: explicit, manual"] cmd["go command: build, list, vet"] comp["compiler"] link["linker"] fix["go fix and gopls"] src --> gen gen -. "writes more source" .-> src src --> cmd cmd -- "go:build, go:embed, go:debug" --> comp comp -- "go:noinline, go:nosplit, go:noescape" --> link link -- "go:linkname check" --> link src --> fix fix -- "go:fix inline" --> src

Code generation with go generate

Go deliberately has no macros and no build-time code execution: go build compiles what is on disk and nothing else. When a program needs derived source (a String method for an enumeration, a table computed from a data file, mocks, protocol buffer bindings), a separate step produces it and the result is committed and compiled like any other file. Generation is explicit and reviewable, and a user who runs go get on your module never executes your generators. The rules, from go help generate:

  • go generate is never run by go build, go test or go get.

  • It scans for lines starting with //go:generate (no leading space) and runs the command in the package’s directory. It does not parse Go, so a line inside a raw string or block comment that looks like a directive is run too.

  • Arguments are space-separated tokens or double-quoted Go strings. There is no shell, no globbing and no pipe; use //go:generate sh -c "…" when you need one.

  • Variables are expanded in the command line, even inside quotes: $GOFILE, $GOLINE, $GOPACKAGE, $GOOS, $GOARCH, $GOROOT, $DOLLAR (a literal dollar sign) and $PATH, which has $GOROOT/bin first so generators that call go use the same toolchain.

  • Packages run in command-line order, files in file-name order, directives in source order. A non-zero exit skips the rest of that package. The build tag generate is set, so a file can be hidden from normal builds.

  • Generated files should contain a line matching ^// Code generated .* DO NOT EDIT\.$ before any other non-comment text; editors, linters and review tools use it to skip or flag the file.

A local generator run with go run

stringer is the traditional generator for enumerations but lives in golang.org/x/tools and must be installed. The example uses the same pattern with a generator inside the module, so go generate needs nothing else. It reads the declaring file named by GOFILE and writes a String method for the LoanState enumeration from the shelf scenario.

module example.com/shelf/gen

go 1.27
package main

//go:generate go run ./internal/enumgen -type=LoanState -output=loanstate_string.go

import "fmt"

// LoanState is the lifecycle of a Loan.
type LoanState int

const (
	Available LoanState = iota
	Reserved
	OnLoan
	Overdue
)

func main() {
	fmt.Println(OnLoan, Overdue, LoanState(9))
}
// Command enumgen writes a String method for the constants in the file that holds the directive.
package main

import (
	"flag"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"log"
	"os"
	"strings"
)

func main() {
	typeName := flag.String("type", "", "type name")
	output := flag.String("output", "", "output file")
	flag.Parse()

	// go generate sets GOFILE, GOPACKAGE and GOLINE for the file holding the directive.
	file, pkg := os.Getenv("GOFILE"), os.Getenv("GOPACKAGE")
	fmt.Printf("enumgen: GOFILE=%s GOPACKAGE=%s GOLINE=%s\n", file, pkg, os.Getenv("GOLINE"))
	f, err := parser.ParseFile(token.NewFileSet(), file, nil, 0)
	if err != nil {
		log.Fatal(err)
	}
	var names []string
	for _, d := range f.Decls {
		if gd, ok := d.(*ast.GenDecl); ok && gd.Tok == token.CONST {
			for _, s := range gd.Specs {
				for _, n := range s.(*ast.ValueSpec).Names {
					names = append(names, fmt.Sprintf("%q", n.Name))
				}
			}
		}
	}
	src := fmt.Sprintf(`// Code generated by enumgen; DO NOT EDIT.

package %[1]s

import "strconv"

var names = [...]string{%[3]s}

func (s %[2]s) String() string {
	if s < 0 || int(s) >= len(names) {
		return "%[2]s(" + strconv.Itoa(int(s)) + ")"
	}
	return names[s]
}
`, pkg, *typeName, strings.Join(names, ", "))
	if err := os.WriteFile(*output, []byte(src), 0o644); err != nil {
		log.Fatal(err)
	}
}

Before generating, Println shows raw integers because LoanState has no String method. go generate -x -v prints each file scanned (-v) and each command run (-x):

$ go run .
2 3 9
$ go generate -x -v ./...
loan.go
go run ./internal/enumgen -type=LoanState -output=loanstate_string.go
enumgen: GOFILE=loan.go GOPACKAGE=main GOLINE=3
internal/enumgen/main.go
$ go run .
OnLoan Overdue LoanState(9)

The generated loanstate_string.go is ordinary source that is committed: a Code generated … DO NOT EDIT. header, a names array and a String method that returns LoanState(N) for out-of-range values (hence LoanState(9) above).

Aliases, filtering and failures

//go:generate -command name args… defines an alias that lasts for the rest of that file only. -run regexp and -skip regexp select directives by matching the directive’s full original text, including //go:generate and any alias name; -n prints commands without running them. This file has a failing third directive:

package main

//go:generate -command say echo
//go:generate say "first: $GOFILE:$GOLINE ($GOPACKAGE, $GOOS/$GOARCH)"
//go:generate say second
//go:generate sh -c "exit 3"
//go:generate say never-reached
$ go generate -n extra.go
echo first: extra.go:4 (main, darwin/arm64)
echo second
sh -c exit 3
echo never-reached

$ go generate extra.go
first: extra.go:4 (main, darwin/arm64)
second
extra.go:6: running "sh": exit status 3

$ go generate -run second extra.go          # prints nothing: the -command line did not match
$ go generate -run 'command|second' extra.go
second

The failing sh -c "exit 3" stopped the package, so never-reached never ran. -run second alone printed nothing because the -command say directive was filtered out too and the alias was never defined; a selecting expression must also match it.

The //go:build ignore pattern

A generator that does not need to be a package can be a single file excluded from the build with //go:build ignore. The tag ignore is never satisfied, so the file is listed under IgnoredGoFiles, yet go run gen.go still compiles it because files named on the command line bypass constraints. gen.go turns a text file of classification codes into a Go table:

gen.go starts with //go:build ignore, opens classes.txt, writes one "code": "name" map entry per line into a string builder and passes the result through go/format.Source before writing classes_table.go, whose first line is // Code generated by gen.go; DO NOT EDIT..

The package side is //go:generate go run gen.go above a main that prints len(deweyClasses) and deweyClasses["800"]:

$ go build .
./main.go:8:18: undefined: deweyClasses
$ go generate . && go run .
4 Literature
$ go list -f '{{.GoFiles}} {{.IgnoredGoFiles}}' .
[classes_table.go main.go] [gen.go]

The first command shows the cost of the model: until the generated file exists the package does not compile. That is why generated files are committed and why go generate is a documented developer step. A CI job can check freshness by running go generate ./... and failing on a non-empty git diff --exit-code.

stringer (from the documentation, not run)

stringer generates String methods for integer types declared with iota. It was not installed on the machine used to check this page, so the following comes from its documentation and is not run here:

# classic: install a binary and call it by name
go install golang.org/x/tools/cmd/stringer@v0.52.0
# since Go 1.24: record the tool in go.mod and call it through the go command
go get -tool golang.org/x/tools/cmd/stringer@v0.52.0
//go:generate go tool stringer -type=LoanState -trimprefix=Loan -linecomment

Useful flags are -type (comma-separated type names), -trimprefix, -linecomment (use the trailing line comment of each constant as its name), -output and -tags. The generated loanstate_string.go consists of a compile-time assertion that the constants have not changed (an "invalid array index" error means regenerate), one concatenated name string, an index table such as _LoanState_index = [...]uint8{0, 9, 17, 23, 30}, and a String method that falls back to LoanState(N). Compared with the hand-written generator above it packs all names into one string and notices when a constant is inserted. The go tool form pins the generator in go.mod and removes the old tools.go file with a blank import; see Modules and Dependency Management for the tool directive and Variables, Constants and iota for enumerations.

Embedding files with //go:embed

Before Go 1.16 a program that shipped templates, SQL, certificates or a front-end bundle had to find them on disk at run time or generate a Go literal from bytes. The embed package and //go:embed (Go 1.16) let the compiler read the files at build time and store them in the executable, so one binary is the whole deployment. The directive precedes a package-level var and names files, directories or patterns relative to the source file’s directory. The variable type chooses the form:

Variable type Meaning

string

exactly one file, as text (valid UTF-8 is not checked)

[]byte

exactly one file, as a fresh mutable copy

embed.FS

any number of files and directories, as a read-only fs.FS that also implements fs.ReadFileFS and fs.ReadDirFS

The package must import embed, with import _ "embed" when only string or []byte is used.

module example.com/shelf/embeddemo

go 1.27
package main

import (
	"embed"
	"fmt"
	"io/fs"
)

//go:embed banner.txt
var banner string

//go:embed logo.bin
var logo []byte

//go:embed web/static
var plain embed.FS

//go:embed all:web/static
var all embed.FS

//go:embed web/static/*
var star embed.FS

func files(fsys fs.FS) []string {
	var out []string
	fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error {
		if err == nil && !d.IsDir() {
			out = append(out, p)
		}
		return err
	})
	return out
}

func main() {
	fmt.Printf("banner: %q\n", banner)
	fmt.Printf("logo: % x\n", logo)
	fmt.Println("plain:", files(plain))
	fmt.Println("all:  ", files(all))
	fmt.Println("star: ", files(star))

	sub, _ := fs.Sub(plain, "web/static")
	css, err := fs.ReadFile(sub, "site.css")
	fmt.Printf("sub/site.css: %q %v\n", css, err)
	_, err = plain.ReadFile("web/static/nope.txt")
	fmt.Println("missing:", err)
}

The tree holds banner.txt, logo.bin and web/static with index.html, site.css, .htaccess, .hidden/note.txt and _private/draft.txt:

banner: "Shelf catalogue server\nVersion: 1.0\n"
logo: 89 50 4e 47 0d 0a
plain: [web/static/index.html web/static/site.css]
all:   [web/static/.hidden/note.txt web/static/.htaccess web/static/_private/draft.txt web/static/index.html web/static/site.css]
star:  [web/static/.hidden/note.txt web/static/.htaccess web/static/_private/draft.txt web/static/index.html web/static/site.css]
sub/site.css: "body { font-family: serif; }\n" <nil>
missing: open web/static/nope.txt: file does not exist

Pattern rules

  • A pattern uses path.Match syntax, so templates/*.html matches one directory level; there is no **.

  • A pattern naming a directory embeds the tree below it, except files whose names begin with . or _ (the plain variable above).

  • The prefix all: removes that exclusion (all:web/static). A glob ending in * also matched the hidden entries (star), because the dot and underscore filter applies only to files found by walking a directory.

  • Several space-separated or quoted patterns may share one directive, and several //go:embed lines may stack above one variable.

  • Patterns cannot contain . or .. elements, cannot start or end with /, and cannot match files outside the module (including .git/*), symbolic links, irregular files, or files of a nested module.

  • embed.FS paths use forward slashes without a leading slash, and "." is the root. File modes and modification times are not kept (the time is zero).

  • go list -f '{{.EmbedPatterns}}' and {{.EmbedFiles}} show what the go command resolved; for the program above the patterns are [all:web/static banner.txt logo.bin web/static web/static/*].

Compile errors

Each restriction has its own message, produced by one-file variants of the program:

main.go:5:12: pattern nothing/*.txt: no matching files found
main.go:5:12: pattern ../banner.txt: invalid pattern syntax
main.go:5:12: pattern sub/inner/x.txt: cannot embed file sub/inner/x.txt: in different module
./main.go:6:5: go:embed cannot apply to var of type int
./main.go:5:3: go:embed cannot apply to var with initializer
./main.go:3:3: go:embed requires import "embed" (or import _ "embed", if package is not used)
./main.go:4:4: go:embed requires import "embed" (or import _ "embed", if package is not used)
./main.go:5:3: misplaced go:embed directive

In order: no match, a .. path, a file in a nested module (a directory with its own go.mod), an unsupported type, a variable with an initializer, a missing embed import (top level, then inside a function, where a local variable cannot be embedded anyway), and a directive above a const.

Serving embedded files

The classic use is a web UI shipped inside the server binary. fs.Sub re-roots the tree so URLs do not repeat the directory name, http.FileServerFS serves any fs.FS, and http.ServeFileFS serves one file. httptest.NewServer keeps the output deterministic and needs no fixed port.

package main

import (
	"embed"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"net/http/httptest"
)

//go:embed web/static
var content embed.FS

func main() {
	static, err := fs.Sub(content, "web/static")
	if err != nil {
		panic(err)
	}
	mux := http.NewServeMux()
	mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServerFS(static)))
	mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFileFS(w, r, static, "index.html")
	})

	srv := httptest.NewServer(mux)
	defer srv.Close()
	for _, p := range []string{"/", "/static/site.css", "/static/missing.js", "/static/.htaccess"} {
		resp, err := http.Get(srv.URL + p)
		if err != nil {
			panic(err)
		}
		body, _ := io.ReadAll(resp.Body)
		resp.Body.Close()
		fmt.Printf("GET %-18s -> %d %-25s %q\n", p, resp.StatusCode, resp.Header.Get("Content-Type"), body)
	}
}
GET /                  -> 200 text/html; charset=utf-8  "<h1>Shelf</h1>\n"
GET /static/site.css   -> 200 text/css; charset=utf-8   "body { font-family: serif; }\n"
GET /static/missing.js -> 404 text/plain; charset=utf-8 "404 page not found\n"
GET /static/.htaccess  -> 404 text/plain; charset=utf-8 "404 page not found\n"

The content type comes from the extension. .htaccess is a 404 only because the directive named the directory without all:; with all:web/static hidden files become public. Anyone with the binary can extract embedded data, so never embed credentials. Because modification times are zero, responses carry no Last-Modified header; set an ETag or use content-hashed file names when caching matters. For http.Server see net/http Clients and Servers, for template.ParseFS on an embed.FS see Text and HTML Templates, and for io/fs and os.DirFS (a development build can read the same tree from disk) see I/O, Files and the Filesystem.

Pinning behaviour with //go:debug

The Go team keeps old behaviour behind GODEBUG settings (a comma-separated key=value list) when a change could break programs. The go line of the main module selects the defaults: building with go 1.20 gives Go 1.20 behaviours even with a Go 1.27 toolchain. //go:debug and the godebug directive of go.mod override single settings at build time, so the choice travels with the source. Precedence, highest first: the GODEBUG environment variable at run time, //go:debug lines, godebug lines in go.mod or go.work, then the defaults implied by the go line.

Rules for //go:debug:

  • It appears in a .go file of the main package (or a _test.go file of the package under test), before the package clause. A key must be known and given once. A library’s directive does nothing.

  • Since Go 1.27 the go command still accepts a setting whose support was removed if it names the final default, so old go.mod and source files keep building; the removed behaviour itself is an error.

Go 1.21 made panic(nil) raise a *runtime.PanicNilError; panicnil=1 restores the old behaviour in which recover returns nil. debug.ReadBuildInfo shows what was recorded.

//go:debug panicnil=1
//go:debug http2client=0
package main

import (
	"fmt"
	"os"
	"runtime/debug"
)

func main() {
	defer func() {
		r := recover()
		fmt.Printf("recovered: %v (%T)\n", r, r)
	}()
	bi, _ := debug.ReadBuildInfo()
	for _, s := range bi.Settings {
		if s.Key == "DefaultGODEBUG" {
			fmt.Println("DefaultGODEBUG =", s.Value)
		}
	}
	fmt.Println("GODEBUG env =", os.Getenv("GODEBUG"))
	panic(nil)
}
$ go run .
DefaultGODEBUG = http2client=0,panicnil=1
GODEBUG env =
recovered: <nil> (<nil>)

$ GODEBUG=panicnil=0 go run .
DefaultGODEBUG = http2client=0,panicnil=1
GODEBUG env = panicnil=0
recovered: runtime error: panic called with nil argument (*runtime.PanicNilError)

A godebug panicnil=1 line in go.mod has the same effect (go list -f '{{.DefaultGODEBUG}}' printed panicnil=1). The go line alone matters a lot: a module declaring go 1.20 gets a default list beginning containermaxprocs=0,cryptocustomrand=1,…, so raising it changes behaviour, not only language rules (see Toolchain Versions and Compatibility). Mistakes are reported precisely; go vet flags a misplaced directive and one in a library, go build rejects unknown and repeated keys:

main.go:3:1: //go:debug directive only valid before package declaration
lib/lib.go:1:1: //go:debug directive only valid in package main or test
./main.go:1:1: invalid //go:debug: unknown //go:debug setting "nosuchsetting"
./main.go:2:1: repeated //go:debug for panicnil
	./main.go:1:1: previous //go:debug

Migrating callers with //go:fix inline

Deprecating an API normally means a comment saying "use X" and hoping callers read it. Go 1.26 rebuilt go fix so that the author can make the migration mechanical: a function, type alias or constant whose doc comment carries //go:fix inline is replaced at every call site by its body or target. The source-level inliner (The Go Blog: inliner) preserves evaluation order and side effects, adding temporaries when needed, and the same analyzer powers gopls refactorings and editor diagnostics.

// Package oldapi is a catalogue helper whose API is being migrated.
package oldapi

import "strings"

// Title returns a display title.
//
// Deprecated: use [Normalize].
//
//go:fix inline
func Title(s string) string { return Normalize(s, true) }

// Normalize trims s and optionally upper-cases it.
func Normalize(s string, upper bool) string {
	s = strings.TrimSpace(s)
	if upper {
		return strings.ToUpper(s)
	}
	return s
}

// Isbn is the old name of ISBN.
//
// Deprecated: use [ISBN].
//
//go:fix inline
type Isbn = ISBN

// ISBN identifies a book.
type ISBN string

A caller still using the old names has var id oldapi.Isbn = "9780441013593" and fmt.Println(oldapi.Title(" dune "), id). go fix -diff prints what would change and writes nothing; go fix ./... applies it. The analyzer can also run as a vet tool, which lets CI require the migration:

$ go run ./app
DUNE 9780441013593

$ go fix -diff ./...
--- app/main.go (old)
+++ app/main.go (new)
@@ -7,6 +7,6 @@
 )

 func main() {
-	var id oldapi.Isbn = "9780441013593"
-	fmt.Println(oldapi.Title("  dune "), id)
+	var id oldapi.ISBN = "9780441013593"
+	fmt.Println(oldapi.Normalize("  dune ", true), id)
 }

$ go vet -vettool=$(go env GOROOT)/pkg/tool/$(go env GOOS)_$(go env GOARCH)/fix ./...
app/main.go:10:9: Type alias oldapi.Isbn should be inlined
app/main.go:11:14: Call of oldapi.Title should be inlined

(The tool prints absolute paths; they are shortened here.) Points to know:

  • The directive is written by the library author and applied by the user’s go fix, including for modules in the module cache; nothing is rewritten in the library.

  • It works on calls to functions and on references to type aliases and constants. Use it only when the replacement is exactly equivalent at every call site, because the edit is applied mechanically to many callers.

  • go fix also bundles modernizers that need no directive. go tool fix help lists them, for example any, forvar, rangeint, minmax, mapsloop, slicescontains, stringscut, stringsseq, testingcontext, newexpr (Go 1.26’s new(expr)), errorsastype, omitzero, and, new in Go 1.27, atomictypes, embedlit, slicesbackward and unsafefuncs.

  • Fixes are computed for one build configuration; code behind other build tags needs another run with -tags or GOOS set. Always review the diff and run the tests.

See Formatting, vet, fix and Static Analysis for the whole tool family and What’s New and Migrating Older Code for migrations.

Compiler directives

These directives influence code generation. They are documented in the cmd/compile package comment, each removes a safety net, and ordinary programs should rarely use them. Each applies to the function declaration that immediately follows.

Directive Effect

//go:noinline

calls are never inlined; used in benchmarks so the compiler cannot delete the work, and when debugging

//go:nosplit

omit the stack-overflow check in the prologue; the linker verifies the function and its callees fit a small fixed budget

//go:noescape

on a bodyless (assembly) function: pointer arguments do not escape to the heap or into results

//go:norace

the race detector ignores the function’s accesses (runtime internals)

//go:uintptrescapes

a uintptr converted from a pointer in the call keeps its object alive and on the heap for the call (system calls)

//go:noinline and -gcflags=-m

-gcflags=-m makes the compiler explain inlining and escape decisions; more m`s give more detail. Two functions share one body, and one is marked `//go:noinline:

package main

import "fmt"

// Book is a catalogue entry.
type Book struct {
	Title string
	Pages int
}

// newBook is small, so the compiler inlines it and the Book stays on the stack.
func newBook(t string, p int) *Book { return &Book{t, p} }

// newBookNoInline has the same body but is excluded from inlining, so the Book escapes.
//
//go:noinline
func newBookNoInline(t string, p int) *Book { return &Book{t, p} }

func main() {
	fmt.Println(newBook("Emma", 400).Pages, newBookNoInline("Ulysses", 730).Pages)
}
$ go run .
400 730
$ go build -gcflags='-m' . 2>&1 | grep -E 'inline|Book'
./main.go:12:6: can inline newBook
./main.go:20:21: inlining call to newBook
./main.go:12:46: &Book{...} escapes to heap
./main.go:17:54: &Book{...} escapes to heap
./main.go:20:21: &Book{...} does not escape
./main.go:20:73: newBookNoInline("Ulysses", 730).Pages escapes to heap
$ go build -gcflags='-m=2' . 2>&1 | grep 'cannot inline'
./main.go:17:6: cannot inline newBookNoInline: marked go:noinline
./main.go:19:6: cannot inline main: function too complex: cost 152 exceeds budget 80

Once newBook is inlined into main (line 20), the &Book{...} does not escape and can live on the stack, while the non-inlinable copy allocates on the heap. main itself is too costly to inline (cost 152 against a budget of 80). Escape analysis and the allocator are in Memory Management and Garbage Collection, and profile-guided inlining of hot calls is in Performance and PGO.

//go:nosplit and //go:noescape

Every ordinary function starts by comparing the stack pointer with a limit and calls into the runtime to grow the goroutine stack when needed. //go:nosplit removes that check, so the function may use only a small fixed amount of stack. The linker enforces it by adding frame sizes along every nosplit call chain: a nosplit function with a 4000-byte array that calls another function fails to link. The same function without the call compiles, because only chains that might need to grow the stack are checked.

main.big: nosplit stack over 792 byte limit
main.big(1)
    grows 4032 bytes, calls main.helper(1)
        grows 0 bytes, calls runtime.morestack(0)
        3240 bytes over limit

The directive exists for the scheduler, allocator and signal handlers, which run where growing the stack is impossible. //go:noescape is accepted on a function without a body, implemented in assembly; a Go function with no body and no .s file is the error ./main.go:4:6: missing function body. The directive promises that the assembly keeps no pointer argument, so callers may pass pointers to stack variables; a false promise means memory corruption. See unsafe and Low-Level Programming.

//go:linkname and its restrictions

//go:linkname localname importpath.name gives a Go declaration the object-file symbol of another name, so one package can use another’s unexported function. Because it breaks encapsulation and type safety it is accepted only in files that import unsafe; otherwise the compiler reports ./main.go:3:3: //go:linkname only allowed in Go files that import "unsafe". The usual form is a pull: a bodyless function is linked to a symbol elsewhere. runtime.nanotime is a legitimate pull because the runtime marks it for outside use:

package main

import (
	"fmt"
	_ "unsafe" // required for //go:linkname
)

//go:linkname nanotime runtime.nanotime
func nanotime() int64

func main() {
	a := nanotime()
	b := nanotime()
	fmt.Println("monotonic clock advances:", b >= a)
}
monotonic clock advances: true

The opposite push form is the single-argument //go:linkname secret next to the owner’s own declaration, which documents that the symbol is accessed from outside the package. Between packages of your own module the marker is optional: a second package that pulled example.com/shelf/ln2/lib.secret built and printed the same result with and without the marker.

The Go 1.23 linker check

Many third-party packages had pulled unexported runtime functions through linkname, which made it impossible to change runtime internals without breaking them. Go 1.23 made the linker reject any reference to a symbol in the standard library (including the runtime) that is not marked //go:linkname on its definition. Uses found in a large open-source corpus stay supported, but new ones fail. Pulling runtime.casgstatus, which is not marked:

package main

import (
	"fmt"
	_ "unsafe"
)

//go:linkname casgstatus runtime.casgstatus
func casgstatus(gp uintptr, oldval, newval uint32)

func main() {
	fmt.Println("linked")
	_ = casgstatus
}
$ go build -o /dev/null .
# example.com/shelf/inl
link: main: invalid reference to runtime.casgstatus

$ go build -ldflags=-checklinkname=0 -o /dev/null .
$ echo $?
0

The compiler accepts the file; the link step fails. -checklinkname=0 disables the check "for debugging and experimenting purposes", but the program then depends on internals that can change in any release. A symbol that does not exist fails differently, with relocation target … not defined. When a hook is wanted, prefer asking for a supported exported API.

WebAssembly and cgo directives (not run)

Two directive families belong to interoperability pages and are only pointed to here, because running them needs a WebAssembly runtime or a C compiler. //go:wasmimport module name declares a bodyless Go function implemented by the WebAssembly host, and //go:wasmexport name (Go 1.24) exports a Go function to the host; parameters are limited to integers, floats, unsafe.Pointer and certain pointers. See WebAssembly and WASI. The cgo comment forms //export Name and the preamble above import "C" are not //go: directives but follow the same comment-as-instruction idea; see cgo and C Interoperability, and Rust FFI and C Interop for the Rust counterpart.

Pitfalls

  • // go:generate (with a space) or an indented //go:generate is silently ignored, and a misspelt //go:embd is just a comment, leaving the variable empty.

  • go build does not run generators. Commit the output and document a go generate ./... step.

  • A non-deterministic generator (timestamps, map iteration order, absolute paths) produces noisy diffs and breaks freshness checks; sort keys and omit dates.

  • //go:embed does not recurse with **, skips dot and underscore files without all:, and cannot reach outside the module. A []byte variable holds its own copy, so large assets cost memory; prefer embed.FS or string.

  • //go:nosplit, //go:noescape and //go:linkname can corrupt memory or tie a program to one Go release.

Where the books are outdated

Both books predate every directive on this page except go:generate. Bodner’s Learning Go (ch. 10 on modules, packages and imports; ch. 14 on unsafe and cgo) and Vivien’s Learning Go Programming (ch. 10 on the go command; ch. 12 on testing and tooling) describe a world where shipping static assets meant reading files from a known path or generating Go source with a third-party tool such as go-bindata; since Go 1.16 the embed package is the standard answer. They also predate //go:debug (Go 1.21), the Go 1.23 linkname restriction, go tool for pinning generators in go.mod (Go 1.24), go fix as a migration framework with //go:fix inline (Go 1.26) and go/ast.ParseDirective (Go 1.26). The // +build lines they use for conditional compilation were replaced by //go:build (Go 1.17), and go fix now removes the old form.