HackerTrans
TopNewTrendsCommentsPastAskShowJobs

throwaway183019

no profile record

comments

throwaway183019
·4 anni fa·discuss
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.