I'm going to be honest, I always felt like the verbosity was the point of Go. Iirc the whole reason it was invented was to let codebases be as readable as they are writable even for less experienced developers. Why are there Error types when exceptions exist? To force you to acknowledge the possibility for errors. You can easily let your frameworks and libraries handle recovering from panics, but err is unavoidable. You have to, at the very least, put the _.
Which is great until you have to troubleshoot an error code surfaced by a nasty web of code with no idea where it came from because the simplest way to handle err is to re-return it, optionally (and more or less uselessly) wrapping it in a new err. I'll take a panic with a stack trace over that any day.
Go was my favorite language for a long time, and I have written many books and articles about it. However, since the release of Go 1.22 [1], that is no longer the case. Go 1.22 damaged Go's reputation for promoting explicitness and maintaining strong backward compatibility.
Oddly this misses the two big things I hate about Go: Squishy types and no immutability.
1. A nullable SQL string is idiomatically expressed as sql.NullString{String: "my string", Valid: true}. If you forget to initialize `Valid`, it silently sets itself to false and your string is null. I feel like the right solution for this is sum types, but Go says you only need interfaces (and then of course it doesn't use them for stuff like this). And Go doesn't have named arguments, so convention for big functions is to pass a big struct with fields for all the arguments. If you omit a field, it gets zero-intialized, which is nice if you want the default to be zero, but otherwise you can't really have a default value, because there's no way to tell if a given zero was explicit or implicit. And there's no way to ensure that every field gets filled in. I get what they're trying to do with zero values, but it makes the whole type system feel a little bit squishy.
2. I want immutability semantics. Good immutability. Go doesn't even have bad immutability. If you're disciplined, whatever, but people aren't, and I've seen a lot of Go turn into big nasty mutable mudballs.
> otherwise you can't really have a default value, because there's no way to tell if a given zero was explicit or implicit
You can use pointers, or nullable types. These are not ideal, admittedly, but it's not true that "there's no way".
> there's no way to ensure that every field gets filled in
This can also be done with an exhaustive linter. You might think this isn't great either, but then again, always being reminded that you left out some fields is a) annoying, and b) goes against the benefit of default values altogether.
I agree with you on immutability, though.
I also agree with some of the points in the article, and have my own opinions about things I would like Go to do differently. But if we can agree that all programming languages have warts and that language designers must make tradeoffs, I would say that Go manages to make the right tradeoffs to be an excellent choice for some tasks, a good choice for many tasks, and a bad choice for a few tasks. That makes it my favorite language by a wide margin, though that's also a matter of opinion.
11 comments
[ 5.3 ms ] story [ 32.5 ms ] threadHowever, as an engineer I find Go very easy and usable despite these nits.
[1]: https://go101.org/blog/2024-03-01-for-loop-semantic-changes-...
1. A nullable SQL string is idiomatically expressed as sql.NullString{String: "my string", Valid: true}. If you forget to initialize `Valid`, it silently sets itself to false and your string is null. I feel like the right solution for this is sum types, but Go says you only need interfaces (and then of course it doesn't use them for stuff like this). And Go doesn't have named arguments, so convention for big functions is to pass a big struct with fields for all the arguments. If you omit a field, it gets zero-intialized, which is nice if you want the default to be zero, but otherwise you can't really have a default value, because there's no way to tell if a given zero was explicit or implicit. And there's no way to ensure that every field gets filled in. I get what they're trying to do with zero values, but it makes the whole type system feel a little bit squishy.
2. I want immutability semantics. Good immutability. Go doesn't even have bad immutability. If you're disciplined, whatever, but people aren't, and I've seen a lot of Go turn into big nasty mutable mudballs.
You can use pointers, or nullable types. These are not ideal, admittedly, but it's not true that "there's no way".
> there's no way to ensure that every field gets filled in
This can also be done with an exhaustive linter. You might think this isn't great either, but then again, always being reminded that you left out some fields is a) annoying, and b) goes against the benefit of default values altogether.
I agree with you on immutability, though.
I also agree with some of the points in the article, and have my own opinions about things I would like Go to do differently. But if we can agree that all programming languages have warts and that language designers must make tradeoffs, I would say that Go manages to make the right tradeoffs to be an excellent choice for some tasks, a good choice for many tasks, and a bad choice for a few tasks. That makes it my favorite language by a wide margin, though that's also a matter of opinion.