3 ms·
I’ve read this article and debated it to the end of the planet. It’s not that the article is factually wrong, but it seems to be under the impression that there
by jchw 5y ago
I’ve read this article and debated it to the end of the planet. It’s not that the article is factually wrong, but it seems to be under the impression that there is an objective definition for “correct code.”
Of course, there’s degrees. To take an example, filenames. What is a filename? Is it a bag of bytes, a set of UNICODE codepoints, something more elaborate? No operating system agrees 100%. Go handles this by just simply not. It converts stuff to and from UTF-8 and if it doesn’t convert, you can’t use it. This is a limitation of Go programs that use Go’s built-in filesystem support (roughly all of them.)
That decision is a clear-cut simplification of reality. It can matter. However, the fact that it comes up so seldom is a reflection of reality: normally, you are totally able to make the concession that you only handle paths that are valid UTF-8 and nothing else. Go makes this concession on your behalf. It makes many such concessions, and it is documented, though not very well-known, because most developers don’t care. What most developers want is a language that makes sensible tradeoffs so that their programs can remain simple but still reasonable. In TYOOL 2022, I absolutely think it is reasonable that a program may impose valid UNICODE filenames as a requirement.
Rust handles it with OsStr, of course. Now OsStr is a good idea, but it pushes the decision for how to handle the problem further down. On one hand, this is great if you absolutely must handle ridiculous filenames that have essentially arbitrary bytes or old encodings in them, which, to be sure, is a real thing.
The file permissions stuff is similar. If Go is going to present a file permissions interface and then just fudge it, what’s the point of including it at all? Well, in my estimation, the point is that on POSIX systems, you can’t really avoid dealing with file permission bits. In most cases, when a Go program specifies mode bits, its doing so so that on POSIX, the program doesn’t write a file with permissions that don’t make sense, are unusable, or potentially even open up security issues. (You would not want user-uploaded data to go into a file with +x!) On Windows, it can usually be ignored, at least from the scope of the Go software. That’s, again, an opinionated choice. If I needed more granular control over permission, I would probably need more OS-specific code and interfaces anyways, something that is totally doable in Go.
So far Go is oversimplifying things in a very opinionated manner. And in fact, this means that some programs are difficult to write correctly in Go.
But, and here’s the kicker, I don’t usually want to write code that breaks the assumptions that Go makes and requires in its standard library. Even if I’m in Rust, if I want to write some utility code that deals with filenames, often times the string transformations I want to perform on the filename I will want to do in the space of valid UNICODE, because it’s simple and predictable. Even if all I want to do is append a suffix before the file extension, I still would prefer to work in valid UNICODE only. If I’m dealing with say, a broken Shift-JIS filename and want to append a suffix, even if I try to do the right thing and treat it as an ASCII compatible bag of bytes, I could wind up with an even more broken filename as a result, because the state of the filename before the suffix could corrupt the suffix.
The key difference is in perspective. “100% correct” is demonstrably unattainable. You can make more exact abstractions, but for sanity we make assumptions left and right that are “good enough.” You really shouldn’t use the user home directory or user profile directory of a given OS for much directly because it’s honestly a different directory with very different semantics per OS, yet plenty of Rust software (and Go software!) still does this anyways.
Meanwhile, while writing perfectly watertight code in Go is basically impossible, it’s also not a fair benchmark. Nothing does that. Rust is especially prone to stack overflows due to its lack of placement new, and it’s absolutely trivial to do this. Go doesn’t really have stack overflows at all, because it has a dynamically expanding stack. Cargo and its ecosystem has a problem with dependency hell; 200 nested dependencies to install a tool that counts lines of source code is a real thing that actually exists. Go’s ecosystem is definitely in better shape here, for reasons mentioned in the article that I wholeheartedly agree with. There are more things I could say that I don't like about Rust. I could possibly even fill an article about it, but I don't think that it's very productive. I think it's better to just acknowledge that most of these pain points are a direct result of the fact that any decision you make about language design and tooling will have knock-on effects down the ecosystem.
This article is also unfair in that it basically pins Go, a language which intentionally limits scope by taking these concessions, with Rust, a language that happily expands scope to try to cover more edge cases. Both choices are valid and offer different tradeoffs. However, there is a world of different programming languages, and Go is not the first one that implies limitations on filenames, for example. Hell, what happens if you write plain C code that tries to access broken Windows filenames? How much Win32 C code exists that doesn’t handle UNICODE or broken filenames? How many times have you tried to compile some code and had to move it to a directory without spaces in the filename because the program didn’t handle that?
Go is an opinionated language. If you don’t like its opinions, you won’t be happy with it. Orienting this as a correctness issue makes it seem like the Go developers made these tradeoffs haphazardly and without thought. I just flatly disagree.