It's more like every 5 lines. I love it as every error is handled correctly and in scope. No error is left behind. Most languages I've used devolve into the GLOBAL MASTER DEFAULT HANDLE ALL ERRORS which is just gross.
For example, consider:
foo, err := getFoo()
Now your scope has an "err" and you open yourself to logic errors such as: foo, err := getFoo()
... do stuff ...
if errr := makeBug(); errr != nil {
return err
}
Spot the (heh) error? It's caused by ":=" allowing variables to be reassigned.You can avoid this dangerous pollution, but it's super ugly:
var foo *Foo
if f, err := getFoo(); err == nil {
foo = f
} else {
return err
}
(You have to have the return in the second branch, otherwise "go vet" will complain. Sigh.)I have lots of these, and they're not helped by the fact that Go has no type-level mechanism to guard against the misuse of multiple-return values. That is, in the above, either "foo" or "err" are usually set, and if "err" is set, it means "foo" isn't. But the type system doesn't prevent you from accidentally ignoring "err" and using "foo", or vice versus. This gets even worse if the func returns a (non-pointer) struct as its value.
The fact that Go allows shadowing (exacerbated by the convention of using "err" as every single error variable) and promotes the reuse of variables (e.g. the above ":=", or declaring "err" outside a loop) also means it's super easy to assign the wrong thing.
All of the above are my single biggest source of bugs during development. That, and how accidentally using nil/zero values, including the damnable nil map/slice cases.
"go vet" (whose shadowing check is disabled by default (!)) and the various linters only go some ways towards helping here. The errcheck linter (which you can enable through gometalinter) is absolutely essential.
Most functions return an error so err is already defined:
func doIt(a, b) (result string, err error) { ... }
Then you are free to reuse err as needed. func doIt(a, b int) (result string, err error) {
var x, y string
x, err = doA(a)
if err != nil {
return
}
y, err = doB(b)
if err != nil {
return
}
result = x + y
return
} var foo *Foo
if foo, err := getFoo(); err == nil {
return nil, err
}
// foo is usable now ./test.go:14:15: foo declared and not used var foo *Foo
var err error
if foo, err = getFoo(); err != nil {
return nil, err
}
//foo is usable now foo, err := getFoo()
if err != nil {
return nil, err
}
"err" still ends up in the scope.I wonder what is the correlation between volume of complaining about minutia such as this compared to real life ability and level of accomplishment.
It's a decent language, but it's also a deeply flawed one. I could mention many other, bigger flaws, but this particular discussion was about errors.
Also, it's actually okay to debate minutia. Dismissing something as "not real problems" is, well, dismissive.
Not in scope. Because you are forced to deal with the error right here right now, sometimes it's in scope and sometimes, you're simply not able to do anything with the error so you effectively mishandle it.
Exceptions allow you to handle the error where it actually makes the most sense.
Anyway, Go-style error handling's lack of any of these issues is what makes it attractive to me: it's simple, straightforward and get's the job done. Granted, with a bit more typing and the occasional bug that could have been avoided by not having to type the (almost) same three lines once again, but not having to jump through any of the hoops I keep encountering in more sophisticated languages is so, so worth it to me. I feel like Go stays out of my way, lets me focus more on actually getting things done than on dealing with the language's intricacies.
This is a legitimate issue, although in Haskell you can use one error type like Go does if you want to.
> And as if that wasn't bad enough, there are also actual throw-catch-style exceptions, which are not checked in anyway
This is also the case in Go (panics and recover).
> I stopped writing Haskell maybe a year ago, but at least back then getting GHC to print a stack trace when an exception wasn't caught was a major feat that involved recompiling every single one of your hundreds of dependencies with profiling flags.
This is also the case in Go. Errors do not retain anything about their state. You need to use a package like https://github.com/go-errors/errors, which has the exact same issue you describe with dependencies.
It is considered good style to not let a panic escape your library API unless something just went very severely wrong that the library could not possibly continue to function normally.
>This is also the case in Go. Errors do not retain anything about their state.
Errors are an interface, you can put any state inside it you want and I've frequently done so (retain line counts in parser or network data for example)
SomeException is pretty much equivalent to Go's error, but it's not commonly returned by libraries and such so using it barely helps with the need to convert often. In addition all the types used by libraries as error values now need to have Exception implemented, which they don't. So, in practice, no, you cannot. Or rather, it wouldn't solve the core problem and create a new one.
>> And as if that wasn't bad enough, there are also actual throw/catch-style >> exceptions, which are not checked in anyway > > This is also the case in Go (panics and recover).
True, panic/recover exist, but in practice I find them to be more akin to setjmp/longjmp in C than throw/catch-style exceptions in other languages. Off the top of my head I can't recall a case where I had to recover from a panic that I didn't create in my own code (i.e. originated in a library call). Exceptions in Haskell and other languages on the other hand are usually all over the place (ok, usually not in pure code in Haskell) and I feel like I have to constantly keep them in mind to avoid letting one slip through the seams and then later having to track down where the hell that one came from.
>> I stopped writing Haskell maybe a year ago, but at least back then getting >> GHC to print a stack trace when an exception wasn't caught was a major feat >> that involved recompiling every single one of your hundreds of dependencies >> with profiling flags. > > This is also the case in Go. Errors do not retain anything about their state. > You need to use a package like https://github.com/go-errors/errors, which has > the exact same issue you describe with dependencies.
That's fair and indeed it would often be handy to know where an error came from. But since errors are just return values I don't really expect them to have a stack trace attached to them, just like I don't expect any other value to have that. It would still be neat for debugging sometimes, more so for errors than other values. I think there are some proposals for improving on this in Go 2. What really frustrated me about Haskell though was debugging throw/catch-style exceptions without a stack trace, because I basically had zero clues about where it came from and there were no clear paths to trace in the code. In Go on the other hand, errors don't just plop up from out of nowhere and panics, which can, print a nice stack trace when not recovered from.
So yes, it can be messy when using other people's code because this is very much an unstable part of the language right now. I still prefer that over the worst possible strategy (what Go picked) being effectively baked in.
Javascript messed it up a little bit with promises, because it's so easy to swallow errors. IMO, they should have made it so that unhandled promise rejections throw by default at the next turn of the event loop, with a method that could be called to opt out individual promises.
At least, V8 did IIRC. I use Creed[0] myself, which using it's `shim()` function gives you the same behaviour. It's quite lovely.
I don't think so. You can choose to ignore the error (I know it's bad practice), but it's allowed. And sometimes you don't know what to do with the error
I have said so in the intro textbook I wrote.
Even if the error can be ignored for fmt.Println, it is not clear from the example whether it was even taken into account or not. The following could be required:
_ := fmt.Println("hello world")
That means that the function call can return an error, and thus you have to use the return value, but the programmer decided to ignore it. Contrast it with: fmt.Println("hello world")
Did the programmer know there was possibly an error? What happens if there is an error (not specifically for this example, but in general), and you don't check for it? then it is ignored.In another language, the same line could be the source of an error; but only error handling depends on the programmer, not error checking. Moreover, the default handling strategy is to abort execution. Actually ignoring errors requires more work.
And errcheck is apparently no silver bullet: https://github.com/golang/go/issues/20148
Go allows you to do certain things but it's not a fallacy because it's something that's allowed but not necessarily tolerated by what most consider "good Go coding standards"
It strikes me a bit like saying "because most people don't wear seatbelts, cars are unsafe because you may get ejected out of the cockpit in a crash". Wear a seatbelt and use linters, I'm not going to excuse the malpractices of the larger industry.
Optional external tools, are exactly that, optional.
Lint was created for C in 1979, a minority used it or its commercial versions like Gimpel, until clang was introduced with static analysis built into the compiler.
Anything that isn't enforced by the language, tends to be ignored, hence why we need good defaults that make workarounds a big pain.
I'd rather just keep track of linter complaints.
> I'd rather just keep track of linter complaints.
Do you dislike "unused variables" being compilation errors, too?
For example: "if err" being compiled directly to "if err != nil" would be a quick win.
Second more complex idea: add a new keyword like "res := throw MyFunction()", being allowed if "MyFunction()" returns an "error" as its last argument and the calling function returns an "error" as its last argument, simply return the zero value or named return value of any other return values + the error if there is one, otherwise continue.
Point being, the core of Go is really good. When I come across a command line tool written in Go, my first thought is "fuck yeah, this is going to be so easy to install", versus something like Python which is a bitch unless the developers have taken the time to brew it or something. Countless planet-scale infrastructure tools have been written in it; Kubernetes and Docker to start. I can name a dozen startups, both large and pre-seed, which use it with resounding success.
Yes, it has warts. Every language has warts. Go is just "different" because it tries to be simple. Nearly no other language in the world has "simplicity" as one of its core values, and a lot of people don't fully respect what that means.
The real problem with go's errors is complex and significant.
There's no Either/Result type so the multiple return doesn't actually tell you that you get a zero value when there's an error (and in fact there are functions in the stdlib that do this).
There's no good way to check whether an error is a given thing because error chaining isn't built in and errors rarely are typed as sum-type-enums as they are in rust or other decent functional languages. For example, `os.IsNotExist(err)` will not work for a caller if I did `return fmt.Errorf("error opening file: %v", err)` in a function.
The stdlib encourage a dozen really bad methods of doing error handling; there's `io.EOF` (which is a mutable variable btw). There's PathError. There's os.IsNotExist. There's random panics in some functions. There's nil returns with no further info.
The stdlib has multiple examples of 3 or 4 different error handling styles, and they're all horrific because they don't chain and don't allow for exhaustive matching.
Go may be good compared to C or other languages that were designed way back when. In modern times, go is a massive step backwards with its incredibly underwhelming type system, absolutely asinine community, lack of good abstractions, and so on.
With all the things that you mention Go does not offer helps it keep simple. It has already proven much useful for many of of my projects where a 'better' language would fail for variety of reasons.
There is a huge difference.
The Java code you are reading is caused by bad programmers who are not using exception handling correctly. Proper exception use means sometimes catching and sometimes ignoring the exception and declaring it in your `throws` clause.
In Go, testing against `nil` every twenty lines is not only common, it's the ONLY way to write robust code. You simply don't have a choice.
I think same is going to be true for Go. I have written few projects in Go and they have produced much more value than time taken to write them.
At the end of day companies I worked for prefer mediocre code that is written and used vs excellent code in brilliant programing language that was never completed.
I personally like exceptions, as they enable focusing into what's supposed to happen instead of what all could go wrong. And the abstraction power that comes with this approach.
But I wouldn't rush to say I'm right and the meticulous party is wrong. There are many instruments in the orchestra. In the end all that matters is the skill of the player.
I don't like exceptions. Go's error handling is still ridiculous.
It doesn't have sum-types, it has the incredibly stupid interface/struct nil punning so you must return an interface and type-assert down, it doesn't have a tuple object being returned but actually multiple return which is a semi-broken thing, it doesn't have an either/result type or an optional type to express mutual-exclusivity / optionality....
Yeah, the whole thing is disgustingly bad. That doesn't mean I prefer exceptions.
I do prefer meticulous "errors as value" error handling. Go's implementation is the worst implementation of that because the type system doesn't support various features that make handling error values better.
The very best error handling I know of is in CLOS. Instead of exceptions, they have "conditions". A condition is signaled like an exception but with a big difference described below. The thing about errors is, the code that detects the error can never know how to deal with that error. If it did, then it wouldn't be an error it would be a case within that function. However, that code may know ways the error could be dealt with, it's just not in a position to choose. So what conditions do is, you can define a condition and so called "restarts" along with that condition. What the restart does is say "here is a way I could handle this error" and code higher up the stack has the option to (1) use the restart, (2) define additional restarts on that condition or let the condition propogate upwards. The condition signalling does not necassarily unwind the stack: if you choose a restart, the code continues in the stack that restart was defined.
So this means every part in the stack that knows how a condition could be recovered from can provide the option but it is left to code higher in the stack to chose the actual strategy. This is superior to the exception strategy of unwinding the stack until you find a handler because sometimes you really want the code to continue execution where it is.
But, of course, such a system is really expensive to implement. If you can't have that my preference is the Haskell style of using a sum type where errors can happen but writing as much of your code as possible with any error conditions prohibited by the type system.
Nobody used them, so we removed them.
I develop in Common Lisp and conditions&restarts are so much superior to the other error handling paradigms i've seen, that it's not fun being left without them.
Out of curiousity, if this was just a macro and no one was using it, what would have been the harm in just leaving them in? Is this a part of the language that changes so much it made maintainance a pain?
"Just leave it in" isn't a good justification for a feature. We'd then require that every implementation of Rust ever, forever, implements this too. Being in std is a high bar.
Beyond that, there are a host of other reasons: https://github.com/rust-lang/rust/commit/454882dcb7fdb03867d...
And i've seen modules to add condition systems to perl5, Ruby & Clojure. But these are kinda of hacks so the more languages that have it baked-in the better!
Compared to Hotspot, it doesn't STW; it makes a different throughput/latency tradeoff that is more suited for its application domain; and it's actually needed a lot less often because the compiler can put many objects on the stack.
A generational collector is currently under development (but won't be in 1.10).
Stop the world typically happens during compaction phases. Golang "solves" this by just not doing any compaction. That's certainly a big tradeoff.
> it makes a different throughput/latency tradeoff that is more suited for its application domain
I don't really agree. While low latency is nice for consistency, low throughput means you may have to buy more servers to handle your load. That isn't always a win.
> and it's actually needed a lot less often because the compiler can put many objects on the stack
HotSpot can, and does, do this too!
> A generational collector is currently under development (but won't be in 1.10).
Does it? Do you have a link?
I haven't found any information about this.
> Does it? Do you have a link?
https://docs.google.com/document/d/1gCsFxXamW8RRvOe5hECz98Ft...
This is the original proposal, although I believe it has evolved significantly during development (in testing some of the proposed ideas didn't pan out as expected).
Not IBM or Azul, though, their JVMs already have kickass realtime collectors.
Go GC gets to make the claim that it doesn't "stop the world" because it doesn't do heap compaction. Which will become a major problem when dealing with large in-memory data structures or certain usage patterns.
And the fact that they still don't have a generational collector is concerning.
Erlang's solution is interesting, but remember in Erlang processes can't share memory, they only have message passing. So you never have to worry about multiple threads writing to the same data structure at once, which is what makes compaction hard.
The bottom line is that the scavenger will allow the OS to claim free memory only at least 5 minutes after it was GC'ed. For us the solution was to "force" releasing the free memory to the OS [1], and even since then things have been much smoother in the context of our particular app.
On this topic, there's an open issue [2] to look at making the scavenger more prompt and aggressive.
[1] https://medium.com/samsara-engineering/running-go-on-low-mem... [2] https://github.com/golang/go/issues/16930
I get that goroutines are great, but that really seems to be all Go has going for it. The GC sucks, error handling feels like something from a bad language designed in the 70s, missing generics, math library is awful, Rob Pike being super opinionated, etc.