Skip to content

Instantly share code, notes, and snippets.

@JamWils
Last active August 6, 2021 19:49
Show Gist options
  • Save JamWils/397a73e64b8aeb56e9c484e94b8c1d82 to your computer and use it in GitHub Desktop.
Save JamWils/397a73e64b8aeb56e9c484e94b8c1d82 to your computer and use it in GitHub Desktop.
Golang: Interface vs Struct

Golang: Interface vs Struct

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 {}.

Dependencies

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.

Exceptions

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment