A while ago I was asked why I wrote Sim in C++ instead of Go. I stumbled upon my answer again and realized it could be a blog post.

So here’s what I wrote then. I think I stand by it still, and I don’t think the situation has improved.

Why not write portable system tools in Go

My previous experience with “low level” things in Go (being very careful about which syscalls are used, and in which order) has had some frustrations in Go. Especially with portability. E.g. different definitions of syscall.Select between BSDs and Linux, making me have to use reflection at some points. (e.g. see this Go bug.

And to work around those things Go unfortunately uses the antipattern of (essentially) #ifdef __OpenBSD__, which we’ve known for decades is vastly inferior to checking for specific capabilities.

To me the Go proverb “Syscall must always be guarded with build tags” essentially means “Go is not an option for any program that needs to be portable and may at some point in the future require the syscalls package”. And since this tool is meant to be portable, and calls what would be syscall.Setresuid, I’m essentially told by the proverb to choose between proper system capability checking, or Go. And Go loses.

It’s a shame that in my opinion Go chose to go with the known bad solution when it’s known to be bad. It feels to me like “there’s no obvious amazing solution so let’s not try to solve it at all”.

In other words I’ve not seen a good Go solution to the problems autotools solves laid out in this blog post.

I’m planning to add a web UI though. Likely some of that will be in Go.

Ugh, except there might be a problem with that plan, because I need to pass creds over a socket, and Go only has support for that in Linux:

$ grep Getsockopt.*red ~/go/src/golang.org/x/sys/unix/*.go
[…]/syscall_linux.go:func GetsockoptUcred(fd, level, opt int) (*Ucred, error) {

So how should Go make this better?

I don’t know what would be best for Go, here. I know what really works well is the autoconf capability way. And what really works poorly is “if OpenBSD do this, if FreeBSD do that, …”.

Since I’m not interested in if FreeBSD has pledge() or not (in fact since what version of FreeBSD, if so?). Maybe one day Linux will get support. How long do I wait before I drop support for older Linux setups?

I’ve not looked much into how to write these portable packages, but how do you write them? Do you assume that Linux has getrandom(), and other OSs don’t? Can the Go way have build tags about “OpenBSD 5.9 or newer”? What if an API gets deprecated or removed? What if Solaris switches to POSIX getpwnam_r()?

I don’t want to know which OSs have and not have openpty(). I want to support OSs that have openpty(), and ones that don’t.

The Go way maybe works well if both of these hold:

  1. You can mandate that users install updates. Anything older than X you say you just plain don’t support. This can work great in corporate environments with managed machines.

  2. You only support architectures that you personally will actually run on. Again this will work well in corporate environments.

It’s not like select() is not portable, even though it’s in section 2 of the unix manual (i.e. a syscall). I’ve been able to maintain opensource working on many OSs including Mac, without ever having owned a Mac, for decades.

And I have confidence (from experience) that the changes I make will actually work on all architectures. With OS-name based checking I have to actually try to compile on all those and confirm correct behaviour.

I no longer have access to an IRIX machine, but I bet you arping still works just fine on it.

To say that even POSIX is platform-specific and revert to “is OS” instead of “has feature” is to throw the baby out with the bathwater, and give up on portability.

Most differences can be avoided. E.g. the value of the timeout after calling select(). And the ones that can’t be avoided that way you can almost always just check at build time which behaviour you have.

I also don’t know how to call “pledge()” with Go’s model. Call a general “drop some of the privileges”? Call a “pledge_if_you_can()” and check return values? (and differentiating at runtime between “failed” and “not available on this system”?)

But going back to what would be most useful for me for Go:

Not having written such implementation stuff it’s hard to criticise. And I’ve not done so because “it’s not going to be portable anyway” because of the rest of Go. And I’ve been assuming that I won’t be able to to do “HAVE_x” anyway.

Maybe if build tags were things like “HAVE_PLEDGE”, instead of “is OpenBSD”. Combined with causing all transitive dependent packages to fail compile if they use Is <OS name here> as a build tag. And of course some equivalent of ./configure that finds all the HAVE_x stuff.

In other words: I’ve not written these portable packages because it doesn’t look possible to write them to be truly portable in Go, because of these “is” not “have”.

For “is” vs “have” the one exception, I’d say, is Windows. Sometimes just everything is so different that it makes sense to have two copies of code achieving the same goal, because almost every line would be either the Windows way, or the not-Windows way.