Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> Do I particularly like managing errors that way? No, but I do think that it improves the transparency and quality of a lot of Go projects.

So long as we can all agree that it feels super bad, I guess this is fine. But it does sort of mean that Golang approaches the Java world back with checked exceptions where principle trumped ergonomics.

That lead to a world where folks felt "forced" to use Java, and that's a sticky label that's difficult to remove. I think the Golang community is going to find itself growing more slowly as folks increasingly realize other options offer similar benefits without the bad ergonomics.



Literally the first non-trivial code I wrote in go (running a bunch of goroutines to download a ton of files from a website in parallel)... I knew exactly where and how things could fail and where things were failing just by looking at the code. Coming from C++ and C# and Python, there was no comparison. I had never been so confident in the code I'd written, even though I was a newbie at Go and a veteran of the other languages.


I guess then I have to ask, why would try() make that worse?

Because I can't stand Golang error handling. It's repetitive, it's error prone, and other language features interact with it so that when you make a mistake it can be as hard as a double free to track down where the erroneous default value was introduced.

On the other hand, using Rust, Ocaml, F# or Haskell I understand how my code composed and I can be confident I can just use an error handling strategy. The only complexities appear, as with everyone else, when we have asynchronous code.

So I don't mean to disagree with your feelings, but they're sure not mine and they're part of why I don't use Golang. I was excited about try() because it at least addressed the most tedious part.


Try makes it worse because it is so easy to miss when reading the code. Because it encourages nesting function calls, which is harder for a human to parse than separate statements across lines. Because it means you can exit the current function from the middle of a line of code, and what runs before or doesn't run before is based on order of operations rather than requiring the exit to be a statement on its own line whose order cannot be misunderstood. Because it discourages giving more information with an error, so instead of "failed to open config file: EOF", you just get the EOF.

Go's error handling isn't any more error prone that writing if statements for all the rest of your code. if err != nil is no different than if age < 18. Either one is a branch in the code. I can literally count on one hand the number of times in 6 years of full time go development that I've seen people miss writing the if err != nil.

Being explicit is good. Spreading out the logic is good. Cramming a lot of logic into one line is bad.... and that's the sole purpose of try.

Maybe there's another way to make error handling better in Go. I'm not averse to looking into that. But try wasn't it.

You're talking about writing if err != nil being tedious, but what about matching Results from Rust, isn't that tedious? What about writing proper catch blocks in java or C++ or python, isn't that tedious? It's all just logic.


Writing Go professionally for 4 years already and being a Go fanboy since 2009: while endorsing many benefits of "if err" blocks, I do very much have the following issues with them (in no specific order):

- It's hard to spot outliers. This leads to occasional bugs that tend to get easily overlooked in code review. Also, it makes code reading harder when an "if err" block is subtly different. The most common case here being "if err == nil" (sometimes bug, sometimes on purpose) - super hard to notice.

- You say you have seen missed "if err" blocks only a few times. I say that's 100% too many; every one of them in my experience was a subtle bug (possibly comparable to off-by-one errors in C).

- When I need to focus on understanding/analyzing the optimistic path in a fragment of code (always the first thing I do when reading), the everpresent "if err" blocks introduce tiresome visual noise and make the reading/grokking process slower and harder (having to constantly try and mentally filter out some 80% of what my eyes see).


Just curious. Do you have a vision for how Go could change to improve your issues? (One of the key problems is nobody could agree on a better approach...)

Also, what editor/IDE do you use? The reason I ask is because of this: https://youtrack.jetbrains.com/issue/GO-7747


The difference with catch blocks in Java, C++ or Python is that you only need to write them when you actually have something g meaningful to do.

If you only need to propagate the error or cleanup resources then propagate the error, then all you would write is... Nothing. And cleanup+propagation is by far the most common error handling strategy. In Java and Python exceptions even add context for you automatically to help track down what happened.


One foundational principle of Go is that the sad path is at least as important, and maybe more important, than the happy path. The best Go programmers I know write the sad path of their programs first, and then backfill the happy-path logic. So:

> you only need to write [error checking] when you actually have something meaningful to do.

Although it's the subject of a lot of ridicule, `if err != nil { return err }` is actually bad Go code, and not often written by good Go programmers. Errors in Go are, at a minimum, annotated with contextual information before being returned. Frequently, they are programmed-with in other, more sophisticated ways, depending on the domain of the program.

Shifting your mindset to understand errors as something significantly more important than the off-gassing of your program's execution is worthwhile in general, and, in Go, fundamental.


The fact that you need to add context to errors usually exacerbates the problem and makes it even harder to read the code. You often end up with

  err = doThing()
  if err! = nil {
    return errors.New("Error doing thing", err)
  }
This doesn't add any useful information for whoever is reading the code, it's just boilerplate that you learn to skip while reviewing, while hopefully not missing any important thing that does happen on the error path.

As for 'programming with the error', I would like to see an actual use case for constantly doing this, and why exceptions would prevent that pattern. The only one I can remember is something highlighted as a 'good practice' by Rob himself: write everything you want to a bufio.Writer, without checking the error messages, and then calling Flush and only then checking if maybe something failed. If this is good, safe, sad-path-first, errors-are-values style... then my taste in programming is obviously bad. Obviously, the same could be achieved with exceptions.


First, the signature for `errors.New` is `New(text string) error`. It won't take more parameters than that. So I guess you mean `fmt.Errorf`.

If above is true, then how about:

    err := renderTemplate()

    if err! = nil {
        return fmt.Errorf("Error rendering template: %s", err)
    }
The end error could then be something for example:

    Error rendering template: Compiler has failed: Cannot load template: File /tmp/test.tpl was not found
    ------------------------  -------------------  --------------------  --------------------------------
     |                         |                    |                     |
    Returned by that           |                    |                     |
    example                   Returned by the       |                     |
                              fictional compiler   Returned by the        |
                                                   fictional template    Returned by the fictional file 
                                                   Loader                reader
I didn't even twist your example, and yet you can already see more information. And with that information, even a user can understand what's going on clearly. So ... more useful?


Oops, I forgot if errors.New takes the 'cause' as well.

Regarding you example: the code itself still contains redundant information for someone reading it. True, the error ends up being nicer, though I would argue that the user would have been better served with a simple 'failed to load template file: /tmp/test.tpl', no need to show the pseudo call stack (so, only the fictional template loader should have been wrapping the error,for this particular case). And for a developer, the full call stack may be more useful. Exceptions would give you both for free - a nice message that can be shared to the user by whoever caused the most understandable error, and a call stack that can be logged at the upper layer so developers can see it if a bug is logged, and get a much fuller context.


The full call stack is available in Go but it is up to the developer if they want to include it or not which they can do by creating a custom error type and implementing that to be part of their type. And having the choice seems like a benefit to me.


I would argue that if you are using boilerplate annotations, you are doing it wrong. If you really do not need to add context, don't add context. But in my code I find that I want to add context about 90% of the time.

But then I am super zealous about making sure my error messages understandable without the need to track down other information. For example, I want to know that (something like) "the config file needs group read permissions" not "file cannot be opened." But maybe others value ease of programming more than I do and are less concerned about error UX than I am?


> `if err != nil { return err }` is actually bad Go code, and not often written by good Go programmers.

Like the people who wrote the Go stdlib? Because that's filled with those - just look at the net/* packages.


One distinction that is often made by core team members is that you only need to annotate errors at package boundaries, and that returning unannotated errors within a package is fine. But in most code, the packages are not so well-defined, or well-thought-out, or sacrosanct, that they represent a good proxy for annotation decisions.

I would much rather have an error with too much or duplicate annotation than one with not enough. And I would further argue that, yes, in the stdlib, errors are generally under-annotated.


Legitimate question: then what does good Go code that is written by good Go programmers do/look like? Wrap `err` in `errors.New("some function failed", err)`?


At a minimum, an error should be logged with appropriate context and execution allowed to continue; or annotated and returned. Error annotation is currently best achieved with pkg/errors as e.g. `errors.Wrap(err, "error doing thing")`. (The xerrors suggestion of `fmt.Errorf("error doing thing: %w", err)` is awkward and hacky.)

Many programs can benefit from a more structured approach to error management. But once you get past the minimum (above) there's no one-size solution for what "a more structured approach" looks like. I really enjoy how upspin.io does their errors package, though it is somewhat esoteric. I'm also reading and generally liking how Cockroach does things, though I don't like the coupling to Protobufs.


