| I'm just going to comment on one point of your comment: > to be easily reviewable/checkable by coding teams. I strongly disagree with this. I find Go code to be incredibly difficult to read. This is because of two main reasons. The first is that the lack of expressive power in the language means that many simple algorithms get inlined into the code instead of using an abstraction with a simple and understandable name. I find that the first pass of the code is me reading over the lines (each of which is very simple and understandable) and virtually abstracting it into what the code actually does. I find that the interesting business logic is lost in all of the boilerplate. out := []int{}
for _, v := range input {
number, err := fetchData(v)
if err != nil {
return nil, err
}
if math.Abs(float64(number)) <= 2 {
continue
}
out = append(out, v)
}
return out
vs input.iter()
.map(|&v| fetch(v))
.filter_ok(|number| number.abs() > 2)
.collect()
As I said, every line in the Go is simple (except maybe for append, but generics can help with that). However the actual business logic is lost in the boilerplate. In the second example (Rust with one existing and available helper function) the boilerplate is much less and each line basically expresses a point in the business logic. There are really two bits of boilerplate here `filter_ok` instead of `filter` to handle the errors from `fetch` and the `collect` to turn the iterator into a collection (although maybe you could improve the code by returning an iterator instead of a collection and simplify this function in the process).Secondly the "defaults are useful" idea is in my opinion the worst mistake the language made. They repeated The Billion Dollar Mistake from C. I have seen multiple expensive production issues as a result of it and it makes code review much harder because you need to check that something wasn't uninitialized or nil. It is absolutely amazing in Rust that I don't have to worry about this for most types (depends on your exact coding style, in the above example there is never a variable that isn't "complete"). So while Go may be quick to write. I think the understandably is deceiving. Yes, I can understand every line, but understanding the program/patch as a whole becomes much more difficult because of the lack of abstraction. Humans can only hold so much in our head, making abstraction a critical tool for understandable code. So while too much of the medicine can be worse that the disease I think Go aimed - and hit - far, far below the ideal abstraction level. |
I think this is the fundamental point of contention. Go aims at abstraction at the package level. Each package exports a set of types and functions which are "magic" to outsiders and can be used by outsiders - an API, if you will. Rust seems to aim at abstraction at the line level - each line is an abstract "magic" representation of what it is meant to do.
In your Go code, the only pieces of line-level magic are `range` and arguably `append` (even though I would argue it is integral to the concept of slices). And of course the API magic of `fetchData` and `abs` which is in both versions.
On the other hand, Rust has .iter(), .map(), .filter_ok() and .collect(). So, while I think anyone could understand the Go code if you explained `range` to them, I do not understand the Rust code. Yes, I understand what it does, but I have no clue how it does it. What is the type of .iter()? Why can I map over it? Why can I filter a map?
But that's not the point of the Rust code. The Rust code expresses what should be done, not how it is to be done.
The way Rust deals with complexity is by offering tools which push that complexity into the type system, so you do not have to keep everything in your head. The way Go deals with complexity is by eliminating it and, when that is not possible, by making sure it does not cross API boundaries. In Go you do keep everything in your head.
That's a Go feature.