Backend Development 13 min read

Calling Go Functions from C: Implementation Steps and Underlying Mechanism

This article explains how to call Go functions from C within the same process, covering the three‑step implementation, compilation into a shared library, the C invocation code, and a detailed walkthrough of the cgo and Go runtime mechanisms that make cross‑language calls possible.

IT Services Circle
IT Services Circle
IT Services Circle
Calling Go Functions from C: Implementation Steps and Underlying Mechanism

In this article we explore whether a language can invoke functions implemented in another language within the same process, using C calling Go as a concrete example.

1. C calling Go function example

The implementation consists of three steps:

Define and implement a function in Go.

Compile the Go code into a static or dynamic library.

Call the library from C.

1.1 Go function definition and implementation

package main

//int add(int a, int b);
import "C"

//export add
func add(a, b C.int) C.int {
    return a + b
}

func main() {}

Key points:

The line import "C" enables cgo, causing go build to invoke gcc.

The comment //int add(int a, int b) is not a Go comment but a C declaration.

//export add makes the function visible to external callers.

Parameters must use the C types provided by cgo (e.g., C.int ).

1.2 Compiling Go code into a library

# go build -buildmode=c-shared -o libadd.dylib main.go

The flag -buildmode=c-shared produces a dynamic library ( .dylib on macOS, .so on Linux); -buildmode=c-archive would produce a static archive.

1.3 C code calling the library

#include
#include "libadd.h"

int main(void) {
    int ret = add(2, 3);
    printf("C调用Go函数2+3= %d\n", ret);
    return 0;
}

Compile and link the C program with the generated library:

# gcc main.c -L. -ladd -o main

Running the executable prints C调用Go函数2+3= 5 , confirming the successful cross‑language call.

2. Underlying mechanism of C calling Go

2.1 cgo compilation tool

Running go tool cgo main.go generates intermediate files such as main.cgo1.go , main.cgo2.c , _cgo_gotypes.go , and the export files _cgo_export.c and _cgo_export.h inside the _obj directory.

2.2 Function entry in _cgo_export.c

int add(int a, int b) {
    __SIZE_TYPE__ _cgo_ctxt = _cgo_wait_runtime_init_done();
    typedef struct {
        int p0;
        int p1;
        int r0;
    } __attribute__((__packed__)) _cgo_argtype;
    static _cgo_argtype _cgo_zero;
    _cgo_argtype _cgo_a = _cgo_zero;
    _cgo_a.p0 = a;
    _cgo_a.p1 = b;
    crosscall2(_cgoexp_ec46b88da812_add, &_cgo_a, 12, _cgo_ctxt);
    return _cgo_a.r0;
}

This wrapper ensures the Go runtime is initialized and then forwards the call to crosscall2 , packing the arguments into a struct.

2.3 Go runtime crosscall2

//file:runtime/cgo/asm_amd64.s
TEXT crosscall2(SB),NOSPLIT,$0-0
    PUSH_REGS_HOST_TO_ABI0()
    ADJSP $0x18
    MOVQ CX, 0x0(SP)   // fn
    MOVQ DX, 0x8(SP)   // arg
    MOVQ R9, 0x10(SP)  // ctxt
    CALL runtime·cgocallback(SB)
    ADJSP $-0x18
    POP_REGS_HOST_TO_ABI0()
    RET

The function saves caller registers, prepares the stack, and invokes runtime·cgocallback , which switches from the C thread stack to a Go goroutine stack and locks the OS thread.

2.4 Go stub generated in _cgo_gotypes.go

//go:cgo_export_dynamic add
//go:linkname _cgoexp_ec46b88da812_add _cgoexp_ec46b88da812_add
//go:cgo_export_static _cgoexp_ec46b88da812_add
func _cgoexp_ec46b88da812_add(a *struct{p0 _Ctype_int; p1 _Ctype_int; r0 _Ctype_int}) {
    a.r0 = add(a.p0, a.p1)
}

This stub converts the packed C arguments back to Go types and calls the user‑defined add function:

//export add
func add(a, b C.int) C.int {
    return a + b
}

3. Summary

The cross‑language call involves three cooperating parts: the user code (Go function and C caller), the cgo‑generated stub code, and the Go runtime (functions like crosscall2 , cgocallback , and related helpers) that handle register saving, stack switching, and thread locking.

The first entry point is the stub in _cgo_export.c .

The Go runtime then performs ABI conversion, stack switching, and invokes the actual Go function.

After runtime processing, the call returns through the generated stub back to the C side.

Compared with ordinary in‑process function calls, cross‑language calls incur additional overhead due to runtime coordination, but they remain faster than RPC because they avoid kernel‑level protocol handling.

GoRuntimeC++interoperabilitycgocross-language
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.