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 |
|---|---|---|---|
|
|
any line of a |
run an external command that writes source files |
|
compiler, via the |
directly above a package-level |
include files in the binary |
|
|
before the |
select files for a build |
|
|
before the |
set a default |
|
|
doc comment of a function, alias or constant |
mark a declaration as safe to inline into callers |
|
compiler |
directly above a function declaration |
override optimisation or stack-check rules |
|
compiler and linker |
any position, in a file that imports |
alias an object-file symbol |
|
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)
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 generateis never run bygo build,go testorgo 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/binfirst so generators that callgouse 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
generateis 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.
Source: cmd/go: Build constraints
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 |
|---|---|
|
exactly one file, as text (valid UTF-8 is not checked) |
|
exactly one file, as a fresh mutable copy |
|
any number of files and directories, as a read-only |
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
Source: embed package documentation
Pattern rules
-
A pattern uses
path.Matchsyntax, sotemplates/*.htmlmatches one directory level; there is no**. -
A pattern naming a directory embeds the tree below it, except files whose names begin with
.or_(theplainvariable 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:embedlines 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.FSpaths 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 thegocommand 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.
Source: net/http: FileServerFS and net/http/httptest
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
.gofile of the main package (or a_test.gofile of the package under test), before thepackageclause. A key must be known and given once. A library’s directive does nothing. -
Since Go 1.27 the
gocommand still accepts a setting whose support was removed if it names the final default, so oldgo.modand 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 fixalso bundles modernizers that need no directive.go tool fix helplists them, for exampleany,forvar,rangeint,minmax,mapsloop,slicescontains,stringscut,stringsseq,testingcontext,newexpr(Go 1.26’snew(expr)),errorsastype,omitzero, and, new in Go 1.27,atomictypes,embedlit,slicesbackwardandunsafefuncs. -
Fixes are computed for one build configuration; code behind other build tags needs another run with
-tagsorGOOSset. 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.
Source: cmd/fix and Go 1.26 release notes: go fix
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 |
|---|---|
|
calls are never inlined; used in benchmarks so the compiler cannot delete the work, and when debugging |
|
omit the stack-overflow check in the prologue; the linker verifies the function and its callees fit a small fixed budget |
|
on a bodyless (assembly) function: pointer arguments do not escape to the heap or into results |
|
the race detector ignores the function’s accesses (runtime internals) |
|
a |
//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.
Source: cmd/compile: Compiler Directives
//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
Source: cmd/compile: Compiler Directives
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:generateis silently ignored, and a misspelt//go:embdis just a comment, leaving the variable empty. -
go builddoes not run generators. Commit the output and document ago 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:embeddoes not recurse with**, skips dot and underscore files withoutall:, and cannot reach outside the module. A[]bytevariable holds its own copy, so large assets cost memory; preferembed.FSorstring. -
//go:nosplit,//go:noescapeand//go:linknamecan 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.