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.)