Consider the set of errors returned by file operations in the
os
package.
I/O can fail for any number of reasons, but three kinds of failure
often must be handled differently: file already exists (for create
operations), file not found (for read operations), and permission
denied.
The os
package provides these three helper functions to
classify the failure indicated by a given error
value:
package os func IsExist(err error) bool func IsNotExist(err error) bool func IsPermission(err error) bool
A naïve implementation of one of these predicates might check that the error message contains a certain substring,
func IsNotExist(err error) bool { // NOTE: not robust! return strings.Contains(err.Error(), "file does not exist") }
but because the logic for handling I/O errors can vary from one platform to another, this approach is not robust and the same failure may be reported with a variety of different error messages. Checking for substrings of error messages may be useful during testing to ensure that functions fail in the expected manner, but it’s inadequate for production code.
A more reliable approach is to represent structured error values using a
dedicated type.
The os
package defines a type called PathError
to
describe failures involving an operation on a file path, like
Open
or Delete
, and a variant called LinkError
to
describe failures of operations involving two file paths, like
Symlink
and Rename
.
Here’s os.PathError
:
package os // PathError records an error and the operation and file path that caused it. type PathError struct { Op string Path string Err error } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
Most clients are oblivious to PathError
and deal with all
errors in a uniform way by calling their Error
methods.
Although PathError
’s Error
method forms a message
by simply concatenating the fields, PathError
’s structure
preserves the underlying components of the error.
Clients that need to distinguish one kind of failure from another can
use a type assertion to detect the specific type of the error; the
specific type provides more detail than a simple string.
_, err := os.Open("/no/such/file") fmt.Println(err) // "open /no/such/file: No such file or directory" fmt.Printf("%#v\n", err) // Output: // &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2}
That’s how the three helper functions work.
For example, IsNotExist
, shown below, reports whether an error
is equal to syscall.ENOENT
(§7.8)
or to the distinguished error os.ErrNotExist
(see
io.EOF
in §5.4.2), or is a *PathError
whose
underlying error is one of those two.
import ( "errors" "syscall" ) var ErrNotExist = errors.New("file does not exist") // IsNotExist returns a boolean indicating whether the error is known to // report that a file or directory does not exist. It is satisfied by // ErrNotExist as well as some syscall errors. func IsNotExist(err error) bool { if pe, ok := err.(*PathError); ok { err = pe.Err } return err == syscall.ENOENT || err == ErrNotExist }
And here it is in action:
_, err := os.Open("/no/such/file") fmt.Println(os.IsNotExist(err)) // "true"
Of course, PathError
’s structure is lost if the error message
is combined into a larger string, for instance by a call to fmt.Errorf
.
Error discrimination must usually be done immediately after the
failing operation, before an error is propagated to the caller.