iTranslated by AI
You don't need to pre-declare interface types
This is a usual small tip. It started from the following tweet.
To summarize, the original tweet said:
In Golang, do I have to declare an interface AB to represent a function that takes an argument satisfying both interface A and interface B?
In Rust, you can do it with T: A + B using traits.
And the response was that you can write it like this:
type A interface {
DoSomething()
}
type B interface {
DoAnotherthing()
}
func Do(v interface {A; B}) {
v.DoSomething()
v.DoAnotherthing()
}
Although, if you run gofmt on the Do() function above, it will be formatted like this (lol):
func Do(v interface {
A
B
}) {
v.DoSomething()
v.DoAnotherthing()
}
Actually, this is a very important feature of Go: there is no inheritance relationship between "abstraction" and "concretion." Therefore, as in the Do() function above, you can create an interface type on the fly to specify the desired behavior and impose constraints, regardless of what the actual concrete type of the parameter v is.
For example, the errors standard package has the errors.Unwrap() function, which is implemented as follows:
// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
You can provide the necessary and sufficient functionality without having to go through the trouble of declaring an interface type beforehand, such as:
type Unwrapper interface {
Unwrap() error
}
Similarly, the errors.Is() function is also written as follows:
// Is reports whether any error in err's chain matches target.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
//
// An error type might provide an Is method so it can be treated as equivalent
// to an existing error. For example, if MyError defines
//
// func (m MyError) Is(target error) bool { return target == fs.ErrExist }
//
// then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for
// an example in the standard library.
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target {
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// TODO: consider supporting target.Is(err). This would allow
// user-definable predicates, but also may allow for coping with sloppy
// APIs, thereby making it easier to get away with them.
if err = Unwrap(err); err == nil {
return false
}
}
}
Seeing this was a real eye-opener for me.
Just as I once did, those who are used to nominal subtyping in languages like C++, Java, or Rust might instinctively feel the need to "declare an abstract type" first. However, in Go, there is no need at all to declare abstract types beforehand[1]. In fact, overusing interface types from the start can even hinder the development process (because it forces concrete types to fit the abstract ones).
Instead of "enclosing" concrete types within abstract types, perhaps it's better to think of "connecting" them within the minimum necessary scope as needed. I believe the Go way is to think (and orient) from the concrete to the abstract.
One of the linguistic advantages of Go is its freedom from the yoke of "inheritance." Once you start to feel this, I think programmers coming from C++ or Java backgrounds will be able to write Go code much more freely.
Discussion