Custom Error Type
A sentinel says what went wrong. Sometimes the caller also needs to know where, or which field, or how long to wait - and a string cannot be asked that. When the caller has to act on the detail, the error becomes a struct.
type ParseError struct {
Line int
Key string
Err error
}
func (e *ParseError) Error() string { ... }
func (e *ParseError) Unwrap() error { return e.Err }
error is an interface with one method, so
anything with Error() string satisfies it. Add Unwrap and the sentinel
inside stays reachable, which means you get both:
errors.Is(err, ErrNotNumeric) for the
condition and errors.As(err, &pe) for the
fields.
Pointer receiver
Write the methods on *ParseError, not ParseError. Two reasons: the method
set of T does not include pointer-receiver methods, so mixing the two makes
"does not implement error" appear at a distance; and errors.As compares
concrete types, so a package that sometimes returns ParseError and sometimes
*ParseError forces every caller to check for both. Pick the pointer and be
consistent.
The pointer brings the nil interface trap with it, so return a bare nil on
success. var e *ParseError; return e is a non-nil error.
errors.As
errors.Is asks "is this condition in the chain". errors.As asks "is a value
of this type in the chain", and hands it to you:
var pe *ParseError
if errors.As(err, &pe) {
fmt.Println(pe.Line)
}
The second argument is a pointer to the variable you want filled, so it is
&pe where pe is already a pointer type. It walks the chain like errors.Is
and stops at the first match.
errors.Join
Validation rarely wants to stop at the first problem.
errors.Join builds one error out of
several:
return errors.Join(errs...)
Its message is each error on its own line. Its chain is a tree: instead of
Unwrap() error it has Unwrap() []error, and errors.Is and errors.As
search every branch. errors.Join of nothing, or only nils, is nil, so the
common if len(errs) > 0 guard is belt and braces.
Walking that tree by hand means handling both shapes:
switch u := err.(type) {
case interface{ Unwrap() []error }:
for _, child := range u.Unwrap() { walk(child) }
case interface{ Unwrap() error }:
walk(u.Unwrap())
}
An anonymous interface in a case is unusual-looking and exactly right here:
the thing you care about is the method, not the type that has it.
When not to
A custom type is API surface, and every exported field is a promise. If nobody
branches on the detail, fmt.Errorf with %w
is the whole answer.
Task
Write Error, Unwrap, ParseLine, ParseAll, LineOf, Lines and
Explain. The struct and the three sentinels are given.
Hints
Errorhas two shapes: with a key and without.%qputs the quotes on the key,%vprints the wrapped sentinel.strings.Cut(line, "=")gives you the key, the value and whether the separator was there. You will need to add the import.- A missing
=and an empty key are bothErrMalformed. Only a present key with an empty value isErrMissingValue. - The digit check is the one from the last challenge: reject anything outside
'0'-'9', so-1isErrNotNumeric. ParseAllnumbers lines from 1, skips the empty ones, and appends to an[]errorinstead of returning. On failure returnnilfor the map.LineOfis four lines witherrors.As.Linescannot use it -errors.Asstops at the first match - so write a recursive walk over the twoUnwrapshapes, checkingerr.(*ParseError)first at each node.Explainchecks the sentinels in the documented order, because a join can match more than one.