4 ms·
I don't think the article's `CopyFile` function is a good example for either `guard` statements or exceptions. We can come up with better examples (feel free),
by throwaway183019 4y ago
I don't think the article's `CopyFile` function is a good example for either `guard` statements or exceptions.
We can come up with better examples (feel free), but I'm going to focus on that since it's the one used in the article.
As a library consumer, I would like to know if the error happened in the source file or the destination file, ideally without requiring me to know implementation details of the function, so I don't have to worry if the implementation changes and now my `if errors.Is(err, fs.PathError)` no longer works and instead became a bug.
For example, the caller might want to:
* Show a user-friendly message to the user if the problem is in the source file.
* Return gracefully if there's not enough space to create the destination file (e.g. "copy as many files as you can to this drive, and tell me which ones you copied").
* "Bubble-up" the error in any other situation.
A function that lets me know which file was the cause of the problem might look like this:
type ErrSourceFile struct{ error }
type ErrDestFile struct{ error }
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return &ErrSourceFile{error: err}
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return &ErrDestFile{error: err}
}
defer w.Close()
if _, err := io.Copy(w, r); err != nil {
return err
}
if err := w.Close(); err != nil {
return &ErrDestFile{error: err}
}
return nil
}
Since now errors are actually useful to the caller function, the `guard` statements became unnecessary everywhere except in the `guard io.Copy(w, r)` case, because now we must handle nils (though I guess with generics we can create a wrapper that allows doing stuff like `guard WrapIfNonNil[ErrSourceFile](err)`, but whether that's better is debatable).
If we tried to do this with exceptions so that we can actually catch them, the code would become:
type SourceFileException exception
type DestFileException exception
func CopyFile(src, dst string) throws error {
// Now necessary because the `try` blocks introduce new scopes.
var (
r io.ReadCloser
w io.WriteCloser
)
try {
r = os.Open(src)
} catch err {
// Wrap exception because we don't want to hide
// the inner exception.
throw SourceFileException(err)
}
defer r.Close()
try {
w = os.Create(dst)
} catch err {
throw DestFileException(err)
}
defer w.Close()
io.Copy(w, r)
// The caller might want to check the hash to decide if it
// should delete the file or not.
try {
w.Close()
} catch err {
throw DestFileException(err)
}
}
... which is even more cumbersome than plain `if` blocks.
Exceptions have their advantages in certain cases, but they also have their downsides, it all depends on the situation and what you want to do.
As everything, it's a trade-off.
- tgv 4y agoYes, that's attrocious, but the complaints come from higher functions, which only rarely modify the error in a meaningful way. db, err := openConnection(...) if err != nil { return ..., err } And then elsewhere you call the function that contains that snippet, and return the error again. I think the entire issue is that it looks too much like boilerplate. My preferred solution would be some operator that has sufficient expressiveness. After all, you should not only return an error, but there almost always are other return arguments as well, and sometimes another error should be returned, so it would look like this db := openConnection(...) ~> { error handling } In case of auto-zeroed or named return values, a simple ~> return would suffice. But that's really not much better than the current situation. Go's syntax just resists functional solutions. Better leave it the way it is. In Go 1, certainly.