Errors are bubbled up manually. Loops often use array indices, instead of expressing the desired operations. You often need to use pointers instead of values just to avoid the cost of constantly copying structs.
And every time you encounter one of these things, you get to spend some time thinking about why this or that was chosen,or if you're missing some detail.
Java is much more readable, because it has far fewer of these concerns. Every time you see a catch block, you know it is there for a reason. If someone is making a copy of an object, you know they wanted to make sure the original isn't changed. With Streams, if I want to write an operation which filters a list, I can use filter(), not 'create a new array, go through the original, every time you find an element matching the condition, write it at some index in the new array, increment that index'.
Sure, most experienced people dislike exceptions, except for Bjarne Stroustrup, Herb Sutter, everyone else who designs the 3 popular C++ compilers, everyone who designs some of the the most popular C++ libraries (Boost, newer Qt), Java, .NET, Swift, JavaScript, OCaml, Python, and a few others.
And if you think that
int j;
for i := range someArray {
if hasSomeProperty(someArray[i]) {
filtered[j] = someArray[i]
j++
}
}
is as readable as filtered = someArray.Filter(x => hasSomeProperty(x))
then yes, we have to agree to disagree.You don't need to use indexes like that in Go. You would append to the new array.
The thing is that in practice such loops usually goes beyond just filtering on a boolean basis, so you combine whatever you want to do with that array in a single full iteration.
You're right about the indexes, I should have used append() there.
Related to loops though, the more you need to do in a single loop, the less clear the code becomes in the traditional loop style. Note that stream-style constructs don't iterate more than once either (not that doing M things per iteration vs doing M iterations is necessarily a clear performance win, depending on the size of the array etc).
For example, I would say that the readability difference is even more pronounced between these 2:
for i := range players {
isLocal := false
for _,localPlayer := range localPlayerIds {
if localPlayer.firstName == players[i].firstName
&& localPlayer.lastName == players[i].lastName {
isLocal = true
break
}
}
if !isLocal {
break
}
allLocalMoney += players[i].money
numLocalPlayers++
}
avg = allLocalMoney / numLocalPlayers
vs (C#-ish) avg = allPlayers.Where(p ->
localPlayerIds.Any(lp ->
lp.firstName == p.firstName
&& lp.lastName == p.lastName))
.Average(p -> p.money)
Add a little bit of grouping and things will get even worse for the first example. And sure, you could extract the checking into a separate function, but I wanted to come up with something that takes a few operations.On the other hand, I fully agree that sometimes you just need to do various actions (i.e. anything with side-effects) for each element in a list, and there there is nothing better for readability than plain loops. I just like the option of more explicit transformations when that's what I'm doing.
Debugging is not difficult at all, you can put breakpoints inside the lambda and use continue instead of sigle step. If required, you can also step through the library code, but that is not usually necessary.
And as the code grows, the high-level representation of what the code is meant to do stays clear, while the loop-based version grows in incidental details that you have to take a step back to understand.
Note that I've written commercial software in this style in a team with a about 1-200 other programmers. I am not coming at this from some hobby, 3-5 person project experience. It's just that different industries and different areas value different aspects of code. In my case, this is the middleware portion of a traffic generation solution that can simulate all L2-7 commonly used networking protocols, at the scale of a small city and beyond. Of course, the actual traffic generation code is extremely performance sensitive and is written in C and C++ (and quite a bit of Verilog) with a very different style, probably closer to what you are familiar with[0]. But there are many layers of configuration above that where we usually value clarity and correctness more than raw performance, and where we can afford to use these types of constructs, and our experience has always been that they vastly improve cooperation, not at all hinder it like you seem to imply.
[0] though we do have a real-time traffic stats analyzer library that is written in template-heavy, boost-heavy C++, and is on the critical performance path with soft real-time constraints, so this is also possible.
The examples you give doesn't seem to be specific to Go? In Go the error checks are, as most point quickly out, very explicit and often just err != nil checks. Not sure how they can be confusing, albeit repetitive.
I'm a bit surprised by the amount of down votes my reply has received given that this is a pretty common sentiment among very experienced devs within the game dev industry.