A C function won't know how to behave in Go's runtime environment, so to call a C function Go needs make itself look more like a C program, call the C function, and then restore its magic state.
Other languages like C++, Rust, and Swift are similar enough to C that they can just call C functions directly. CPython is a C program, so it can too. Golang was brave enough to do fundamental things its own way, which isn't quite C-compatible.
Go (gc) was also a C program originally. It still had the same overhead back then as it does now. The implementation language is immaterial. How things are implemented is what is significant. Go (tinygo), being a different implementation, can call C functions as fast as C can.
> ...so it can too.
In my experience, the C FFI overhead in CPython is significantly higher than Go (gc). How are you managing to avoid it?
Which no different than Go, or any other language under the sun. There is no way to call a C function other than trivially, as you put it. The overhead in both Python and Go is in doing all the things you have to do in order to get to that point.
A small handful of languages/implementations are designed to be like C so that they don't have to do all that preparation in order to call a C function. The earlier comment included CPython in them. But the question questioned how that is being pull off, as that isn't the default. By default, CPython carries tremendous overhead to call a C function — way more than Go.
Other languages generally fall into either camp of having a C-like stack and thread model and easy FFI (e.g. Ruby, TCL, OCaml) and maybe having futures/async but not in an invisible/magic way, or having a radically different threading model at the cost of FFI being slow and painful (e.g. Erlang). JavaScript is kind of special in having C-like stack but being built around calling async functions from a global event loop, so it's technically the first but feels more like the second.
https://docs.oracle.com/en/java/javase/24/docs/specs/jni/int... mentions JRI.
But it seems like JNI has been replaced by third party solutions multiple times as well.
https://developer.okta.com/blog/2022/04/08/state-of-ffi-java...
Calling C safely is then slow because you have to allocate a larger stack, copy data around and mess with the GC.
It's about the same as most other languages that aren't specifically optimized for C calling. Considerably faster than Python.
Which is funny as everyone on HN loves to extol the virtues of Python being a "C DSL" and never think twice about its overhead, but as soon as the word Go is mentioned its like your computer is going to catch fire if you even try.
Emotion-driven development is a bizarre world.
Calling C from Go (or vice versa) often requires switching from Go's lightweight goroutine model to a full OS thread model because:
- Go's scheduler manages goroutines on M:N threads, but C doesn't cooperate with Go's scheduler.
- If C code blocks (e.g., on I/O or mutex), Go must assume the worst and parks the thread, spawning another to keep Go alive.
* Cost: This means entering/exiting cgo is significantly more expensive than a normal Go call. There’s a syscall-like overhead.
... This was only the first issue, but then it follows with "Go runtime can't see inside C to know is it allocating, blocking, spinning, etc.", then "Stack switching", "Thread Affinity and TLS", "Debug/Profiling support overhead", "Memory Ownership and GC barriers"All here - https://chatgpt.com/share/688172c3-9fa4-800a-9b8f-e1252b57d0...
Please do not take the numbers below at face value. I still expect an actual reply to my initial comment.
Per-call overhead:
C (baseline) - ~30 ns
Rust (unsafe) - ~30 ns
C# (P/Invoke) - ~30-50 ns
LuaJIT - ~30-50 ns
Go (cgo) - ~40-60 ns
Java (22, FFM) - ~40-70 ns
Java (JNI) - ~300-1000 ns
Perl (XS) - ~500-1000 ns
Python (ctypes) - ~10,000-30,000 ns
Common Lisp (SBCL) - ~500-1500 ns
Seems like Go is still fast enough as opposed to other programming languages with GC, so I am not sure it is fair to Go.Language/API | Call Overhead (no-op C) | Notes
Go (cgo) | ~40–60 ns | Stack switch + thread pinning
Java FFM | ~50 ns (downcall) | Similar to JNI, can be ~30 ns with isTrivial()
Java FFM (leaf) | ~30–40 ns | Optimized (isTrivial=true)
JNI | ~50–60 ns | Slightly slower than FFM
Rust (unsafe) | ~5–20 ns | Near-zero overhead
C# (P/Invoke) | ~20–50 ns | Depends on marshaling
Python (cffi) | 1000–10000 ns | Orders of magnitude slower |
you can verify this easily with Deno ffi which is pretty much optimal for JS runtimes. also, from everything i have seen and read, luajit should be even lower overhead than this.
you really shouldn't be asking chatgpt questions like this imo. these are facts, that need to be proven, not just vibes.
As if there is an alternative :)
More seriously, it’s “unsafe” from the perspective of the library calling into C, but usually “safe” for any layer above.
For what it is worth, I asked about LuaJIT after I have shared the link, and the numbers are now different for some languages, albeit not by much. Go (cgo) became ~50-100 ns. That said, I still believe it is unfair to single out Go when it does way better than some other GC languages.
"ants_everywhere" is right.
The anti-LLM activists that patrol HN will often downvote comments just for stating facts that disagree with their hatred of LLMs.