- Audience: Intermediate Go Developers
- Prerequisites: Basic knowledge of Go, C/C++ syntax, and CLI build tools (gcc/clang).
- Focus: Creating Go shared libraries and consuming them safely from C and C++.
Understand how to compile Go code into a C-compatible shared object (.so / .dll) and header file using cgo.
package main: A library export must still be amainpackage.import "C": Enables cgo.//export SymbolName: Comment directive to expose functions to C.-buildmode=c-shared: The compiler flag that creates the artifact.
package main
import "C"
import "fmt"
// main is required but ignored in shared libraries
func main() {}
//export HelloFromGo
func HelloFromGo() {
fmt.Println("Go: Hello from the Go Runtime!")
}-
Compile the library:
go build -o libgo.so -buildmode=c-shared golib.go
Result: Generates
libgo.so(binary) andlibgo.h(C header). -
Inspect
libgo.hto see how Go types map to C types.
Learn to pass string data from C/C++ into Go.
- Immutability: Go strings are immutable and distinct from
char*. C.GoString: Converts a Cchar*to a native Go string (copies memory).- Memory Safety: Go's Garbage Collector (GC) does not see C memory; C does not see Go memory. Copies are usually required.
package main
import "C"
import "fmt"
func main() {}
//export PrintMessage
func PrintMessage(msg *C.char) {
// Convert C string to Go string (Safe Copy)
goStr := C.GoString(msg)
fmt.Printf("Go received: %s\n", goStr)
}Create loader.c to test the library.
#include "libgo.h" // Generated by Go
#include <stdio.h>
int main() {
printf("C: Calling Go...\n");
PrintMessage("Hello from C Land");
printf("C: Finished.\n");
return 0;
}Build & Run:
go build -o libgo.so -buildmode=c-shared printer.go
gcc -o loader loader.c ./libgo.so
./loaderWrap the C-compatible Go library in a C++ application.
- Name Mangling: C++ mangles function names. Go exports C symbols.
extern "C": Tells the C++ compiler to treat the Go header as raw C code, preventing name mangling mismatch.
Do not include libgo.h directly in multiple C++ headers. Create a wrapper class to isolate the C-style dependency.
#include <iostream>
// extern "C" is mandatory for linking Go libraries
extern "C" {
#include "libgo.h"
}
int main() {
std::cout << "C++: Starting application..." << std::endl;
// Pass a C-style string literal
PrintMessage((char*)"Greetings from C++");
std::cout << "C++: Done." << std::endl;
return 0;
}Maintain state in Go between C++ calls (e.g., a Go class instance).
You cannot pass a Go pointer (like *MyStruct) directly to C++ and pass it back later. The Go Garbage Collector might move the struct, invalidating the pointer held by C++.
Use cgo.NewHandle (Go 1.17+) to map a Go object to a uintptr safe for C/C++.
package main
import "C"
import (
"fmt"
"runtime/cgo"
)
type Worker struct {
Name string
}
//export CreateWorker
func CreateWorker(name *C.char) C.uintptr_t {
w := &Worker{Name: C.GoString(name)}
// Return a handle (an integer ID)
return C.uintptr_t(cgo.NewHandle(w))
}
//export DoWork
func DoWork(h C.uintptr_t) {
handle := cgo.Handle(h)
// Retrieve the Go object safely
w := handle.Value().(*Worker)
fmt.Printf("Go Worker [%s] is working.\n", w.Name)
}
//export DeleteWorker
func DeleteWorker(h C.uintptr_t) {
cgo.Handle(h).Delete() // Allow GC to reclaim memory
}
func main() {}- Stack Size: C threads have large stacks. Go goroutines start small. The Go runtime handles the stack switching, but there is overhead (cgo overhead).
- Signal Handling: The Go runtime installs signal handlers (SIGSEGV, SIGINT). Be careful if your C++ app relies on custom signal handlers.
- Performance: C-to-Go calls are not free.
- Bad: Calling Go inside a tight C++ loop 1 million times.
- Good: C++ collecting data, batching it, and sending it to Go once.
- Always verify inputs (
nilchecks) on the Go side. - Ensure
cgo.Handles are deleted, or you will leak memory. - Run binaries with
GODEBUG=cgocheck=1during development to catch illegal pointer passing.
A high-performance C++ simulation engine needs a modern, asynchronous logging service. We will write the logger in Go (utilizing its channels and string handling) and consume it from a C++ class.
/project
├── logger.go (The Go Shared Library)
├── LoggerWrapper.h (C++ Header)
├── LoggerWrapper.cpp(C++ Implementation)
├── main.cpp (The Application)
└── Makefile (Build Automation)
package main
/*
#include <stdlib.h>
*/
import "C"
import (
"fmt"
"runtime/cgo"
"sync"
"time"
)
// LogSystem represents our Go object
type LogSystem struct {
msgChan chan string
wg sync.WaitGroup
}
//export NewLogger
func NewLogger() C.uintptr_t {
ls := &LogSystem{
msgChan: make(chan string, 100),
}
// Start a background goroutine for non-blocking logging
ls.wg.Add(1)
go func() {
defer ls.wg.Done()
for msg := range ls.msgChan {
// Simulate I/O
fmt.Printf("[GO-LOG] %s - %s\n", time.Now().Format(time.TimeOnly), msg)
}
}()
return C.uintptr_t(cgo.NewHandle(ls))
}
//export LogMessage
func LogMessage(h C.uintptr_t, cMsg *C.char) {
handle := cgo.Handle(h)
ls := handle.Value().(*LogSystem)
// Copy string immediately so C++ can free its memory
goMsg := C.GoString(cMsg)
// Send to channel (non-blocking unless full)
select {
case ls.msgChan <- goMsg:
default:
fmt.Println("[GO-LOG] Dropped message, buffer full")
}
}
//export CloseLogger
func CloseLogger(h C.uintptr_t) {
handle := cgo.Handle(h)
ls := handle.Value().(*LogSystem)
close(ls.msgChan)
ls.wg.Wait() // Wait for drain
handle.Delete() // Clean up handle map
}
func main() {}#pragma once
#include <string>
#include <cstdint>
// Forward declaration to avoid including go header here
extern "C" {
typedef uintptr_t GoHandle;
}
class LoggerWrapper {
private:
GoHandle handle;
public:
LoggerWrapper();
~LoggerWrapper();
void Log(const std::string& message);
};#include "LoggerWrapper.h"
#include <vector>
// Include the Go-generated header strictly inside implementation
extern "C" {
#include "liblogger.h"
}
LoggerWrapper::LoggerWrapper() {
this->handle = NewLogger();
}
LoggerWrapper::~LoggerWrapper() {
CloseLogger(this->handle);
}
void LoggerWrapper::Log(const std::string& message) {
// Cast const char* to char* for the C interface
LogMessage(this->handle, (char*)message.c_str());
}#include <iostream>
#include "LoggerWrapper.h"
int main() {
std::cout << "APP: Engine Initializing..." << std::endl;
{
// RAII: Logger is created here
LoggerWrapper logger;
logger.Log("System boot sequence started");
logger.Log("Loading textures...");
logger.Log("Connecting to database...");
std::cout << "APP: Doing heavy C++ math..." << std::endl;
// Logger destroyed when leaving scope
}
std::cout << "APP: Shutdown complete." << std::endl;
return 0;
}all: clean build run
build:
# 1. Compile Go into shared library
go build -o liblogger.so -buildmode=c-shared logger.go
# 2. Compile C++ with link to Go library
# Note: -L. looks for library in current dir, -llogger links against liblogger.so
# -Wl,-rpath=. tells the binary to look in current dir for .so at runtime
g++ -o app main.cpp LoggerWrapper.cpp -L. -llogger -Wl,-rpath=.
run:
./app
clean:
rm -f liblogger.so liblogger.h appAPP: Engine Initializing...
[GO-LOG] 10:00:01 - System boot sequence started
[GO-LOG] 10:00:01 - Loading textures...
APP: Doing heavy C++ math...
[GO-LOG] 10:00:01 - Connecting to database...
APP: Shutdown complete.