Some notes on "closed interfaces" in Go
One reaction to basic proposals for union types in Go is to note that "closed interfaces" provide a lot of these features (cf). When I saw this I had to refresh myself about what such a closed interface is, and then think about some of the potential issues and limitations involved, leaving me with some things I want to note down for my later reference.
What I've seen called a closed interface is an interface that requires an unexported method:
type Closed interface { isClosed() NormalMethod1(...) ... NormalMethod2(...) ... [...] }
Although it's not spelled out exactly in the Go language specification, an interface with an unexported method
like this can't be implemented by any type outside of its package,
because such an external type can't have the right 'isClosed()
'
method on it (since it's not exported, the identifier for the
method is unique to the package, or at least I
believe that's how the logic flows). The 'isClosed()
' method
doesn't do anything and need never be called, it just has to be
declared as an (empty) method function on everything that is going
to be part of the Closed
interface.
This means that there is a finite and known list of types that
implement the Closed
interface, instead of the potentially unknown
list of them that could exist for an open interface. Go tooling can
use this knowledge to see, for example, that a type switch on
Closed
is exhaustive, or that a given type assertion will never
succeed. However, in the current state of godoc, I don't believe
that generated package documentation will automatically tell you
which types implement Closed
; you'll have to remember to document
that explicitly.
(I don't know if there's any Go tooling that does this today,
especially when run in the context of other packages instead
of the package that defines the Closed
interface.)
Code in other packages can still construct a nil Closed
interface
value, or zero values of any exported types that implement Closed
(and then turn such a zero value into a non-nil Closed
interface
value). If you want to be extra tricky, you can make all the types
that implement Closed
be unexported; at that point the only way
people outside your package can easily create or manipulate those
types is through an instance of Closed
that you give them, and
implicitly only through its methods. The Closed
interface is not
merely a closed interface, it has become an opaque interface
(short of people getting out the chainsaw of reflect
and perhaps unsafe
).
However, this is also a limitation of the Closed
interface, and
of closed interfaces in general. For good reason, you can't add
methods to types declared outside your package, so you can't make
an outside type be a member of the Closed
interface, even though
you control the interface's definition in your package. In order
to induct a useful outside type into the world of Closed
, you
must wrap it in a type from your package, and this type must be a
concrete type, even if what you want to pull in is another interface.
I believe that under at least some circumstances, this will cost
you some extra memory. More broadly, I don't think you can really
have two separate packages that cooperate so each defines some types
that are part of Closed
. One way or another you have to put all
the types in one package.
In my view, this means that a closed interface isn't really useful to document inside your package that this particular interface will only ever contain a limited number of outside types (or a mixture of outside types and inside types), including outside types from a related package. You can use it for this but there's a chunk of bureaucracy involved for each outside type you want to pull in. If you go to this effort, presumably you have tooling that can deduce what's going on and take advantage of this knowledge.
(These days you could define a generic type that wraps another type
and implements your Closed
interface for it, making this sort of
bureaucracy easier, at least.)
|
|