Recently the Go team introduced the xerrors package for improved error management: https://godoc.org/golang.org/x/xerrors

So using these functions is becoming part of what good Go programmers do.


That statement is still correct even in the context of the stdlib.


> Errors in Go are, at a minimum, annotated with contextual information before being returned.

What surprised me when I last wrote Go was that there was no out-of-the-box solution to adding a stack trace to the error.


Stack traces are, for me, too much noise, and not enough signal. I prefer reading annotations added (prefixed) by programmers deliberately. File and line information for the call stack leading to the error maybe provide value in the dev cycle (e.g. when fixing tests) but basically don't in logs in production.

This is all to say: I can understand why they aren't more naturally part of errors, and I think it also helps explain why they are part of panics.

And with all that said, I wouldn't object to making stack traces easier to add to errors, as long as it was opt-in.


Does anyone know if this was a conscious decision? I mean IIRC in Java you're generally discouraged from throwing errors for control flow because creating the stack trace is a relatively heavy process. In Go this is of less concern and returning an error is pretty normal for control flow (as in errors are expected, not exceptional), and you shouldn't have to worry that an error path would be 100x as expensive as a normal flow because a stack trace is being generated.


Java allows since ~a decade time to omit the generation of stacktraces, for exactly such cases


yeah; we're getting there though, see https://github.com/golang/go/wiki/ErrorValueFAQ


JMTCW, but since I generally program Go in GoLand with the debugger, I have full access to the stack all the time so I have not found this to be a major concern. But YMMV.


But the issue with this is that it can be hard to know what all the possible error conditions are, and thus whether you have anything meaningful to do.

Using Rust, which makes errors explicit like Go, has been eye-opening to me. My programs never crash because I've handled every error condition. No effort on my part. No tests needed.


Java also makes you handle every possible error condition, unless of course you chose to use an escape hatch. Rust allows the same.

By the way, Go is much happier to crash than Java - for example, a simple array index out of range will cause a program crash in a typical Go program, where it would only cause a request failure in a typical Java program. Not sure how Rust handles this.

Finally, choose that isn't tested (manually or automatically) is very unlikely to work. Maybe you can guarantee it doesn't crash, which is a much weaker guarantee, but I doubt even fully proven code (like seL4) is all bug-free before ever being run.


Rust's use of Result is very different from try/catch and exceptions in Java, even if you opt-in to checked exceptions. The big difference is ergonomics and what patterns are used in underlying libraries - opting out of the idiomatic way in Rust feels wrong if you try doing it.

Rust handles your out of range scenario the same way Go does.

If any of this matters to you, the good news is that Kotlin's sealed classes (and soon, Java's sealed classes) allow you to easily implement your own Result-like sum type.


So how did Rust handle the case of a function that may return an error or nothing? Can you forget to check the error, or does it force you to explicitly handle it in some way?

I understand how Result forces you to handle the possibility of an error when you want to access the actual return value, but I don't know what happens if you aren't planning on accessing the return.

I'm also curious how Result-based error handling composes. For example, if I want to sort a list of structs where my comparison function may fail, can I use the built-in sort function, and still get any error that may have occurred back?

With (unchecked) exceptions, this is trivially easy - the sort() function doesn't need to be aware of exceptions in order for them to be propagated through it. With checked exceptions in Java, you need to go through a little dance of wrapping and unwrapping, but it can still be done. If I understand correctly, in Haskell this can be done with the Either monad and liftM, though I can't claim to understand the specifics.

Is there a Rust solution?


You get a warning if you don’t check the error.

Yes, the default semantics is that it will stop when the first comparison fails and give you that error. You can write slightly different codebase if you want a list of all errors instead.


Rust will also crash if you use the indexing operator and go out of bounds. For arrays, you can also use the .get(index) method which returns an Option<&T> instead of &T, so it doesn't have to crash. For most things, iterators get used instead of indexing anyway.


> Because it encourages nesting function calls, which is harder for a human to parse than separate statements across lines.

I absolutely agree. Beyond the human parsing aspect it also makes commit changes easier to reason about and review. I want functionality to be limited per-line and view the ability to combine a lot of functionality into one line as a liability more than a benefit.

Go's error handling isn't carefree or hands-off, but that's because error handling is serious. Especially in network code and cryptography.


I thought it could lead to doing method chaining for a fluent like API which I find cleaner than how things work now.


I really dislike method chaining. I'd much rather have 5 lines than 5 chained methods. If that's too much to read, you can always encapsulate it in a well-named function.


But five functions that return a value and an error would each have to run the if err != nil dance whereas with method chaining it's cleaner


not if each one can fail. What if call #1 and call #3 can return the same error.. how does the caller know which one failed? This is the same as wrapping a bunch of calls with a catch (Exception) ... you lose context of what failed and can't behave differently for different failures. All you can do is perform a generic "something went wrong" behavior.


Interestingly you _can_ implement method chaining to require a terminal method call i.e. err := GetFoo().SetBar(1).SetBaz(2).Run() and then each chained method would set an error property in the object and if err!=nil then do nothing but return the object, and then the last method could return the error value.

That said, I am not a huge fan of fluent interfaces. I much prefer passing in a struct as an "args" parameter, in most case (but not all.)


That can easily be done right now with the current way of error handling.


Do you have any examples?


How returning (val, err) is error prone? It's verbose but it's clear and definitely not error prone. I spent so much time working with Java and useless giant stacktraces or with Python and people not knowing what to do inside a try / except.


Repetition and verbosity in a language can create errors in at least two ways. First, by the developer losing track of which error case is which (and/or copy-pasting error-handling logic) and doing the wrong thing in the error case. Second, by reviewers who have become trained to notice and gloss over error-handling boilerplate not noticing when there's something wrong with a particular case.

Concise languages can be more challenging to read because you have to understand more about each symbol/word in the language. But verbose languages can be more challenging to comprehend because there's a lot of symbols which don't signify anything.


Interesting perspective. Are you expressing an opinion about "explicit is better than implicit", or is your point on a different axis?

I suppose concise / implicit is fine when the thing that's being hidden can't go wrong, like in:

[i * 2 for i in 1...10]

The loop counter increment logic can't possibly go wrong, so it's fine to not think about it.

Regarding error-handling, don't you want to think about? If you're calling a() followed by b(), what should you do if a() fails? In some cases, b() shouldn't be called, but in others, it should, like deleting a temp file. And if you have to think about it, it's better to be explicit?


My preferences are that error handling is expressed in and enforced by the type system (Haskell's Maybe/Either, Rust's Result), that common error handling tasks be supported by the standard library and by specialized syntax when necessary (Haskell's many Monad/Applicative tools, Rust's "?" operator), and that if a developer neglects or chooses not to handle an error that the most likely outcome is that it bubbles up to some kind of top-level crash handler that terminates the current task and produces whatever useful diagnostics are possible (exceptions in many languages).

To put it more simply: yes, the developer should have to think about what they do in the case of an error. And then the amount of work they do -- and the code produced, and thus the work reviewers have to do -- should be proportionate to how unusual the necessary error handling is. When I see explicit error handling, that signals to me "hey, this is an important case that we need to handle in a particular way".


the amount of work they do -- and the code produced, and thus the work reviewers have to do -- should be proportionate to how unusual the necessary error handling is.

Great comment. I would add how unusual _and critical_.

One of the things I love about Python is that while I know errors can occur on practically every statement written, I only have to add error handling for likely / expected / critical errors. Any unlikely errors that occur, even in production, will show a detailed stack trace (lots of context), making them easy to fix.

In my experience, things work as expected 98% of the time. For some software, like a pacemaker, checking the execution of every single line of code and even having redundant error checking is not overkill. For other software, like the backup software I work on, having one customer out of 1000 get a weird error is something I'd rather deal with as a support ticket rather than having to anticipate it while writing code.

Of course error handling is important, but requiring 3 lines of error handling for every 1 line of actual code has kept me from investigating Go to replace Python for HashBackup. I'd love to get the performance increase, but not for a 4x expansion of LOC.


Honestly when I look to rewriting a python thing to be 'faster' either try using PyPy first, or rewrite it in OCaml instead. OCaml is extremely simple, similar to python in a lot of ways (GIL and all), but runs at native code speeds, near C the majority of the work, and super easy to bind to C libraries if you need.

