As I was writing yesterday's entry on a subtle over-broad
try
in Python , it occurred to me that one advantage of a language with explicit error checking, such as Go , is that a broad exception catching problem mostly can't happen, especially accidentally. Because you check errors explicitly after every operation, it's very hard to aggregate error checks together in the way that a Python
try
block can fall into.
As an example, here's more or less idiomatic Go code for the same basic operation:
for _, u := range userlist {
fi, err := os.Stat(u.hdir)
if err != nil || !(fi.IsDir() && fi.Mode().Perm() == 0) {
fmt.Println(u.name)
}
}
(Note that I haven't actually tried to run this code so it may have a Go error. It does compile, which in a statically typed language is at least a decent sign.)
This does the
stat()
of the home directory and then prints the user name if either there was an error or the homedir is not a mode 000 directory, corresponding to what happened in the two branches of the Python
try
block. When we check for an error, we're explicitly checking the result of the
os.Stat()
call and it alone.
Wait, I just pulled a fast one. Unlike the Python version, this code's printing of the username is not checking for errors. Sure, the
fmt.Println()
is not accidentally being caught up in the error check intended for the
os.Stat()
, but we've exchanged this for not checking the error at all, anywhere.
(And this is sufficiently idiomatic Go that the usual tools like
go vet
and
golint
won't complain about it at all. People ignore the possibility of errors from
fmt.Print*
functions all the time; presumably complaining about them would create too much noise for a useful checker.)
This silent ignoring of errors is not intrinsic to explicit error checking in general. What enables it here is that Go, like C, allows you to quietly ignore all return values from a function if you want instead of forcing you to explicitly assign them to dummy variables. The real return values of
fmt.Println()
are:
n, err := fmt.Println(u.name)
But in my original Go code there is nothing poking us in the nose about the existence of the
err
return value. Unless we think about it and remember that
fmt.Println()
can fail, it's easy to overlook that we're completely ignoring an error here.
(We can't do the same with
os.Stat()
because the purpose of calling it is one of the return values, which means that we have to at least explicitly ignore the
err
return instead of just not remembering that it's there.)
(This is related to how exceptions force you to deal with errors , of course.)
PS: I think that Go made the right pragmatic call when it allowed totally ignoring return values here. It's not completely perfect but it's better than the real alternatives, especially since there are plenty of situations where there's nothing you can do about an error anyways.
Sidebar: how you can aggregate errors in an explicit check language
Languages with explicit error checks still allow you to aggregate errors together if you want to, but now you have to do it explicitly. The most common pattern is to have a function that returns an error indicator and performs multiple different operations, each of which can fail. Eg:
func oneuser(u user) error {
var err error
fi, err := os.Stat(u.hdir)
if err != nil {
return err
}
if !(fi.IsDir() && fi.Mode().Perm() == 0) {
_, err = fmt.Println(u.name)
}
return err
}
If we then write code that assumes that a non-
nil
result from
oneuser()
means that the
os.Stat()
has failed, we've done exactly the same error aggregation that we did in Python (and with more or less the same potential consequences).