Huh? Having defer statements and error returns vs. dealing with exceptions gives Go the win all by itself.
And then there is the business of being able to make a type automagically satisfy an interface. (Java may have fixed some of the agony of creating classes just to satisfy a required interface. Last I used Java, it didn't have delegates and whatnot.)
Go's defer is verbose and unwieldy compared to Python's `with` statement or Rust's lifetime system. In Python, you don't have to manually write any code to close the file, you just do:
with open('foo') as f:
...
And the file will be closed after that block of code finishes executing. Similarly in Rust if you do "let file = File::open("foo")?;", when the variable drops the file will be closed.
If you want to loop over 10k files, both of those handle that just fine, because the active file will be closed at the end of each loop. Go's defer will try to wait until after the loop to close everything. You can make things even more verbose to fix that, but it gets ugly real quick.
I wish defer was block-scoped rather than function-scoped; that would solve the "defer in a loop" problem. Right now I just wrap the lot in a function for that, which works and isn't too bad, but meh.
defer is more flexibly and explicit though; I rather like that. It's pretty unclear what exactly that "with" does, what you can and can't use inside "with", you can't "just" run any arbitrary code without creating your own class, and you sometimes end up with 3 or more levels of nested "with"s that could have been one defer.
In short, I don't think there's a clear winner. Both are clunky in some scenarios where the other is easier.
Go doesn't really warn you if you forget to handle an error return, which in my experience continuing silently in the face of error conditions is far scarier than crashing loudly.
Check out ErrCheck. It's a static analysis tool that detects exactly this. We added it to our common Makefile that we use for testing/building Go projects. Typically I have it test for this among other things before I commit, and then it runs again in CI scripts before a merge to main is allowed.
In my experience, if you're not checking errors, then you're often just going to crash loudly. Likely at a similar point that the comparable Python would have had its runtime error. In my experience, if Go would have continued silently, the comparable Python might also just continue silently anyway.
The problem with error checking is that Go doesn't cover every case BY FAR.
EVERY memory allocation can fail. And I mean EVERY.
var x := 5 // where's the error handling?
EVERY kernel call can fail. Even this is still not a 100% correct way to call fmt.Printf("Hello, World!"):
s := "Hello, World!")
writtenSoFar := 0
while writtenSoFar < len(s) {
bytesWritten, err := fmt.Print(s[writtenSoFar:]) // and even this is a recent syntax addition.
if errno, ok := err.(syscall.Errno); ret == -1 && ok {
// error signaled
if errno == C.EAGAIN {
time.Sleep()
continue
} else {
return err
}
writtenSoFar += bytesWritten
}
If you don't do this, you will find, for example, that writing large amounts of data to a network socket suddenly only sends half the output to the other side. Plus anything could set O_NONBLOCK on stdout, which would require this. And time.Sleep() is required in some cases where the program redirects os.Stdout to itself.
Even this does not take have proper reactions if an OOM occurs somewhere. So it is still not correct.
It's like C. Simple Go looks correct and just chugs along, destroying data instead of crashing. This makes people feel programs run correctly ... but they don't.
In practice, how often does your rant on memory safety really apply though? Because I currently feel that you're inflating the argument substantially to make it seem like a much bigger, much more common problem, than it is. For reference, in 5-6 years of Go programming, memory allocation has been a problem for me exactly one time, and it was because I was a noob and tried to push about 60GB of data into a variable at once on a virtual machine with 32GB of memory available to it. And it wasn't a silent error, the systemd service I wrote for the application was crashing each time it attempted to load up that mega-variable until I rewrote it in a sane way.
Well that's the problem with correctness. I've seen this fail, in 20 years (that I noticed) about 20x. Let's assume I caught it 1% of the times I actually saw it so ... about once a week. More on slower networks.
This issue, not checking the number of bytes written, usually combined with incorrect EAGAIN handling, is a pretty pervasive problem in network programming. You will find the closer you get to 100% cpu usage, the more common this problem becomes, just like threading bugs. It's one of the ways a service goes from handling 5 Gbit at 90% cpu usage, then handling 5 kbps at 95% cpu usage (because everything suddenly errors out, then retries eat all the bandwidth). It's impossible to find if you don't know what you're looking for.
It's not this issue specifically: Golang programs, like C programs, are strongly incentivized to just keep going with incorrect data when other languages would crash.
CDs have the information to perfectly reproduce frequencies up to the limit of human hearing. But to do that perfect reproduction, you need to sum up the sinc functions for all the samples. CD players do not do this.
As the article points out, the sum of sinc functions constitutes an extremely non local spline. And analog filtering of the stair step function coming off the A/D converter is not equivalent to summing the sinc functions.
I am absolutely not getting into arguments with "audiophiles", I come here to get away from those. You like LPs? Go buy some. They sound so much better if you use solid gold connectors on your 1/4″-thick speaker cables too.
And then there is the business of being able to make a type automagically satisfy an interface. (Java may have fixed some of the agony of creating classes just to satisfy a required interface. Last I used Java, it didn't have delegates and whatnot.)