Go’s hidden pragmas(dave.cheney.net) |
Go’s hidden pragmas(dave.cheney.net) |
Actually, I wish Rust had done one thing that Go did: namespacing the pragmas. That would have been a more future-proof thing to do, because macros effectively give us user-definable pragmas, and once you have user-definable pragmas you have name collision problems. Kudos to the Go team for being forward-thinking there. I suspect we'll have to figure out some kind of solution here, but it won't be as pleasant as if we had just used namespaces in the first place.
Yeah, of all the papercuts that the Rust 1.0 macros system had the idiosyncratic modularization/namespacing rules were the most unfortunate. Happily there's already an accepted RFC for the design of macro modularization (https://github.com/rust-lang/rfcs/blob/master/text/1561-macr...) that simply makes all macros operate under the same namespacing rules as all other items, and it looks to be mostly implemented as well (though it won't hit stable Rust until the larger macros 2.0 initiative is finished). And as for future-proofing, I'm not too concerned: all the "standard" macros can be exported from the stdlib prelude without a problem, and any libs that update to macros 2.0 can easily export the updated macros just like any other public item and consumers can update their code with only a single `use` declaration (it's not like the old macros system doesn't require explicit importing anyway, it's just unique and janky). Very much looking forward to the simplicity and consistency of the new system.
So in the future when macros are namespaced just like every other item (which is actually already designed and largely implemented, see my other comments here for links), all that needs happen is to export the newly-transitioned macros from the prelude and all will continue to work as usual. As for potential collisions with third-party macros, the old system already requires anyone who wants to import macros to stick a hacky "macro_use" pragma on their import statement, and old-style macros are specified to shadow rather than collide so there will be no need to be cautious with updating the stdlib. Third-party libs will be free to update to "macros 2.0" at their leisure (though the need to have users explicitly import macros will require those libraries to issue a breaking change when they do so), and old-style macros will be supported for quite a while though eventually they will be deprecated and discouraged (and presumably removed in some future epoch).
It's time that language designers include language pragmatics in the core language. That includes for example big O information about data structures, packing of structures, alignment properties, memory access information, etc. Currently, in most if not all languages this information is spread all over levels, from nonstandardized compiler flags over pragmas up to the core language. It's a huge mess.
> It's time that language designers include language pragmatics in the core language. That includes for example big O information about data structures, packing of structures, alignment properties, memory access information, etc.
So pragmas are "always a bad idea" but you should have them "in the core language"… Don't you feel your comment is pretty contradictory?
A pragma is a directive for the system (mostly compiler), that's orthogonal to it being implementation-specific.
It's true that if pragmas were all fully specified in the core language and not optional, then they wouldn't pose any problems. In reality, however, some pragmas are regulated by the core language and others are implementation specific additions. The result is a huge mess, it's the #1 source of incompatibility of standardized languages like Ada. Even just having optional pragmas in the core language is problematic, because at one point or another developers will start relying on the optional functionality to do something that one implementation does and another doesn't. Optional optimization and packing directives are typical examples. In theory they shouldn't be able to break programs, in reality they do.
They bring back my painful memories of the old days when I had to use conditional comments to support IE6...
Honestly, between documentation, compiler pragmas, linter directives, packaging and linking instructions, and etc, we are getting into a point where languages will need to specify something like "comments starting with this string must be ignored".
which lanugages do that?
Which are pretty useful when you want to target tests to different versions of Go when std lib exhibits different behaviour (behaviour changed as std lib matured).
// +build ignore
near the top of it.However, the design philosophy of Go pulls in the exact opposite direction! Go emphasizes simplicity and large-scale engineering -- i.e. standardization. (gofmt is the canonical example of this.) Giving programmers the power to manipulate the compiler in arbitrary ways would be a nightmare for Go's designers. It opens the door to "clever" code that you tear your hair out debugging 6 months later. The strength of Go is precisely that it makes it difficult to write "clever" code. Go looks pretty much the same everywhere. Which is a little boring, sure; but for me (and many others), that's an acceptable trade-off.
1 - https://nim-lang.org/ 2 - https://nim-lang.org/docs/manual.html#macros-macros-as-pragm...
Performance, maybe?
Performance for running race detector (debug) binaries? Are you worried slower code hides a race?
I can't think of a reason to ship or use race detector compiled binaries in production. Or do you have something in mind?
Edit: Granted, this is not a directive for the compiler though.
Simplicity implies a small set of features; it's a property of design.
Facility implies ease of use; it's a property of operation.
Go unambiguously favors simplicity, at the expense of facility. This is why we're given raw CSP primitives (sharp edges included, e.g.: goroutine leaks) instead of a full-fledged actor model.
Much of go’s “simplicity” is a Faustian bargain that comes at the cost of unnecessary complexity in each and every project that winds up being written with it.
Smalltalk all over again!
Programming languages don't get complex just for fun, their designers are tackling actual relevant issues.
Go community doesn't seem to have learned much from the past.
> Programming languages don't get complex just for fun, their designers are tackling actual relevant issues.
Yes and yes.
> Go community doesn't seem to have learned much from the past.
Well... if you start with something simple, and complexity comes, you can still try to keep it as simple as possible. But if you started with something that was already complex (but complex in ways that your theory said it needed to be, not in the ways that the real world said it needed to be), and you try to fix that, you wind up with something really complicated. Ditto if you start off with complexity to handle all the use cases of the past.
Go started off simple, and is letting real-world use push them into becoming more complicated. That's a defensible approach, even today.
And yet, to this day, C is just as, if not more popular, than C++. Why is that? I can do so much more in C++, but I, and my colleagues, pick plain-old C every time.
Have you ever used C++ templates? I mean every popular languages have issues related to design complexity.
Pragmas, incidentally, aren't really a source of bad complexity. Per the abstract definition of the language, they really indeed do have no effect at all and are just comments. Yay!
Implementations have properties too—they aren't just rude practicalities. Compiler's, in particular, connect one language (the input) to another (the output). Programas mediate how those additional properties apply to the language at hand. It's an interesting mental challenge to formalize them in the absence of a compiler having an comcrete stable ABI.
So in conclusion, go people once again don't understand good design. Pragmas are not an ugly wart, but actually a great example of layering—a rare example of an abstraction that doesn't leak!
# could be treated as syntactic sugar for //go: until v2 if they want
Your previous comment literally states that pragmas are always bad, then goes on to state that they should be included in the core language. This one does not retract the original statement that pragmas are "always a bad idea" but further asserts pragmas "must be mandatory and in the core language".
I don't think I misread your comment, no. You may have miswritten your comment when you meant that, say pragmas should not be optional and/or implementation dependent[0], but that's on you.
[0] I've no idea whether that's you mean given again you state that pragmas should both not exist and be part of the core language.
I also argued that the functionality of pragmas must be represented as non-optional choices in the core languages. This is one of the lessons that the Ada community has learned over time - but it's too late to change this now in Ada.
So yes, pragmas are always a bad idea if we talk about the way they are implemented in most languages. The functionality of pragmas should be part of the core language, with proper syntax and semantics.
I hope that clarifies what I've stated and which you haven't understood, and that, your nitpicking aside, my position now makes sense to you. If not, there is not much I can do, I just wanted to point out a lesson learned in the Ada community, which is very stringent about language definitions. If you're interested in these kind of topics, I can recommend Michael Scott's Programming Language Pragmatics.
Ant was a DSL that managed to become turing complete and the results were pretty horrible.
They do actually. Though when people do say that it tends to be phrased "keeping business logic in stored procedures is a bad idea".
People argue that all the damn time.
Accidental turing completeness usually signals a design flaw somewhere (would you also consider it too controversial to argue that C++ templates mentioned in your link are nasty and people complain about them a lot?).
The question is: For debug builds that explicitly have the "-race" flag passed to the compiler, why would you want to disable race detection for a specific function?
We use -race during our automated testing. The setup for some of our tests involves CPU mining (rapid blake2b hashing). This code definitely doesn't have any races, and it runs waaaaay slower when race detection is enabled. So we could speed up our tests significantly by disabling race detection just for the setup phase.
Besides C++ templates are a result of creating a conceptually simple, one size fits all solution for generics, metaprograming, library tuning, and some dozens of other problems that other languages have specialized tools to solve. Turns out that the complex set of features works better.
My first use of C++ templates was in Turbo C++ 3.5 for Windows 3.1.
Not if you are dishonest about it. Not if you refuse to learn from the past couple decades.
I don't mean they are outright lying, or are bad people or anything like that.
A fallout from this is that you can have one actor supervise another, to protect your program from errors: https://hexdocs.pm/elixir/Supervisor.html
The Golang runtime provides only these actions: (spawn). It’s up to the author to create communication channels, and it’s impossible to query for the status of a Goroutine. This means you have no explicit control over goroutines, so you can’t kill misbehaving ones like you could in another language.
In a pure actor model raw channels wouldn't be directly available and I would be required to push to and read from a concrete goroutine.
Ultimately anything that changes how the program is executed is going to add complexity, so they might as well "make it official" and add a keyword for it.
The problems that C has with pragmas, and that C++ imported from pragmas, can not be naively imputed to other languages without demonstrating there's actually a problem here. This wouldn't even make my top 10 issues with Go; I'm not sure it's even an issue at all.
No, actually. The syntax that people use to invoke the pragma ("use strict [arg]...") is not a pragma at the language level, it's just the syntax for importing symbols from modules. For example,
use strict ('vars', 'refs');
expands to BEGIN { require 'strict'; strict->import('vars', 'refs'); }
because that's how the "use" statement is defined. `BEGIN{...}` cause the statements in the block to be executed as soon as the BEGIN block has been fully parsed [1]. `require 'strict'` loads the module `strict.pm` from the library path (the source code is on CPAN at [2], if you're interested), then its `import()` method is called with two string arguments. The implementation of that method is: sub import {
shift;
$^H |= @_ ? &bits : all_bits | all_explicit_bits;
}
There's a lot of weird Perl syntax in there, but the gist is that it modifies the $^H variable. And THAT is the actual pragma which is defined by the language. [3] The module strict.pm is just a wrapper around $^H to make things a bit more user-friendly.I know that's sorta kinda off-topic, but since we're talking about language design, I figured I'd contribute this small anecdote that illustrates really well how the more recent parts of Perl are designed: a ton of metaprogramming on top of relatively small changes to the core language. If you want another example, have a look at how object-oriented programming was tacked on to Perl as a tiny afterthought, yet the way it interacts with all the other parts of the language makes hugely powerful OOP frameworks like Moose possible. (OTOH, that approach also makes the language pretty messy, but it always gets the job done for me, at many scales.)
[1] Usually, execution only begins when the entire file has been parsed, but this code needs to run earlier because it changes the parser's behavior.
[2] https://metacpan.org/source/SHAY/perl-5.26.1/lib/strict.pm
[3] Notably, $^H behaves differently from other variables: Every assignment to it is scoped only to the current block, whereas regular variables need to be shadowed explicitly. This is particularly useful to temporarily lift a strictness requirement for a single statement, similar to how `unsafe` is used in Rust:
use strict;
...
my $function_name = 'implementation_' . ($x + 2 * $y);
$function_name(); //error: cannot call string value
{
no strict 'refs'; //"no" is like "use", but in reverse (calls the module's unimport() instead of import())
$function_name(); //works: calls the function with the name stored in the variable
}Furthermore the $^H (there's also %^H) facility you mention is just one way pragmas are implemented, e.g. "use overload" is a core pragma that doesn't use that method at all, instead it defines special functions in the importing package which the compiler is aware of.
Then there are other "pragmas" that are really just utility wrapper functions, e.g. "use autouse". The "Pragmatic modules" section of "perldoc perlmodlib" has the full list.
This is the same mentality as people who throw up their hands and say government is broken, so we should deprive it of resources to make it as small as possible. Doing this just winds up makes the problem worse, when there’s plenty of evidence that well-funded governments can work well.
It’s also the same broken mentality behind schemaless databases. Schemas are hard, so let’s get rid of them. This backfires because you haven’t actually rid yourself of schemas, they’re just implicit and now you lack any tools to operate on them meaningfully.
“Hard problems are hard, so let’s just avoid dealing with them” is not a sustainable solution in the long term. Sometimes they’re really hard and ignoring them makes it worse. Sometimes they’re only hard because we haven’t thought about them in the right context. And sometimes hard problems can be sidestepped entirely with a bit of cleverness. But outright ignoring them and hoping they go away just punts the hard problems to others.
C was already on the way out when Linus created Linux.
Apple was migrating from Object Pascal to C++.
IBM had CSet++ for OS/2.
Borland, Microsoft, Zortech, Symatec were selling C++ frameworks.
UNIX vendors were playing with Taligent and CORBA.
BeOS and Symbian were developed in C++.
Then came Linus, made Linux with GNU on top.
GNU project for a long time always mentioned that the go to language for GNU projects should be C.
All major C compilers are written in C++ nowadays, there is hardly any reason to stay with C outside UNIX world.
That seems a little... fanciful. There was a lot of C++ and it was a great way to show how modern and forward-looking you were (and to sell compilers, tools, frameworks) but standardization hadn't got far, interoperability was poor, problems great and small abounded. A number of the things you mention above were spectacularly unsuccessful.
Nevertheless, all major desktop OSes were going C++ for their application frameworks, before the widespread adoption of GNU software.
> A number of the things you mention above were spectacularly unsuccessful.
Mostly due to politics between corporations and very little to do with C++ itself.
That's true, but not really fair: even ANSI C standard was only 2 years old at that time, despite C being way older than C++. Standardization takes a lot of time …
It uses some C++ features, but it's philosophically much closer to C code.
Anecdotally, 99% of the complaints I read WRT Go can be solved by using libraries that already exist. If one wants a batteries-included-in-the-stdlib form of facility, one should look at languages like Python.
Go's "simplicity" of error handling (read: lack of any actual error handling abstractions) means you don't get useful things like stack traces and have to manually grep through code for nested error messages. It also makes go code difficult to read at a glance, since virtually every statement winds up wrapped in repetitive error-handling code that doubles or even triples the amount of code in the happy path.
The error-handling pattern of using tuples, but no syntactical ability to operate on data within a tuple means you almost never have the ability to chain function calls like `a.b().c().d()`. Instead you have to manually unwrap the value and error, return if there's an error, call the next function, manually unwrap the value and error, ad nauseam. The "idiom" of gift-wrapping error messages is absurd — you are replacing machine-based exception handlers with expensive, slow, error-prone, and less-capable meat-based exception handlers.
Having a half-baked type system means you end up having to frequently write type-switches which are checked at runtime to do any sort of generic code. There's no functionality in the language to ensure that all possible options for that type switch are exhausted, so you are virtually guaranteed to get runtime bugs when a new type gets written and later is passed in.
Speaking of type switches, they interact poorly with go's indefensible decision to have interfaces implemented implicitly rather than explicitly. I have seen types get matched to the wrong typeswitch in producion code because a new method implemented on one type caused it to accidentally "implement" an interface used elsewhere in a typeswitch. Good luck ever catching this before it hits you in production.
Go's concurrency primitives are useful, but the lack of ability to abstract over them means that you have "advanced go concurrency patterns" dozens of lines long and involving multiple synchronization primitives for what amounts to `a | b | c` (https://gist.github.com/kachayev/21e7fe149bc5ae0bd878). God help you if you want to implement something like parallel map. God help you if you want to implement something like parallel map for n > 1 types.
Go requires you to manually remember to release resources you've acquired with `defer`, instead of sanely having There is no capacity in the language to enforce that you've done so, and it is virtually impossible to find e.g., a missing `defer fd.Close()` in a large code base. God help you if you leak file descriptors and need to track down the source.
Go's inability to perform any meaningful abstractions also means that you have to know all the details of code you import. It's difficult to make code a black box. Case in point: to do something as painfully simple as reading a file, you need to import bufio, io, io/util, and os.
During the course of writing this post, I forgot more examples than I listed — I literally could not remember them all in my head as I was writing them down. This isn't simplicity, this is utter madness.
This baffles me. I think I basically never use any type-switches, with the exception of interfaces being used as a sum-type - in which case the problems you mention with type-switches just don't come up.
> Case in point: to do something as painfully simple as reading a file, you need to import bufio, io, io/util, and os.
I don't know what you mean here. `ioutil.ReadFile` reads the whole file, done. Even if you prefer linewise-scanning, you still only need `bufio` and `os`.
But even if you'd need all those packages to read a file: So what? Like, I honestly don't understand what's the problem with that.
Here's how to read a whole file then loop over the lines:
file, err := ioutil.ReadFile("data.txt")
// some error handling
for _, line := range strings.Split(file, "\n") {
fmt.Println(line)
}
Here's how to stream a file one line at a time: file, err := os.Open("data.txt")
// some error handling
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
// more error handling
}
Here's how I, at least, would like it to work: fileContents := file.Read("data.txt")
for _, line := range fileContents {
fmt.Println(line)
}
if fileContents.Err() {
// some error handling
}
fileContents := file.ReadStreaming("data.txt")
for _, line := range fileContents {
fmt.Println(line)
}
if fileContents.Err() {
// some error handling
}
The critical point is that I don't want to care whether `file` is a byte slice or a byte buffer, and Go doesn't let me not care. I want to be able to write code that deals with "enumerable data of some sort", once, and then works no matter how the caller decides to provide that data.Go (intentionally) makes it exceedingly difficult to obscure how a piece of code works from the rest of the codebase, and I personally think that's a fatally poor design decision. In my experience, it makes it difficult to decouple modules since code often has to be at least somewhat aware of quite a few implementation details of a library in order to use it correctly. It makes it really difficult to build higher level abstractions that don't leak. I have a much harder time in Go getting away from thinking in `int`s and `floats` and staying terms of the domain objects that I actually do care about.
"How" a piece of code functions is at best the third, and probably only the fourth most important question (behind "why", "what", and probably "when" if you use any concurrency at all), but Go forces it to be front and center at all times.
/snark
But seriously, for a long time before Rust was fully baked, I kept wishing it would be done, so that people hating on Go could go use Rust instead. Now it's fully baked, which is awesome.
When I first started go, I was incredibly excited to start learning it. From everything I'd heard, it was everything I was hoping to find in a language. The more I started using it, the more and more its poor design decisions and the hollow defenses of these decisions by its community started to grate on me.
Rust, on the other hand, I was loathing learning. I'd already just learned go and was extremely disappointed with it. I really didn't want to learn something else in this language space that I assumed overlapped so much, and I really entered into it hesitatingly. But I am exceedingly glad I did — unlike with go, every day I used Rust I came to appreciate its design more and more. Features of the language seem to have been designed to coordinate and work with one-another, instead of all being bolted on separately without regard to how they'd interact.
On top of that, Rust has some of the most friendly, dedicated, and talented developer community I've ever seen.
It's notable that the vast majority of the above are complaints by experienced developers working on large projects that need to be maintained and expanded for years. They're not the sort of thing you notice over a weekend of tinkering on something fun, and they're not the sort of issues that beginners will run into under any normal circumstances.
All of that adds up to a honeypot for newcomers. The language has some deep issues, but they're not the kind of things you're likely to notice until you already have 200k lines written and it's too late to switch to something with a steeper learning curve. Saving two weeks of confusion when you're learning ends up causing a lifetime of headaches down the road once you're an expert.
In other words, Go is PHP for systems programmers.
I once had hopes for Go. But the team working on it decided not to fix any of its flaws that became obvious over time and even outright denied their existence. So, not much Go for me anymore, but I'm still looking forward to see what such approach can bring in Go 2.
I write Go at work. I haven't counted in a while, but I guess the code of my team comprises significantly more than 200k lines (and that doesn't even count all the code I'm working on that isn't from my immediate team).
IMO, the more LOC you have, the more the design of Go becomes a godsend. Because it becomes much easier to dive into code you haven't written yourself.
But anyway, that's just my opinion, I just wish that people would stop this "anyone who disagrees with me just doesn't have the correct set of experiences" FUD.
Obviously any set of abstractions is going to wind up making tradeoffs, and choices that make one particular set of problems easy can cause negative consequences in other areas. Just the more that I used go, the more I grew to question the logic behind these design decisions. Perhaps they make sense at Google, but to me they don't seem to make sense outside of its internally-controlled confines.
I'm more than fine with people preferring Rust. I'm fine with Go's choices rubbing people the wrong way. But the frequent caricature of the Go Team being condescending or clueless simply rings false to anyone who has enjoyed being part of the Go community, where kindness and ability abound.
I'm always curious about the frequent ravings about how friendly the Rust community is. I'm super glad, and I love seeing (hearing) the reports from the Increasing Rust's Reach program on the New Rustacean podcast, for example. I'm glad Rust is so welcoming and friendly.
But I guess for this (increasingly) greybeard, it's funny, because the whole thing seems so cyclical: the Ruby community back in the PragProg-induced-popularity days, and the Perl community back in the pre-Perl6 days had a similarly lovely feel (minus the more recent but oh-so-welcome proactive diversity bent).
Anyways, enough ramblings. I'm glad Rust fits so well for you. I keep meaning to learn it properly by porting an Apple II emulator or something, but… time :-)
I still don't get what your problem is. It seems what you want is to just take an io.Reader and pass that to bufio.NewScanner, solving your problem and letting your caller figure out what Reader to pass you? I mean, to me, this seems to be a solved problem and exactly one of Go's major strengths.
> it makes it difficult to decouple modules since code often has to be at least somewhat aware of quite a few implementation details of a library in order to use it correctly.
You still haven't described a single piece of your code that requires, in any way to know any implementation details of any of the libraries you are using. Like, you don't have to care how os.File is implemented, it just gives you a Read method that you can use to read from it, just like a thousand other Readers. And then you can use that in a bufio.Scanner to read lines (words, whatever tokens), without that having to care in any way about how the Read method is implemented. You want to scan lines from a byte-slice, use bytes.Reader, that's it's sole purpose and your scanning code does not have to care what Reader it gets passed.
Like, I seriously don't understand your problem here. It would seem to me, what you are describing is exactly how Go works.
> "How" a piece of code functions is at best the third, and probably only the fourth most important question (behind "why", "what", and probably "when" if you use any concurrency at all), but Go forces it to be front and center at all times.
Sure, I agree that Go does not encourage you to build deep abstractions. But I fundamentally disagree that you have to know any implementation details - anymore than any other language. Yeah, the type system doesn't lend itself to build extra abstractions, but "having to care about implementation details" just is not one of the symptoms of that o.O
These are literally implementation details. Except you're having to implement them yourself. Reading a file delimited by tokens is a solved problem. There is zero reason why I should be having string together code from four different modules to accomplish this myself. This is the entire reason we have come up with the concept of abstraction.
This is plainly wrong. All components I mentioned exist in the stdlib.
You have to glue them together yourself, sure, but that's the point of having components with separated concerns, which is usually considered a good thing in software engineering.
> There is zero reason why I should be having string together code from four different modules to accomplish this myself.
You don't. You have to use at most 2. And also, to repeat the question: who cares? Like, what is the actual downside of having to import 2 packages? I also took the liberty of looking for solutions to how to do this in other languages. Here is a Java solution, which is in line with what's requested that has 4 imports and one of them is third-party: https://stackoverflow.com/a/1096859. Here's rust code with 4 imports: https://users.rust-lang.org/t/read-a-file-line-by-line/1585. Python and Haskell get away without imports; because they just make reading files a language-builtin/part of the prelude, which TBQH is pretty cheaty.
Like, even if I'd buy into the notion that modularity and composability are bad things, it's not even as if Go would be in any way an outlier here. And even if it where then at best this is the mild complaint that no one has yet wrapped this in a ~10-line library; the language certainly does allow it, contrary to what's claimed.
I'm sorry, but this complaint is just forcibly trying to make up a problem where none exist to fit your narrative of Go being a bad language. It's not productive.
I guess you could chalk it up to a difference of aesthetic opinion. I don't want to think about buffers or readables. I want to think about loops and strings. Looping over a collection means `for range`, so I'm gonna assume that Just Works. Maybe `for range` is just syntactic sugar for `bufio.Scanner` under the hood, but I don't want to care while I'm using it.
I want to think of a file as a black box full of strings. What's actually in the box? Don't care. How do I get lines out of the box? `for _, str := range blackBox`, same way I loop over every collection. How does that actually work? Don't care. Whoever implemented the box has to care, of course, but I sure shouldn't. I've got more important things to worry about, like whatever it is I actually want the code to do. Every character that isn't about whatever it is I actually want the code to do is a problem.
Having primitives and builtins that only work sometimes (specifically, with a short list of builtin types and aliases for same) means I can't just use the builtins without thinking about what I'm using them on. Having to crush down to a lowest common denominator means that what's in my brain while I'm reading and writing code isn't strings and what I actually aim to do with them, it's how the Reader API works, and whether I read a full or a partial line, and whether I need to handle errors before or in or after the loop this time. I want to think about my problem domain, but Go keeps dragging me down into the weeds.
Sure, fair enough. But note that you've now shifted the criticism from "I have to import 4 packages" (which was wrong) over "I need to know implementation details of packages" (which was wrong) to "I don't like that Go doesn't have operator overloading".
> I want to think of a file as a black box full of strings.
And what exactly is preventing you from doing that? Like, how exactly is the language preventing anyone from providing this much higher level API? You could even make it work with range, if you so desire (it would be considered very unidiomatic, but presumably you don't care).
The stdlib provides you with composable pieces to achieve the job you want. I still find this complaint incredibly weird, unless you assume that everyone wants to view files as just a bunch of lines (I'd argue, these days, the overwhelming majority of files probably aren't). Like, you will still need the lower-level API; where's the problem with having an stdlib which focuses on providing composable pieces and then having some library do the composition for higher-level concerns?
Last time I checked, having small composable units of code with clearly separated concerns was pretty much universally considered a good thing.
> it's how the Reader API works, and whether I read a full or a partial line
This is just a random aside, but: You never have to care about that, unless you specifically want to. But I would argue that code that calls io.Reader.Read is likely wrong - unless it does so to wrap it. Use io.ReadFull.