Eo is a C++ compatibility layer for porting existing Go code with minimal semantic and structural changes.
It is intentionally not an idiomatic C++ framework. The primary goal is to make mechanical Go-to-C++ translation practical when preserving the original control flow and concurrency structure matters more than redesigning the code around C++ conventions.
If there is enough time to rewrite a Go codebase properly in C++, that is usually the better choice. Eo exists for cases where a close, fast port is more useful than a clean rewrite.
Eo is under active development and its APIs are not stable.
Eo means "I go" in Latin and its pronunciation is "Ay-Oh", like what Freddie Mercury shouted at Live Aid.
- C++23
- Boost.Asio with coroutine, experimental channel, and awaitable-operator support
- fmt
- CMake 3.25+
Dependencies are discovered through standard CMake find_package calls. Eo no longer downloads or configures a package manager during CMake configuration.
For more examples, refer here.
Goroutines are emulated with C++ stackless coroutines and Boost.Asio awaitable.
func<R = void> is an alias for boost::asio::awaitable<R>.
// Go
func f(s string) {
fmt.Println(s)
}
func main() {
go f("hello")
go func() {
fmt.Println("world")
}()
}// C++
func<> f(std::string s) {
fmt::Println(s);
co_return;
}
func<> eo_main() {
go(f("hello"));
go([]() -> func<> {
fmt::Println("world");
co_return;
});
co_return;
}Channels are currently implemented on top of Boost.Asio experimental concurrent channels.
Go receive <-ch and send ch <- value are represented by *ch and ch << value.
// Go
func main() {
ch := make(chan string)
go func() { ch <- "ping" }()
msg := <-ch
fmt.Println(msg)
}// C++
func<> eo_main() {
auto ch = make_chan<std::string>();
go([&]() -> func<> { co_await (ch << "ping"); });
auto msg = co_await *ch;
fmt::Println(msg);
}Select is currently implemented with Boost.Asio awaitable composition.
// Go
func f() {
for {
select {
case msg := <-ch:
fmt.Println(msg)
default:
return
}
}
}// C++
func<> f() {
for (;;) {
switch (auto select = Select{*ch}; select.try_index()) {
case 0: {
auto msg = select.recv<0>();
fmt::Println(msg);
break;
}
default: {
co_return;
}
}
}
}A function containing Go defer declares one eo_defer_scope at function scope. Deferred callees, receivers, and arguments are saved in source order before eo_defer(...) registers a nullary callable. Translated normal exits call eo_defer_run before return or co_return, so deferred calls run in LIFO order while function locals are still alive.
// Go
func f() {
defer fmt.Println("world")
fmt.Println("hello")
}// C++
func<> f() {
eo_defer_scope;
auto _eo_defer_arg_0_0 = std::string{"world"};
eo_defer([=] { fmt::Println(_eo_defer_arg_0_0); });
fmt::Println("hello");
eo_defer_run;
co_return;
}Deferred method receivers follow Go's receiver binding: value receivers save a value, while pointer receivers on addressable values save the corresponding address. A deferred closure may use eo_defer([&] { ... }); only when every referenced translated object remains alive until the function-level drain; captures that require Go-style lifetime extension across a nested C++ scope are not yet frozen. See docs/TRANSLATION_RULES.md for the canonical source-order, receiver, and return translation shapes.
Some frequently used Go APIs are mirrored for easier source translation. Translation-facing APIs preserve the corresponding Go identifier spelling when it is a valid, non-reserved C++ identifier, so mechanically ported code does not need unrelated naming transformations. API resemblance does not imply complete behavioral equivalence; semantic compatibility is tested and documented separately as the project evolves.
See Translation Principles for the identifier-preservation policy and related translation rules.