Calling Go from C
cgo is bidirectional. As well as letting Go call C, it lets a C program — or any program in any language with a C-compatible foreign-function interface — call into Go. A Go function annotated with the
//exportdirective becomes a C-callable symbol; the toolchain generates a C header declaring it and the glue that translates the C calling convention into a Go call, starting the Go runtime if it is not already running. Two build modes package this for consumption:c-archiveproduces a static library (.a) plus header, andc-sharedproduces a dynamic library (.so/.dll/.dylib). This note explains the//exportmechanism, the build modes, the_GoString_glue type, and the constraints that make reverse cgo more restrictive than the forward direction.
Mental Model
In the forward direction (Go calling C) the Go runtime already exists and merely steps aside for a C call. In the reverse direction the C program is in charge, and the Go runtime is a guest that must be bootstrapped, given an OS thread, and handed control for the duration of each call.
flowchart LR subgraph C["C / host program"] M[main or library init] --> X[calls exported Go symbol] end subgraph G["Go shared/static library"] X --> R[cgo entry glue] R --> S{runtime started?} S -- no --> I[initialize Go runtime: scheduler, GC, init funcs] S -- yes --> T[acquire a goroutine on a thread] I --> T T --> F[run the exported Go function] F --> RET[marshal return values back to C ABI] end RET --> X
Diagram: a C program calling an exported Go function. The key insight versus forward cgo is the runtime started? decision — the very first call into a Go c-shared/c-archive library pays a one-time cost to spin up the entire Go runtime (scheduler, garbage collector, all package init functions); every subsequent call is a comparatively cheap boundary crossing.
The //export Directive
A Go function is made callable from C by placing the directive //export Name in a comment immediately above the function declaration (cmd/cgo docs). The name after //export is the C-visible symbol; it conventionally matches the Go function name.
package main
import "C"
import "fmt"
//export Add
func Add(a, b C.int) C.int { // 1
return a + b
}
//export Greet
func Greet(name *C.char) *C.char { // 2
s := C.GoString(name)
return C.CString(fmt.Sprintf("Hello, %s", s)) // 3
}
func main() {} // 4Line 1: Add is exported; its parameters and return are C numeric types (C.int), which map directly onto the C ABI. Line 2: Greet takes a *C.char (a C string) and returns one. Line 3: it converts the incoming C string to a Go string, builds a result, and converts back with C.CString — which mallocs; the C caller now owns that memory and must free it (a classic leak point — see Failure Modes). Line 4: even a library needs a package main with a (possibly empty) main for c-archive/c-shared builds.
The toolchain generates a header, _cgo_export.h, declaring each exported function in C:
extern int Add(int a, int b);
extern char* Greet(char* name);Functions returning multiple values are mapped to a generated struct:
//export Divide
func Divide(a, b C.int) (C.int, C.int) { return a / b, a % b }generates
struct Divide_return { int r0; int r1; };
extern struct Divide_return Divide(int a, int b);— the return fields are named r0, r1, … in declaration order (cmd/cgo docs).
Preamble restriction in //export files
A Go file that contains //export directives has a restricted preamble: the C comment block before import "C" may contain only declarations, never definitions. The reason is mechanical — the generated C code #includes both the preamble and _cgo_export.h, and a function definition in the preamble would be compiled into multiple translation units, producing duplicate-symbol link errors. Put declarations in the //export file and move any actual C function bodies into a separate .c file or a separate Go file without //export (cmd/cgo docs).
The _GoString_ Glue Type
C has no native Go-string type. When an exported Go function takes a string parameter, cgo represents it in C as the opaque type _GoString_. C code cannot index it directly; it uses two helper functions that cgo emits:
size_t _GoStringLen(_GoString_ s);
const char *_GoStringPtr(_GoString_ s);//export Process
func Process(s string) {} // C sees: void Process(_GoString_ s);On the C side:
void use(_GoString_ s) {
printf("%.*s\n", (int)_GoStringLen(s), _GoStringPtr(s));
}The rules around _GoString_ are strict (cmd/cgo docs): the pointer from _GoStringPtr is read-only — C must not modify it; the bytes are not guaranteed to be NUL-terminated (a Go string is a length-delimited slice, not a C string), so %.*s with an explicit length is mandatory and strlen is wrong; and a _GoString_ cannot be pinned with runtime.Pinner, so it is valid only for the duration of the call — C must not stash it. If C needs the string longer, copy it into C-owned memory immediately.
Build Modes: c-archive and c-shared
A Go package is turned into something C can link against with go build -buildmode=... (cmd/go docs). Two modes matter here, both introduced in Go 1.5 (Go 1.5 release notes).
-buildmode=c-archive produces a static archive — libname.a plus libname.h. The C program compiles and links against it; the resulting executable contains the Go runtime statically.
go build -buildmode=c-archive -o libgreet.a . # 1
cc -o app main.c libgreet.a -lpthread # 2Line 1 builds the archive and its header from the current Go package. Line 2 compiles a C main.c and statically links the archive; -lpthread is required because the Go runtime uses pthreads (on some platforms -ldl/-lm are also needed). The Go header libgreet.h is #included by main.c.
-buildmode=c-shared produces a dynamic library — .so on Linux, .dll on Windows, .dylib on macOS — plus the same header. This is what you use to expose Go to a non-C language: Python (ctypes/cffi), Ruby, Java (JNI), Node.js (N-API), or any runtime that can dlopen a C ABI.
go build -buildmode=c-shared -o libgreet.so .import ctypes
lib = ctypes.CDLL("./libgreet.so")
lib.Add.restype = ctypes.c_int
print(lib.Add(2, 3)) # -> 5A subtle difference: with c-archive, the Go runtime initializes when the host process starts (it is linked statically into a normal executable, so Go’s init runs as part of process startup). With c-shared, the runtime initializes when the library is loaded (dlopen) or on the first exported call — and any package init() functions run then. Either way, the first call across the boundary may be markedly slower than the rest because it triggers full runtime startup.
What the generated files actually contain
It is worth seeing the full set of artifacts a c-shared build of an //export-bearing package produces, because debugging reverse cgo means reading them. cgo (driven by the go command) generates, per package: _cgo_gotypes.go (Go type definitions for the C bridge), _cgo_export.c and _cgo_export.h (the C-side declarations and trampolines for every //exported function), and one *.cgo1.go / *.cgo2.c pair per input file (the rewritten Go and the C glue). The //export Add directive specifically causes cgo to emit, in _cgo_export.c, a C function named Add whose body marshals the C-ABI arguments into Go’s calling convention, performs the boundary crossing (the reverse of runtime.cgocall — runtime.cgocallback), and marshals the result back. The header _cgo_export.h is the only file a C consumer needs to see; the rest is internal. When a build mode is requested, the go command additionally links the Go runtime and all package object code into the archive or shared object.
The cgocallback path
Mechanically, a call from C into an exported Go function is the mirror image of forward cgo (cgo Performance and Pitfalls). The C-ABI trampoline in _cgo_export.c invokes runtime.cgocallback, which must: find or create an m (an OS-thread abstraction) and a g (a goroutine) for the calling thread — if C created this thread itself, the runtime adopts it by creating a fresh m bound to it; switch from the C stack onto a goroutine stack; re-enter the Go scheduler’s accounting so the GC and preemption see the goroutine; run the Go function; then unwind back to the C stack. The first call on a brand-new C-created thread is the expensive one because the thread must be adopted; subsequent calls on the same thread are cheaper. This adoption is also why a Go c-shared library is safe to call from arbitrary host threads — each is given its own m on first entry.
Failure Modes and Common Misunderstandings
Leaking C.CString results across the boundary. When an exported Go function returns *C.char built by C.CString, the Go GC does not own that memory and will never free it; the C caller must free() it. A C caller that does not know this leaks on every call. Document ownership explicitly, or return into a caller-provided buffer instead.
Returning Go pointers, slices, maps, channels, or functions to C. An exported function may return a Go pointer only to pinned memory, and may not return a string, slice, channel, or function value (cmd/cgo docs). These types have no stable C representation and reference the moving Go heap. Marshal to C types (C.CString, C.CBytes, or a runtime/cgo.Handle) instead.
runtime/cgo.Handle for opaque Go values. When C must hold a reference to a rich Go value (a struct, a closure) across calls, you cannot hand it a raw pointer. Wrap it: h := cgo.NewHandle(v) yields a uintptr token C can store; h.Value() retrieves the value; h.Delete() releases it. The handle keeps the value alive and GC-safe without exposing a movable pointer.
Thread affinity. C code that calls exported Go functions from many threads is fine, but if the host requires Go callbacks on a specific thread, the Go side must runtime.LockOSThread — the same discipline as forward cgo (see cgo Performance and Pitfalls).
Signal handling conflicts. When Go is the guest library, the host program may install its own signal handlers; the Go runtime also wants SIGSEGV, SIGPROF, etc. c-shared/c-archive mode makes the runtime more conservative about signal handling, but conflicts (especially over SIGPROF used by profilers, or SIGSEGV used by managed hosts like the JVM) are a recurring source of crashes.
Forgetting package main / empty main. Building a library still requires package main with a main function for these build modes; func main() {} is correct and necessary.
//export typos are silent. The directive must be //export Name with no space after // and the comment placed immediately above the func line with no blank line between. // export Name, or a blank line in between, is treated as an ordinary comment — the function is simply not exported, and the only symptom is a link error in the C program (“undefined reference to Name”). There is no warning from the Go side.
Exporting from a non-main package. //export works in any package, but the build modes (c-archive/c-shared) require the package being built to be package main. A common mistake is annotating functions in a library sub-package and then being unable to build a shared object from it.
Returning a Go struct or array by value. The //export mechanism does not support Go struct or array types as parameters or returns — only C-representable types and the multi-value r0/r1 struct cgo generates itself. To pass structured data, define the struct in C (in the preamble) and exchange a pointer to it, or marshal field by field.
Global state and re-entrancy. Because the Go runtime is a singleton inside the host process, two host threads calling exported Go functions share one Go heap, one GC, one set of package-level variables. Exported functions must be written to be safe under concurrent calls from C exactly as ordinary Go code must be safe under concurrent goroutines — there is no implicit serialization at the boundary.
Alternatives and When to Choose Them
Embedding Go into a C/other-language host via c-shared/c-archive is the right tool when you have substantial existing Go logic that a non-Go program must reuse in-process (a shared parsing/validation library, a crypto routine). When the integration can tolerate process boundaries, running the Go code as a separate service or sidecar (HTTP, gRPC, or a Unix socket) is dramatically simpler: no shared runtime, no signal conflicts, independent crash domains, and no FFI marshalling rules. For exposing Go to the browser or a sandbox, GOOS=js GOARCH=wasm or the newer wasip1 target compiles Go to WebAssembly with a cleaner boundary than C FFI. Choose c-shared specifically when in-process, low-latency, same-address-space calling is a hard requirement.
Two More Build Modes Worth Knowing
c-archive and c-shared are the two reverse-cgo modes, but the -buildmode family (cmd/go docs) has neighbours that clarify the design space. -buildmode=default produces an ordinary Go executable. -buildmode=plugin produces a Go plugin — a .so loadable at runtime by another Go program via the plugin package; it is reverse-loading but Go-to-Go, not Go-to-C, and it shares the parent’s runtime rather than embedding its own. -buildmode=pie produces a position-independent executable. The distinction that matters: c-archive/c-shared are the only modes that emit a C header and expose Go through the C ABI — they are for non-Go consumers. If the consumer is Go, plugin mode is the better fit; if the consumer is C or anything with a C FFI, c-shared/c-archive is the only option.
A practical constraint on all of these: the build mode is a property of the whole build, and not every mode is supported on every GOOS/GOARCH. c-shared and c-archive are broadly supported on the major platforms; plugin is notably restricted (Linux, macOS, FreeBSD) and is fragile across toolchain-version mismatches between the host and the plugin. This fragility is itself an argument for the process-boundary alternative discussed below.
Production Notes
The most-cited real-world use of reverse cgo is shipping Go libraries to other ecosystems: Go code compiled c-shared and consumed from Python, mobile apps (the gomobile tool builds on these modes to generate Android .aar and iOS frameworks), and database drivers. Teams report that the dominant operational issues are not the FFI mechanics but the lifecycle mismatches: the one-time runtime-startup cost on first call, signal-handler fights with the host, and the memory-ownership contract for returned strings. The pragmatic guidance that emerges is to keep the exported surface small and coarse-grained — a handful of functions that take and return plain C scalars, C strings (with a documented free contract), or cgo.Handle tokens — and to avoid threading rich Go object graphs through the boundary at all.
See Also
- cgo Internals — the
import "C"machinery and toolchain code generation - cgo Performance and Pitfalls — the forward direction, pointer rules,
LockOSThread - The unsafe Package —
unsafe.Pointer, used to bridge C and Go pointer types - The Go Runtime — what gets bootstrapped on the first reverse-cgo call
- Cross Compilation in Go — build-mode and
CGO_ENABLEDinteractions - Go Internals MOC — parent map (§14)