Go 1.21 will (likely) have a static toolchain on Linux
utcc.utoronto.ca
utcc.utoronto.ca
Last time i ran my go test suite on the CI i thought it had a bug because it was so fast compared to what i'm used to with swift.
I do still have a weird feeling of getting back to the stone age whenever i get a nil pointer panic though. i wish go devs could figure out a way of fixing those last quirks without impacting the rest too much.
In most cases static dispatch is faster. In many cases dynamic dispatch can be just as fast (speculation by CPU negates the cost) and in some cases it can be faster (better code density). This 0-cost abstraction principle from c++ is actively harmful because there’s almost no such thing. Performance is such a subtle thing that what’s 0-cost in one scenario is a non 0 cost in others. Also spending compile time on the “dark matter” of code that’s rarely executed is probably not the best way to spend the developer time budget. I want CI to generate fully optimal code for production in release builds. For running tests in presubmit, I probably want a little bit less optimization. For local development I want it to be just fast enough that compile times are quick and I can iterate quickly (except for the cases I’m tuning performance in which case I have no choice but to spend max compilation time).
Surely a better approach is to use incremental compilation, and only do this expensive stuff potentially only once?
However I do agree with your overall point - dev compilation in rust is too slow.
Nothing prevents Rust of doing the same, other than resources.
-Emily
-Emily
Did you mean 'linker' here?
This is pretty different from swift for example. Which had a sane basis very early on, but decided to expand the language by adding features over features, moving the language to a lot of different directions, turning it into an ugly beast ( i still like it though, but i’m not sure for how long).
https://blog.polybdenum.com/2023/03/05/fixing-the-next-10-00...
...Nah. We shouldn't go back (and I am not saying you claim so, I am kind of just developing the thought here). Let's have memory safety and algebraic data types and maybe pluggable GC runtimes (if lifetime management proves too difficult which it has at times; not every task is perfectly suited for Rust's borrow checker) and async, and any other goodies that help us solve real problems, and let's not dream of a simpler life.
That "simpler life" has failed. We should acknowledge that and move on and work with the reality in front of us.
Rust's lifetime management and async runtime(s) can be maddeningly difficult but after being on all parts of the spectrum -- from bash scripts and JS Wild West projects to Rust -- I confidently claim that the more strictness the better.
...Though I wouldn't refuse Rust having compile times like those of Golang and OCaml, can't deny it.
> At this point in history it should be more or less apparent that "here's a gun you can shoot your foot with, just don't do it lol" is not a viable strategy.
However in the case of Haskell, manually mucking around with mutable aliases, references and pointers is culturally similar to using unsafe in Rust. By default in Haskell you are using immutable variables. And if you have multiple threads and want to share mutable state, you typically use software transactional memory.
> ...Though I wouldn't refuse Rust having compile times like those of Golang and OCaml, can't deny it.
Well, Golang has quick compile times, partially because it makes the human do half of the compiler's job. OCaml is indeed something that's more worth aspiring, too.
As a compromise, I found that `cargo check` is much quicker than a build and does most of what I need when developing: most of the time, I don't actually care about the resulting binary, I just want the compiler to tell me quickly whether I introduced any errors it can detect.
I love Rust and I hate having to emulate enums in Golang but the speed of development becomes more and more a deal-breaker the more time I spend with Rust.
I might just settle at OCaml, if at all possible. Or, since this is the real life and there are no simple final solutions, I'll likely just become a master of all three.
But really, as much as I appreciate Rust for being super strict, I also don't think strict lifetime management is as important for many tasks (though it's a life-saver for some).
That said, I’m starting to do a bit more systems development so I’m excited to dabble a bit more in Rust.
Which gc don't help at all..
Then over time you can use only 3rd party packages that are fully safe (same as has happened naturally with rust but just "not unsafe").
> It’s important to understand that unsafe doesn’t turn off the borrow checker or disable any other of Rust’s safety checks: if you use a reference in unsafe code, it will still be checked.
Going from the other side would be really hard to design for. The whole standard library in rust is relying on the lifetimes being there. If you started without them you'd end up with things which can't be implemented safely anymore. Even with the lifetimes available, some traits took ages to stabilise and ensure no edge cases. You'd have two standard libraries and the compiler would have to know how to make them interact. Or maybe they couldn't interact and you'd have to remember some hashes have get/set you started with and others need `.entry(...).and_modify(...)`. That sounds terrible for usability.
it is indeed quite easy to fix most of the time, but it's also a non-issue in a lot of modern PL.
Most go types have useful default value. Nil slice is a 0 length slice. Default byte.Buffer is an empty buffer. Default of struct is all fields default. That’s all great.
Except for map. Which panics. Oops!
People can write Java if they prefer. No programming language is going to please everyone.
By the way, not having ADTs is not a fixed decision either.
You seem to mistake the fact that the Go team is in no rush to add things to the language as a general rejection of these things.
- Awkward transition period between a stdlib with and without generics: [1]
- Completely different APIs for built-in data structures (slices, maps) and generic ones
- Lack of obvious follow-up features that would have been there at 1.0 if generics were added, e.g. iterators
We really shouldn’t cater to the average user, as they honestly don’t know what’s good for them.
If anything, I wish people would have main.go be a bit longer so I can see the main bones of the application but people always like to a := app(conf) ; a.run()
Also generics is mostly stuff that make libraries more convenient, not average user code. It also reduces bugs where otherwise interface{} and type checking would be used.
By them acting as if they never wanting Generics, not having Generics from day zero, delaying their implementation for a decade with BS excuses, pretending they are some kind of unsurmountable problem....
They were literally pressureed into getting them in, after years of resistance, when they recognized the mess they've made
https://youtu.be/RIvL2ONhFBI?t=1018 (starts here)
https://youtu.be/RIvL2ONhFBI?t=1892 (he expresses his opinion here)
We haven't made our little heads out of nowhere.
That was the official excuse (while each and every proposal coming in was shot down, just to get.a sub par, half-thought, Generics implementation, full of sui generis and NIH details implementation.
It's not rocket science, there are 100s of languages with Generics, including languages with many orders of magnitude more than the adoption Go has.
It's strange to describe the current implementation has "half thought". A lot of work was done to make sure it was correct: https://arxiv.org/pdf/2005.11710.pdf It's probably one of the most carefully thought through generics implementations in a mainstream programming language.
>It's not rocket science, there are 100s of languages with Generics, including languages with many orders of magnitude more than the adoption Go has.
It's easy to add generics but not so easy to get it right (see e.g. Java's soundness issues, the total mess of C++ templates). Rust's generics also have some dark corners (e.g. https://github.com/rust-lang/rust/issues/84857).
Also, to make algebraic data types useful, you really want parametric polymorphism. But yet again, the others of Go weren't familiar with this. The only vaguely related technique they knew about were C++ templates, and they (reasonably!) decided that they didn't want C++ template hell in their language.
That last part about templates is the least speculative of the bunch: I read some of the discussion they had about generics, and they explicitly mentioned templates (and how complicated they are) and pretty much mentioned nothing else for how to design or implement generics.
Go recently got some generics, partially thanks to some help from Phil Wadler who's otherwise more known for his work in functional programming.
https://kranfix.medium.com/pattern-matching-in-golang-195c73...
I don't know enough about Golang: do you know whether it's possible to add pattern matching with destructuring as a fairly shallow syntactic sugar?
Generics were a much bigger change to the underlying language (and so would be Rust-like lifetimes, or even immutability); but pattern matching seems like something that should be relatively easy to add with only local repercussions?
I only found that previous kind of pattern matching useful occasionally. But I guess I would miss it a lot, if it was gone?
It seems to me - although I could be entirely wrong - that the mindset that produces this kind of idea ("nil pointers are teh devil!") is incapable of producing the kind of experience that Go produces with its toolchain (trivial cross-platform compilation, etc).
Again I can't prove it, but it just seems that way.
For instance, if you believe nil pointers are the devil and you should statically enforce checking them at compile time, it seems that it would only be natural that you would also think the following other things are very important to be enforced at compile time:
- Read-only references
- Move semantics
- And all the related baggage
What does this have to do with compile times and cross compilation? Maybe not much, but time and attention are limited resources, and when you focus on concepts typical related with "nil pointers are evil" you take away precious resources that could otherwise have been dedicated to building the kind of developer experience that Go provides.
- Goroutines
- Garbage collections
I'm not saying these are good or desireable per se.
The point is a bit more general: you can't just accumulate features without this accumulation having other effects on the language and its toolchain and causing degradations in other ways.
Unfortunately fixing the "billion dollar mistake" as a retrofit is pretty much impossible without breaking backward compatibility. I see what they were trying for with nil and zero values, but IMO they should have introduced a "result" and "option" special type (or probably more Go-like would be separating nullable and non-nullable types via '?' like Dart did). If they did it like Dart, they could intro the concept without breaking backwards compatibility, wait X years until mostly adopted everywhere, and THEN break backwards compatibility by making it mandatory.
They did the opposite when they designed the language, with their 'multiple return values', which are like tuples but only allowed in one special position in the language. But as you already suggested, tuples are the exact opposite of the "result" type you'd want here.
Similarly, Go has tuples, but only for the language designers who get to use them for multiple return values from a function. No tuples to be used elsewhere where users of the language might find them useful.
Now, Go also has sum types. But, yet again, only for the language designers: they get to stick `nil` as one of the constructors (to use Haskell terminology) in their favourite data types, but you as a language user don't get to make new sum types.
Another example is operator overloading: the designers get to overload eg arithmetic operators, but you don't get to do that.
In contrast C++ even for all its other faults at least makes an attempt to hand you many of the tools the language designers get. (Though, I like Rust's and Haskell's approach here more.)
“Nil” does not a sum type make; it’s just the zero value for a reference type and the type-checker doesn’t check that you are handling nil and non-nil cases as a type-checker would do for sum types (if Go’s pointers are sum types, then integers and floats in any language are sum types as well).
Go has never had operator overloading for the language designers or anyone else. The operators that the language designers use behave the same way as when users use them.
I have a lot of respect for Rust, and I was a C++ developer in a past life, but most of the software I write these days cares a lot more about developer velocity than about eeking out every last bit of performance or correctness, and most of the time Go is the ideal candidate for those sorts of tradeoffs.
While intimately aware of the phenomenon in a technical context and something I constantly check myself on, I’ve never heard it described quite like that. ‘distracted by the flow of circumstances.’ So relevant to many parts of one’s life, it’s a nice turn of phrase, yet also complete BS.
I learned a lot from this article and discussion here!
https://utcc.utoronto.ca/~cks/space/blog/programming/GoWhyNo...
Interesting from a project I currently work on, we use a static compiled binary and I thought just the DNS resolver was a thing which needs to be switched and use the internal DNS resolver of the Go sdk.
But as we noted, once you have something like PAM plugins and using ldap sssd to get users from your LDAP, the static Go binary cannot resolve the user IDs.
So there is more to it and we have to decide whether we implement a user lookup natively in the Go binary or rely on libc and how the actual Linux system is configured to login users.
Also I have not tested this hypothesis, but I assume that with all the troubles I have seen with a Linux vpn client pushing their resolvers into the Linux client and how systemd resolver and other kinds of resolvers mess up your DNS, I wonder if the Go internal DNS resolver implementation picks up on this mess.
Anyways for the Go Compiler here this might not be such a big problem.
What we really need is something similar for doing users and groups when you get down to it: which we should have because at the end of the day we're really just asking the system tell us some UIDs and GIDs, or what names to assign to such things.
The DNS situation you just implement the protocol and you're done.
I'm not that familiar with plan9 (which is where I think most of the namespacing concepts came from), does it allow direct syscalls, or is linux the only system where you can syscall directly?
IIRC they couldn't even do it natively on MacOS due to system restrictions, unsure if that's still the case.
A different change [2] had a similar effect of using the system facilities for certificate validation in 1.18.
In general, using the custom stuff “works” (until it doesn’t!) but results in janky programs that don’t behave with respect to split tunnel DNS and so forth - very common with cross compiled programs until recently.
There is no "native" way to do this unless you consider libc (or libsystem) "native". There is no kernel interface to do dnd lookups just standard userspace tooling.
Not all OSes have libc per se, only those of UNIX/POSIX origin.
Secondly, on most UNIX environments, libc isn't the ISO C standard library, rather the full OS public API, static linking to it is possible (GNU libc being the exception due to how it is designed), however it limits what the binary would be able to call across OS versions if at all, e.g. the actual implementation semantics change.
Apple does not ship a statically linkable libSystem (I don’t think they ever have), and openbsd is trying to move away from it to tie up origin verification.
Also take into consideration all the other surviving commercial UNIXes, POSIX embedded OSes and POSIX environments for mainframes and micro-computers.
On solaris, freebsd, and so on, the operating system ships the kernel and libc versions together, libc makes syscalls, and everyone else makes C ABI calls to libc.
The libc ABI is in fact the only interface the kernel, on solaris bsd etc, gives you to do something like "open a file" or "make a network call".
On those operating systems, the kernel syscalls may change between versions, so if you statically link libc, or if you make syscalls directly, your program may break when the OS version is upgraded. Every program is meant to use the libc ABI, which they won't break, and then internally the OS may refactor libc+the kernel syscall interface together without worrying about breaking anything.
macOS is similar to this.
Linux is unusual in that the libc and kernel implementation are maintained separately, and both keep their own stability promises, so on linux it is viable to make a pure go implementation which makes syscalls directly. Which is exactly what go has done, and why this static toolchain is possible.
Most programming languages don't bother with the linux syscall ABI because they'll have to use libc for other unixes as well (like solaris etc), so they might as well do it for linux too.
Example of small nuance that can ruin your day/week: the size of syscall(2) arguments is fixed, but for some syscalls the arguments must be smaller. Hence you have to pack and aline the values properly in the registers. This example is from the syscall(2) manpage.
I've recently been trying to fix some of them, but so far getting traction on their mailing list is a bit slow.
AFAIK you can link libc++ against musl.
I have a pretty complex C++ command line tool which works just fine with MUSL and results in a distro-agnostic executable (https://github.com/floooh/sokol-tools). What potential problems should I be aware of?
For C++ we were faced with some issues, so the process we ended up with is:
- build musl, install it in some location
- inject a few GCC libs and linux headers required for C runtime to have the above location be a proper sysroot for clang to use
- build LLVM libc++ and a few libs (e.g libunwind) as static libs against that sysroot using clang, and inject them into the sysroot
- build whatever C++ final product we want against the sysroot using clang, statically linking libc++ in
- for a dynamic lib, remove " "needed" dynamic reference to libc.so in ELF. also, hide all symbols from libc++ and load with bind local so that when loaded the shared lib prefers its internal symbols (which would make it crash if it jumps to another libc++) and does not pollute the symbol namespace with its internal ones (which would make another lib crash if it jumps to the internal libc++)
- for an executable binary instead of a lib, dynamic references may instead need to be altered so that it works for both
It all hinges on musl being a subset of glibc, which is not entirely true either (see the musl website for differences in behaviour, which may or may not matter depending on the piece of software)
See:
https://wiki.musl-libc.org/functional-differences-from-glibc...
https://github.com/DataDog/libddwaf/blob/c6a90d39d93f04ebb5e...
The CL description describes the changes, then:
"Combined, these four changes (along with Go 1.20's removal of installed pkg/*.a files and conversion of macOS net away from cgo) make the output of make.bash fully reproducible, even when cross-compiling: a released macOS toolchain built on Linux or Windows will contain exactly the same bits as a released macOS toolchain built on macOS.
The word "released" in the previous sentence is important. For the build IDs in the binaries to work out the same on both systems, a VERSION file must exist to provide a consistent compiler build ID (instead of using a content hash of the binary)."
A library for calling C functions from Go without Cgo.
Speaking of portability and golang, this sounds promising: https://github.com/tetratelabs/wazero Can anyone tell me if this is close to working as well as wasmtime, which uses CGO?
So it's not just relevant to musl or musl-based distributions (perhaps especially because musl is almost always linked statically anyway, so the distribution doesn't really matter in that case either).
* albeit with a minimum kernel version
I would expect anyone doing sysadmin work on Linux in any sort of centrally managed network would have exposure to it. It is certainly relevant any time you are messing with NIS/LDAP configs and troubleshooting.
Your typical developer, even doing C app development I wouldn't expect to have interacted with nsswitch unless their local name resolution was broken somehow and they were trying to fix their own problems.
The build failure is easy to fix, so I created a repo at [2] which builds a program against a glibc with static nss. I verified with strace that it does indeed check nsswitch.conf and try and load dynamic libraries (I'd at least submit my patch [3] for the build failure but I find mailing lists to be a hassle)
All this said, I wouldn't call it undocumented - it's documented in the `configure --help` itself as well as the online version [4], and it has an FAQ entry [5].
[1] https://sourceware.org/bugzilla/show_bug.cgi?id=27959
[2] https://github.com/aidanhs/gcc-static-linking
[3] https://github.com/aidanhs/gcc-static-linking/blob/1f04425e2...
[4] https://www.gnu.org/software/libc/manual/html_node/Configuri...
[5] https://sourceware.org/glibc/wiki/FAQ#Even_statically_linked...