Question
In Go, how can I call existing C++ code and wrap C++ classes so they can be used from Go?
Short Answer
Go can call native code with cgo, but cgo is designed around the C ABI rather than C++ classes. You will learn how to expose a small C-compatible API around a C++ class, represent the class as an opaque pointer in Go, manage object lifetime safely, and build the Go and C++ code together.
Concept
Go does not directly understand C++ language features such as classes, method overloading, templates, exceptions, or C++ name mangling. However, Go's cgo feature can call functions that use the stable, broadly supported C application binary interface (ABI).
The usual solution is therefore a two-layer design:
- Keep the real implementation in C++.
- Write a small wrapper with functions declared as
extern "C". - Call those wrapper functions from Go using cgo.
An extern "C" declaration tells the C++ compiler to export a function using C linkage. This prevents C++ name mangling and gives cgo a simple function-based interface to call.
Because a C++ object is not a C type, the wrapper normally returns it as an opaque handle: a void* pointer. Go stores that pointer but does not inspect or modify the C++ object directly. All operations happen through wrapper functions.
This separation matters because it creates a small, explicit boundary between Go and C++. It also makes memory ownership, error reporting, and API compatibility easier to control.
Mental Model
Think of a C++ class as a machine in a locked workshop.
- The C++ code owns the machine and knows its internal parts.
- The C wrapper is the workshop service desk.
- Go receives a claim ticket—an opaque pointer—not the machine itself.
- Go asks the service desk to create the machine, perform operations, and dispose of it.
Go should never try to open the workshop door and manipulate the C++ object's memory directly. It should only use the wrapper functions provided by the C++ side.
Syntax and Examples
A C++ wrapper exposes plain functions with C linkage. A common pattern is create, operation functions, and destroy.
// counter_api.h
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void* counter_create(int initial_value);
int counter_increment(void* handle);
int counter_value(const void* handle);
void counter_destroy(void* handle);
#ifdef __cplusplus
}
#endif
The implementation can use a normal C++ class internally:
// counter_api.cpp
#include "counter_api.h"
class Counter {
public:
explicit Counter(int initial) : value_(initial) {}
int increment() {
return ++value_;
}
int value() const {
return value_;
}
private:
int value_;
};
extern "C" void* counter_create(int initial_value) {
return new Counter(initial_value);
}
extern "C" int counter_increment(void* handle) {
return static_cast<Counter*>(handle)->increment();
}
extern "C" int counter_value(const void* handle) {
return static_cast<const Counter*>(handle)->value();
}
extern "C" void counter_destroy(void* handle) {
delete static_cast<Counter*>(handle);
}
Go imports the C declarations in a cgo preamble and calls them through the C pseudo-package:
package main
/*
#cgo CXXFLAGS: -std=c++17
#cgo LDFLAGS: -lstdc++
#include "counter_api.h"
*/
import "C"
import (
"fmt"
"runtime"
)
type Counter struct {
handle C.uintptr_t
}
func NewCounter(initial int) *Counter {
ptr := C.counter_create(C.int(initial))
if ptr == nil {
return nil
}
c := &Counter{handle: C.uintptr_t(uintptr(ptr))}
runtime.SetFinalizer(c, (*Counter).Close)
return c
}
func (c *Counter) Increment() int {
return int(C.counter_increment(unsafePointer(c.handle)))
}
func (c *Counter) Value() int {
return int(C.counter_value(unsafeConstPointer(c.handle)))
}
func (c *Counter) Close() {
if c.handle != 0 {
C.counter_destroy(unsafePointer(c.handle))
c.handle = 0
}
}
func unsafePointer(h C.uintptr_t) unsafe.Pointer {
return unsafe.Pointer(uintptr(h))
}
func unsafeConstPointer(h C.uintptr_t) unsafe.Pointer {
return unsafe.Pointer(uintptr(h))
}
func main() {
counter := NewCounter(10)
defer counter.Close()
fmt.Println(counter.Increment()) // 11
fmt.Println(counter.Value()) // 11
}
The preceding Go example illustrates the wrapper shape, but it requires importing unsafe. In practice, it is simpler and more idiomatic to store the handle directly as unsafe.Pointer, as shown in the mini-project solution.
Key syntax rules:
import "C"must appear immediately after the cgo comment; no blank line may separate them.- Files containing C++ implementations should use the
.cc,.cpp, or.cxxextension so Go invokes a C++ compiler. - Functions used by cgo should have C-compatible parameter and return types, such as integers,
char*, andvoid*. - Put
extern "C"around exported wrapper declarations and definitions.
Step by Step Execution
Consider this smaller call flow:
counter := NewCounter(5)
defer counter.Close()
next := counter.Increment()
NewCounter(5)callsC.counter_create(C.int(5)).- The C++ wrapper runs
new Counter(5)and returns the object's address asvoid*. - Go stores that address as an opaque handle. Go does not know the fields inside
Counter. counter.Increment()passes the same handle tocounter_increment.- The wrapper converts
void*back toCounter*withstatic_cast<Counter*>(handle). - C++ calls
Counter::increment(), increasing the internal value from5to6. - The wrapper returns
6, which cgo converts toC.int, and Go converts toint. - At the end of the function,
defer counter.Close()callscounter_destroy. counter_destroyusesdeleteto release the object allocated bynew.
The important rule is symmetry: the side that creates a resource should provide the API that releases it. Here, C++ uses new, so C++ must use delete.
Real World Use Cases
This pattern is useful when Go needs functionality that already exists in a C++ library.
- Image and video processing: wrap a C++ image decoder, encoder, or computer-vision library.
- Database and storage engines: call a mature C++ embedded engine through a narrow API.
- Hardware SDKs: use a vendor's C++ library for cameras, sensors, or industrial devices.
- High-performance algorithms: reuse a tested C++ implementation of compression, geometry, or signal processing.
- Legacy migration: build a new Go service while preserving a valuable C++ domain library.
For each case, keep the wrapper API small. Prefer functions that accept simple inputs and return simple results rather than exposing a large C++ object graph.
Real Codebase Usage
In production code, developers usually avoid exposing every C++ method directly. Instead, they design a stable boundary API.
Use a Go type to own the native handle
type Engine struct {
ptr unsafe.Pointer
}
The Go type groups the C++ handle with Go-friendly methods such as Start, Process, and Close.
Provide an explicit Close method
Native resources are not Go heap memory. Release them deterministically:
func (e *Engine) Close() {
if e.ptr != nil {
C.engine_destroy(e.ptr)
e.ptr = nil
}
}
Use defer engine.Close() immediately after a successful constructor call when appropriate. A finalizer can be a backup, but it should not be the primary cleanup mechanism because its timing is not predictable.
Validate before crossing the boundary
Validate ordinary Go inputs before calling C++:
if width <= 0 || height <= 0 {
return errors.New("dimensions must be positive")
}
This reduces native calls and keeps predictable errors in Go.
Convert C++ failures into error codes or messages
Do not allow C++ exceptions to cross the C ABI boundary. Catch exceptions in the wrapper and return an error code, nullptr, or an error string that Go can interpret.
Keep Go pointers out of long-lived C++ storage
C++ must not retain a pointer to ordinary Go-managed memory after the cgo call returns. If native code needs to retain data, copy it into C/C++ allocated memory or use an approved mechanism such as runtime/cgo.Handle for passing Go values through C safely.
Common Mistakes
Trying to include a C++ class directly in cgo
This does not create a usable Go class binding:
/*
#include "Counter.hpp"
*/
import "C"
cgo is built around C declarations. Create extern "C" wrapper functions instead.
Forgetting extern "C"
Broken wrapper declaration:
void* counter_create(int initial_value);
Without C linkage, the C++ compiler may mangle the symbol name. cgo or the linker may then fail to find the expected function. Use:
extern "C" void* counter_create(int initial_value);
Letting C++ exceptions escape
Broken code:
extern "C" int parse_value(const char* text) {
return std::stoi(text); // may throw
}
An exception crossing a C ABI boundary is unsafe. Catch it in C++ and return a status code or failure value.
Leaking native objects
Broken Go usage:
counter := NewCounter(0)
fmt.Println(counter.Increment())
// Close is never called.
Call Close, usually with defer, to release the C++ object.
Using an object after Close
After counter_destroy, the pointer is invalid. Set the Go handle to nil or zero and make methods reject closed objects.
Passing Go memory for C++ to keep
A C++ library must not store a pointer into Go memory for later use. Copy the data on the C++ side if it must outlive the call.
Assuming cgo makes all calls free
Crossing the Go/C++ boundary has overhead. Prefer batch operations, such as processing a whole buffer at once, over calling C++ once per byte or item.
Comparisons
| Approach | Best for | Main limitation |
|---|---|---|
| cgo with a C wrapper around C++ | Reusing a specific C++ library from Go | Requires native builds and careful ownership rules |
| Direct C library through cgo | Libraries that already expose a C API | Still has cgo boundary and build complexity |
| Reimplementing in Go | Portability, simple deployment, idiomatic Go | May require substantial development effort |
| Separate C++ service using RPC | Strong process isolation or independent deployment | Adds network/process communication overhead |
| C++ feature | What to expose to Go instead |
|---|---|
| Class constructor | create function returning void* |
| Instance method | Function whose first argument is void* handle |
| Destructor | destroy function |
| C++ exception | Error code, null handle, or allocated error message |
std::string | const char* plus length, or a dedicated buffer API |
| Template | A concrete wrapper for each required type |
Cheat Sheet
- Use
cgoto call C-compatible functions from Go. - Wrap C++ APIs in functions declared with
extern "C". - Compile implementation files as
.cpp,.cc, or.cxx. - Represent C++ instances as opaque
void*handles. - Typical wrapper API:
create, operation functions,destroy. - Store the handle in a Go struct, usually as
unsafe.Pointer. - Add an idempotent
Close()method and call it explicitly. - Do not let C++ exceptions cross the wrapper boundary.
- Do not let C++ retain ordinary Go pointers after a cgo call.
- Prefer coarse-grained native calls to many tiny calls.
- Put
import "C"immediately after its cgo comment. - Use
#cgo CXXFLAGSfor C++ compiler options and#cgo LDFLAGSfor linker libraries.
FAQ
Can Go call C++ directly?
Not in the same way it calls Go functions. Go uses cgo to call C-compatible functions, so C++ code normally needs an extern "C" wrapper.
Why is extern "C" needed when the code is compiled as C++?
It requests C linkage, avoiding C++ name mangling and making the exported functions callable through a C ABI.
Can Go use a C++ class as a Go struct?
No. Treat the C++ object as an opaque native handle and expose its operations as wrapper functions.
Should I use runtime.SetFinalizer to free a C++ object?
Use an explicit Close method as the primary mechanism. Finalizers are nondeterministic and should only be a backup when appropriate.
Can a C++ exception be caught in Go?
No. Catch it inside the C++ wrapper and translate it into a C-compatible error result.
Can I pass a Go slice to C++?
You can pass its data during a call under cgo's pointer rules, but C++ must not save that Go pointer for use after the call returns. Copy the data if it must be retained.
Does cgo work with cross-compilation?
It can, but the target platform needs an appropriate C/C++ cross-compiler and compatible native libraries. This is more involved than pure-Go cross-compilation.
Mini Project
Description
Build a small Go wrapper around a C++ Counter class. The project demonstrates the standard create/use/destroy lifecycle for a native C++ object and exposes it through an idiomatic Go type.
Goal
Create, increment, read, and explicitly release a C++ counter from a Go program.
Requirements
Requirement list is provided below.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.