Or try Rust. ^.^


Concise and implicit are kind of different axes. For example, Python's "x += 1" is more concise than AppleScript's "set variable x to x + 1.", but the exact behavior of the statement is just as clear from reading it, so it is no less explicit.

In this case, I don't think anyone is arguing that error handling should be implicit. They're saying there should be an explicit way of saying "handle this error in the common way." This actually makes the distinction between common and uncommon cases more explicit, because their differences aren't buried in boilerplate.


You're using defer to close the temp file either way.


you have 'catch' to handle those cases. Try and catch are easy to notice when scanning the codebase.


Copy/paste is very error prone. Golang code is full of it. I see lots of similarities between Visual Basic and Golang, incl. the passionate communities behind the languages.


Curious, do you see other communities that are not passionate about their languages?


With the difference that Visual Basic is an academic language full of needless features from Go's community point of view.


Visual Basic is hardly academic; they is a tremendous amount of line-of-business code that has been written in VB over the past 25 years.

But yeah, Go dev do not see VB's features as being "features."


The point was that many of VB.NET features are what many in the community attack as being academic and not worthy of being adopted by Go.


Given that I have worked with VB long before .NET even existed, I have a broader view of it than just the more recent criticisms.

VB's biggest problem IMO is that it tried to compete with C# instead of maintaining its original raison d'être which was to be a highly productive tool that required very little programming skill to be able to start building real solutions for business use.

But I digress...


It's error prone in that you aren't forced to handle the error. In languages such as Rust or Haskell, you have a Result type which can either be an Ok(val) or an Err(err). In order to "unwrap" a Result, you have to check the error case. Basically there's a compile time guarantee that errors are handled.


I'm not a Rust expert but afaik Rust doesn't enforce error checking since you explicitly need to unwrap(). It's very possible to panic because you forgot to check something.

It's similar in Go since you can't compile with unused variable so you need to explicitly discard the error with _. Ex: result, _ := func() This is for multi-value returns, for single value you can even omit the _

https://golang.org/doc/effective_go.html#blank


Having unwrap() in your Rust code is like littering your code base with panic(). It’s not appropriate to use in most production code, but is convenient in prototypes, examples and tests.

Your example re Go errors is incorrect. The go compiler allows you to ignore errors in returns without any compiler error.

For example

err := doThingThatErrs()

and

doThingThatErrs()

are both valid Go code.


There's nothing wrong with unwrap. It's just an assert. Even a[i] is just shorthand for a.get(i).unwrap(). Asserts are definitely appropriate in production code, just not for handling run-time errors.


Then how is this better than anything else? Except for syntactical sugar for:

Try Return [Bla(), null] Catch err Return [null, err] End

I do like this syntax better, bc the different scopes cause a lot of nesting


My example is correct I explained all of that, multi values -> need to omit, single value can ignore everything.


Yup. I misspoke. We’re both right.


I don't particularly mind try, but would prefer that they addressed the boilerplate which is actually annoying to type out (the convention is to return zero values and the error (annotated or not depending on what other functions have already annotated it)):

if err != nil { return ...,...,err }

If there were a shortcut for returning that error + zero values without interfering with the function call which produces the error (as try does), I'd prefer it. Something more like check(err). We'll see what they come up with next though to try to address this.

I can't say I've ever had problems tracking down an error, not sure what you mean about default values - surely if you check the error you won't use the values returned. My only problem with go error handling is the verbosity, which isn't a huge deal.


I don't see how allowing ergonomic features like try into the language would hamper this. For example, Rust also represents errors as return values — you know exactly where and how things could fail just by looking at the code for a function — but it still has the equivalent of Go's proposed try.


Did you know where every page fault would happen, and manually check every memory access and fix the situation?

You didn't have to because there is an precise, robust non-checked exception handling system which takes care of that: the hardware catches the situation, dispatches a handler in the operating system which fixes it and re-starts your program at the original machine instruction to try the memory access again.


You do not have to be precise in Go either, and you don't have to know all the faults. All you have to know if where a return value that implements the interface `error` is not nil.


> Literally the first non-trivial code I wrote in go… I knew exactly where and how things could fail and where things were failing just by looking at the code.

Could you give an example?

I think you’re talking about something different than what I’m understanding. One of the major frustrations I have with Go error handling is the lack of stack traces, which means I often have to modify code in order to find out where an error occurred.

I’m pretty sure that’s not what you’re talking about, though.


This is interesting, I didn't feel that with go I understand my code better, but I also don't think I was lost in other languages.

My impression of go, is that it is very boring to program in it, and some decisions weren't thought well. For example if you use anything else for numbers than int, int64 or float64 you will have very bad time. Lack of generics forces you to duplicate your code, duplicating increases chances of errors and make it harder to fix bugs. The errors are passed as values, but then you need to use different return value to pass them, defeating the whole point of having that. On top of that the language is very rigid.

I'm wondering if introducing macros could solve a lot of those issues.


>I knew exactly where and how things could fail and where things were failing just by looking at the code.

Same could be said about assembly language.


> I knew exactly where and how things could fail and where things were failing just by looking at the code

Don't you just mean you knew where fatal exceptions could be raised? That's substantially different from "fail".


I'm never more confident than when I'm a newbie.


> I think the Golang community is going to find itself growing more slowly as folks increasingly realize other options offer similar benefits without the bad ergonomics.

Personally I enjoy writing Go code similar to the way that I enjoy writing Python code. So they did something right because I wouldn't ever say the same about Java or PHP.

Edit: As a fun tangent... Python experienced the same kind of fracturing that adding generics or other dramatic features to a language can cause with the 2to3 move. There are still projects based on 2.7. It was hard to get people all on the same page once the community was split. What's also interesting is to watch the same thing happen with asyncio, which is practically an all-or-nothing thing to use in your Python project, so much so that most Python code still doesn't use it. Just an observation, but breaking changes to a language can really do a number on the community.


I don't think folks are on 2.7 because they love it. I think they're there because it's difficult and expensive to migrate Python code.


There were multiple reasons based on my experience. New people were still using 2.7 tutorials because there was way more information out there than for 3. And package managers in Linux systems took a long time to change.

But the asyncio example is probably a better example of a fundamental program design change that's causing weird fracturing and incompatible library designs. Trying to use non-async from an async framework is a mess, trying to use async from a non-async is hard to integrate, etc.


I don't believe that python 2/3 change has much to do with sync/async issues. In python 2 we had twisted which was its own async universe. The sync and async worlds are disconnected in pretty much every language, but it's mostly a non-issue. You just don't mix them and it doesn't become a mess.


That's not what I said, they were two times that the community fractured.

There is fracturing with asyncio. Look at popular libraries. The "requests" library comes to mind, but there are others. It's hard to use different 3rd party components between the two paradigms which causes fragmentation in the libraries. You end up with aiohttp-requests and stuff.

PS: it does make a huge difference. If someone posts "how do I make a web server?" on a forum you'll get two different frameworks and approaches as answers. The community basically goes in two directions and much of the work becomes incompatible. The multithreaded sync vs event loop async just don't play well with each other. There still aren't many 3rd party libraries that really focus on asyncio because much of the community is still in the "threaded" design.


I don't know about love, but it's significantly better than python3. I'm on 2.7 because the authors refuse to continue development of python in favor of a different, inferior language. Difficulty and expense of migration have nothing to do with it.


pg wrote a bunch of really good essays about Java vs Python vs Lisp and how those were perceived by programmers. I always remember them when I see Go being compared to Java while still being liked by Hackers. I wonder what he would write about this phenomenon.


pg has a blind spot wrt Java. Lots of hackers liked (and still like) Java ecosystem (see Kotlin for more recent fun). like lisp hackers, they hung out on their own and didn't mingle.


Java without generics and lambdas was terrible. Yegge's https://steve-yegge.blogspot.com/2006/03/execution-in-kingdo... was spot on because the only way to pass an expression or a block of statements was to wrap it in an object and give it to something that knows which method to call.


Java’s success was built in those days though. Personally I liked the language just fine without those features, Java generics in particular make code a lot less readable IMO.

This is partly why I like Golang so much, they’ve been focusing on readability.

I will agree that Lambdas are better than anonymous inner classes.


>This is partly why I like Golang so much, they’ve been focusing on readability.

