Some one asked me the question, how do you decide when to make an interface vs just instantiating a struct?
Honestly, this is not a perfect answer, but here are some simple guidelines that I would follow when creating an interface
or just creating a simple struct
.
Let's start with a simple object Person
. There is nothing astronomical about the object and it contains some simple information. Here there isn't much to create
type Person struct {
firstName string
lastName string
}
func NewPerson(firstName, lastName string) Person {
return Person {
firstName: firstName,
lastName: lastName,
}
}
func (p Person) FullName() string {
return fmt.Sprintf("%s %s", p.firstName, p.lastName)
}
Sure, we can create an interface to this such as Identifiable
which has a function func ID() string
, but that is really getting away from the more crucial times you would want to use an interface in Go.
Bonus fact on the above struct, since it is simple types and has no pointers or slices this can be used as a key in a map map[Person][]CreditCard
. Go will automatically hash structs unlike Java
or Swift
. You can also do comparisons such as if person1 == person2 {}
.
The interface paradigm is really useful when the structs can be either complex internally or they have dependencies. Let's fetch some people ([]Person
) from a database. Here we can really leverage the power of interfaces. Below I have an interface called PersonStore
. It has one method List()
(yes, Create would also be useful, but I am aiming for simplicity).
type PersonStore interface {
List() ([]Person, error)
}
type personSQLStore struct {
conn *db.Conn
}
func NewPersonStore(conn *db.Conn) PersonStore {
return &personSQLStore{ conn: conn }
}
func(ps personSQLStore) List() ([]Person, error) {
var allThePeople []Person
/* Code to fetch humans */
return allThePeople
}
Ultimately, I would want to write unit tests for this code, by either mocking it out. Or ideally using an in-memory SQL library written in Go that allows me to flex this code. I'm a happy camper here:
One, I am injecting the database connection and if we pivot to a different database I can write a new implementation such as personEtcdStore
to store the people in etcd
. Or I could wrap a caching layer around it.
Two, I can pass Person
throughout the code and it doesn't contain any ugly dependencies which means we probably don't need an interface
for that. Note: there might be reasons you would want an interface. That is outside the scope of this explanation.
Finally, I can create a mock for the store PersonStoreMock
. I can now write unit tests for other structs such as PersonRestHandler
and PersonCLI
without dealing with the nasty internals of personSqlStore
.
type PersonRestHandler struct {
personStore PersonStore
}
func NewPersonRestHandler(personStore PersonStore) PersonRestHandler {
return PersonRestHandler { personStore: personStore }
}
func(pa PersonRestHandler) List(w http.ResponseWriter, r *http.Request) {
people, err := pa.List()
if err != nil {
/* sql went ka-boom return 5xx */
}
/* fill out the response writer with 200 and JSON with public fields */
}
type PersonCli struct {
personStore PersonStore
}
func NewPersonCli(personStore PersonStore) PersonCli {
return PersonCli { personStore: personStore }
}
func(pa PersonCli) List(w io.Writer) {
people, err := pa.List()
if err != nil {
/* sql went ka-boom write error out to console asking user to try again */
}
/* spam the user's console with ALL of the people */
}
Whether PersonRestHandler
and PersonCli
can share the same interface for List()
is beyond the scope of this article.
We can now inject personSqlStore
into these structs and we can pass PersonStoreMock
into them from our unit tests.
ctx := context.Background()
conn := db.NewConnection(ctx)
personStore := NewPersonStore(conn)
personHandler := NewPersonHandler(personStore)
router.Handler("/peeps", func(w, r) {
personHandler.List(w, r)
})
/ ** UNIT TESTS **/
personStore := &PersonStoreMock{}
personHandler := NewPersonHandler(personStore)
Could PersonCli
and PersonRestHandler
have interfaces as well? Sure, but given this simple use-case it doesn't seem like there is a need. Now if you are going to pass them into some other struct then yes I would argue that you should.
Of course, this isn't an all encompassing article on every caveat around interfaces and structs. My opinion is that while you can do OOP in Go, the language wasn't designed in the same vein as Java or C#. Things are simpler in Go, and while you can apply SOLID principles, the types are generally simple in nature.
You might find some method receivers so complex in a struct that it is worth mocking out so you can write tests more easily. An interface would be useful in this case, but I would also ask. Why is it so complex and can it be broken down?