go-forceexport is a golang package that allows access to any module-level
function, even ones that are not exported. You give it the string name of a
function , like "time.now", and gives you a function value that calls that
function. More generally, it can be used to achieve something like reflection on
top-level functions, whereas the reflect package only lets you access methods
by name.
As you might expect, this library is unsafe and fragile and probably shouldn't be used in production. See "Use cases and pitfalls" below.
Tested on Linux (Go 1.23–1.26, including Delve -gcflags="all=-N -l" builds)
and on macOS with older Go versions. On Go 1.23+ without checklinkname_off:
- Linux — maps-based scan of the executable RW segment (recommended path)
- macOS / Windows / other — legacy GC-anchored scan (±32 MiB); use
checklinkname_offif that is not enough
$ go get github.com/szmcdull/go-forceexport
Here's how you can grab the time.now function, defined as
func now() (sec int64, nsec int32)
var timeNow func() (int64, int32)
err := forceexport.GetFunc(&timeNow, "time.now")
if err != nil {
// Handle errors if you care about name possibly being invalid.
}
// Calls the actual time.now function.
sec, nsec := timeNow()The string you give should be the fully-qualified name. For example, here's
GetFunc getting itself.
var getFunc func(interface{}, string) error
GetFunc(&getFunc, "github.com/alangpierce/go-forceexport.GetFunc")- 1.26.1
- 1.25
- 1.23
- 1.21
- 1.18 beta2
- 1.17.5
- 1.16.13
- 1.14.15
Due to the restriction to go:linkname in recent Go versions, you can compile with
-tags=checklinkname_off -ldflags=-checklinkname=0 to enable go:linkname and let
forceexport link directly to runtime.firstmoduledata (fastest, most reliable).
If you cannot require those flags on downstream consumers (e.g. a library used by
others), forceexport falls back to a runtime scan to find runtime.firstmoduledata
in memory. The scan strategy is selected by OS at compile time; see
Runtime moduledata discovery below.
GetFunc walks runtime.firstmoduledata to resolve function names. On Go 1.23+ with default
checklinkname=1, forceexport cannot link directly to that symbol and must locate it first.
| Path | Build flags | Behaviour |
|---|---|---|
| linkname | -tags=checklinkname_off -ldflags=-checklinkname=0 |
go:linkname runtime.firstmoduledata |
| runtime scan | default | OS-specific; see platform table |
Entry point: locateModuleDataWithoutLinkname (called from go_1_23.go / go_1_26.go).
The version-specific files do not embed scan logic themselves.
| OS | Files | Strategy |
|---|---|---|
| Linux | locate_moduledata_linux.go → moduledata_maps_linux.go |
/proc/self/maps RW-segment scan |
| non-Linux | locate_moduledata_legacy.go |
scan ±32 MiB around runtime.GC |
On Linux, runtime.firstmoduledata lives in the linker-generated .go.module section. In the
loaded image this sits in the executable's read-write data segment, after .text / .rodata.
Algorithm (moduledata_maps_linux.go):
- Take
runtime.GCPC and find the containing r-x mapping in/proc/self/maps. - Collect rw- mappings of the same executable path with start address ≥ end of that r-x map.
- Walk only those RW regions and return the first address that passes
isValidModuleData.
Why not a fixed window from runtime.GC? On large Delve / debug binaries
(-gcflags="all=-N -l"), .go.module can sit beyond any practical fixed limit
(e.g. >32 MiB above code). Scanning the whole RW segment avoids both missed lookups and
slow blind scans over tens of megabytes of code/rodata.
Typical virtual layout (Linux PIE):
[ .text r-x ] [ .rodata r-- ] [ .go.module / .data rw- ]
runtime.GC here runtime.firstmoduledata here
macOS executables use Mach-O, not ELF. A Go binary is usually Mach-O 64-bit
(MH_EXECUTE or MH_PIE). Rough segment mapping:
| Mach-O segment | Role | ELF analogue |
|---|---|---|
__TEXT |
code, read-only metadata | .text |
__DATA_CONST |
read-only globals | .rodata |
__DATA |
writable globals | .data, .go.module, .noptrdata, … |
runtime.firstmoduledata is still in the writable non-executable segment after code, but
there is no /proc/self/maps. Memory regions are queried via Mach APIs
(mach_vm_region, vm_region_recurse_64) or dyld (_dyld_get_image_header, etc.).
Current status: no Mach-O maps scanner yet. Non-Linux builds use the legacy ±32 MiB scan
(locate_moduledata_legacy.go). If that fails, use checklinkname_off, or contribute a
Mach-O RW-segment walker analogous to the Linux implementation.
Not covered by the Linux maps scanner. Use checklinkname_off or the legacy / Windows-specific
paths in this repo where applicable.
| Error | Likely cause | Fix |
|---|---|---|
moduledata not found! |
runtime scan failed before any function lookup | Linux: should not happen on recent builds; report a bug. Other OS: try checklinkname_off |
Invalid function name: … |
moduledata found, but symbol missing (inlining / dead-code elimination) | call a reference to keep the symbol; build with -gcflags="all=-l"; or use checklinkname_off |
panic in go-cancelContext init |
same as moduledata not found! during GetFunc in init() |
ensure forceexport version with Linux maps scan, or add checklinkname_off to debug build flags |
This library is most useful for development and hack projects. For example, you might use it to track down why the standard library isn't behaving as you expect, or you might use it to try out a standard library function to see if it works, then later factor the code to be less fragile. You could also try using it in production; just make sure you're aware of the risks.
There are lots of things to watch out for and ways to shoot yourself in the foot:
- If you define the wrong function type, you'll get a function with undefined behavior that will likely cause a runtime panic. The library makes no attempt to warn you in this case.
- Calling unexported functions is inherently fragile because the function won't have any stability guarantees.
- The implementation relies on the details of internal Go data structures, so later versions of Go might break this library.
- Since the compiler doesn't expect unexported symbols to be used, it might not create them at all, for example due to inlining or dead code analysis. This means that functions may not show up like you expect, and new versions of the compiler may cause functions to suddenly disappear.
- If the function you want to use relies on unexported types, you won't be able to trivially use it. However, you can sometimes work around this by defining equivalent copies of those types that you can use, but that approach has its own set of dangers.
-
Locate
runtime.firstmoduledata- With
checklinkname_off: directgo:linknameto the linker symbol. - Otherwise:
locateModuleDataWithoutLinkname(Linux maps scan or legacy GC scan).
- With
-
Walk the function table — same idea as
runtime.FuncForPC: iteratemoduledataentries until the requested name matches, then read the code pointer. -
Build a callable
funcvalue —reflect.MakeFuncplusunsafe.Pointerto swap in the target code pointer.
Needless to say, it's a scary hack, but it seems to work!
MIT