Speaking as someone who's just returned to coding after a two-decade absence (mostly for fun), this is the #1 thing I love about Golang. I can actually read code from very experienced and skilled developers and understand what they'd written. And yet the language itself is highly capable - I don't feel like I'm learning some "beginner" language, but a "real" one that I can use for practical purposes.


This, but also I suspect a lot of people that claim to dislike Java are tainted by it's framework heavy ecosystem especially early-mid 2000s.

The language itself is fairly clean and very pragmatic.


FWIW the essays I mentioned are from the early 2000s. Java's Cover is from 2001: http://www.paulgraham.com/javacover.html


That essay has proven to be astonishingly wrong. It’s 18 years later and I suspect he’d say the same things about Golang.


Possibly, though Java these days is largely “Spring”.


Disclosure: I am not very impressed by Paul Graham as a programmer.

However I think he came by his dislike honestly. When he was actively working, Java was really quite frustrating.


I have no doubt. I was one of the pro-Java people in 2001 (dating back to 1996). Language religious wars were really strong in those days, like there could only be one true language, everything else was a toy. The Java backers and community fought against that too aggressively relative to their own limitations.

Today each language ecosystem is much more self sustaining and has its own bizarre culture that looks frustrating to outsiders.


Those essays were written a long time ago and reflected the situation at the time. I can't really say how accurate they were, but you're using the present tense and mentioning present technology, so I thought it would be worth to emphasize that.


I understand what you mean by “bad ergonomics”, but I think of those things as “ergonomics in the small”. You end up writing for loops and error checks. It’s verbose but not complex, and it’s all very localized. Further, people get really hung up on these small language issues and miss go’s killer features: simplicity and consistency. Go is a small, simple language with few surprises. No guesswork about which feature subsets to use. But simplicity and consistency are themes that runs throughout the developer experience. Everyone uses gofmt so everyone’s style is the same. The standard library comes with a testing library and everyone uses it. Also, there is only one test runner, it comes with the toolchain, and you can run it on any project without special project knowledge. There is only one build system and it doesn’t make you learn a new project configuration DSL to use it, nor does it require anyone to have a working knowledge of compilers or linkers. You just run “go build” and you get a binary, and it works on almost every project (some larger projects have FFI or code generation steps). Static linking is the default. You don’t have to configure a CI job to package your libraries and push to a package registry. Nor do you have to make documentation packages or operate a webserver to serve your docs (or push to a hosted server). And the documentation tooling doesn’t require you to learn a custom documentation markup language—it’s just comments. The GC has only a couple of tuning knobs. There is no dichotomy between sync libraries and async libraries (e.g., flask vs aiohttp) nor are there questions about which coroutine / thread / async frameworks to use. I could go on and on and on, but I think I’ve made my point. I think these are the features that people like about Go and are going to continue to drive its growth until other languages wise up.


Go the language is surprisingly complex and error prone compared to other GC languages because of its decision to allow explicit references. This adds an extra layer of semantics to nearly every aspect of the language.

For example, the semantics of the for-over-collection statement in Java are that it repeats the block of code, with the loop variable holding each value of the collection in turn. In Go, the equivalent statement also needs to document what it means to take the reference to the loop variable. Composite data types have semantics for taking references to sub-parts (struct and arrays/slices allow taking references, maps do not).

Slices and arrays are another complicated area of the language, with gotchas like append sometimes modifying the original array, sometimes not.

Regarding splits, you still have that in Go as well - do you use goroutines as coroutines, sending copies of objects through channels? Or do you use them as threads, with shared memory and locking? Do you use the testing package as is, with its lack of any user-friendly asserts? Or do you pick up an assertion library? Do you use raw http,or something la Gorilla? Sql or some ORM? Do you log to stdout, or do you pick up a logging library?

Regarding gofmt, I personally can't understand the passion some people have for enforcing a common style.

Regarding build tools, Go is hardly unique among modern languages in having built-in tooling for that. It's relative ease of deployment also means that Go's build tools can't be used alone in a multi-language project, so many of the apparent simplicity is only useful in a subset of projects that actually use only Go. Even then, if you have any other build artifacts you may find you need to reach for something other than Go's tooling. By comparison, Maven can easily handle a Java+minor bits-in-other-languages project out-of-the-box.

Not to mention that Go is probably the only language in any kind of popular use today that doesn't have a built-in way to interface with C code (you need to use a separate compiler if you want that!).

To me, Go has a single killer feature: binary and memory size for a GC language. And Java may be catching up on that area...


You largely missed the point of my post. I was showing that Go values simplicity and consistency. The examples I used to illustrate that were exactly that: examples. They were not a list of features that were novel to Go, as you seem to have interpreted.

> Go the language is surprisingly complex and error prone compared to other GC languages because of its decision to allow explicit references.

It's true that having value types in addition to reference types (while other GC languages often only have reference types) adds some complexity to the language, it's not much and it's still much less complex and error prone than other GC languages. Also, C# has value types and Java desperately wants them, so I think it's pretty clear that they're worth the extra bit of complexity.

> This adds an extra layer of semantics to nearly every aspect of the language.

I don't think this is meaningfully true. You have to think about whether a thing is a value or a reference type. This is exactly one bit of additional complexity.

> Slices and arrays are another complicated area of the language, with gotchas like append sometimes modifying the original array, sometimes not.

Slices are actually quite simple, but people run into issues because they expect them to behave exactly like Python lists or JavaScript arrays. Slices are views into an underlying array, and appending to a slice always modifies the backing array; however, if the append causes a grow, then the backing array is now a different array than the original. Of course, your point stands in that this difference can be frustrating and that frustration is a real cost--but you run into that cost once or twice and you update your understanding and rarely encounter it again.

> Regarding splits, you still have that in Go as well - do you use goroutines as coroutines, sending copies of objects through channels? Or do you use them as threads, with shared memory and locking? Do you use the testing package as is, with its lack of any user-friendly asserts? Or do you pick up an assertion library? Do you use raw http,or something la Gorilla? Sql or some ORM? Do you log to stdout, or do you pick up a logging library?

