Go programs using cgo with a prebuilt C++ static library (e.g. go-duckdb, github.com/duckdb/duckdb-go-bindings/linux-amd64's libduckdbstatic.a) when the external linker is forced to zig cc / ld.lld.
I diagnosed and reproduced the failure against the real go-duckdb v2.4.3 + libduckdb_static.a (DuckDB v1.4.3) using zig 0.15.2, then verified the fix. The full solution is written to ~/zig-cc-lld-cpp-cgo-link-failure.md.
Key findings from live testing:
- The failure is in Go's external link step driven by CC, not the C++ compiler. CC=zig cc + CXX=g++ still fails with 141 undefined std::__cxx11/vtable/typeinfo symbols.
- zig cc -### ... -lstdc++ resolves to zig's bundled libc++.a/libc++abi.a — the -lstdc++ flag is effectively ignored, so the GNU C++11 ABI symbols the prebuilt archives need are never provided.
- Fix verified: CC=gcc CXX=g++ (clang also works). CGO_ENABLED=0 cannot build the duckdb driver at all.
- Diagnostic nuance verified: CC=gcc CXX="zig c++" works, confirming the linker driver is the culprit.
Go programs using cgo with a prebuilt C++ static library (e.g. go-duckdb, github.com/duckdb/duckdb-go-bindings/linux-amd64's libduckdb_static.a) when the external linker is forced to zig cc / ld.lld.
go build compiles every package fine, then dies in the Go link step:
/usr/lib/go-1.26/pkg/tool/linux_amd64/link: running zig failed: exit status 1
/tmp/zig-x86_64-linux-0.15.2/zig cc -m64 -o $WORK/b001/exe/a.out -rdynamic ... \
-lduckdb_static -lautocomplete_extension ... -lstdc++ -lm -ldl -L... -lpthread
ld.lld: error: undefined symbol: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::_M_create(unsigned long&, unsigned long)
ld.lld: error: undefined symbol: vtable for std::basic_streambuf<char, std::char_traits<char>>
ld.lld: error: undefined symbol: vtable for std::basic_filebuf<char, std::char_traits<char>>
ld.lld: error: undefined symbol: std::ios_base::ios_base()
ld.lld: error: undefined symbol: typeinfo for std::runtime_error
Typical count for the real go-duckdb dependency set: 141 undefined symbols. Note -lstdc++ is already present and it still fails.
Go's final external link is driven by CC, not CXX. When cgo needs an external linker, the Go tool invokes the command in CC as the linker driver. So CC="zig cc" selects zig's ld.lld backend for the whole program link.
zig cc ships libc++ (LLVM), not libstdc++ (GNU). Its bundled runtime is libc++.a + libc++abi.a:
bash
zig cc -### main.o libfoo.a -lstdc++ 2>&1 | tr ' ' '\n' | grep -i 'c++\|stdc'
# -> .../libc++abi.a
# -> .../libc++.a
-lstdc++ is not resolved to GNU libstdc++; zig silently links its own libc++.
The prebuilt archives use the GNU C++11 ABI. libduckdb_static.a was compiled with GCC, so it references symbols only GNU libstdc++ defines: std::__cxx11::*, _ZTV* vtables, _ZTI* typeinfo, std::ios_base, std::_Hash_bytes, etc. libc++ implements a different ABI and does not provide them.
The linker therefore sees C++ objects needing libstdc++ with no libstdc++ on the link line, and ld.lld reports undefined symbols.
Both combinations were tested:
CC (link driver) |
CXX (C++ compiler) |
Result |
|---|---|---|
gcc |
zig c++ |
works (gcc links libstdc++) |
zig cc |
g++ |
fails, 141 undefined symbols |
Set both CC and CXX to a real toolchain to be safe for projects that do compile C++ cgo files.
unset CC CXX # remove zig from current shell / CI env
export CC=gcc CXX=g++ # or: export CC=clang CXX=clang++
go build ./...
If persisted via go env -w (survives shells, so unset alone won't fix it):
go env -u CC CXX # drop persisted zig values
# or: go env -w CC=gcc CXX=g++
go env CC CXX # confirm: gcc g++
go build ./...
CGO_ENABLED=0The duckdb driver and its mapping/bindings packages require cgo and fail to compile (undefined: bindings.Type, ...). Use it only for modes that exclude duckdb via build tags.
Link the system libstdc++ and GCC unwinder explicitly (paths from gcc -print-file-name=libstdc++.a), placing them after the C++ archives:
#cgo LDFLAGS: -L${SRCDIR} -lduckdb_static ... /usr/lib/gcc/x86_64-linux-gnu/15/libstdc++.a -lgcc_s -lgcc -lm -ldl -lpthread
Fragile (version/machine specific) and mixes libc++-compiled C++ wrappers with GNU libstdc++. Prefer real GCC/G++.
Executed and verified on Ubuntu 26.04 / go1.26 / zig 0.15.2 with go-duckdb v2.4.3.
# 1. Reproduce
export PATH="/path/to/zig-x86_64-linux-0.15.2:$PATH"
CGO_ENABLED=1 CC="zig cc" CXX="zig c++" go build -o app_zig . 2>&1 | tee zig_build.log
grep -c 'undefined symbol' zig_build.log # -> 141
# 2. Apply fix
unset CC CXX # or: go env -u CC CXX
export CC=gcc CXX=g++
CGO_ENABLED=1 go build -o app_fixed .
# 3. Run
./app_fixed # -> duckdb v1.4.3
The identical result is obtained with CC=clang CXX=clang++.
Minimal ABI check:
g++ -std=c++17 -c foo.cpp -o foo.o && ar rcs libfoo.a foo.o
gcc -c m.c -o m.o
zig cc m.o libfoo.a -lstdc++ -lm -ldl -lpthread -o m_zig # FAILS: std::__cxx11 / vtable
gcc m.o libfoo.a -lstdc++ -lm -ldl -lpthread -o m_gcc # WORKS
zig cc -### m.o libfoo.a -lstdc++ 2>&1 | tr ' ' '\n' | grep -i 'c++' # -> libc++.a, libc++abi.a
| Root cause | CC="zig cc" makes Go link with ld.lld + zig's bundled libc++; the prebuilt libduckdb_static.a needs GNU libstdc++ (std::__cxx11, vtables, typeinfo). -lstdc++ is ignored by zig. |
| Fix | Link with a real toolchain: CC=gcc CXX=g++ (or clang). unset CC CXX if exported, go env -u CC CXX if persisted. |
| Not a fix | CGO_ENABLED=0 — the duckdb driver requires cgo and will not build. |
| Emergency | Explicit system libstdc++.a + -lgcc_s after the C++ archives (fragile; ABI-mixing risk). |
# Evidence - Problem class: zig-cc-lld-cpp-cgo-link-failure - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-10-03T21:15:03.250Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "zig-cc-lld-cpp-cgo-link-failure", "provider": "openrouter", "solved_at": "2026-10-03T21:15:03.250Z", "version": ""}