Maybe I'm just too dumb for Go, but this is not consistent with my experience at all. Go's insistence on pretending that complexity doesn't exist would get in my way all the time. Go's extreme hostility toward FFI calls got in my way several times.
Maybe I'm just too dumb for Go, but this is not consistent with my experience at all. Go's insistence on pretending that complexity doesn't exist would get in my way all the time. Go's extreme hostility toward FFI calls got in my way several times.
Your complex programs aren't easier in Go, in my experience. The simplicity of the language doesn't help me much when my day is spent fighting to figure out how to make a problem which is inherently complex easy to maintain, reason about, and be without bugs.
I want a language that makes my day simpler. Where at the end of the day, my "net complexity" is less. Go leaves all the complexity to you and offers you very few tools to solve this. Bugs, spread out logic, and even runtime costs of the overuse of Interface{} (prior to Generics at least) left me with a lot of things to solve myself. My days in aggregate were more complex with Go.
Just my experiences.
Generics aren't going to improve this situation - at least not the current iteration of generics :(
All languages w/ obligate GC are "hostile" to FFI in some way or another. The Go default implementation also uses split stacks or something for its goroutines, that cannot feasibly interop with FFI code. But it's usually easy enough to just isolate Go code to it's own process/address space and use IPC or network communication to enable the interop one would usually achieve via FFI.
Lots of garbage-collected languages make FFI not only easy but plenty fast. Go does neither.
As with many things, there's nothing stopping you from just sticking with pure Go if you don't like C toolchains. While C build issues are a valid theoretical concern, in practice I've never had any Python package fail to install because of a C dependency problem that wasn't trivially resolved, nor any Rust project fail to compile because of a C dependency problem at all.
I wouldn't know. I've never run into an issue where Go's performance was a real bottleneck, and anyway every mainstream language with easy FFI still has significant FFI overhead (so much so that many programs actually run slower with FFI). This isn't really true for Rust (Rust makes it easy to define types which are essentially C structs and thus require little/no marshaling), but performance also isn't the reason you FFI out of Rust.
> As with many things, there's nothing stopping you from just sticking with pure Go if you don't like C toolchains.
Right, that's my point. You viably can stick with pure Go because such a large share of the Go ecosystem is pure because FFI is rarely worth the hassle.
> While C build issues are a valid theoretical concern, in practice I've never had any Python package fail to install because of a C dependency problem that wasn't trivially resolved
Try building a significant Python project on anything except a recent version of RHEL, Debian, MacOS, or Windows. For example, try getting your Python project running on something like a scratch Docker container. Or try packaging a Python package (which depends even transitively on a C library, especially one which isn't already packaged for Nix) with Nix.
This isn't true for almost any language to the extent it's true for Go, and for many compiled languages it isn't really true at all.
> Try building a significant Python project on anything except a recent version of RHEL, Debian, MacOS, or Windows. For example, try getting your Python project running on something like a scratch Docker container. Or try packaging a Python package (which depends even transitively on a C library, especially one which isn't already packaged for Nix) with Nix.
While these are legitimate theoretical problems, none of them are really problems in practice. Containers don't need to be scratch, and if you're building a Python project, you're already not running a scratch container, so the addition of FFI doesn't change that. Nix is not an environment I've ever seen a requirement to support, let alone had a requirement.
It really seems to me that you like Go and you like Go's design decisions, but "I like this" is not the same as "this is better than that". I'm not particularly interested in rehashing the same conversation over and over again.
Go's object structure is much closer to C's. In many cases, it's just casting pointers (e.g., you can convert a Go slice to a C array by taking a pointer to its first element and casting it to a pointer of the C element type, provided the element types are binary-compatible). This means fewer allocations than a language like Java or Python where you would have to allocate a new slice and chase pointers around the heap for every field in every element in the array. In my experience, most of Go's overhead comes from function call bookkeeping at the FFI boundary rather than marshaling data.
> for many compiled languages it isn't really true at all.
I'm pretty sure it's still true for many compiled languages (e.g., Java, Haskell, etc).
> While these are legitimate theoretical problems, none of them are really problems in practice.
They were problems for me in practice.
> Containers don't need to be scratch, and if you're building a Python project, you're already not running a scratch container, so the addition of FFI doesn't change that.
Containers don't need to be scratch, but they often need to be lightweight with low cold-start latencies (including pulling). Not having to pull in a whole distro is advantageous. And a pure-Python project certainly could run in a scratch container (i.e., a container with just the interpreter, the program, and the program's transitive dependencies).
> Nix is not an environment I've ever seen a requirement to support, let alone had a requirement.
"Reproducible builds" is the requirement. Our customers' security teams vetted our dependencies individually, and if a dependency changed we would have to have that dependency re-vetted.
> It really seems to me that you like Go and you like Go's design decisions, but "I like this" is not the same as "this is better than that".
Clearly, no one here is conflating those things. I'm arguing that Go has a particular strength, not that that strength is the only factor.
That trophy is owned entirely by the Java ecosystem. Thanks to that, once Loom arrives, basically the whole ecosystem will automagically become reactive-aware.
In practice, whether by accident or design, the Go ecosystem is really, really nice because it avoids FFI to a high degree. An overwhelming majority of programs can be cross compiled into a truly static binary (it may not even depend on libc unless--as is the case with Windows and MacOS--the host platform requires it). It also means that there are very few "C-shaped libraries", by which I mean thin bindings around some C library which exposes idiomatic C semantics rather than idiomatic Go semantics. Moreover, your programs aren't running a bunch of inherently unsafe code under the hood, and are consequently more likely to be secure as a result.
It's kind of nice that C FFI is possible such that libraries which are unlikely to be ported to Go (e.g., ffmpeg) or which cannot be ported to Go (e.g., opengl) are still available, but not so easy that people pull in C libraries for every little convenience.
If you have C dependencies, then it does become a pain, depending on how well those dependencies' -sys packages interface with whatever build system they use.
In Go, it's just `CGO_ENABLED=0 GOARCH=armv7 GOOS=linux go build` for pure Go programs.
Lots of people run stuff in CI, which isn't exactly that, but is close enough to make it not as big of a pain as it might otherwise be.
It can also help if their docs aren't great; I've looked at CI configs to realize how to install some sort of system dependency before.
> but also the process for cross compiling is (or at least was) pretty complicated
Most of this article is talking about installing and setting up both Docker and a C cross-compiled toolchain. So, you're right, but also not, sorta kinda. That is, this is certainly more hard than Go, but we're not talking about pure Rust at this point, so the fair comparison would be cgo with some C dependencies, which would also involve setting up a C cross-build toolchain, (and maybe docker). But at the same time, it doesn't have to be this way: Zig includes a full C cross toolchain in its compiler, so that you don't have to do this installation. It is, in my opinion, currently best-in-class here, far surpassing both Go and Rust.
It is also worth nothing that, IIRC, Go had to switch to dynamically linking libc on many platforms, since the idea of a "fully statically linked binary" is basically only coherent on Linux.
CI has a whole lot of variation. On the extreme end, there are people running Jenkins jobs on the same hosts as other jobs, and everyone just pre-installs whatever they need onto the base image for the host (i.e., not even working with a fresh OS image). Moreover, many people are just going to run their CI on amd64 Debian or RHEL and assume it works for all targets.
> That is, this is certainly more hard than Go, but we're not talking about pure Rust at this point, so the fair comparison would be cgo with some C dependencies
My whole thesis here is that Go leans less on FFI than other ecosystems, so you shouldn't need CGo in most cases where you would have to use FFI in other languages. It's a lot easier to get a pure-Go dependency tree than it is a pure-Rust dependency tree. Of course, that's an emergent property derived from weaknesses of Go's FFI, but it ends up being a really nice property in practice.
> But at the same time, it doesn't have to be this way: Zig includes a full C cross toolchain in its compiler, so that you don't have to do this installation. It is, in my opinion, currently best-in-class here, far surpassing both Go and Rust.
I think this is true if you assume that all ecosystems lean on C equally, but it's better by far to depend on C less because including the C cross toolchain doesn't absolve you from humans packaging C dependencies (in which case it's either easy because you neglect a bunch of packages or you test in a hermetic environment a la Nix and it becomes more bothersome than maintaining a pure-$hostLang version of the same package).
Rust has a strong concept of "tiered platforms", and so lots of people support at least Mac/Windows/Linux. Nobody uses Jenkins (for open source packages that will become your dependencies, at least), they use GitHub Actions or CircleCI, which make it easy to support many platforms. I personally run Windows, no WSL, and 99.99% of the time, everything Just Works for me.
But yes, that's why I wasn't saying it's purely just as good. For sure. But it does generally work well.
> My whole thesis here is that Go leans less on FFI than other ecosystems,
Gotcha, that's fair. Pure-x for any x often is really, really nice! Full agreement there.
Yeah, and it's really interesting how the relative ease/difficulty of FFI shapes an ecosystem. On one extreme, you have Go which has a lot of purity, but on the other you have Python where FFI is so easy that the maintainers can't really change anything including optimizations without breaking compatibility which means pure Python packages are relatively slow driving more reliance on FFI. It also means the package management tooling has to solve for the universe of C packages to be worthwhile, which drives a whole bunch of other problems. A decade ago, if you had asked me whether easy FFI was a good thing or a bad thing, I would have unequivocally said "a good thing". That might've been the correct answer if the lingua franca had a standard concept of reproducible builds.
This has not been true since Go 1.2, back in late 2013.
Probably the most accurate and concise summary of my problems with go also.
The article mentions the whole filename thing as an example, and that's one of the first places where I felt I was at home with Rust. It's not unnecessarily complicated but it does force me to acknowledge that yeah, the name of a file might be incoherent nonsense. It's probably a String, but it might not be. I can write code that says "I don't care, we're probably fine" and accept that if it's not fine the code will fail at runtime in a defined way - or I can write code that actually cares about this problem, even if just to explicitly ignore such files as if they didn't exist.
In too many languages the second isn't really an option (which is frustrating if I'd like to write reliable software) or worse, the first isn't an option and so I'm stuck writing endless boilerplate even for a toy or one-shot.
The latter is arguably OK if your language is really just for space rockets and medical implants where failure is not an option. But that's never really how things work out.
Rust is plenty interactive with its similar mode of working. Incremental builds are fast.
If you do have a really long running superoptimizer discovering things, then you'd want a way to write that back into the code so you don't need to discover it again.
Also, most of your program should be at -Os because it's not hot code and the important thing is to stop it from disturbing the fast parts. (Or because the aggressive optimizations actually make it slower. Totally possible with fancy ones like autovectorization.)