I never claimed Go makes every decision for you, only that there are fewer decisions and the happy path is more obvious. You always use goroutines as threads and whether you use channels or locks is a design question (different use cases). The standard testing library is the happy path. You can add on an assert library if you really need it (although you probably don't). Similarly you use the raw HTTP library until you really need something more (you probably don't). Stdlib database/sql package or ORM? Again, database/sql until you need an ORM (again, you probably don't). Standard library logging package or a logging library? Again, you use the stdlib (happy path) until your requirements outgrow it. Notice the pattern?

> Regarding gofmt, I personally can't understand the passion some people have for enforcing a common style.

It avoids wasting time in nitpicky style conversations in code review and makes things easier to read. You're welcome to your opinion, but that's the rationale.

> Regarding build tools, Go is hardly unique among modern languages in having built-in tooling for that.

I didn't claim otherwise, only that Go's standard build tool is yet another example of simplicity and consistency. And many languages don't have a standard built-in tool, and the ones that do are often complex. The only language with a nicer build tool IMO is Rust, and like everything Rust vs Go, the Rust build tool prefers pragmatic complexity to simplicity (a philosophical difference that I can respect).

> Even then, if you have any other build artifacts you may find you need to reach for something other than Go's tooling. By comparison, Maven can easily handle a Java+minor bits-in-other-languages project out-of-the-box.

I don't see why every language should have a build tool that can build it + small bits of other languages. A build tool should build that language well, and extend it for bigger projects using a wrapper tool like Make or Bazel depending on use case. Unix philosophy and all that.

> Not to mention that Go is probably the only language in any kind of popular use today that doesn't have a built-in way to interface with C code (you need to use a separate compiler if you want that!).

Every language (even C++) needs another compiler to compile C code before it can be called into. And once it's compiled, it's no more difficult to call into it from Go than from Java or C# or Python or etc. It's also often easier, since you can use C values directly in Go without needing to write shims (e.g., PyObject shims). That said, like all GC languages, it's fundamentally hard to correctly manage ownership for references across the C/$LANG boundary.


> It's true that having value types in addition to reference types (while other GC languages often only have reference types) adds some complexity to the language, it's not much and it's still much less complex and error prone than other GC languages. Also, C# has value types and Java desperately wants them, so I think it's pretty clear that they're worth the extra bit of complexity.

My point was not about value types, but about taking the address of some thing; neither C# (except the extremely rarely used `unsafe` subset) nor planned versions of Java have this ability. To be fair though, C# does have something somewhat equivalent - lambdas may capture a local variable such as a loop variable, in which case you do need to know if each iteration creates a new variable or changes the value of the same variable. Java doesn't allow this at all. Even in C#, you can't capture a part of a structure, so the complexity is more limited.

There are other aspects to Go's complexity as a language - multiple syntaxes for declaring a variable, multiple other ways of declaring constants, iota, named return variables, function scope VS local scope, the limits of what can constitute a map key, what kind of structs can be compared for equality,special syntax that looks like multiple assignment but isn't, and probably others. My point is that Go is not a very simple or consistent language. Java for example is still simpler. C# is more complex, but more consistent.

> I don't see why every language should have a build tool that can build it + small bits of other languages. A build tool should build that language well, and extend it for bigger projects using a wrapper tool like Make or Bazel depending on use case. Unix philosophy and all that.

The reason is tracking dependencies. If I have a pure Go project, I can rely entirely on modules to manage my dependencies. If I need one python package, I now need to find a new tool to declare my dependency and orchestrate things. With Maven, for example, I can just export the python package as a Maven module and keep using Maven for my entire build.

To me, a dependency management system that is strictly language specific is only useful as a starter tool - almost certainly, once you are working on a real project, you will drop that tool entirely and have to use soemthing else, as dependencies are a cross-language problem.

That said, it is nice to have a dependency tool available as you're getting your feet wet, so I shouldn't really be complaining that Go offers this.

> Every language (even C++) needs another compiler to compile C code before it can be called into. And once it's compiled, it's no more difficult to call into it from Go than from Java or C# or Python or etc.

First of all, I must admit that I was wrong about how Go handles CFFI. I was under the wrong impression that cgo is an alternative to the standard go compiler, and that you have to build your program with cgo instead of go build in order to be able to link to C code. Since I now understand that cgo is simply a tool to generate the necessary boilerplate to interoperate C and Go, you're right, it's actually much nicer than what Java or Pyhton offer. Note, I am aware that you need a C compiler to build your C code; I just thought that there are 2 Go compilers, one for pure Go, and a different one for Go plus dynamic linking to C.

C# is still simpler, since it doesn't need any external tool or C compiler - you simply declare the C# headers for the C functions you want to invoke, and annotate them to declare the shared library they should be searched in, and the CLR handles the glue for you.

---

There were a few other points, like gofmt where we simply have different experiences and I don't think it's productive to argue. There are others where I had misunderstood you as claiming Go is especially good at, and I understand your point was simply that it checks those boxes, which I agree with.


golang's "simplicity" (i.e. unexpressivity and weak modeling ability) translate into complexity in real world code bases. There's no way around it.


Those things do translate into some complexity in some real world code bases, but other features save much more complexity. On balance, Go comes out ahead IMO.


I think you described it well. The go designers spent a lot of time on the end to end full workflow, and not just on the language. And had production constraints in mind for working on a backend service in a team at a boring tech company.

That's also why I think Go will forever be a "to each his own" language. By targeting that lowest common denominator, choices are made for you, some people will always dislike it, and others will always love it.


>So long as we can all agree that it feels super bad, I guess this is fine.

Actually, I don't think everyone agrees it feels super bad. I personally like having all of my error handling be explicit, painfully explicit even.

>approaches the Java world back with checked exceptions where principle trumped ergonomics.

I also have to disagree here. To me, checked exceptions are the worst of both worlds. Here you have additional language features, but nearly the same verbosity. Your 'happy path' must be surrounded by try blocks. Worse, everything that happens in a try block is 'flattened.' Not only does this mean you may need many try blocks, sometimes it can even be difficult to take individual statements and put them in one try block. For example:

    try {
        doThing(doOtherThing());
    } catch(...) {
        // What happens if doThing and doOtherThing throw the same exception? Do I have to use a temporary variable?
    }
Also, exceptions have been overloaded to handle everything, including runtime errors. I think this is more subjective but I strongly dislike it. I do think runtime errors should be possible to handle, just ideally through a separate, more explicit paradigm.

    try {
        doThing(blah[0]);
    } catch(...) {
        ...
    } catch(IndexOutOfRange) {
        // Handling runtime errors at the same level as application-level errors!!
    }
Go doesn't even really force you to check your errors, it just makes it harder to accidentally not check them. Like:

    result, err := doThing(); // Error
    return result

    result, _ := doThing(); // NOT an error
    return result
I think this is excellent. It may lead to complaints about an annoying compiler, but most importantly it leads to fewer mistakes. You can still explicitly tell the compiler to shut up, but it is obvious.

Language ergonomics are complicated, but the benefits of Go's approach are hard to deny. Unfortunately, nothing comes without downsides, and it seems like solving the error handling ergonomics issue is a tough one. I think repetitiveness aside, the Go error handling ergonomics are great, and that's exactly why they attempted to reduce repetitiveness. But, in reflection, a lot of that repetitiveness can also be reduced by refactoring your code, so it may not even be quite as bad as it seems.


> Worse, everything that happens in a try block is 'flattened.' Not only does this mean you may need many try blocks, sometimes it can even be difficult to take individual statements and put them in one try block.

Go has exactly the same issue. But with exceptions at least you can group multiple statements together and handle them with one catch block. With Go you have to use multiple if blocks to get the same semantics.

> Go doesn't even really force you to check your errors, it just makes it harder to accidentally not check them.

Go doesn't make it harder to accidentally not check errors. If you call a function that only returns an error, such as os.Mkdir(), then the compiler will not warn you when you forget to handle the error.

> Language ergonomics are complicated, but the benefits of Go's approach are hard to deny.

I don't really see any benefit to Go's approach over exceptions or result types. If making it obvious that errors are handled is important, there's a solution for that that's much more elegant than if-err-nil blocks everywhere. It's precisely the solution that the Go community just rejected.


>Go has exactly the same issue. But with exceptions at least you can group multiple statements together and handle them with one catch block. With Go you have to use multiple if blocks to get the same semantics.

Go does not suffer from this issue at all. I am not talking about flattening from the call hierarchy, I am talking about flattening the try scope itself.

(In case it isn’t obvious: in Go you’d be forced to separate two error checks. But it results in still less cumbersome code, since you only need scoping for the error handling portions.)

But forgetting that, because passing errors down in Go is explicit, it is actually customary to use error wrapping to add context as an error is propagated, which does allow for more precise error handling actually.

>Go doesn't make it harder to accidentally not check errors. If you call a function that only returns an error, such as os.Mkdir(), then the compiler will not warn you when you forget to handle the error.

Go vet will do that. It is a good 'first step' to configure when setting up your CI/CD (many will do it by default.) Go vet's 'unusedresult' checker does this.

>I don't really see any benefit to Go's approach over exceptions or result types.

It is a simpler language than Java. You get most of the benefits of checked exceptions without all of the calories from exceptions.


> But it results in still less cumbersome code, since you only need scoping for the error handling portions.)

There isn't a meaningful difference in cumbersomeness between having two try-catch blocks and two if-err blocks. There is a meaningful difference in cumbersomeness between what Go has today and "try foo(try bar())". Which is why it's so unfortunate that the community killed the try proposal.

> But forgetting that, because passing errors down in Go is explicit, it is actually customary to use error wrapping to add context as an error is propagated,

People say this, but in practice any code search reveals that "if err != nil { return err }" is everywhere. I believe that many Go projects aspire to annotate all errors, but much fewer actually do.

Ironically, the nice thing about exceptions, as well as Rust error chaining crates, is that they do this automatically, so in practice programs written in languages with those features tend to have better error diagnostics than Go programs do. Computers are better at doing things consistently than humans are.

> It is a simpler language than Java. You get most of the benefits of checked exceptions without all of the calories from exceptions.

Go is a more complex language than Java is overall, because of all the special cases the language adds to magic types like maps and errors that are just part of the library in Java.


> any code search reveals that "if err != nil { return err }" is everywhere

Code searches in languages with exceptions tend to wrap the tryblocks around massive portions of code instead of the individual function calls to the point that you have a top level doing:

  try:
      ...program here...
  except:
      print('¯\_(ツ)_/¯')


