How to Use go

linkname for Accessing Private Runtime Functions

Use the //go:linkname directive to create a public alias for private runtime functions by linking a local name to the external package path.

The backdoor to the runtime

You are writing a memory profiler that needs to track allocation events at the lowest possible level. The Go runtime has a function that fires exactly when a new object is allocated. It does the job perfectly. The problem is the name. The function starts with a lowercase letter. It lives inside the runtime package. The compiler refuses to let you call it.

You cannot export the function because it belongs to the standard library, not your code. You cannot use reflection because reflection respects visibility rules. You need a way to tell the compiler, "Ignore the access rules. I know this function exists, and I need to call it."

That is what //go:linkname does. It is a compiler directive that creates an alias for a symbol in another package. It lets you bridge the gap between your code and private runtime internals. It is powerful, dangerous, and restricted. Use it only when the public API leaves you with no other option.

What go:linkname actually does

The Go compiler and linker work together to resolve symbols. When you call a function, the compiler generates a reference to that function's name. The linker takes all the object files and matches those references to actual implementations. If a reference cannot be matched, the linker fails with an undefined symbol error.

//go:linkname manipulates this process. It tells the compiler to rename a local symbol in the object file so that it matches a remote symbol. When the linker runs, it sees your local name and treats it as if it were the remote name. The result is that your code can call the private function as if it were exported.

The directive has a specific syntax. It must appear as a comment immediately before the function or variable declaration. There can be no blank lines between the directive and the declaration. The comment starts with //go:linkname, followed by the local name, then the remote name. The remote name includes the full import path.

package main

import "unsafe"

//go:linkname myPrivateFunc runtime.privateHelper
// The directive aliases myPrivateFunc to runtime.privateHelper.
// The linker will resolve calls to myPrivateFunc using the runtime symbol.
func myPrivateFunc() unsafe.Pointer

// main calls the aliased function.
// The body of myPrivateFunc is never executed; the directive overrides it.
func main() {
    ptr := myPrivateFunc()
    _ = ptr
}

The function declaration in your code acts as a stub. You must provide a signature that matches the remote function exactly. The body of the function is irrelevant when you are importing a symbol. The compiler may warn about unused code, but the linker replaces the symbol entirely. If you are exporting a local function to the runtime, the body matters, but that is a different use case.

The directive works for functions and variables. You can alias a private variable just as easily as a function. The rules are the same. The type must match. The signature must match. The linker does not check types; it only matches names. If you get the type wrong, your program will crash at runtime.

Linkname is a bridge, not a door. Cross it only when the map is missing.

How the linker resolves the alias

When you compile a Go program, the compiler produces object files containing symbol tables. Each function and variable has a name and an address placeholder. The linker merges these tables. It matches references to definitions.

With //go:linkname, the compiler modifies the symbol table before the linker sees it. It takes your local symbol and renames it to the remote symbol. The linker then resolves the reference using the renamed symbol.

This happens at link time, not compile time. The compiler checks that the directive syntax is valid. It checks that the import path is allowed. It does not verify that the remote symbol exists. That check happens later. If the remote symbol does not exist, the linker fails.

The resolution process is strict. The symbol name must match exactly. This includes the package path. If you alias runtime.gcStart, you must use runtime.gcStart in the directive. You cannot use gcStart alone. The linker uses the full path to distinguish between symbols with the same name in different packages.

The directive also affects visibility. Normally, private symbols are stripped or hidden during linking. //go:linkname forces the symbol to remain visible. This is why the directive is restricted. Allowing arbitrary access to private symbols would break encapsulation and make the runtime fragile.

The linker doesn't care about your excuses. Match the signature or crash.

Realistic usage: accessing runtime internals

A common use case for go:linkname is accessing runtime functions that are not exported but are needed for low-level tools. Profilers, custom schedulers, and memory allocators often need this capability.

Consider a scenario where you need to call a function that manages goroutine stacks. The runtime has internal functions for stack growth and shrinkage. These functions are private. If you are writing a custom scheduler, you might need to call them directly.

Here is how you would alias such a function. The example uses a hypothetical runtime function. In practice, you would replace the name with the actual function you need.

package main

import "unsafe"

//go:linkname myStackGrow runtime.morestack_noctxt
// Alias the private runtime function to a local name.
// The signature must match the runtime function exactly.
func myStackGrow()

// growStack calls the aliased function.
// This demonstrates how to invoke the private function from user code.
func growStack() {
    myStackGrow()
}

func main() {
    growStack()
}

