Skip to content(if available)orjump to list(if available)

Go is portable, until it isn't

Go is portable, until it isn't

54 comments

·December 7, 2025

pjmlp

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.

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:

https://www.arp242.net/static-go.html

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.

tptacek

You can even cross-compile doing that.

swills

Yes, indeed, I do.

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?

https://pkg.go.dev/os#UserHomeDir

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".

ncruces

There are at least a couple of ways to run SQLite without CGO.

tptacek

I think the standard answer here is modernc.org/sqlite.

apitman

Careful, you're responding to the author of a wasm-based alternative.

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

Try https://github.com/lib/pq

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

[deleted]

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.