But that is exactly how error handling usually works, especially if cleanup is handled separately - you just need to propagate the errors, usually all the way up to the user, who is the only one who can take a meaningful decision.

Almost all actual error handling in code is either error translation and re-throw, resource cleanup, or automatic retries (sometimes you retry the same request, sometimes you try a fallback option, but it's still the same idea).

The user however may be able to check and actually fix their internet connection, they may fix the typo they did in the config file, they may call support to see what's happening to the database etc. - your program can't do any of these things.

That's why exceptions work so well in most languages, especially GC languages where you have dramatically fewer resources to cleanup: they bubble up automatically towards the initial caller, which is often the user. Threading messes with this, but if you use the more modern async style (async/await in most languages) you get proper exception propagation even then.


> That's why exceptions work so well in most languages

But there are many people including some well-known and highly-respect people who believe that exception handling in fact does not actually work well:

- https://www.joelonsoftware.com/2003/10/13/13/ - https://blogs.msdn.microsoft.com/larryosterman/2004/09/10/st... - https://www.atlassian.com/blog/archives/exceptions_are_bad - https://stackoverflow.com/a/1736320/102699 - http://xahlee.info/comp/why_i_hate_exceptions.html - http://www.lighterra.com/papers/exceptionsharmful/

#justsaying


... I could do this with Golang too, couldn't I?


On the opposite end of the spectrum, yes, as they pointed out you can just return all of the err's up the stack.

I wasn't saying that Go's approach solves this, just that it's not a problem unique to Go.

And in the case of Go it's painfully obvious that you're ignoring all of those errors whereas in other languages you can't always tell, visually, that they're being ignored because of the magic of exceptions.


>There isn't a meaningful difference in cumbersomeness between having two try-catch blocks and two if-err blocks. There is a meaningful difference in cumbersomeness between what Go has today and "try foo(try bar())". Which is why it's so unfortunate that the community killed the try proposal.

    Object thing1;
    try {
        thing1 = doStuff();
    } catch(SameException e) {
        // handle error 1
    }

    try {
        return doOtherStuff(thing1);
    } catch(SameException e) {
        // handle error 2
    }
vs

    thing1, err := doStuff()
    if err != nil {
        // Handle error
    }

    thing2, err := doOtherStuff(thing1)
    if err != nil {
        // Handle error
    }

    return thing2
Happy path is flat. Control flow is obvious and simple. I have not much more to add.

>People say this, but in practice any code search reveals that "if err != nil { return err }" is everywhere. I believe that many Go projects aspire to annotate all errors, but much fewer actually do.

https://github.com/search?l=Go&q=%22errors.Wrap%22&type=Code

>Ironically, the nice thing about exceptions, as well as Rust error chaining crates, is that they do this automatically, so in practice programs written in languages with those features tend to have better error diagnostics than Go programs do.

Rust is a different ball game. Rust does not try to be simple. It comes at its own costs. (I like Rust too.)

>Computers are better at doing things consistently than humans are.

These empty platitudes come up frequently when debating language decisions online. But, it's so meaningless in so many dimensions. I mean, we could also 'use the computer' by adding C macros on top of Go and use them to reduce repetitiveness, but I don't think many people will applaud you for it. Simply applying computer code to solve a problem does not constitute good design.

Go's proof is in the pudding. It's been extremely reliable for me in real world applications.


  doOtherStuff(doStuff())
is the happy path, but it's been hidden among lines of noise that do nothing more than return to the callers who know what to do. Generating this using cpp or m4 would suck, but it's still better than not generating it due to wasted effort (especially re-reading) and mistakes.


How often do you actually need to handle those errors differently? In my experience, it is vastly more likely that a function which can throw errors in Java looks like this:

  Stuff foo() throws SameException {
    return doOtherStuff(doStuff())
  }
Whereas in Go the exact same function must be written like this:

  func foo() (Stuff, SameException) {
    thing1, err := doStuff()
    if err != nil {
        return err
    }

    thing2, err := doOtherStuff(thing1)
    if err != nil {
        return err
    }
  }
Propagating is by far the most common "error handling" and Go constantly makes you break the flow of the code to do it.


FWIW when debugging i prefer the second style if for no other reason than that i can place a breakpoint in doOtherStuff while skipping doStuff. Also reading it, it is more obvious that the code calls both doStuff and doOtherStuff (though with just two calls it isn't a big different, imagine having a 2-3 more calls in there).

(also why debuggers still insist on line-based breakpoints is beyond me, why can't i right click at a call and put breakpoint at the call itself instead of the line where the call lies on?)


You could also write the first function body as

  Stuff thing1 = doStuff()
  return doOtherStuff(thung1)
It's still easier to read without the explicit error handling.


After working with Go for a while I actually find code with error handling easier to read because it is more clear to me what it happening. I look for the error handling as an indicator that the code can actually fail vs. code that cannot fail.

And maybe it's because I've developed a very consistent pattern, and when I find that there are too many errors to handle in a function it helps me realize the function is probably doing too much and needs to be split into multiple functions.

IMO it is all about training your brain and being accepting enough to work in the dialect of the land, as opposed to demanding to speak English when living in France. :-)


The fact that it allows for more nesting on a single line is a bug, not a feature, IMO.


I like your points about diagnostics. I definitely feel this—it seems like one of Go’s weakest points. That said, Go is still one of the best tools available for building software today.


[flagged]


I'm not saying you should use Rust! I'm saying that Go should add try.


I was one of the many who argued strenuously that they not add try(). I spent an entire day writing up my reasons who I posted to the ticket on GitHub.

Bottom line, try() had too many flaws — especially related to consistency with the rest of Go — to be an appropriate addition to the Go language.

Adding try() to Go would be like adding a disco ball to the Sistine Chapel. Sure, disco balls have their place, but some places are just not appropriate for disco balls.

Or as Yoda says:

"Do or do not. There is no try." (Fortunately!)


> Your 'happy path' must be surrounded by try blocks.

Not if you're doing it right, which means bubbling up (read: adding the exceptions to the "throws" clause) exceptions that you can't handle _then and there_. You leave the "real" exception handling to the code that's closest to the end user and can actually handle the error in a meaningful way.

For example, in this pattern when you implement the business logic in a REST service, generally there's no catching at all, instead the exceptions are declared as rethrown and way up the stack you would have a global catch all that serializes the errors into JSON and sends them to the user. Simple. In Go you're forced to do "if err{}" checks everywhere, particularly in your endpoint's business logic. It's actually _way less_ verbose and burdensome in Java if you do exceptions the right way.


> Go doesn't even really force you to check your errors, it just makes it harder to accidentally not check them. Like:

I dislike it because it does the opposite, it makes it too easy to accidentally continue execution when there is an error:

    doThing(); // Error
    doOtherThing();
Which isn't possible with Exceptions.

The only thing that would signal that error handling is missing is the absence of boilerplate to handle it, which is an ugly UX for a language to rely on esp given there's no indication from scanning code that all methods that return errors are handled.


The nice thing about a statically typed language having standard golint and gofmt is that code is generally self documenting. So I wouldn't ever type that function call without seeing the function definition (in my editor or wherever else I found the API reference).

But I agree, the fact that Go allows this is bad, imo. It would be better if you had to explicitly suppress errors, even with something like this "_ = doThing()" just to make it harder to miss.


> So I wouldn't ever type that function call without seeing the function definition (in my editor or wherever else I found the API reference).

How often do you handle or explicitly ignore the error returned by fmt.Print? This is just proof that golang error handling is error prone.


I don't know go, so I'm confused here. If nothing failed, the error is just made silent? The program will move to doOtherThing, as if nothing failed, and everything will move forward?


The error was just ignored. if doThing() had some side effect that doOtherThing() depended on then you will never know why doOtherThing() isn't working the way you expect it to be.


Ya that seems pretty unsafe to me. I'm not sure then why others suggest the Go error handling makes things safer. Silent failures have always been some of the most impacting issues in the systems I've maintained. They cause slow corruption and they take a long time to be found, at that point, the damage is done and hard to revert.


> Go doesn't even really force you to check your errors, it just makes it harder to accidentally not check them.

Quite the opposite. You have to explicitly ignore exceptions, whereas it’s easy to accidentally miss an error in Go. For example, how many times have you seen this?

    defer f.Close()
Close() returns an error; this code ignores it.


To be fair, reporting that cleanup also failed with a different error is inherently complicated, and so many people got the try-finally version wrong that Java finally added try-with-resources and Throwable#addSuppressed.


Definitely true. Mostly I just avoid using defer with Close, but I sometimes end up writing terrible stuff like:

    defer func() {
        err2 := f.Close()
        if err == nil {
            err = err2
        }
    }()
Not perfect, but better than nothing. I could use Wrapf to add information is err is already set. :-p


It is not difficult to accidently not check them. Like:

  result, err := doThing()
  if err != nil {
    return nil, err
  }

  result1, err := doSecondThing(result)
  return result1, nil
Will not trigger a compile time error.


A case could be made that the syntax is somewhat nicer but you're paying for that by losing proper scoping and typing without typed exceptions.

With the amount of boilerplate in go that is simply about bubbling up error codes, it really doesn't seem that much cleaner than Java exceptions. An if block just as many lines as a catch block but again, you lose the typing.

And the more I think about it, the more I think well written Java is cleaner than well written go simply because the throws keyword leads to strictly less boilerplate. I feel like this stigma is simply about how much bad Java exists (and there's a lot). I can't help but feel like a WebSphere written in Go would be just as ugly as its current Java incarnation.


We don't all agree that it feels super bad. I think you are completely wrong on the ergonomics. Go's error handling looks ugly to people used to languages that try to _hide_ error states, but it's so much easier to work when you are unfamiliar with the codebase or when it's been a few months since you last touched the code. Clarity of unfamiliar code is part of language ergonomics as well.

I'm not sure I follow your argument about Go "approaching" checked-exceptions in Java. Nothing is being forced on you. Go has worked the same way for a decade now. There is a lot of hugely successful software written in Go. In all that time, errors have worked the same way. In terms of choosing principles over ergonomics, in fact the Go committee chose ergonomics over the mistaken principle that "explicit error checking is so ugly and onerous that it's worth adding magic behavior and removing clarity".

In any case, no one is arguing we shouldn't or couldn't improve Go's ergonomics. Rather, the conclusion from people who work with Go every day and are happy with the tradeoffs for their particular projects was that this specific change would make more things worse than it made better.


So long as we can all agree that it feels super bad, I guess this is fine

Exceptions feel super good, even as you're taking too many shortcuts and glossing over things. Proper handling of the unhappy paths is often going to feel like a slog, because it often is complicated, and it's often a slog. Glossing over error handling in golang can feel bad exactly when it should.


> Exceptions feel super good, even as you're taking too many shortcuts and glossing over things.

I'm not sure I follow this talk about "short cuts.". Exceptions provide stronger guarantees by default than multiple-value-bind and manual if statements. Your code fails closed as opposed to open.

> Proper handling of the unhappy paths is often going to feel like a slog, because it often is complicated, and it's often a slog.

I'm used to languages where this generally isn't the case, but sure. Why twist the knife in the wound though?

> Glossing over error handling in golang can feel bad exactly when it should.

When you capture but fail to check an errval and an untraceable default value starts rattling around inside your code causing unexpected behavior? It's like an NPE but worse because it won't immediately trap. It's more like a double free.


go vet catches that scenario and won’t pass CI. You have to explicitly decide to ignore it by using an underscore for the err value and then you get just what you asked for.


Sure, but it doesn't catch the case where you have multiple error variables in scope and you refer to the wrong one in one place but not the other or if you reuse an error.

I've seen that happen more than a few times. Some of it in new production code.


fmt.Println says otherwise. That and it's possible to overwrite previous error variables, something that is not possible with exceptions.


In some contexts, exceptions are just cleaner and easier. If I’m writing a web backend, my error handling is going to almost always be, “stop trying to do things and generate a 4xx/5xx response”. Throwing an exception from anywhere and then handling it at the top of the request handling does that without having to tediously carry errors all the way up the call stack.


In some contexts, exceptions are just cleaner and easier.

Definitely. The question is, are the benefits in those contexts worth the cost of the potential abuses? As a codebase and company get larger and larger, the higher the probability that a problem will get into the codebase.

There is an analogy here with C++ templates and with method_missing meta-programming. There are some contexts where they make things a lot easier. However, in a large codebase, the probability that some programmer, somewhere is going to cause a costly problem with a bit of nifty overreach goes up and up.


While true, HTTP services in particular are a very, very common context.


Then use panic() and recover() for that special type of use-case. Just as easy as exception handling.


That's just exception handling with extra steps! ;)


But then you have to spend lots of extra time trying to discern between the errors that were okay, and the errors that actually indicate flaws in your code.

As they say, an ounce of prevention...


It never really seemed like that to me, to be honest.

Any error that I have forgotten to handle is a bug, in the sense that it was a bug to have forgotten to handle the error in the first place. So the standard behavior of "treat every unhandled exception as a 500 response" is technically correct there. Upon consideration, some errors are actually so rare and/or catastrophic that I don't even have to worry about handling them because I wouldn't do anything different anyway. For example, suppose I have a database-backed service that has no meaningful failover if the database is down, and it throws an exception whenever the database is down. There is no reason for me to have to handle that error.

There are other error cases where I have to stop handling the request but it isn't my fault. So if my service has an endpoint that expects a JSON POST body and the body doesn't even parse as JSON, maybe I need to handle that so it throws a 400 instead of a 500. But I could just wrap that exception with a custom class that my error handler generates a 400 response for, and then the rest of my code could just go on assuming that the JSON did parse and that it was valid.

Probably the closest equivalent would be if you had a forking web server where each request had its own thread, and you could effectively kill the thread with a response object at any time. It's not really a case of "using exceptions for flow control" or whatever; it's more a case of "this entire unit of execution is finished and can kill itself just as soon as it's done leaving a note".


> I think the Golang community is going to find itself growing more slowly as folks increasingly realize other options offer similar benefits without the bad ergonomics.

I'm curious about what you think these other options are? There are of course tons of languages and you can find just about any set of features you want, but the combination Go has along with the momentum, community, etc. it has means that there are not many that can compare in its niche.

Ignoring the language/semantics (ie. the bad ergonomics) and just focusing on the external aspects. Which languages targets the same niche that have the "similar benefits" of fast compile times, static builds, great tooling, good/improving libraries, easy deployment, and mainstream backing/acceptance?


There are competitors with just as much momentum and community, if not more.

Kotlin is now the official language of Android, and is being very rapidly picked up all across the Java ecosystem. Oh but it also compiles to JavaScript and LLVM native binaries if you don't want the JVM.

The syntax and ergonomics of Kotlin are great. Really nice language. The compiler is also very fast, just as fast as Go's I believe. It does incremental compilation and all the mod cons.


> it has means that there are not many that can compare in its niche

Which is? "devops"? Otherwise, offerings on the JVM and .NET are strictly superior. And now with native compilation being available for both platforms, they will also be a better fit at devops than golang. Other alternatives include Rust.


I would say the niche is basically clouds systems programming and general server side apps.

JVM based language can't compare due to the JVM. The last thing you want for that type of programming is a huge, complex virtual machine to worry about. That plus memory issues, slow compiles, complex deployments makes it not really in the running.

The .net based languages are a bit more interesting, but they have a _lot_ of ground to make up for due to their roots. Not having their roots in the native OS/culture of the internet will make it tough road.

Rust is nice but has its flaws as well. I think it will end up nailing a lot of use cases where C++ would have been picked before. Go really targets what JVM based languages or things like Python+C would have been used for... not C++. That is, I think it is really a different niche.


The last think you want for general server side programming and cloud systems programming is Java? Because of the JVM?

You should probably tell Google and Amazon that. Both very heavy users of Java on the server, despite Go. In fact the server space is where Java is most dominant!

The JVM is not actually complicated to use, especially not in the latest versions. It has the same number of 'default knobs' (that you should probably tweak in production) as Go does, but they're simpler to understand, for instance, Go requires you to control GC overhead by specifying "heap overhead" whereas Java just lets you specify a pause time goal directly, which is what you actually care about.

As for slow compiles, where did you get that idea? Java is extremely fast to compile. You can compile hundreds of thousands of lines of code and start executing them in seconds. I wonder to what extent you really used the ecosystem at all, with comments like that.


> "The last think you want for general server side programming and cloud systems programming is Java? Because of the JVM?"

Yes, for the smaller companies that do not have the effectively unlimited resources of Google and Amazon. Go is just so much easier to work with and deploy for my tiny company.


> The last thing you want for that type of programming is a huge, complex virtual machine to worry about. That plus memory issues, slow compiles, complex deployments makes it not really in the running.

Sorry, but I don't buy this at all. golang also ships with a runtime, that's why binaries are dozens of MBs.

The JVM is very configurable, which is what I assume you mean when you say "complex". This configurability is what allows it to be tuned to the task at hand. You can cap the maximum memory it's allowed to use, try out different GCs based on your use cases, etc.

Compilation times are not slow the least bit, especially with incremental compilation. Incidentally, for the majority of the time, it takes way less time from when I press the run button for a Java/Kotlin project in the IDE to having it up and running, compared to projects I worked on in golang, where the entire code base has to be rebuilt.

People keep repeating things like "memory issues" or "complex deployments". For a long time now, people have been shipping self-contained "fat" or "uber" jars. Running them is as simple as `java -jar program.jar`. And since the JVM defaults to G1, memory usage is generally less - the JVM is tuned for performance at the expense of memory saving, but that can obviously be changed, even more so recently with GraalVM[1]. Java has already been used on embedded systems (including card readers) - even Spring that has a reputation for being "bloated" can run on a Raspberry Pi.

[1] https://quarkus.io/


> "Running them is as simple as `java -jar program.jar`."

That is several steps too complicated:

1. Do you have Java installed? 2. Is it in the path? 3. Do you have the Required Version of Java installed?

None of those need be issues with a self-contained compiled Go program. And that is one of the key reasons we chose Go over any other language.

(And for the other languages that can do the same, Go was the easiest to learn and potentially master.)


I think it is unfair to label Go with "bad ergonomics."

It might be fair to say the ergonomics are suboptimal for the code _writer_, but I would say they are much closer to optimal for the code _reader_.


Yeah but as a prospective Go user, the fact that the language is opting for pain turns me off.

Enjoy your ecosystem, I'm going to continue to use languages that actually like me, and care how my experience is.


I would say you can't know if it will cause you pain until you try it. Using the metaphor, some people actually like certain types of pain, and pain is something that is very hard to understand simply by listening to the complaints of others.


It is all fun and games until someone at your job introduces Go into your ecosystem unfortunately then you're sort of stuck.


You can't compare it to Java checked exceptions. Java checked exceptions are one of those billion dollar "mistakes". It is the single worst thing in Java for me. Checked exceptions add nothing and break almost everything around clean design & code.

For that comparison to work, java would have needed to only have checked exceptions, with some added language features to deal with them cleanly, and then it might actually have been a viable design. Littering your code with try-catch is what makes exceptions infeasible for error handling.


> It is the single worst thing in Java for me. Checked exceptions add nothing and break almost everything around clean design & code.

Should I compare it to "Golang Dependency Hell Claimed My Project?"

> For that comparison to work, java would have needed to only have checked exceptions, with some added language features to deal with them cleanly, and then it might actually have been a viable design.

Why? That's not what happened with Golang's error handling?


> Should I compare it to "Golang Dependency Hell Claimed My Project?"

As someone just learning Go, what is Golang Dependency Hell?


It's not a problem you have to deal with but those of us from the world before bundled vendoring list projects because of Go's creators not having sympathy with people not living in a single global monorepo universe and most of its audience not having the discipline to vendor stuff by hand.


google: it's a community project, we swear

also google: we don't have that problem at google, so it's not a problem


As someone who started Go when Modules were already available, I don't think you'll run into it (unless you try to use older packages.)


> Should I compare it to "Golang Dependency Hell Claimed My Project?"

Of course you can. But it would be hilarious considering Java Module system (Jigsaw) which took decade in making still does not support versioning. People are left to use bloated crap like Maven or Gradle. But since these products are called "enterprise grade" developers are not supposed to call them crap that they really are.


Ah yes the good 'ol "bloated crap" argument developers love to fall back on when they don't know what they're talking about.

What exactly is "bloated crap" about maven? It works exactly as advertised and a heck of a lot better than go modules/vgo.

If you say XML you lose (it is a config fmt get over yourself...). If you say it downloads the world I suggest you look at the output scroll by on any decent sized project in Go land.

So enlighten us all please with your wisdom.


> If you say XML you lose (it is a config fmt get over yourself...).

"Besides that Mrs. Lincoln, how was the play?"


My most hated checked exception is the IOException Jackson throws when parsing JSON I can guarantee to be valid JSON.

I could live with a runtime exception, or perhaps an API that differentiates parsing "potentially not-parseable JSON" and "JSON I can vouch for".


I usually end up writing a bunch of utility methods for stuff like this. Typically it's called something like `parseQuietly`, and it just Pokemons any exceptions.


I am stealing Pokemon as a verb, my thanks.


> Littering your code with try-catch is what makes exceptions infeasible for error handling.

I honestly don't see the big difference between littering your code with if/else blocks versus littering them with try/catch blocks. Can you elaborate?


If/else provides a clear and easy to follow control flow. try/catch is like a roaming goto that works it's way back up your stack in ways you can't predict.

http://www.lighterra.com/papers/exceptionsharmful/


The context here is Java's checked exceptions, which have to be handled explicitly at the site where they may occur, leading to control flow that - in my view - isn't substantially different from handling errors with if/else.


Fair enough.


I don't understand the argument that stack unwinding is unpredictable -- the most naive implementation of exceptions would just be multiple-function returns with automatic propagation (effectively the same as automatically putting the try! macro on every call in Rust).

Nothing about try/catch is like a "roaming goto".


Both are bad.


What's the alternative?


Good exception-based code shouldn't be littered catch blocks; you only need to catch in places where you can reasonably recover. No matter how complex the application, there are typically only a few places you can meaningfully recover from a real exceptional or unexpected error situation.

Checked Exceptions in Java, for example, force you to litter your code with catch blocks and error handling code that doesn't actually recover from the error. It transforms it. It propagates it. Languages like Go with explicit error handling do the same thing. I don't think that's bad in all situations but often it comes down to what one thinks is an unexpected error for a given project.


I thought extending _throws_ with an _as_ clause would make it far more manageable:

   void foobar() rethrows IOException as AppException rethrows FooError, BarError as WtfError {
Being able to express that a bunch of internal exceptions should be wrapped in an application exception would save a ton of boilerplate.

The exception syntax itself is also clunky. If you have a complex expression, you have to extract it and assign it to a variable. But you still have to declare that variable outside the try/catch. And, of course, every exception requires Yet Another Class Definition. I'd do:

    class SomeClass {
        exception ThingBroke handles ArithmeticException;
        exception DatabaseDown handles SQLException;

        void method(int a) {
            try {
                int x = (555 / a rethrows ThingBroke) + (getNum() rethrows DbDown);
            } catch(ThingBroke e) {
I think the other issue is the separate hierarchy for unchecked exceptions. In theory, they were supposed to be issues you couldn't recover from, thus the correct response was to let the caller fail. But now UncheckedIOException is not a subclass of IOException.

Maybe a better approach would be to add an Unchecked interface, though from my reading of the source for exceptions, they seem to be very brittle and maybe there's just no fixing it.


> Being able to express that a bunch of internal exceptions should be wrapped in an application exception would save a ton of boilerplate.

It would but I think it's the wrong approach -- you're actively changing the error information that really provides no additional value except to make the type-checker happy because the alternative is too verbose.


Doesn’t wrapping usually mean adding (semantic) information without changing the original?


You no longer propagating the same error; if you were type-checking on network exceptions, for example, you would entirely miss it if it was wrapped in LibraryException instead.

Wrapping does have value if you are really adding useful information but the example here is just changing the type which really doesn't add anything useful.


Java checked exceptions were copied from CLU, Modula-3 and C++, even though Java's somehow made them famous.

And even C++, after dropping exception specifications in C++17, might get a simplified version back in C++23 as part of value based exceptions proposal.


The irony there is that Result<T,E> is entirely equivalent to "checked" exception specifications. The quality of surrounding support to "deal with [error states] cleanly" in one vs. the other makes all the difference.


The bad ergonomics? Golang is an excellent language.


I don't think it qualifies for that name.


That's your opinion, in the mean time a lot of us will keep producing good applications using Golang and enjoying it :)

There's no amount of downvote that will change that fact.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: