My concern is not nomenclature, but that correct code must handle paniceptions. And that it must also handle return value errors. Go code IMHO ends up spending a lot of code on error handling, I think because of this double taxation.
My concern is not nomenclature, but that correct code must handle paniceptions. And that it must also handle return value errors. Go code IMHO ends up spending a lot of code on error handling, I think because of this double taxation.
No, correct code doesn't have to handle panics. Panics are for programming mistakes.
At least in my world, every time I type "go", I must ensure that I am starting a goroutine that has some sensible top-level recover mechanism, and, honestly, for any Go program that actually plans on using concurrency, I think there's no alternative. You MUST handle panics. Why? Because an important aspect of Go's concurrency is maintaining composition of independent processes, and there are few things more uncompositional as a completely unrelated computation in a completely unrelated thread that trashes your entire OS process.
Panics may be for programming mistakes, but for any non-trivial code, you have some. Hopefully you can work out a better way of handling them than completely bringing the entire program down.
I am willing to assert that my code maintains enough isolation that continuing on is a reasonable thing to do. (It's a port of Erlang code anyhow. This is not a very freaky claim about such code.)
Without defer, to be correct you would have to explicitly 'recover' (and re-panic?)
You seem really hung-up on this point. Can you link to some code that illustrates your concerns?
It's even used in the standard library: http://golang.org/src/pkg/text/template/exec.go#L93
(I use the pattern myself in certain situations. It's extremely useful.)
I have exactly two panics in my ~15k line server. Both are in initialization code that will probably never get called, so it will fail very early on in the code. The rest of my code looks like this:
func getRecord(args...) (err error) {
if err := doSomethingRisky(); err != nil {
return err
}
if err := doSomethingElseRisky(); err != nil {
return err
}
... other code ...
return nil
}
func processRecord() error {
if err := getRecord(args...); err != nil {
return err
}
... do other stuff ...
return nil
}
All the way down the stack. It's certainly a little more code, but it forces you to at least acknowledge all errors. If you want a stack-trace, you can always use the runtime package.I'm not talking about panicing instead of returning errors. I'm talking about using an idiom---which is used in the Go standard library (see my link up-thread)---to make error handling more terse when you're working with code that is otherwise profligate with checking errors.
At no point is a panic exposed to the user of a program or to the client of a library. At no point are errors ignored. The panics are kept within package and converted to error values.
Without seeing specific code I can't say for sure, but it's very unlikely that any database interaction code is best modeled with panic/recover for error handling. I'm very curious to see the source, at this point.
Using panic/recover doesn't automatically make your code nonidiomatic.
> (That the stdlib uses panic/recover in a few specific places does not make it broadly idiomatic.)
That the stdlib uses panic/recover in several places is a good indicator that "never use panic/recover" is bad advice. Note that while I agree that just because something is in the stdlib doesn't mean it's idiomatic, I also cite that this particular approach is used to make the structure and organization of code more clear. Since it's used in several packages, I claim that this is a strong hint that panic/recover is appropriate in limited scenarios.
> Without seeing specific code I can't say for sure, but it's very unlikely that any database interaction code is best modeled with panic/recover for error handling. I'm very curious to see the source, at this point.
It's really not that hard to imagine. For example: http://play.golang.org/p/fhpRLd8EHY
We seem to have some wires crossed. Let's be clear, shall we?
* The panic/recover idiom is rarely used, but it is an idiom.
* There are trade-offs involved with using panic/recover. In my sample linked in this comment, many of the functions in database/sql need to be stubbed out so that they panic. However, the cost of this is relatively small, since it can mostly be isolated in a package.
* The idiom is most frequently seen in parsing because there are a lot of error cases to handle and the response to each error is typically the same.
* While parsing is the common scenario, I claim it is not the only one. I cite that database work is profligate with error checking, and depending on your application, there's a reasonable chance that the response to each error is going to be the same. When doing a lot of it, it can pay off to use the panic/recover idiom with similar benefits as for doing it with parsing.
* There may well be other scenarios where such handling is appropriate, but I have not come across them.
I've done an unusual amount of work with parsers and have done some database work, so I've had the opportunity to bump up against the panic/recover idiom a bit more than normal. As with anything else, it can be abused. But I find it extraordinarily useful in certain situations.
* generic functions (e.g. append)
* generic data types (e.g. slices)
* exceptions (like illustrated above)
* special case syntax
* Custom event loops
* precompiler macros (very bad to use, horrible, blah blah ... except of course for the people imposing this restriction, and YES they're using it amongst other things to workaround the lack of generics in C)
...
This attitude was common in middle-90s "generic" programming languages like Ocaml, Modula-2 and others. You should simply look at Go as one of those languages and treat it as such.
If this attitude bothers you, you should look at C++0x and D.
The canonical examples are closing a file and releasing a mutex. Both have code samples here: http://blog.golang.org/defer-panic-and-recover
I'm confused as to what you guys are saying: are you saying that you don't need to handle exceptions (whether using defer or recover), or that it's better to use defer over recover? I take exception to the former, totally agree with the latter.
> are you saying that you don't need to handle exceptions
> (whether using defer or recover)
Go doesn't have exceptions. You don't need to handle (i.e. explicitly deal with) panics via recover. If you do, especially if you're not making the panics yourself in e.g. a parsing package, that's a bad code smell and you're probably doing something wrong.If you do still think I have gaps in my knowledge, I humbly suggest that you briefly fill in those gaps with facts; it should save you time in the long run and will likely win you a few converts!
Defer is primarily used to make sure that clean-up happens in functions that have multiple exit points (return statements). It's a convenient side effect that defers are executed while a panic unwinds the stack, but it is rarely the first thing on the mind of the Go programmer when they type "defer".
Embrace error values. Return them! Check them! Panic when shit goes really bad. That's it. If you're writing Go code and you're thinking about "throw" "catch" or "finally", you're doing it wrong. Go's features do not map cleanly to those concepts, because Go doesn't have exceptions.
This won't unlock the mutex on panic, which is observable if someone is trying to recover():
func F() {
mutex.Lock()
... do something here that panics ...
mutex.Unlock()
}
But this will: func F() {
mutex.Lock()
defer mutex.Unlock()
... do something here that panics ...
}
This is basically the same set of hazards as maintaining exception-safety in C++ or Java. So in this regard panic is very much like an exception system. (Of course, it has very different idiomatic use.)You should probably be using defer() all the time anyway, unless you have a good reason not to. It also helps with code evolution, in the cases where some yahoo adds a new return statement in the middle of a function.
try {
... do something that throws ...
} catch (...) {
... deferred code
}
... other code
If you don't use `catch`, then I could agree that try/finally is the same as defer, but I would argue that defer is a much cleaner design since cleanup code is located next to the thing they're cleaning up.I think it's much easier to audit this:
mutex.Lock()
defer mutex.Unlock()
Than this: mutex.Lock()
try {
... code that panics
} finally {
mutex.Unlock()
}
Other languages also make a distinction here, for example Python's `with` and D's `scope` [1].Also, it's trivial to make a `panic` that is unrecoverable: `go panic("broke your code, lol!!")`. This just cements the idea that `panic` is semantically different than exceptions, and should be treated as such.
You can do that by creating another function.
> Also, it's trivial to make a `panic` that is unrecoverable: `go panic("broke your code, lol!!")`. This just cements the idea that `panic` is semantically different than exceptions.
That's not different. In, say, Java, you can set the default uncaught exception handler to get the same behavior and then you can write:
new Thread() { throw new RuntimeException("..."); }You can't emulate the behavior of continuing the current block after an exception is caught. You have to recover() and copy/extract into a function any code that you'd want to run in the recover.
For example:
try {
... code that throws
} catch {
}
... other code
In Go, to run "other code", you'd have to duplicate all of that logic in the recover(): defer func() {
if err := recover(); err != nil {
... other code (duplicated from below)
}
}()
... code that panics
... other code
This isn't really the same thing, but I suppose you could technically get the same effect if you move all of "other code" into a function and called that in both places, but you're still duplicating code.Panics and exceptions are two very different things, which is why there are different idioms in place to make working with them safe.
Right. It's a pretty trivial transformation, and that's why it's not inaccurate to call panic/recover equivalent to exceptions: you can straightforwardly express every exception-based pattern using defer/panic/recover, and also the other way round. Sometimes you have to make more functions to make panic/recover work, but that's part of the "tied in with function declarations" nature of panic/recover/defer—there's nothing semantically that deep about it because the transformation is still quite simple.
Anyway, I assume you're the same pcwalton from Rust? I really like the design of error handling so far, especially the bit about trapping conditions. From what I read, it looks failure just kills the task, instead of the entire program (like it does in go if unhandled).
I'm really looking forward to 1.0. Keep up the good work!