A recent post on the front page of HN made a comparison between different SQLite packages for Go. Some bundled the official C implementation, while others offered a pure Go implementation.
The C implementation was twice as fast as the Go one, but it saves you from having to deal with CGo. Is the tradeoff worth it? I would say no.
Firstly, SQLite is famous for being tested extremely thoroughly and, while the Go version is the result of machine translation of the original code, I would trust the original code over any reimplementation.
Secondly, dealing with CGo is actually not that bad nowadays.
In this post I'm going to show all the good and bad of compiling for different OSs mattn/go-sqlite3
, the package that relies on CGo to compile the original SQLite source file.
The setup
I'm going to create the following builds on a aarch64 linux machine:
- aarch64 linux
- aarch64 macos
- x86_64 windows
Each build will consist of the test suite that the package comes with. Normally you would just run go test
, but in my case I'll add -c
to make Go produce the test executable without running it, allowing me to move the executable on the correct machine.
Of course I will be using zig cc
as the C compiler. If you want to follow along, get yourself a copy of Zig (v0.9 or above), a copy of Go (v1.18 or above) and clone the repo in question.
Linux aarch64
This is the native target and it's going to be super easy.
$ CGO_ENABLED=1 CC="zig cc" CXX="zig cc" go test -c
$ ./go-sqlite3.test
PASS
This is so easy that I want to do something extra
$ ldd go-sqlite.test
linux-vdso.so.1 (0x0000ffffa4840000)
libpthread.so.0 => /nix/store/34k9b4lsmr7mcmykvbmwjazydwnfkckk-glibc-2.33-50/lib/libpthread.so.0 (0x0000ffffa47e0000)
libc.so.6 => /nix/store/34k9b4lsmr7mcmykvbmwjazydwnfkckk-glibc-2.33-50/lib/libc.so.6 (0x0000ffffa466d000)
libdl.so.2 => /nix/store/34k9b4lsmr7mcmykvbmwjazydwnfkckk-glibc-2.33-50/lib/libdl.so.2 (0x0000ffffa4659000)
/nix/store/34k9b4lsmr7mcmykvbmwjazydwnfkckk-glibc-2.33-50/lib/ld-linux-aarch64.so.1 (0x0000ffffa480e000)
As you can see:
- It's a dynamic executable
- I'm using NixOS btw
Let's make this a static executable
$ CGO_ENABLED=1 CC="zig cc -target native-native-musl" CXX="zig cc -target native-native-musl" go test -c
$ ./go-sqlite3.test
PASS
$ ldd go-sqlite3.test
statically linked
Nice.
MacOS aarch64
This is going to be a bit more involved. I will post all the intermediate steps that result in an error, scroll to the end of the section if you just want the working command.
$ CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 CC="zig cc -target aarch64-macos" CXX="zig cc -target aarch64-macos" go test -c
# runtime/cgo
warning(link): framework not found for '-framework CoreFoundation'
warning(link): Framework search paths:
error: FrameworkNotFound
Apparently SQLite Go needs CoreFoundation on mac, so we need to provide it. In my case, the linux machine is a VM running inside a physical Mac Mini, so I can just find where the framework libs are on the host and mount them into the VM.
# MacOS
$ echo $(xcrun --show-sdk-path)
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
# Inside that path there's System/Library/Frameworks, which I then mount to /host/Frameworks
# Linux
$ CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 CC="zig cc -target aarch64-macos -F/host/Frameworks" CXX="zig cc -target aarch64-macos -F/host/Frameworks" go test -c
# github.com/mattn/go-sqlite3.test
warning: unsupported linker arg: -headerpad
warning: unsupported linker arg: 1144
warning(link): unable to resolve dependency /usr/lib/libobjc.A.dylib
/home/kristoff/golang/go/pkg/tool/linux_arm64/link: /home/kristoff/golang/go/pkg/tool/linux_arm64/link: running dsymutil failed: exec: "dsymutil": executable file not found in $PATH
The warnings are fine, but the final message is a hard error: Go is expecting to be able to run dsymutil
, which I don't have on my system. I tried to get dsymutil
by installing LLVM (nix-shell -p llvm
) but compilation would still fail as it apparently didn't like the executable produced by Go.
One way forward is to ask Go to not add debug info to the executable by passing -ldflags="-s -w"
, which is annoying, but will help us move forward.
$ CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 CC="zig cc -target aarch64-macos -F/host/Frameworks" CXX="zig cc -target aarch64-macos -F/host/Frameworks" go test -c -ldflags="-s -w"
# github.com/mattn/go-sqlite3.test
warning(link): unable to resolve dependency /usr/lib/libobjc.A.dylib
I then ran the executable on the host and it passed all tests.
Windows x86_64
This one was surprisingly smooth.
$ CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC="zig cc -target x86_64-windows" CXX="zig cc -target x86_64-windows" go test -c
Running the tests on windows show a couple failed tests, but they have to do with my locales, so everything went well also in this case.
Conclusion
Not the smoothest experience, but not that bad either. Go could stop depending on dsymtool
, the libobjc
warnings could be silenced by providing the actual file, like we did with CoreFoundation (I didn't out of lazyness since everything succeeds anyway), and everything would be pretty much seamless.
Pretty good for a programming language and toolchain that is not yet v1.0 :^)
Want cross-compilation to improve even more?
Consider sponsoring the Zig Software Foundation.
Art by kprotty.
Top comments (1)
Very good combination, and finally compiles a pure static executable