Closed pantsmann closed 4 years ago
Don’t use:
type extendedError interface {
error
fmt.Formatter
errors.Wrapper // this interface intentionally does not exist
}
type spcl struct { extendedError }
return spcl{ io.EOF } // this will not work
We are unlikely to ever adopt returning anything other than a simple error
. A central design principle of Go is that errors should try to always be returned with the error
interface type, regardless of what the underlying implementation or method set implementation is.
You are struggling here because of the strict typing in Go. This strictness allows strong assertions to be made at compile-time about what you’re trying to do to what. But this means that when you embed a type into a struct, that struct (because it is a concrete type) has to have a known-at-compile-time unchanging implementation set. This means that when embedding an interface, the struct can only guarantee the specific methods implemented by the interface.
If you want to be able to pass on Format
etc, then you need to properly expand the type, and not just embed the error
type directly into the type.
The solution here is to properly implement a wrapping error type, there are no shortcuts:
type spcl struct{ wrap error }
func (s spcl) Error() string { return s.wrap.Error() }
func (s spcl) Unwrap() error { return s.wrap }
func (s spcl) Format(f State, verb rune) {
if formatter, ok := s.wrap.(fmt.Formatter); ok {
return formatter.Format(f, verb)
}
switch verb {
case 'v':
if f.Flag('+') {
fmt.Fprintf(f, "%+v", s.wrap)
return
}
fallthrough
case 's':
io.WriteString(f, s.wrap)
case 'q':
fmt.Fprintf(f, "%q", s.wrap)
}
}
func (s spcl) Special() bool { return true }
I guess what I'm saying is that there IS a short cut but I'm unsure why I shouldn't use it. So I guess two questions. (Note I'm using the github.com/friendsofgo/errors fork of github.com/pkg/errors, but they are very similar)
First, why not do one of the following, all of which appear to work correctly and represent what I was trying to explain before more completely. (all this code was originally separated out into different packages roughly where '///////'s are. See spcl1, spcl2, and spcl3, the point being not rewriting the stuff in BaseError over and over again) :
type ExtError interface {
error
fmt.Formatter
errors.Wrapper
}
type BaseError struct{ Err error }
func (b BaseError) Error() string { return b.Err.Error() }
func (b BaseError) Unwrap() error { return b.Err }
func (b BaseError) Format(f fmt.State, verb rune) {
switch verb {
case 'v':
if f.Flag('+') {
fmt.Fprintf(f, "%+v", b.Err)
return
}
fallthrough
case 's':
io.WriteString(f, b.Err.Error())
case 'q':
fmt.Fprintf(f, "%q", b.Err)
}
}
/////////////////////////////////////////////////////////
type spcl1 struct {
ExtError
}
func (s spcl1)Special1() bool {
return true
}
func GetError() error {
return spcl1{errors.Wrap(GetError2(), "I'm special").(ExtError)}
}
//////////////////////////////////////////////////
type spcl2 struct {
ExtError
}
func (s spcl2) Special2() bool {
return true
}
func GetError2() error {
err := errors.Wrap(GetError3(), "I'm special too")
ee, ok := err.(ExtError)
if ok {
return spcl2{ee}
}
return spcl2{BaseError{err}}
}
///////////////////////////////////////////
type spcl3 struct {
BaseError
}
func (s spcl3) Special3() bool {
return true
}
func GetError3() error {
err := errors.New("I'm special three?")
return spcl3{BaseError{err}}
}
////////////////////////////////////////////////////
func IsSpecial3(err error) bool {
type spcl3 interface {
error
Special3() bool
}
var s spcl3
ok := errors.As(err, &s)
return ok && s.Special3()
}
func IsSpecial2(err error) bool {
type spcl2 interface {
error
Special2() bool
}
var s spcl2
ok := errors.As(err, &s)
return ok && s.Special2()
}
func IsSpecial1(err error) bool {
type spcl1 interface {
error
Special1() bool
}
var s spcl1
ok := errors.As(err, &s)
return ok && s.Special1()
}
func main() {
err := errors.Wrap(GetError(), "root")
fmt.Printf("is one: %v\n", IsSpecial1(err))
fmt.Printf("is two: %v\n", IsSpecial2(err))
fmt.Printf("is three: %v\n", IsSpecial3(err))
fmt.Printf("%v\n", err)
fmt.Printf("%+v\n", err)
}
Second, I'm aware that idiomatic Go would return 'error' and not some other type of error. In this case, however, I see a bit of an advantage in having errors.Wrap(), errors.WithStack(), and errors.WithMessage() returning a type that correlates to ExtError from the example above. That return type still fulfills 'error' and could be used wherever an 'error' could be used. It would save me from needing to define ExtError myself and save me from some type assertions. The bigger advantage is that it defines what is necessary for Wrap, WithStack, and WithMessage to work properly from the top of an error chain to the bottom, so somebody like me doesn't come along and break everything by adding special behaviors that don't meet pkg/errors' semantics. So my second question is why does this not trump the Go suggestion/requirement of always returning the 'error' type instead of a more specific, expansive error type?
If anyone is curious here is the output of running my example code (when it was still separated into multiple files in various packages).
is one: true
is two: true
is three: true
root: I'm special: I'm special too: I'm special three?
I'm special three?
.../temp/temp/temp.GetError
/c/gocode/src/.../temp/temp/temp/temp.go:17
.../temp/temp.GetError
/c/gocode/src/.../temp/temp/temp.go:18
main.GetError
/c/gocode/src/.../temp/temp.go:20
main.main
/c/gocode/src/.../temp/temp.go:57
runtime.main
/usr/local/go/src/runtime/proc.go:201
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1333
I'm special too
.../temp/temp.GetError
/c/gocode/src/.../temp/temp/temp.go:18
main.GetError
/c/gocode/src/.../temp/temp.go:20
main.main
/c/gocode/src/.../temp/temp.go:57
runtime.main
/usr/local/go/src/runtime/proc.go:201
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1333
I'm special
main.GetError
/c/gocode/src/.../temp/temp.go:20
main.main
/c/gocode/src/.../temp/temp.go:57
runtime.main
/usr/local/go/src/runtime/proc.go:201
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1333
root
main.main
/c/gocode/src/.../temp/temp.go:57
runtime.main
/usr/local/go/src/runtime/proc.go:201
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1333
With your recommended “return a wider interface”:
err := errors.Wrap(io.EOF, "foo")
…
err = json.Unmarshal(data, &var) // compilation error: error does not implement ExtendedError
Errors should always be returned as error
s, because even changing the return type to an expanded error type can break compatibility of types in existing code.
Also, you are still not going to know when a downstream call is narrowing the interface on you, so you’re still always going to need to do the type assertion anyways:
func myLibraryFunc() error {
return errors.Wrap(doThing(), "bar")
}
If you know that your types will always be wrapping something that implements your extended interface, then go ahead and embed the extended interface in your own types locally, and type assert the return from errors.With*
and 'errors.Wrap*`, that’s what type assertion is there for.
But returning that extended type from pkg/errors
itself is not something that is going to happen, because doing so can unreasonably break other people’s code. Note also, nothing stops you from forking the repo, and have it return the extended interface. But a PR to do this would not be entertained, as it would break too much code.
That makes sense. It is still a bit tempting to do this in my own code, but I think I won't. It makes sense to me to use the type assertions. As you say, that's what they are for. The example at the top of your comment really clears up my second question. Danke.
I've been experimenting with asserting errors for behavior. I've found this gets more complicated if the behavior is not added at the error origin. Suppose we have the following:
We also have a long sequence of function calls that wrap errors before returning them. Somewhere in this chain of calls we have the following:
We also have this in another location of the chain:
Various functions in the call chain could add special and extra special behaviors to errors like this:
At the root of this call chain, we might want to test for these behaviors with the IsSpecial and IsExtraSpecial functions, but we'd find there were problems. First, since neither spcl or xspcl implement the Wrapper interface the tests might not work. Second, if we tried to print out the stack traces all along the error chain we wouldn't be able to since spcl and xspcl don't implement fmt.Formatter. We can add these functions of course but it seems silly since the errors we are using to instantiate spcl and xspcl already implement the proper functions. I could change things as follows:
Now adding the special behavior to an error would look like this:
Great, this works, but I ask myself why not have pkg/errors define ExtendedError once and have Wrap return this type instead of error. This would make adding these kinds of special behaviors really easy and cheap. The IsSpecial and IsExtraSpecial functions would stay the same. Everything else would be as follows:
What issues/problems might there be with doing this? Idiomatic problems? etc?
If I also tweaked the errors.foundational struct to implement Wrapper but always return nil (and tweaked Cause() to recognize this) I could add behaviors to the original errors with equal ease.