Problem Link to heading
The go language spec has no way to simultaneously add or augment behavior to an interface while maintaining commonly accepted best programming practices around minimal interface design. While not unique to Go itself, a combination of Go’s feature set and evolved best practices have caused Go library authors to suffer disproportionately to other language developers.
Abstract example Link to heading
Given an interface I of a concrete type S, there is no way to add behavior to I while returning a type of I that also implements the functions of S, without knowing S’s functions a priori. Specifically the below example will panic.
package main
// Given an interface
type I interface {
Func()
}
// And another interface
type I2 interface {
Another()
}
// And a struct that implements both
type S struct {
}
var _ I = &S{}
var _ I2 = &S{}
func (s *S) Func() {}
func (s *S) Another() {}
// If you want to augment an interface (but didn't know about I2)
type IChain struct {
I
}
func Augment(i I) I {
return &IChain{i}
}
func main() {
var s I
s = &S{}
_ = s.(I2)
s = Augment(s)
// You lose your implementation of the base interface
_ = s.(I2)
}
Concrete examples Link to heading
Go library github.com/pkg/errors Link to heading
A tenet of Go interface design is to create minimal, small interfaces. It is generally accepted as a best practice for go programmers and the error package of Go is often pointed to by Go’s core team themselves as an embodiment of simplicity: which I 100% agree with.
Go community developers often, however, want to attach optional information to errors: specifically stack traces. Dave Cheney’s almost perfect errors library has quickly become a favorite in the Go community as a way of attaching stack information to errors.
Another bit of optional information that could be attached to an error is if the error is temporary, as described by the golang blog itself.
package net
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
// ...
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}
A problem arises when these two philosophies are mixed. If a temporary error is wrapped by the errors package, it is no longer Temporary and the above code will not detect this behavior. The current solution, as described in Dave’s Don’t just check errors, handle them gracefully, is to tell programmers to use the Cause method of errors package.
// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
te, ok := errors.Cause(err).(temporary)
return ok && te.Temporary()
}
In operation, whenever you need to check an error matches a specific value or type, you should first recover the original error using the errors.Cause function.
Given the combination of package errors’s strength used as close to the error creator as possible, combined with the size of callstacks in a large, complex codebase that separate the temporary check from the errors.Wrap call, the safest way that maintains treating errors as opaque is to always check for an error being temporary by using the IsTemporary function above instead of type asserting.
The safest consequence of this is that any time anyone ever checks the type of an error, they should use pkg/errors’s Cause() function. Stepping back, this is a very impractical and unscalable solution. It assumes errors package as the only error type wrapper and assumes no other wrappers around an error for other behavior. There is also the developer cost of not realizing why type assertions for optional behavior does not work.
Go library github.com/zenazn/goji Link to heading
Go’s built in HTTP server is another example of the conflicting needs of API simplicity and optional behavior. The ReponseWriter object of a http.Handler reflects perfectly Go’s accepted best practice of minimal interfaces: it only has 3 functions. However, the internal implementation of Go’s HTTP server realizes that a full implementation of an HTTP connection could make optimal use of multiple optional behaviors such as CloseNotifier or Hijacker. That is why Go’s HTTP server uses a response object that implements not only ResponseWriter, but also CloseNotifier and Hijacker.
This causes trouble when wrapped by middleware such as Goji. Goji is one of Go’s most popular http middleware frameworks. If it were to wrap a ResponseWriter and only expose ResponseWriter functions, it would no longer create a middle layer that could take advantage of these features. Most importantly, by not implementing io.ReaderFrom it would severely hamper many memory optimizations built inside Go. For example, Go’s built in io.Copy function takes advantage of these functions to avoid memory allocations.
// If the reader has a WriteTo method, use it to do the copy.
// Avoids an allocation and a copy.
if wt, ok := src.(WriterTo); ok {
return wt.WriteTo(dst)
}
// Similarly, if the writer has a ReadFrom method, use it to do the copy.
if rt, ok := dst.(ReaderFrom); ok {
return rt.ReadFrom(src)
}
Goji’s solution is to enumerate the interfaces they believe are important.
_, cn := w.(http.CloseNotifier)
_, fl := w.(http.Flusher)
_, hj := w.(http.Hijacker)
_, rf := w.(io.ReaderFrom)
bw := basicWriter{ResponseWriter: w}
if cn && fl && hj && rf {
return &fancyWriter{bw}
}
Most of these are to create implementations purely for satisfy a type that the wrapped interface satisfies.
func (f *fancyWriter) Flush() {
fl := f.basicWriter.ResponseWriter.(http.Flusher)
fl.Flush()
}
A major concern of this approach is that it requires internal implementation information about which methods and interfaces a ResponseWriter should implement in order to be processed efficiently. It also removes any private unimplementable interfaces the ResponseWriter may have had leaving the net/http package, which could be useful if it ever returns back to the package. For example, it does not currently support http.Pusher. Finally, it doesn’t deal gracefully with the situation of a ResponseWriter implementing only a subset of the 4 types it attempts to be.
Internal Redis caching library Link to heading
Internally at twitch, I wrote a caching library that has as one backing implementation redis. It has an API that looks similar to the below.
type ObjectCache {}
func (r *ObjectCache) Cached(ctx context.Context, key string, callback func() (interface{}, error), storeIntoPtr interface{}) error {
// Check cache for object, if it exists, get it from cache, otherwise call callback, store to cache, and put result
// into storeIntoPtr pointer
}
func (app * Application) UserCode(ctx context.Context) (*UserProfile, error) {
var ret *UserProfile
err := app.Cache.Cached(ctx, "user:" + userID, func() (interface{}, error) {
// This is only called if the object isn't in the cache
return app.BackingStore.GetUser(ctx, userID)
}, &ret)
return ret, err
}
From the user side, the application can cache anything that is serializable. This worked great until we needed to add a TTL that depends upon the content of the backing store’s User object. The best implementation was to add this as optional logic on the interface{} returned by callback.
type ObjectWithTTL **interface** {
CacheTTL() time.Duration
}
// ...
var ttl time.Duration
if objectHasTTL, ok := vals.(ObjectWithTTL); ok {
ttl = objectHasTTL.CacheTTL()
} else {
ttl = defaultTTL
}
An alternative implementation would be to modify the callback of Cached function from
callback func() (interface{}, error)
to
callback func() (ObjectWithTTL, error)
My motivations for not doing this are the same motivations that Go’s HTTP server and error interface have for not adding stack traces or Flush to their implementation: it goes against Go best practices around simplicity in API (among others).
Using this abstraction, I ran into the same issues as the error library around wrappers of wrappers of returned interfaces. Maintaining the code myself, it is easy to see who is wrapping which cached objects, but is unscalable as the codebase and wrapper classes grows.
Solutions in other languages Link to heading
Python/ruby Link to heading
Looking at the errors package as an example, dynamic languages have multiple solutions. One solution is to enumerate the functions of the wrapped object and attach them to the returned object. Another, more common one, is to hook into functions on the object itself that help determine object or class resolution, such as get or instancecheck.
Similar features exist in most dynamic languages, but in my view don’t fit with Go due to either speed costs or language fit.
Java Link to heading
This issue is mitigated in Java due to a combination of language features and coding best practices.
I think the best practices of Java vs Go make the most interesting comparison. Best practice design in Java, as mirrored by their standard library, means much fatter interfaces with more methods. Taking the errors wrapping example, the Exception class in Java extends Throwable which already has a method for getting stack traces, as well as a dozen other features. Another example: Go’s io.Reader (which has a single method) compared to InputStream in java has 9. Similar to the reasons I explored previously about Preemptive interfaces, the best practice towards fatter interfaces stems from Go’s choice of implicit interfaces. These fatter base interfaces mitigate many of the needs for interface wrapping by shoving them on the interface itself.
Another best practice that has evolved inside Java is the use of AbstractBaseClass. Your types wouldn’t implement an interface at all, instead they would extend the AbstractBaseClass which implements the interface. This allows graceful upgrade from otherwise breaking changes: functions can be freely added to the base interface with default implementations in the AbstractBaseClass. As long as your company has a practice of extending the AbstractBaseClass rather than implementing the interface, old code will have some reasonable implementation for a new function (such as TTL()), while new code can optionally add a TTL if it needs one. For many reasons, best practice in Go has eschewed the use of AbstractBaseClass.
Aside from how interfaces are designed in Java vs Go, Java has language features that allow it to mitigate many of the interface wrapping issues exposed in Go. When a type is already known, the combination of Java’s ProxyClass and Generics allow Java to wrap interfaces while preserving type. Similar implementations could exist via Java’s ClassLoader.
Ideal solution from the client side Link to heading
An ideal solution from the client side would be for the errors package’s IsTemporary function to work strictly with type assertions.
// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}
This would require the errors package to be able to wrap an error while still maintaining the Temporary() function on the returned object.
Possible solutions in Go Link to heading
language keyword Link to heading
Go’s type assertions are used to see if an interface satisfies another interface.
te, ok := err.(temporary)
The resolution of this check could be extended. One example is with a keyword in the struct the errors package uses to wrap errors.
type withStack struct {
error
*stack
}
You could imagine this struct instead appearing as
type withStack struct {
wraps(error)
*stack
}
In this situation, the wraps keyword would be picked up by the err.(temporary) type assertion and resolution would chain down to the wrapped object.
Reflection member variable Link to heading
Another solution would hide this inside a reflection package. Consider where the withStack type is created.
func WithStack(err error) error {
if err == nil {
return nil
}
return &withStack{
err,
callers(),
}
}
Instead, we could attach metadata to the returned object that signals type assertions should fall to the err variable.
func WithStack(err error) error {
if err == nil {
return nil
}
return reflect.WrapsType(&withStack{
err,
callers(),
}, err).(error)
}
In this implementation, WrapsType would attach information to an interface that future type assertions of the interface would use when resolving what an object implements. Potentially, this could require withStack including a member variable that reflect package uses to store this information for the runtime.
type withStack struct {
reflect.TypeCheck
error
*stack
}
This option would pass go’s backwards compatibility promise since no existing go code could possibly include a member variable of a type that does not exist yet: reflect.TypeCheck.
Dynamic class creation Link to heading
With dynamic class creation, the errors package could create classes on the fly that implement the interface required. This would require a combination of walking the functions of the incoming type and attaching them to a new class of the outgoing error type.
There is some more context with this solution on github here and here. My only extension is that while these are concerned with general dynamic type creation abilities, the specific use cases I see are from wrapping an existing interface.
type assertion function on the wrapping object Link to heading
Another solution is to have type assertions understand a special function that could exist on the wrapper struct, which is used to return an acceptable object.
// TypeCast attempts to return an object of type t from withStack.
func (t *withStack) TypeCast(t reflect.Type) interface{} {
}
Open resolution questions Link to heading
Open questions around interface wrapping include edge cases where half the functions are on the base class and half the functions are on the wrapped class.
type I interface {
Func()
}
type I2 interface {
Another()
}
type I3 interface {
Func()
Another()
}
// If half the functinos are in the base struct
type Iimpl struct{}
func (i *Iimpl) Func(){}
// And half are in the larger struct
type I2impl struct{
wraps(Iimpl)
}
func (i *I2impl) Another(){}
x := I2impl{}
// Does that struct implement the combination interface?
_, isBoth := x.(I3)
In this example, would x implement I3? Another open question is if a type is allowed to wrap multiple interfaces.
Caveats of blind wrapping Link to heading
Blind interface wrapping works best when you add functionality (such as stack traces or TTL), rather than attempt to extend functionality. Take for example Go’s LimitedReader which attempts to limit how many bytes can be read from the wrapped interface. If it blindly wrapped all the functions of the contained Reader R, it could inherit functions such as WriteTo which would cause it to read more content than the contract allows.
Overview and personal suggestions Link to heading
Go’s combination of static typing and implicit interfaces have created a best practice of minimal, compact interfaces. While ideal in most situations, years of experience with the language have revealed that internal optimizations and varied object requirements force us to have some way to extend an interface with optional behavior, while not removing optional optimizations or type information.
My personal opinion is that creative minds could invent some extension of the reflect package to resolve this. It would allow library authors to simultaneously exploit the advantages of compact interfaces and efficient, expressive implementations.