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.
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.
for-loop variable capture was maybe the #1 worst decision in the language. It was never what you wanted. I appreciate Go's commitment to backwards-compatibility, but in this case breaking it was the right choice.
for i, p := 0, (*int)(nil); p == nil; fmt.Println(p == &i) {
p = &i
}
Be honest, how many times have you actually seen code which depended on the address of a variable declared in a 3-clause for-loop remaining stable across loop iterations? Nobody does this. It's extremely weird and un-idiomaic. Heck, 3-clause for-loops are somewhat uncommon in and of themselves. Conversely, everyone who writes Go has experienced unintuitive capture issues with for-range loops.
Are you actually aware of any breakages this change has caused?
this is a repeating pattern with languages; there's no commitment to design coherency, no governance, just a mad scamper to stuff features into the project until it becomes an absurd caricature of itself.
C++ did it so egregiously & disastrously you'd think language maintainers would have been scared straight. No, like moths to a flame, it is the preferred hill to die on.
This is how C99 keeps winning when it bleeping should not. It's settled science, however imperfect. It's not getting rearranged because someone read a blog post. It has stability in real-world clock-on-the-wall terms like nothing else.
The change made in Go 1.22 for 3-clause-for loops is not a new feature. It simply broke backward compatibility and old Go principles. It is much worse than C++'s stuff features.
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.
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.
Not to disagree with the author as I feel their passion for language semantics is probably valuable “somewhere”.
However, as an engineer I find Go very easy and usable despite these nits.
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.
[1]: https://go101.org/blog/2024-03-01-for-loop-semantic-changes-...
for-loop variable capture was maybe the #1 worst decision in the language. It was never what you wanted. I appreciate Go's commitment to backwards-compatibility, but in this case breaking it was the right choice.
It is only right for for-each loops.
For 3-clause-for loops, if you have read https://go101.org/blog/2024-03-01-for-loop-semantic-changes-... carefully, it is hard to think it is right.
All the examples in that article are very exotic.
Be honest, how many times have you actually seen code which depended on the address of a variable declared in a 3-clause for-loop remaining stable across loop iterations? Nobody does this. It's extremely weird and un-idiomaic. Heck, 3-clause for-loops are somewhat uncommon in and of themselves. Conversely, everyone who writes Go has experienced unintuitive capture issues with for-range loops.Are you actually aware of any breakages this change has caused?
this is a repeating pattern with languages; there's no commitment to design coherency, no governance, just a mad scamper to stuff features into the project until it becomes an absurd caricature of itself.
C++ did it so egregiously & disastrously you'd think language maintainers would have been scared straight. No, like moths to a flame, it is the preferred hill to die on.
This is how C99 keeps winning when it bleeping should not. It's settled science, however imperfect. It's not getting rearranged because someone read a blog post. It has stability in real-world clock-on-the-wall terms like nothing else.
The change made in Go 1.22 for 3-clause-for loops is not a new feature. It simply broke backward compatibility and old Go principles. It is much worse than C++'s stuff features.
What is your language of choice now then?
zig now.
The author should try Odin.
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.
damn this is old. how relevant is this today?
All of it.
From my reading, almost all of it.
[dead]