The function myStackGrow has no body. The directive provides the implementation. When you call myStackGrow, the linker resolves it to runtime.morestack_noctxt. The runtime function executes.

You must be careful with signatures. The runtime function might take arguments or return values. Your alias must match. If the runtime function takes a pointer, your alias must take a pointer. If it returns an integer, your alias must return an integer.

Using unsafe is common with linkname. Runtime functions often return raw pointers or operate on internal structures. You will need unsafe.Pointer to handle the results. The unsafe package lets you bypass type checks, but it does not protect you from logic errors.

Runtime internals change. Your linkname breaks. Test on every Go version.

The Go 1.21 restriction

Historically, go:linkname allowed you to link to any symbol in any package. You could alias private functions in third-party libraries. You could reach into encoding/json and call hidden helpers. This flexibility came with risks. Code using linkname would break when the target package changed its internals. It also allowed malicious code to access private data.

Starting with Go 1.21, the compiler enforces strict restrictions on go:linkname. You can only use the directive to link to symbols in the runtime package or the unsafe package. You cannot link to symbols in other standard library packages. You cannot link to symbols in third-party packages.

This restriction protects the stability of the ecosystem. It prevents user code from depending on private implementation details of packages that may change. It also reduces the attack surface for security vulnerabilities.

If you try to use go:linkname to link to a disallowed package, the compiler rejects the code. The error message is clear.

package main

//go:linkname myJsonFunc encoding/json.privateFunc
// This directive attempts to link to a private function in encoding/json.
// Go 1.21+ forbids linking to packages other than runtime and unsafe.
func myJsonFunc()

func main() {
    myJsonFunc()
}

The compiler produces an error like //go:linkname: invalid import path: encoding/json. The build fails. You cannot work around this restriction. The only way to access private symbols in other packages is to use an older Go version, which is not recommended for production code.

The restriction applies to both importing and exporting. You cannot use go:linkname to export a symbol to a third-party package. You can only export to runtime or unsafe.

This change means that go:linkname is now primarily a tool for runtime hacking. If you need to access private functions in other packages, you must find another way. You might need to file a feature request to export the function officially. You might need to use reflection if the data is accessible. You might need to restructure your code to avoid the dependency.

Linkname is now a key to the runtime vault, not a master key for everything.

Pitfalls and crashes

Using go:linkname introduces risks that normal Go code does not have. The compiler cannot verify the correctness of the alias. The linker cannot verify the signature. The runtime cannot verify the usage. You are responsible for ensuring everything matches.

Signature mismatches are the most common cause of crashes. If your alias has a different signature than the remote function, the compiler will not catch the error. The linker will resolve the symbol. The program will compile. When you call the function, the arguments and return values will be interpreted incorrectly. This leads to garbage data, corrupted memory, or segmentation faults.

Always check the runtime source code to verify the signature. The runtime code is available in the Go distribution. Look for the function definition. Copy the signature exactly. Do not guess.

Another pitfall is ABI changes. The runtime function might change its signature or behavior in a future Go version. Your code will still compile, but it will break at runtime. The linker will resolve the symbol, but the function might expect different arguments. Or the function might return a different type.

You must test your code on every Go version you support. If you use go:linkname, you are binding your code to the internal ABI of the runtime. That ABI is not stable. It can change without notice.

Variables can also be aliased. If you alias a variable, you must ensure the type matches. If the runtime variable is a pointer, your alias must be a pointer. If the runtime variable is a struct, your alias must be a struct. Mismatched types lead to undefined behavior.

The compiler may optimize away your alias if it thinks the function is unused. This is rare when you call the function, but it can happen in complex scenarios. Use go:noinline if you suspect the compiler is optimizing too aggressively.

The worst goroutine bug is the one that never logs. The same applies to linkname crashes. They happen silently and corrupt your program.

Decision matrix

Use go:linkname when you need to call a function inside the runtime package that has no exported equivalent and the public API is insufficient for your low-level tool.

Use the standard library API when the exported function provides the behavior you need, even if it adds a small overhead.

Use unsafe pointer arithmetic when you need to read or write memory at a specific offset without calling a function.

Use reflection when you need to inspect types and values at runtime, accepting that you cannot access unexported fields or methods.

Use a feature request or issue when you need a capability that the runtime currently hides; the Go team may export it officially if there is a valid use case.

Use go:linkname to export a local function to the runtime when you are writing a custom allocator or scheduler that the runtime needs to call back into.

Trust the restriction. If the compiler blocks the import path, there is a reason. Find another way.

Where to go next