> This style of error signalling is best used when the error handling is binary,
> either "No error, continue" or "Errored out, abort", but not branched
> out path like "If encountered error A, do this. If encountered error B,
> do that. Otherwise, abort".
If I'm understanding you correctly, you can implement your example in a fully idiomatic way, and the stdlib does this in a bunch of places.
Errors are just values, so you can return whatever best suits the use-case. The gamut of expected errors might not be part of type signature, but it can be part of your documentation.
The "signal code" you refer to could be implemented via:
1. Returning error structs (e.g. ErrFoo, ErrBar) and using `errors.As()` access those structs -- useful if you have a highly structured error with additional fields.
type ErrFoo struct { // fields // }
type ErrBar struct { // fields // }
if var errFoo *ErrFoo; errors.As(err, &errFoo) {
// handle ErrFoo
}
if var errBar *ErrBar; errors.As(err, &errBar) {
// handle ErrBar
}
2. Returning a single error struct (MyErr) with an `Op` field, as `net.OpError` does. Similarly accessing via `errors.As()`
// top-level, e.g. exported by package
type MyErrorStruct struct {
Op string
Path string
}
var (
OpFoo string = "foo"
OpBar string = "bar"
)
var opErr *MyErrorStruct
if errors.As(err, &opErr) {
if opErr.Op = OpFoo {
// handle OpFoo at path opErr.Path
}
if opErr.Op = OpBar {
// handle OpBar at path opErr.Path
}
}
3. Returning predefined errors, e.g. `var ErrFoo = errors.New("foo")` and checking for them via `errors.Is()`
// top-level, e.g. exported by package
var ErrFoo = errors.New("foo")
var ErrBar = errors.New("bar")
if errors.Is(err, ErrFoo) {
// handle ErrFoo
}
if errors.Is(err, ErrBar) {
// handle ErrBar
}
Sidenote: I feel like people struggle to internalise that Go errors are
just values* and are therefore are wildly flexible, yet not special in any way.I wonder if this is because other languages treat errors as something special?