I’ve been coding in Go for a while now and I would like to add that I’m very happy to have adopted it as one of my daily use programming languages.
I haven’t seen much code from other projects except the standard library, but I noticed that most open source projects developed in go don’t follow a common pattern when declaring scopes, specially in the structs.
The following code has been extracted from a well known project and it’s been used around the world:
type DiscoveryInterface interface {
RESTClient() restclient.Interface
}
type DiscoveryClient struct {
restClient restclient.Interface
LegacyPrefix string
}
func (c *DiscoveryClient) RESTClient() restclient.Interface {
if c == nil {
return nil
}
return c.restClient
}
func NewDiscoveryClient(c restclient.Interface) *DiscoveryClient {
return &DiscoveryClient{restClient: c, LegacyPrefix: "/api"}
}
If we analyse the previous code, we see:
- An interface called
DiscoveryInterface
. Despite the name of the interface is not right (See Effective Go). - A public struct called
DiscoveryClient
that implementsDiscoveryInterface
. - A method called
NewDiscoveryClient
which returns a pointer to aDiscoveryClient
struct. This is the constructor.
The purpose of creating a constructor is to encapsulate the struct creation logic in order to create structs with default values as well as enforce business logic.
client1 := &DiscoveryClient{}
client2 := NewDiscoveryClient(c)
// client1 != client2
In the previous example we are getting two structs with different states. Most of the time this will not be an expected behaviour.
So what is the purpose of have this constructor if we are also allowing DiscoveryClient
to be instantiated directly, getting a struct with an unexpected state?
Usually, because no reason. And here my explanation:
- The constructor should return the
DiscoveryInterface
interface, since it is the only type that we should know from outside the package, avoiding any kind of unwanted coupling. DiscoveryClient
, since we will not deal with the implementation from outside the package anymore, should be private- Therefore, the
legacyPrefix
field should also be private. In fact, from the right moment we work with theDiscoveryInterface
interface, making use (from another package) of a struct method/field that is not in the interface would violate the Liskov Substitution Principle. In case we need read access to that field, we should consider declaring a getter in the interface (See Effective Go).
At a glance, the code would look like:
type DiscoveryInterface interface {
RESTClient() restclient.Interface
}
type discoveryClient struct {
restClient restclient.Interface
legacyPrefix string
}
func (c *discoveryClient) RESTClient() restclient.Interface {
if c == nil {
return nil
}
return c.restClient
}
func NewDiscoveryClient(c restclient.Interface) DiscoveryInterface {
return &discoveryClient{restClient: c, legacyPrefix: "/api"}
}
As examples of this approach, we can find some packages in the standard library like context and io as well.
Share this post
Twitter
Google+
Facebook
Reddit
LinkedIn
StumbleUpon
Email