> Sure, the former are language features while the latter are library features but that doesn't seem to be a meaningful difference when it comes to comprehension.

Absolutely. The difference is that Go has a limited number of such features and once you have learnt them that's all you need to know, in that sense, and can understand any code base.

One thing which I have not expressed very well in my reply is what exactly I meant by "understanding what the code does". When you look at func fetchData(T1) (T2, error), it's easy to understand what it does: it fetches some data from T1 and returns it as T2, with the possibility of it failing, and returning an error. If you know what T1 and T2 are (which you should if you're inspecting that code), that's usually sufficient. You understand (almost) all of it's observable behavior, which is different from it's implementation details. Similarly, `abs` returns the absolute value of a number

`append` also has easy to understand observable behavior: it appends the elements starting at position len(slice) and reallocating it if necessary (generally, if the capacity is not big enough), but it's actual implementation is undoubtedly very complex. `range` is harder to explain, but rather intuitive when you get the hang of it.

Of course you also want to keep in mind the behavior of all the language primitives as well: operators, control flow etc. In Go, you have to keep all of these things in your head to understand what is happening in the code, but once you do you really understand it.

We can call all of these things: variables, language primitives, API functions etc. atoms of behavior. In Go, to understand a piece of code, you first have to understand what the observable behavior (but usually not the implementation) of all of the atoms in that code are, and then understand all of the interactions between those atoms that happen as a result of programmer instructions.

What I mean by line-level vs package-level abstraction is quite simple (maybe not the best names, but hey, I'll stick with them). With package-level abstraction, the atoms, as well as the interactions between them, remain conceptually easy to understand, but become more powerful as you move up the import tree. The observable behavior of an HTTPS GET is easy to understand, but very complex under the hood.

With line-level abstractions the atoms, and especially the interactions between them, become very complex. The programmer no longer "has to understand" the observable behavior of every single function he uses. Odd one-off mutators are preferred to inlining the mutation because it "makes the code more expressive" - in that it makes it look more like english, it makes it easier to understand what the programmer is trying to do. It does not, however, make it easier to understand what the programmer is actually doing, because the number of atoms - and their complexity - increases substantially. If you want to get a feel for this look at the explanation for any complex feature in C++ on cppreference.com. I must have read the page on rvalue references 20 times by now and I still don't grok it.

Of course, with line-level abstraction, the programmer doesn't need to constantly keep in mind 100% of the behavior of the atoms he's using, much less whoever's reading.

I can't tell you which one's better - probably both have their place - all I'm saying is that I, personally, can't work with C++/Rust/other languages in that style. I've tried to use them but I can't. C is easier to use - for me.