Go is portable, until it isn't
54 comments
·December 7, 2025pjmlp
dwattttt
You're thinking of cross platform codebases. There's nothing about cross compilation that stops the toolchain from knowing what APIs are present & not present on a target system.
mmulet
I ran into this issue when porting term.everything[0] from typescript to go. I had some c library dependencies that I did need to link, so I had to use cgo. My solution was to do the build process on alpine linux[1] and use static linking[2]. This way it statically links musl libc, which is much friendlier with static linking than glibc. Now, I have a static binary that runs in alpine, Debian, and even bare containers.
Since I have made the change, I have not had anyone open any issues saying they had problems running it on their machines. (Unlike when I was using AppImages, which caused much more trouble than I expected)
[0] https://github.com/mmulet/term.everything look at distribute.sh and the makefile to see how I did it.
[1]in a podman or docker container
[2] -ldflags '-extldflags "-static"'
jchw
IMO this is the best approach, but it is worth noting that musl libc is not without its caveats. I'd say for most people it is best to tread carefully and make sure that differences between musl libc and glibc don't cause additional problems for the libraries you are linking to.
There is a decent list of known functional differences on the musl libc wiki:
https://wiki.musl-libc.org/functional-differences-from-glibc...
Overall, though, the vast majority of software works perfectly or near perfectly on musl libc, and that makes this a very compelling option indeed, especially since statically linking glibc is not supported and basically does not work. (And obviously, if you're already using library packages that are packaged for Alpine Linux in the first place, they will likely already have been tested on musl libc, and possibly even patched for better compatibility.)
tasuki
Huh. Does term.everything just work, or are there some gotchas? This seems like it could be supremely useful!
apitman
Note that you don't have to compile on an Alpine system to achieve this. These instructions should work on most distros:
cxr
I didn't see an explanation in the README that part of what the first GIF[1] shows is an effect created by video editing software (and not a screencapture that's just demonstrating the program actually running). "Screen images simulated" are the words usually chosen to start off the disclaimers in fine print shown at the bottom of the screen when similar effects appear in commercials. I think that it would make sense to adopt a similar explanation wrt the effect used for the GIF.
1. <https://github.com/mmulet/term.everything/blob/main/resource...>
plufz
Why would an open source project need to have any disclaimer? They are not selling anything.
nofriend
Because lying is wrong even when open source projects do it.
nebezb
> “in commercials where such effects appear”
Good thing this isn’t a commercial then.
hmans
[dead]
bitbasher
Once you use CGO, portability is gone. Your binary is no longer staticly compiled.
This can happen subtley without you knowing it. If you use a function in the standard library that happens to call into a CGO function, you are no longer static.
This happens with things like os.UserHomeDir or some networking things like DNS lookups.
You can "force" go to do static compiling by disabling CGO, but that means you can't use _any_ CGO. Which may not work if you require it for certain things like sqlite.
PunchyHamster
> Which may not work if you require it for certain things like sqlite.
there is cgo-less sqlite implementation https://github.com/glebarez/go-sqlite it seems to not be maintained much tho
swills
You can definitely use CGO and still build statically, but you do need to set ldflags to include -static.
hiAndrewQuinn
You don't need CGO for SQLite in most cases; I did a deep dive into it here.
https://til.andrew-quinn.me/posts/you-don-t-need-cgo-to-use-...
silverwind
> This happens with things like os.UserHomeDir or some networking things like DNS lookups.
The docs do not mention this CGO dependency, are you sure?
purpleidea
I was surprised too, that I had to check the docs, so I assume the user was misinformed.
bitbasher
Perhaps I misremembered or things changed? For instance, the os/user results in a dynamicly linked executable: https://play.golang.com/p/7QsmcjJI4H5
There are multiple standard library functions that do it.. I recall some in "net" and some in "os".
nunez
You hit this real quick when trying to build container images from the scratch. Theoretically you can drop a Go binary into a blank rootfs and it will run. This works most of the time, but anything that depends on Go's Postgres client requires libpq which requires libc. Queue EFILE runtime errors after running the container.
nateb2022
> anything that depends on Go's Postgres client requires libpq which requires libc
mxey
> For users that require new features or reliable resolution of reported bugs, we recommend using pgx which is under active development.
AlbinoDrought
I've also seen https://github.com/jackc/pgx used in many projects
ghola2k5
I’ve had some success using Zig for cross compiling when CGO is required.
dilyevsky
There's still some bugs when interacting with gold and cross-compiling to linux/arm64 but fixable with some workarounds...
hansvm
That's Uber's approach, right?
CGamesPlay
Use dlopen? I haven’t tried this in Go, but if you want a binary that optionally includes features from an external library, you want to use dlopen to load it.
necovek
This seems to imply that Go's binaries are otherwise compatible with multiple platforms like amd64 and arm64, other than the issue with linking dynamic libraries.
I suspect that's not true either even if it might be technically possible to achieve it through some trickery (and why not risc-v, and other architectures too?).
cxr
For a single binary that will actually run across both architectures, see <https://cosmo.zip/>.
Original discussion: <https://news.ycombinator.com/item?id=24256883>.
khazit
Of course you still need one binary per CPU architecture. But when you rely on a dynamic link, you need to build from the same architecture as the target system. At that point cross-compiling stops being reliable.
necovek
I am complaining about the language (phrasing) used: a Python, TypeScript or Java program might be truly portable across architectures too.
Since architectures are only brought up in relation to dynamic libraries, it implied it is otherwise as portable as above languages.
With that out of the way, it seems like a small thing for the Go build system if it's already doing cross compilation (and thus has understanding of foreign architectures and executable formats). I am guessing it just hasn't been done and is not a big lift, so perhaps look into it yourself?
arccy
they're only portable if you don't count the architecture specific runtime that you need to somehow obtain...
go doesn't require dynamic linking for C, if you can figure out the right C compiler flags you can cross compile statically linked go+c binaries as well.
vbezhenar
Is it some tooling issue? Why is is an issue to cross-compile programs with dynamic linking?
cxr
It's a tooling issue. No one has done the work to make things work as smoothly as they could.
Traditionally, cross-compilers generally didn't even work the way that the Zig and Go toolchains approach it—achieving cross-compilation could be expected to be a much more trying process. The Zig folks and the Go folks broke with tradition by choosing to architect their compilers more sensibly for the 21st century, but the effects of the older convention remains.
dekhn
In general, cross compilers can do dynamic linking.
swills
I happily and reliably cross build Go code that uses CGO and generate static binaries on amd64 for arm64.
null
kccqzy
Interesting that it uses the C API to collect journals. I would’ve thought to just invoke journalctl CLI. On platforms like macOS where the CLI doesn’t exist it’s an error when you exec, not a build time error.
ajross
That's really not such a weird choice. The systemd library is pervasive and compatible.
The weird bit is the analysis[1], which complains that a Go binary doesn't run on Alpine Linux, a system which is explicitly and intentionally (also IMHO ridiculously, but that's editorializing) binary-incompatible with the stable Linux C ABI as it's existed for almost three decades now. It's really no more "Linux" than is Android, for the same reason, and you don't complain that your Go binaries don't run there.
[1] I'll just skip without explaination how weird it was to see the author complain that the build breaks because they can't get systemd log output on... a mac.
cosmin800
Well, that was pretty obvious that the portability is gone, especially when you start linking into systemd, even on the host system you have to link with the shared libs into systemd, you cannot link statically.
daviddever23box
This is an (organizational) tooling problem, not a language problem - and is no less complicated when musl libc enters the discussion.
laladrik
The conclusion of the article says that it's not the language problem either. Under the title "So, is Go the problem?" Or do you mean something else here?
saghm
Given that the title implies the opposite, I think it's a fair criticism. Pointing out clickbait might be tedious, but not more so than clickbait itself.
nurettin
Go is portable until you have to deploy on AS/400
cyberax
Cgo is terrible, but if you just want some simple C calls from a library, you can use https://github.com/ebitengine/purego to generate the bindings.
It is a bit cursed, but works pretty well. I'm using it in my hardware-backed KMIP server to interface with PKCS11.
And a set of people rediscovered why cross compiling only works up to certain extent, regardless of the marketing on the tin.
The point one needs to touch APIs that only exists on the target system, the fun starts, regardless of the programming language.
Go, Zig, whatever.