I know this is always a hot take but I actually really like ‘if err != nil’, I didn’t to begin with but now I appreciate and rely on its explicit and obvious simplicity.
It’s a nudge towards the style of ‘exit early’ when a precondition isn’t met, so I apply that philosophy to other code as well. It’s comforting later on down a function body because you’re sure that you’ve already handled ‘bad’ cases, and it discourages nested if statements which in turn makes code more readable.
I haven’t done any Go programming in 6 or 7 years but I agree on the err thing. My general style is to perpetually exit early with the goal of minimizing the number of nested conditionals and, like you say, verifying preconditions. Lots of my C++ functions are written in a similar way: if x, bail; if y, bail; if z, bail; do the actual thing. The “do the actual thing” portion of the code is almost always at the top level of the function (not inside a conditional) because anything that works cause it to fail has always been dealt with and returned early.
I love what it does for my mental model and dislike what it does for aesthetics. Ultimately, that’s a win in my book. I can’t imagine my view is original.
I think the collection of primitives Rust offers strikes a better balance on that aspect, even if they took the same approach as with async/await and you need to either use Box<dyn std::error::Error> or pull in a third-party implementation for the interfaces they defined, like anyhow or thiserror, to make them properly ergonomic. (Though, to be fair, anyhow and thiserror are dtolnay projects and about as actively developed and well-maintained as you can get in a “don’t do change for change’s sake, please” situation.)
…especially a simple, concise ? (or .context("Message")? with anyhow) for throwing the error or None up the stack as long as you’re using Box<dyn ...> or anyhow or an appropriate thiserror#[from] annotation to handle converting error types to match.
I recently took advantage of that. I triggered a function and then didn’t check the error code for another several statements so that I could achieve other work, regardless of the error. Because I had explicit control over the error handling, I didn’t even need to think about how to do it.
To each their own, I respect your decision. I’m going to react as a golang fanboy (you’re welcome in advance), but truly not trying to preach golang gospel here.
Instead of defending go, I instead shall do the public service of savaging Java, the buck-nakedest emperor of all the languages/ecosystems.
If you’re choosing to go BACK to Java, I think that’s the key unspoken context here: you are already familiar with java and investing in its er, unique ecosystem will not be starting from zero. So the tradeoffs for you should be understood more explicitly in this context. And I can see how this decision makes sense, in that context.
People choosing go vs java with little or no experience in either? Very different decision.
Aside: I love go but must of course admit that it does have its weaknesses. ‘if err != nil’ all over the page is indeed absurd. I would argue it is also simultaneously a glorious triumph of the explicit, but that does not solve the absurdity. This is the golang meat once must consume to get their golang pudding. Not all folks will or should accept this deal.
Were I to pretend to be a newcomer to both (I am new to neither), and I had to decide to invest between either learning the wonder and warts of the go ecosystem vs the kafka-esque harrowing of the java ecosystem? Seems like a no brainer to this flesh node. Java makes some sense, assuming you have already - willingly or through necessity- turned off your critical faculties, and chugged the flavoraid.
Yes I do hate Java, deeply. It has earned my ire, even after having wiped the flavoraid from my lips. I understand that some must use it, but I hope to save the souls of those who have not yet invested their precious years into Java.
With Rust, Golang, Node, or even today’s C++ as alternatives, I think human beings can be happier (less miserable?) while delivering software in a commercial context, never having to submit to Java (the programming equivalent of amazon’s leadership principles).
P.S. you mention that go vs java is a different set of tradeoffs depending on the software being made - i must agree Java would just be easier to use for some software usecases. I’d glady write seven go libraries from scratch rather than write one line of Java, but I am a special snowflake.
P.P.S. i know java’s whole situation has gotten much better, but it still has huuuuuuge bloat problems all over the software lifecycle when you choose java. Weighing the annoying golang boilerplate baggage of ‘if err != nil’ vs the Java ‘getterSetter / five chained methods to do one operation / decorators everywhere / gradle / everything ’ boilerplate baggage? Another no brainer decision to this one.
I’m not sure you entirely intended it this way so please take this as the complement I intend it to be, but this is a gloriously-written spiral from rationality into sheer loathing. I need this narrated by someone with a meticulous, ominous voice… not James Earl Jones, perhaps more like Agent Johnson’s “I hate this place” monologue from The Matrix. Truly the flavoraid is most bitter to those who hath chugged it most deeply.
The whole article just seems like a skill issue to me, i write go and rust both professionally and as a hobby and I love Go for all the points mentioned in the article, the error handling is consious, the std library has everything I need (the exception is go-fiber), I like short variable names (names should be proportional to their lifetime), one way of doing a thing makes reading foreign code very easy, and I regularly work on go applications with pprof, gdb and simple print debugging, I can not relate to any of these being disadvantages of go. I could never imagine choosing the language riddled with boilerplate and cursed with worse tooling than Go and criticizing these points as Go issues in the same breath.
Held my curiosity until, “Go’s biggest ORM package, Gorm, is light-years behind Hibernate/Entity Framework in terms of functionality, and full of undocumented weird behaviour. What do Go nerds say? You don’t need ORM in Go! Do It Yourself!”, then lost me. I’ve looked at Gorm once or twice, but never considered using it. I’ve used nhibernate and EF in the past and wouldn’t choose to again. I’ve never used hibernate itself but the few times I looked at the docs it seemed significantly hairier than nhibernate. The one large java codebase I’ve worked with (mercifully) didn’t use an ORM at all and data access and “manual” relational mapping never presented a problem (granted, N=1).
If the Golang’s lack of an ORM is viewed as a legit criticism of Golang and its ecosystem, and we’re using (n)hibernate or EF as the standard of comparison…I have no words.
To be clear, I think ORMs can be good. It’s been forever since I worked with activerecord and I really liked it. However, that’s almost certainly because it was so tightly integrated into rails.
Depending on the usecase, an ORM introduces mental overhead and hidden complexity. If you ever have weird problems that you don’t understand, it feels like using a black box to handle your data.
I prefer understanding what happens and being able to optimise where I think it is necessary.
As for go, I like the concept of simple SQL query builders. They introduce some compile time safety and are still considerably manual.
As for Java, I have abandonded ORMs in favor of EclipseStore.
I agree regarding ORMs - my nhibernate experience really burned me out on the idea in general. EF I don’t recall being much better (at the time, it has likely improved, although I seem to recall something about a vote-of-no-confidence for a release or two around the time I was working with it.)
I haven’t worked with Java for some time now. This is the first I’ve heard of EclipseStore. Definitely looks interesting. Apparently it’s pretty flexible as far as persistence goes, and you’re storing an object-graph so no impedance-mismatch. Is it considered an object-database?
Want a simple HTTP handler? Batteries included, right? Nope, that line is rightfully reserved for Python only. It’s indeed very simple to bootstrap a server or client, but as soon as you require default middleware (exponential backoff, cross-site whatever, etc etc), you have to scrape together multiple packages and hope (1) they’re still actively maintained and (2) they work as you expected them to.
So does Python contain middleware for exponential backoff?
conflicting philosophical statements of the core Go team (who are not consistent themselves)
That is a very weird statement because it links to an explanation about the specific inconsistency?
Except you don’t know that errors are being handled, and the logic isn’t local to the caller.
Nothing I’m aware of in Go requires you to actually check if err != nil, meaning your program will still compile if you don’t do it, and will still run. Just when the error condition finally does happen, you’ll get a mysterious problem at some other location.
Compare this to exceptions or sum types, both of which force you to address the problem before moving on. An uncaught exception doesn’t allow execution to proceed, and in languages with sum types a failure to resolve them before use is generally a compile-time error.
Similarly, the logic is non-local; the near-universal pattern in Go is if err != nil {return nil, err} or variant forms of it that wrap err in some sort of nice reporting structure. That’s not “logic”, that’s just yeeting the error up the call stack in hope it will finally encounter an actual error handler (and if we’re being really cynical, is just try/catch but implemented manually). Figuring out which handler that might be and what it will do can easily be a non-trivial bit of analysis, and is basically never obvious from just reading the surrounding code.
Nothing I’m aware of in Go requires you to actually check if err != nil
I’ve only contributed to one open-source project written in Go, and they had a linter that made sure that err returns were either checked or forwarded to the caller. I think it was a standard go thing, I don’t know why it isn’t a compiler warning.
My biggest problem with Go is that the type system does nothing to help you avoid footguns. In Go, it is undefined behaviour to concurrently modify an object from two goroutines (and, yes, this can break memory safety if you do). Yet Go has primitives that make it trivial to send references to objects to other goroutines. In Erlang, you have the same kind of send (messages to actors rather than messages via channels), but Erlang always passes objects by value (or by reference as an optimisation, but they’re immutable so this isn’t visible in the abstract machine). Concurrent mutation is impossible by construction. In Rust, the Send trait ensures that objects are passed as a move operation and the borrow checker ensures that you don’t hold any pointers to objects dominated by the sent object after you send it. In Go, sending a pointer via a channel is just as easy as sending a by-value copy. Easier, in fact, because copies are shallow so sending an object by value will send any referenced objects by reference and end up with accidental aliasing.
All of this means that there’s a huge cognitive load writing concurrent Go, yet concurrency was meant to be one of the main selling points of Go. The people I’ve spoken to who are happy with Go treat it as a replacement for Python that produces a statically linked binary.
Yeah that limitation of channel sends was annoying, and I had to adopt conventions to reduce the “cognitive load” and avoid errors.
I’d have liked the ability to have some form of “const/immutable pointer” such that a read only reference to an object could be passed around.
As for the lack of move operation, I simply adopted a convention of immediately nil’ing the pointer on the sending side after sending it. Possibly a linter could be written to verify that convention?
Obviously someone subsequently updating the code could fail to be as rigorous, and introduce an issue. I was pondering catching that with my UTs, by building upon the finalisation facility in the runtime, but never got around to doing it.
All languages have their issues, and sharp corners…
Nothing I’m aware of in Go requires you to actually check if err != nil, meaning your program will still compile if you don’t do it, and will still run.
True. What we rely on is a weaker property: unused variables are a compiler error.
So at first this seems almost as good. You do in fact have to do something with err. It gets a little trickier when you realize that err can be reused so you could do 2 a, err = operations and only check the second operation and that would make the compiler happy.
But in a worse is better sort of way Go code frequently won’t compile if you don’t check the err.
You can choose at what level to handle it, and i think it’s pretty strong consensus that exceptions are a bad model over something like returning errors/result types
You can choose at what level to handle it, and i think it’s pretty strong consensus that exceptions are a bad model over something like returning errors/result types
Reiterating for clarity: if you follow all the recommended best practices for error handling in Go, what you will wind up with is a sort of Greenspun’s-Rule-ish ad-hoc implementation, not of Common Lisp, but of a try/catch exception system. Which is to say: you’ll halt execution at the point where err != nil and begin passing some type of error object up the stack, wrapping it with new frame-local information as it bubbles up through each frame of the stack, until it encounters code which actually handles the error and stops it from propagating further.
But you’ll get none of the ergonomics of an actual try/catch exception language because Go will demand that you implement it yourself every single time, and will demand that you remember to do it at every point where err might not be nil, because Go cannot force you to check that.
you can also lint rules like forcing error checks
Compared to error-handling systems which actually make you handle the error, “you can lint for it” is not great. You see how that’s not great, right?
Want a simple HTTP handler? Batteries included, right? Nope, that line is rightfully reserved for Python only. It’s indeed very simple to bootstrap a server or client, but as soon as you require default middleware (exponential backoff, cross-site whatever, etc etc), you have to scrape together multiple packages and hope (1) they’re still actively maintained and (2) they work as you expected them to.
Last I checked, neither Python nor Java have a comprehensive universe of HTTP middleware in their standard libraries, and it’s far easier to get a production web server up and running in Go than in either of the other languages.
I know this is always a hot take but I actually really like ‘if err != nil’, I didn’t to begin with but now I appreciate and rely on its explicit and obvious simplicity.
It’s a nudge towards the style of ‘exit early’ when a precondition isn’t met, so I apply that philosophy to other code as well. It’s comforting later on down a function body because you’re sure that you’ve already handled ‘bad’ cases, and it discourages nested if statements which in turn makes code more readable.
I haven’t done any Go programming in 6 or 7 years but I agree on the err thing. My general style is to perpetually exit early with the goal of minimizing the number of nested conditionals and, like you say, verifying preconditions. Lots of my C++ functions are written in a similar way: if x, bail; if y, bail; if z, bail; do the actual thing. The “do the actual thing” portion of the code is almost always at the top level of the function (not inside a conditional) because anything that works cause it to fail has always been dealt with and returned early.
That’s the story for a lot of things Go does :)
I love what it does for my mental model and dislike what it does for aesthetics. Ultimately, that’s a win in my book. I can’t imagine my view is original.
Explicit is good… boilerplate is bad.
I think the collection of primitives Rust offers strikes a better balance on that aspect, even if they took the same approach as with
async/awaitand you need to either useBox<dyn std::error::Error>or pull in a third-party implementation for the interfaces they defined, likeanyhoworthiserror, to make them properly ergonomic. (Though, to be fair,anyhowandthiserrorare dtolnay projects and about as actively developed and well-maintained as you can get in a “don’t do change for change’s sake, please” situation.)…especially a simple, concise
?(or.context("Message")?withanyhow) for throwing the error orNoneup the stack as long as you’re usingBox<dyn ...>oranyhowor an appropriatethiserror#[from]annotation to handle converting error types to match.I recently took advantage of that. I triggered a function and then didn’t check the error code for another several statements so that I could achieve other work, regardless of the error. Because I had explicit control over the error handling, I didn’t even need to think about how to do it.
This is a fine thing to do, but I’ll note you can still have that and ergonomics (Rust
?, Zigtry).To each their own, I respect your decision. I’m going to react as a golang fanboy (you’re welcome in advance), but truly not trying to preach golang gospel here.
Instead of defending go, I instead shall do the public service of savaging Java, the buck-nakedest emperor of all the languages/ecosystems.
If you’re choosing to go BACK to Java, I think that’s the key unspoken context here: you are already familiar with java and investing in its er, unique ecosystem will not be starting from zero. So the tradeoffs for you should be understood more explicitly in this context. And I can see how this decision makes sense, in that context.
People choosing go vs java with little or no experience in either? Very different decision.
Aside: I love go but must of course admit that it does have its weaknesses. ‘if err != nil’ all over the page is indeed absurd. I would argue it is also simultaneously a glorious triumph of the explicit, but that does not solve the absurdity. This is the golang meat once must consume to get their golang pudding. Not all folks will or should accept this deal.
Were I to pretend to be a newcomer to both (I am new to neither), and I had to decide to invest between either learning the wonder and warts of the go ecosystem vs the kafka-esque harrowing of the java ecosystem? Seems like a no brainer to this flesh node. Java makes some sense, assuming you have already - willingly or through necessity- turned off your critical faculties, and chugged the flavoraid.
Yes I do hate Java, deeply. It has earned my ire, even after having wiped the flavoraid from my lips. I understand that some must use it, but I hope to save the souls of those who have not yet invested their precious years into Java.
With Rust, Golang, Node, or even today’s C++ as alternatives, I think human beings can be happier (less miserable?) while delivering software in a commercial context, never having to submit to Java (the programming equivalent of amazon’s leadership principles).
P.S. you mention that go vs java is a different set of tradeoffs depending on the software being made - i must agree Java would just be easier to use for some software usecases. I’d glady write seven go libraries from scratch rather than write one line of Java, but I am a special snowflake.
P.P.S. i know java’s whole situation has gotten much better, but it still has huuuuuuge bloat problems all over the software lifecycle when you choose java. Weighing the annoying golang boilerplate baggage of ‘if err != nil’ vs the Java ‘getterSetter / five chained methods to do one operation / decorators everywhere / gradle / everything ’ boilerplate baggage? Another no brainer decision to this one.
I’m not sure you entirely intended it this way so please take this as the complement I intend it to be, but this is a gloriously-written spiral from rationality into sheer loathing. I need this narrated by someone with a meticulous, ominous voice… not James Earl Jones, perhaps more like Agent Johnson’s “I hate this place” monologue from The Matrix. Truly the flavoraid is most bitter to those who hath chugged it most deeply.
Truly
Agent Smith?
yeah, probably. Could have sworn I looked it up, but it seems like I looked it up and typed the wrong thing anyway. Whoever, this scene.
Thank you, I enjoyed this.
The whole article just seems like a skill issue to me, i write go and rust both professionally and as a hobby and I love Go for all the points mentioned in the article, the error handling is consious, the std library has everything I need (the exception is go-fiber), I like short variable names (names should be proportional to their lifetime), one way of doing a thing makes reading foreign code very easy, and I regularly work on go applications with pprof, gdb and simple print debugging, I can not relate to any of these being disadvantages of go. I could never imagine choosing the language riddled with boilerplate and cursed with worse tooling than Go and criticizing these points as Go issues in the same breath.
Held my curiosity until, “Go’s biggest ORM package, Gorm, is light-years behind Hibernate/Entity Framework in terms of functionality, and full of undocumented weird behaviour. What do Go nerds say? You don’t need ORM in Go! Do It Yourself!”, then lost me. I’ve looked at Gorm once or twice, but never considered using it. I’ve used nhibernate and EF in the past and wouldn’t choose to again. I’ve never used hibernate itself but the few times I looked at the docs it seemed significantly hairier than nhibernate. The one large java codebase I’ve worked with (mercifully) didn’t use an ORM at all and data access and “manual” relational mapping never presented a problem (granted, N=1).
If the Golang’s lack of an ORM is viewed as a legit criticism of Golang and its ecosystem, and we’re using (n)hibernate or EF as the standard of comparison…I have no words.
To be clear, I think ORMs can be good. It’s been forever since I worked with activerecord and I really liked it. However, that’s almost certainly because it was so tightly integrated into rails.
Depending on the usecase, an ORM introduces mental overhead and hidden complexity. If you ever have weird problems that you don’t understand, it feels like using a black box to handle your data.
I prefer understanding what happens and being able to optimise where I think it is necessary.
As for go, I like the concept of simple SQL query builders. They introduce some compile time safety and are still considerably manual.
As for Java, I have abandonded ORMs in favor of EclipseStore.
I agree regarding ORMs - my nhibernate experience really burned me out on the idea in general. EF I don’t recall being much better (at the time, it has likely improved, although I seem to recall something about a vote-of-no-confidence for a release or two around the time I was working with it.)
I haven’t worked with Java for some time now. This is the first I’ve heard of EclipseStore. Definitely looks interesting. Apparently it’s pretty flexible as far as persistence goes, and you’re storing an object-graph so no impedance-mismatch. Is it considered an object-database?
I guess you could call it that.
What makes it interesting, is that there is no crud api or dsl to interact with the data.
So does Python contain middleware for exponential backoff?
That is a very weird statement because it links to an explanation about the specific inconsistency?
This feels emotionally charged
I like the err != nil because I know that errors are clearly being handled somewhere, and the logic for that is local to the caller
Except you don’t know that errors are being handled, and the logic isn’t local to the caller.
Nothing I’m aware of in Go requires you to actually check
if err != nil, meaning your program will still compile if you don’t do it, and will still run. Just when the error condition finally does happen, you’ll get a mysterious problem at some other location.Compare this to exceptions or sum types, both of which force you to address the problem before moving on. An uncaught exception doesn’t allow execution to proceed, and in languages with sum types a failure to resolve them before use is generally a compile-time error.
Similarly, the logic is non-local; the near-universal pattern in Go is
if err != nil {return nil, err}or variant forms of it that wraperrin some sort of nice reporting structure. That’s not “logic”, that’s just yeeting the error up the call stack in hope it will finally encounter an actual error handler (and if we’re being really cynical, is justtry/catchbut implemented manually). Figuring out which handler that might be and what it will do can easily be a non-trivial bit of analysis, and is basically never obvious from just reading the surrounding code.I’ve only contributed to one open-source project written in Go, and they had a linter that made sure that err returns were either checked or forwarded to the caller. I think it was a standard go thing, I don’t know why it isn’t a compiler warning.
My biggest problem with Go is that the type system does nothing to help you avoid footguns. In Go, it is undefined behaviour to concurrently modify an object from two goroutines (and, yes, this can break memory safety if you do). Yet Go has primitives that make it trivial to send references to objects to other goroutines. In Erlang, you have the same kind of send (messages to actors rather than messages via channels), but Erlang always passes objects by value (or by reference as an optimisation, but they’re immutable so this isn’t visible in the abstract machine). Concurrent mutation is impossible by construction. In Rust, the Send trait ensures that objects are passed as a move operation and the borrow checker ensures that you don’t hold any pointers to objects dominated by the sent object after you send it. In Go, sending a pointer via a channel is just as easy as sending a by-value copy. Easier, in fact, because copies are shallow so sending an object by value will send any referenced objects by reference and end up with accidental aliasing.
All of this means that there’s a huge cognitive load writing concurrent Go, yet concurrency was meant to be one of the main selling points of Go. The people I’ve spoken to who are happy with Go treat it as a replacement for Python that produces a statically linked binary.
Yeah that limitation of channel sends was annoying, and I had to adopt conventions to reduce the “cognitive load” and avoid errors.
I’d have liked the ability to have some form of “const/immutable pointer” such that a read only reference to an object could be passed around.
As for the lack of move operation, I simply adopted a convention of immediately nil’ing the pointer on the sending side after sending it. Possibly a linter could be written to verify that convention?
Obviously someone subsequently updating the code could fail to be as rigorous, and introduce an issue. I was pondering catching that with my UTs, by building upon the finalisation facility in the runtime, but never got around to doing it.
All languages have their issues, and sharp corners…
I seem to recall that go has no compiler warnings. Only compiler errors. Warnings are for linters.
True. What we rely on is a weaker property: unused variables are a compiler error.
So at first this seems almost as good. You do in fact have to do something with err. It gets a little trickier when you realize that err can be reused so you could do 2 a, err = operations and only check the second operation and that would make the compiler happy.
But in a worse is better sort of way Go code frequently won’t compile if you don’t check the err.
You can choose at what level to handle it, and i think it’s pretty strong consensus that exceptions are a bad model over something like returning errors/result types
you can also lint rules like forcing error checks
Reiterating for clarity: if you follow all the recommended best practices for error handling in Go, what you will wind up with is a sort of Greenspun’s-Rule-ish ad-hoc implementation, not of Common Lisp, but of a try/catch exception system. Which is to say: you’ll halt execution at the point where
err != niland begin passing some type of error object up the stack, wrapping it with new frame-local information as it bubbles up through each frame of the stack, until it encounters code which actually handles the error and stops it from propagating further.But you’ll get none of the ergonomics of an actual try/catch exception language because Go will demand that you implement it yourself every single time, and will demand that you remember to do it at every point where
errmight not benil, because Go cannot force you to check that.Compared to error-handling systems which actually make you handle the error, “you can lint for it” is not great. You see how that’s not great, right?
Yeah but it’s habit now for me, but I can see why someone that’s not used to it might feel uncomfortable with it
tell me you never actually learned Go or its idioms without telling me that you never actually learned Go or its idioms.
Last I checked, neither Python nor Java have a comprehensive universe of HTTP middleware in their standard libraries, and it’s far easier to get a production web server up and running in Go than in either of the other languages.
[Comment removed by author]
Very weak arguments, meh.