92 karma · joined May 21, 2012
I sympathise with this view, but personally, as a full-time Go programmer, when I've been hacking up little piece of throw-away or explorative code, I've generally found that not-used errors have as often been helpful as annoying.
For example, the compiler might say "i not used", and I'll think "bloody compiler, let's delete the declaration", but I'll actually find that I've named a loop variable wrongly and that this would have been a hard-to-find error requiring a few edit-run iterations, and thus I've actually saved more time by doing what the compiler asked than if I'd been able to code sloppily.
If I do find myself adding and removing many print statements from within a package, I'll sometimes add a little function:
func logf(f string, a ...interface{}) {
log.Printf(f, a...)
}
then as long as that function exists, I can add a logf call to any file in the package with no need to add and remove imports.We can still start with mud, but it's good mud.
BTW we have warnings too, but they're generated by a different tool, go vet, which printf format checking and the like.
The unused variable thing (particularly with local variables) has caught many bugs quickly for me. If there's a package that I find myself always adding and removing, it's easy to write a function that freezes it. For instance, for the debugging print example:
func printf(f string, args ...interface{}) {
log.Printf(f, args...)
}
(I tend to use log for debugging prints rather than fmt, because it's guaranteed thread-safe, and it's easier to separate debugging prints from necessary prints at a later stage).On the few occasions that I've really felt I lacked a ternary operator, I've just coded one up for the occasion:
func either(cond bool, a, b string) string {
if cond {
return a
}
return b
}
No big deal. I do find myself writing little functions as building blocks quite a lot, but I think this works well. Functions are a great building block, and Go is great for assembling blocks.I have seen this (the error monad) mentioned before as a "nice" way of handling errors, even with explicit error returns. I beg to differ - the error messages produced by such a program will be obscure, as all the context is lost - if the first call to someFunction fails, for example, there's not necessarily any indication that the error came from that call rather than the one after it.
Go's explicit error handling means that it's easy to add meaningful context wherever relevant - the error messages printed by such a program are likely to be considerably more useful.
In the end, adding error checks is doing useful work. Each error case should be considered individually, and I've often found it to be the case that it's useful to treat errors as regular values (for example by collecting a bunch of errors, or returning the most important error only).
I can understand the control flow of a Go function by inspecting it on the page (without a glance at the documentation). That's a huge plus.
Much of the power of Lisp comes from the fact that the result of executing an S-expression is also an executable S-expression itself, so programs that write programs become trivial.
This property is true of acme too - we execute text with a middle-button click or drag, and the command that's executed can produce more text, which is itself executable text.
Thus I can extend the functionality of acme by writing tiny shell scripts that print useful commands. I can run a command that produces a list of stuff, use structural regexps to transform that stuff into an acme command, then execute that to transform something else.
The power of structural regexps combined with proper Undo/Redo is also great - rather than pipe something through sed or awk, I'll often iteratively build up an expression or set of expressions that perform some particular one-off transformation, rewinding and retrying as necessary.
I could go on!
Acme certainly isn't perfect, but it is wonderful.
Yes, every intermediary must agree on the error type - it's the language-defined error type, which defines a single method, Error, which returns the error as a string. There's no need for any further agreement.
Functions encapsulate errors. If I call a function, it is entirely up to me how I wish to handle that error. "The code that finds an error" is the code that calls the function. All the context is local. There's no need to know how that function first encountered that error - that's part of the implementation detail of that function. At every stage, we make error handling decisions based on local context. This makes the code more maintainable, because there is genuine separation of concerns.
If you really want exceptions (a classic example is a recursive descent parser where you don't want to check every call), you can use panic and recover, making sure that callers will never see it - it's a local contract only.
I think Go's main approach is this: errors are not exceptional. Errors happen all the time, and should be considered part of the normal control flow of the program, not something to be shoved into a hidden compartment and dealt with later.
Thus using the usual control flow statements to check for errors is entirely appropriate, and it's immediately obvious when reviewing the code if someone has failed to do the right error checking.
BTW there is no os.Error type any more. There's a language-defined "error" type, which is an interface containing a single method:
type error { Error() string }
You only need to be intimately aware of what you're looking at if you want to take some different action based on the kind of error. This is actually quite rare (usually you care only that something failed, not how it failed), but when you need to, generally the calling package defines an error type that represents a particular kind or class of errors (for example http://golang.org/pkg/os/#LinkError)
To make Go run well under Inferno, you'd need to change the language to allow dynamically loaded modules (can we use the type compatibility model when we can't link everything together at once?).
These issues are doable, but even if you've done that, the performance trade-offs when writing a program to run in a VM are quite different from running natively - in particular, much idiomatic Go code would probably be dog slow under Inferno, because we're not steaming along at native code rates.
All that assumes we'd translate Go to Dis though. There may be alternative routes.