I agree with this. I learnt Rust before Go, and using Go makes me feel like The Oatmeal piracy guy[1]:
"I'm not sure if I should use Go to write this HTTP service. I'd lose immutability tracking, I'd lose compiler-enforced thread safety, I'd lose the powerful type system, I'd lose the comprehensive error handling, I'd suffer from a million little papercuts, I'd have to use the weird date formatting system, I'd have to check nil pointers, I'd...
...oh, it's seven days later and I've already accomplished more writing networking servers and clients in Go than I ever have in years with Rust."
This isn't to say the points raised about Go aren't true. They are true, and if a better language were available, I wouldn't stand my ground and argue their benefits, I'd switch to it. The last comment I happened to post on this website is about how Go is insufficient without its army of linting tools [2]! Yes, I'm incredibly happy to have learnt both Go and Rust as their combination has expanded my skillset and the range of programs I'm willing to write tremendously. But if someone said to me "you should just use Rust instead of Go for your production services", I'd think the "just" was doing some incredibly heavy lifting.
An article that I'd like to see is one comparing the two languages for this niche (networking servers and clients), contrasting not just the language pitfalls but the third-party libraries necessary, the iteration speed, and the choices you'll have to make up-front. My guess is that the languages would be judged more closely together.
[1]: https://theoatmeal.com/comics/game_of_thrones [2]: https://news.ycombinator.com/item?id=30749921
A long time ago, in a Haskell community chat, I saw someone dismiss Go with a pithy comment along the lines of, "Go isn't a programming language, it's a DSL for writing network services." I think I may need to re-assess that comment as actually being a really compelling elevator pitch for the language.
Armed with that perspective, I'm seeing why I wasn't terribly convinced by the article's specific complaints about Go. "Traditional IPC is a PITA and forces you toward talking over a socket? Well, yes, exactly. That's kind of the whole point."
Sometimes I wonder if we are all suffering unnecessarily because of our incessant demanding that all languages try to be all things to all people.
Programmers aren't. Programmers are building stuff and talking about that. Opinion bloggers are suffering for clicks.
Personally I lean towards strict compilers (I suppose years of JavaScript has traumatized me), but 5x dev time is a big tradeoff! Of course this is just one data point, but it did seem worth mentioning.
Golang gets out of the way in me doing what I want.
Rust actively resists me doing things I will later regret.
Also, unexpectedly, I have gotten some positive comments on my C coding style after I coded some Rust.
All of this is completely anecdotal and personal experience, of course.
Trillion dollar companies run on "incorrect" software.
It is impossible to run "correct" software, but we can do better, maybe?
"I'm not sure if I should use Go to write this HTTP service. I'd lose immutability tracking, I'd lose compiler-enforced thread safety, I'd lose the powerful type system, I'd lose the comprehensive error handling, I'd suffer from a million little papercuts, I'd have to use the weird date formatting system, I'd have to check nil pointers, I'd...
None of the modern language do it beside Rust so every language are then bad?
Java / C# / Python / Ruby so they fall in the same bucket as Go I assume?
For starters "without null" is a myth. They both have null. They have to; there is no other practical option. The JDK uses null all over the place, so you need to have null in order to talk to the JDK.
Now, they do still have mechanism to make null easier to handle. And they're both pretty impressive designs. (Especially Kotlin's, though I haven't tried Scala 3 yet so maybe I'm missing something wonderful there. Aesthetically, I just prefer "we're going to openly acknowledge it and tame it as much as we can" over "we're going to try to sweep it under the carpet.") But there's a sort of Amdahl's Law analogue hiding in that situation: the upper bound on how much practical null safety you can achieve is constrained by how much you can avoid relying on modules that were written in Java. And I know that's basically true of languages like Haskell, too, because you often have to rely on at least a little bit of code that was written in languages like C. But it doesn't preoccupy me the same way it does in Scala or Kotlin, where interacting directly with Java code is a much more everyday kind of affair.
Kotlin code requires you to use Type? if the value can ever be null.
Java code that is annotated by @Nullable/@NonNull will automatically map to Type?/Type (and it is up of course to the developer to not fuck up their nullability promises.)
Java code that is not annotated is a Type!, and encourages you to be wary about what could happen.
The nullability story is miles better than Java.
Additionally, all Java code (including the one you wrote) is only optionally null checked. All Kotlin code _is_ null checked, no matter what.
- Know about it (first, big problem for many java shops)
- Integrate it with Gradle/Maven/Ant/yourbuildtool
- Enable the null checks because they are not enabled by default.
Compare this with "it's already in the compiler"
Not just because of the null problem. It's also that option types push you toward a "conditional logic everywhere" way of doing things, because that's how you handle the options. That's all well and good and holy in a functional language, and perhaps even a procedural one. But it's the opposite of good object-oriented design.
To quote Dr. Mark Crislip, when you serve cow pie with apple pie, it does not make the cow pie better. It just makes the apple pie worse.
BurgerWithFriesMeal subclasses BurgerMeal?
"Object oriented design", in the religious sense, is an obsolete 1980s fad that took a good idea (encapsulation of mutable state) to comical extremes.
The original idea of OO design was to strive to eliminate stateful idioms. Which is a slightly different idea than what we're used to. The state existed, but the point was that you were supposed to design your system so that objects didn't need to know - or even attempt to infer - information about other objects' state.
The whole intellectual lineage that includes pervasive use of explicit state querying and manipulation methods such as getters and setters could be characterized as a whole lot of procedural programmers collectively missing the point. It's right up there with when people over-use the State monad in Haskell, effectively doing their darnedest to Greenspun imperaive programming on top of a lazy functional language because they haven't quite internalized this new paradigm yet.
So what is good OO design? NPEs? Nil checks?
- nil
- None
- Some(nil)
- Some(non-nil)
As far as how to do good OO design, ideally you try to avoid explicit branching whenever possible, and instead use dynamic dispatch to decide what to do. In principle, if you've architected things well, you should generally be able to avoid conditional branching.An ironically useful example of how this works is Smalltalk's implementation of Booleans and conditionals. Smalltalk doesn't actually have an if statement. Instead, it has a Boolean class that defines three methods: ifTrue:, ifFalse:, and ifTrue:ifFalse:. Each takes one or two blocks, which are effectively anonymous functions.
And then the implementation is that the True subclass as an ifTrue: that executes the block, an ifFalse that doesn't, and an ifTrue:ifFalse: that executes its first argument. And the False implementation does the opposite.
This isn't meant to be an example of "look, OOP doesn't need conditional branching, just use {library implementation of conditional branching}," so much as a small, self-contained example of the kinds of ways that you can achieve conditional-style logic without explicit branch statements. A more real-world example might be something like having separate NotLoggedInUser and LoggedInUser classes that behave differently in relevant situations, rather than having an isLoggedIn field to have to keep checking at every use site.
The big thing working against us on this is that most the popular OO languages - C++, Java, Python, C#, etc - come from the same approach of trying to layer object-oriented features on top of a procedural core. In my not-so-humble opinion, this has been about as successful as more recent efforts to support functional programming by pulling a mess of functional features into existing OO languages. Technically it gets you somewhere, but the resulting language is not an ergonomic pit of success.
But, at the other end of things, teams that were already using Java and want to start incorporating Scala don't have that option. And 10 year old Scala projects didn't originally have that option, and doing something about it now may be a lift on the scale of a complete rewrite.
> often it's not worth taking on that burden just to theoretically handle cases that rarely occur and even more rarely cause any problem. Programming is a means to an end, and the cost of [Edit] adding if err != nil to every line of code [/Edit] (increased development time) is often not worth it.
I basically like Go, I really want to like Go, but as a Python person who has used exceptions with good success for 13 years in HashBackup, I can't see myself adding an if test to practically every line of Python code, just to catch an exception that might occur once in a blue moon.
A lot of software is not so mission critical that lives depend on it. If that is the case, sure - add error checking to every line of code. But for a lot of software, IMO, it's too much work for too little reward. If something blows up in production in an unexpected way, sometimes that's okay. You log the problem, fix it, and it's fine.
I re-read the Go error handling page before posting, because maybe it's not as bad as I had initially thought. Nope, still looks pretty cryptic to me:
as the person on call for such events, it's really not fine.
would you rather handle these errors upfront during development time or unexpectedly and uncontrollably, during runtime? having been on-call in one way or another for ~10 years, i know which i'd prefer.
after having used golang in production, i will never go back to using a dynamically typed language if i can avoid it. almost 0 thought or effort is required to handle errors correctly in go, and it's still possible to ignore them (just use `_`). not that i'd recommend ever doing that.
Exceptions are in my honest opinion better on every single front. Checked exceptions would be the panacea but Java’s version is not the best due to subclassing. But overall bubbling up exceptions is the correct thing to do — in most cases a problem can’t be meaningfully handled at the call site. Let’s say you want to read from a file during a HTTP request but the read fails n methods deep. There can be any number of other error occurrences and the only sane choice here would be to return some error code to the web client requesting the site. Why do you want to handle it n times instead of only at the goal?
ruby:
begin
res = Net::HTTP.post_form(APIURL, ...)
puts res.body
rescue e => e
# do error handling
end
go: response, err := http.PostForm(APIURL, url.Values{...})
if err != nil {
# handle PostForm error
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
# handle ReadAll error
}
fmt.Printf("%s\n", string(body))
Now, for which errors do you handle? In golang, you can see clearly where errors may occur. In Ruby, it's unclear whether HTTP.post_form is even capable of throwing errors, much less what they are. For the record, here are a few of the many types of errors that can be thrown by the ruby code. Here are a few: Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, ...
there's no way to tell _where_ the error will even come from - you might get a Net:: error, you might get a Timeout:: error, hell you could even get an Errno::! What most programmers wind up doing is handling no errors at all until they occur in runtime - or wrapping every single "suspicious" statement in a catch-all begin/rescue block, even when it's not needed.In golang, each function capable of returning an error simply returns it, and you must handle it at that point in time. It is very straightforward. In my experience, it results in much more reliable code. In Ruby, you have no idea what's coming. Do you see what I mean?
Been programming Ruby for 8 years and have never seen anyone do that, so no... If that block of code can really throw so many different types of exceptions (usually - it can't) you can always catch StandardError.
``` try { var response = http.postForm(APIURL,) } catch (Exception e) { # handle PostForm error }
try { var body = ioutil.ReadAll(response.Body) } catch(SomeSpecificExceptionType e) { # handle specific exception differently } catch(Exception e) { # run for every other kind of error }
If a given method is defined as `void postForm(…) throws IOException` you have to handle the exception at the call site, or mark the containing call itself with a similar throws clause.
I really don’t see anything not handled by these. Also, the defer part can be done by a try-with-resources construct that will clause the resource in every case, whether exited without exception or with one.
I think because it sets up the right psychological incentives. Because almost any option other than those two ends up looking like, "Hmm, I don't know how to handle this, but maybe someone else will, so let's bubble it up and hope they do," in practice. It's just the way we humans are.
And that sets up an intractable moral hazard situation. It creates this middle ground where two different owners of two different parts of the code can finger-point and blame and try to pass the buck. And, knowing that (though probably only unconsciously), they won't just become sloppier about handling errors. They will become sloppier about allowing errors to happen in the first place, and doing things that make it harder to handle errors. And they won't even know they're doing it.
That's what I think of when I read your hypothetical, "Let’s say you want to read from a file during a HTTP request but the read fails n methods deep." That sounds to me like code that's been structured in a way that, on this team I was alluding to above, probably wouldn't have happened in the first place and definitely wouldn't have made it past code review. The pushback would be something along the lines of, "Why on earth are you doing both file and network I/O within a single business operation but at two very different depths in the call stack? Did you even notice what you were doing here?"
Here's a counterexample from HashBackup (I'm the author):
A filesystem is being backed up. Some weird error occurs, let's say it tries to read an extended attribute and gets some goofy error that raises an exception and it's not in a try/except. That error bubbles up to the try/except for "save a file", prints the exception message, and continues by backing up the next file.
Is it more reliable to crash when reading an extended attribute, aborting the whole backup? Or is it better to just tell the user about the error, finish the backup, fail at the end of the backup with a non-zero exit code, and report it in an email?
At the time of failure, the only way I can see that the error could have been handled at the call site would be to return an empty extended attrbute. To me, that's more like the "silent but wrong" solution. And there are many of these situations for saving a single file: get the flags, read the extended attribute, read the permissions and times, figure out if it's sparse, etc. Instead of having error handling at each of these steps, they can have no error handling and just raise exceptions and the high-level "save a file" handler will do everything.
Of course, along the way there are some low-level try/except blocks around code that needs cleanups, like:
fd = None
try:
fd = os.open(...)
do some stuff
finally:
if fd != None:
os.close(fd)
But you have the same thing with defers in Go.(Ab)using Result types without good syntax for dealing with the very common case of “handle it higher up” (Rust’s ? macro is ok, not perfect but it is a much better compromise) is coming from a wrong angle imo.
The problem is if it doesn’t quickly blow up, but instead silently corrupts data or causes other problems down the line that are hard to backtrack to that specific missing check.
try:
do something
except Exception, err:
pass
If a piece of code is has a try/except, then it's because there is the expectation that it might fail and certain kinds of failures can be handled. If a failure is not one of the expected failures, then do a raise and let the exception go higher up. It's either going to hit a handler in the main program or will exit to command level. In either case there's a stack trace to see where the problem occurred. Exceptions tell you exactly where things blew up, whereas the "if err return" paradigm only does that if the test at each level adds identifying information.I've seen some Go stack traces. Talk about hard to backtrack: the ones I've seen go on and on. I understand it's because there are multple Goroutines running and each one has a stack trace, but they look pretty daunting to me.
* First you construct a request, and check the error on that
* Then you "Do" the request and check the error on that
* Then you check the status code (you did check it, right?)
* Then you deserialize the response and check the error on that
* Then you close the response body (you did remember to close it, right?) and check the error on that
Actually making an HTTP request with Go stdlib is a many-stage process and it is very easy to screw up or forget one of the steps. I see it all the time. Actually I have seen outages related to people being too zealous and reporting errors related to closing the response body on otherwise successful requests.
I kinda get it though, because the compiler or linters will show that as a warning or error going "bruh you forgot to handle this error", and as a perfectionist one wants to get rid of all of those.
I mean IMO those should be logged anyway, if only so you get some data on how often it happens, if there's anything that can be done about it, and if it's harmless, to ignore the error - but make it explicit.
The takeaway - if you gloss over the ranting and look for the good bits - seems to be that you should choose a language that handles a lot of the boilerplate for you (e.g., no verbose error handling) and has FFI.
It's not a bad argument, and it's making me think about how and when I use Go. For that, at least, it's worth reading.
Go and Rust both have strengths and weaknesses.
And I think they both have a love/hate dynamic because they make pretty strong choices in their own ways, and so people react strongly in response. Personally I'm a fan of both.
And yet, Go's HTTP client doesn't have a good default [1] because of zero values, which are argued harmful in the article.
[1]: https://medium.com/@nate510/don-t-use-go-s-default-http-clie...
Nope, a better alternative is Java/C#/Kotlin/etc. Those offer sum types, async programming, while being more productive than golang, and provide superior tooling and a superior developer experience.
Now suddenly we go from a poor type system (Java) to a decent one in Go (this is my personal opinion). Or from no type system to SOME type system. On top of this, we are getting all the benefits of a compiled language.
Essentially this is looking Go as a system language, while I think of it as an application language.
Aside from that, the author is right in some ways.
No, the better alternative is thousands of different languages.
HN is just hype-driven as usual and can see only Go and Rust.
I think this is going to change. I couldn't find the relevant blog post now, but I believe to remember there was an initiative to provide a standard async runtime.
What is planned is to standardize the API: that is, provide common traits for IO, tasks, etc. so libraries can be runtime-agnostic and you can change your runtime by only touching Cargo.toml (hopefully).
It would've never got to where it's at if it wasn't for Google, that's the most important reason why its successful.
In my experience it is only increased initial development time.
I find that initial development time is frequently conflated with development time overall.
So obviously worse is better languages will be preferred.
It seems that C is a language/compiler/toolchain combo that lets you shoot yourself in the foot, but gives you enough access to stuff to be maximially performant in a random (i.e. not specialized) use case. (C++ is in this camp too).
Rust is a language/compiler/toolchain combo that tries its best to not let you shoot yourself in the foot by presenting those footguns for you to fix up front, without giving up much of the access to stuff (C++ tried some things in this camp like destructors and smart pointers. GC is also something that's in this camp)
Golang is a language/compiler/toolchain combo that lets you shoot yourself in the foot in some circumstances, and does not give you enough access to be maximally performant in every random case, but rather it seems to posit to give you opinions that will get you really, good performance in most random cases (if you need to tune the GC, for instance, you're probably worse off, like here: https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i...). Meaning it's making the tradeoffs of upfront complexity vs runtime issues, and Tim Toady vs The One True Way for you, because in the general case those are the most optimized tradeoffs. I don't disagree, especially when the closed-world ecosystem is good enough to not need to reach out to external tools whether those are via cgo or in the build toolchain.
In general, I think that if you reach the point where Golangs admittedly major warts are hindering your progress you're doing better than most companies, so its succeeded in not getting in the way of progress at least until that point.
In particular since the article keeps comparing Golang to Rust, for personal projects I do like the correctness of Rust, but some things are frustrating, like async being basically unusable without Async Drop, an uncertain future for completion apis, and apis that really want HKT but can't use them (yet?). I having to make up a lot of opinions that Go makes for you adds risk and uncertainty to a project where the most likely failure modes are not, in fact, those opinions.
I'm excited for the future of Java, with Loom and Valhalla coming to address as what I see as two core problems of Java in particular and other languages in general. I'm also excited for the present of js/node.js/maybe Deno. typescript has become good enough that I also think it's a really good competitor to Golang in the "just write code" arena, especially since there's significant synergy between the front end and the back end to help with build tooling, validation logic, and programmer expertise.
But I don't find myself agreeing with his position, which is "you shouldn't use Go for production services" (he explicitly says this in one of his Go posts, I forget which one and don't have time to look right now).
The better alternative to Go is Rust. Okay, sure, I'm willing to admit that in the examples presented, Rust handles things better. But Rust isn't a panacea. Rust is complicated and hard, and often it's not worth taking on that burden just to theoretically handle cases that rarely occur and even more rarely cause any problem. Programming is a means to an end, and the cost of using Rust (hiring, increased development time) is often not worth it.
The reason Go is successful is because it's easy for companies to use to solve production problems with teams of varying expertise. Its stdlib is well-featured, its ecosystem is good. There's generally one correct way of doing things. The same doesn't appear true in Rust. For example, an _incredibly_ common thing to do in production code is to make a web request. In Go, there is no need for debate, you use the stdlib. In Rust, you have to use a crate, which requires a decision to be made. Even worse is the async story, in Rust you have to decide whether to use Tokio or whatever. That burden is just not present with Go.
Btw this article should not be flagged and it's pathetic that people have flagged it.