DEV Community

Lucas Caparelli
Lucas Caparelli

Posted on • Edited on

Should my methods return structs or interfaces in Go?

Versão em Português


Like most things in software design, it depends. And I cannot stress this enough, it really depends on the situation and what you wish to achieve.

Now, I hear some of you already screaming at the top of your lungs a very useful rule of thumb (or even proverb, if you will): "Accept interfaces, return structs". That's very good advice, really, but there always are exceptions. No pattern is so flexible that it fits every situation.

With that said, I'd wager this rule does make sense more often than not, so let's have a look at the reasoning behind returning structs.

But first, some basics about how interfaces work in Go. If you're already familiar with them, feel free to skip ahead to the next section.

Interfaces in Go

It's important to keep in mind that Go interfaces are not strictly bound to types. What I mean by this is that you never explicitly say that a type implements a particular interface. All that you need to do in order to implement an interface in Go is to implement the methods that satisfy the interface itself.

Think of an interface as a set of behaviors that are expected from a type. You don't usually see ducks with a label written "Duck", but you know it is one because it looks like a duck, it quacks like a duck, it has feathers like a duck, etc. You infer what it is from its behavior, and that is exactly what Go does in compile time.

If you're wondering, Go uses "structural typing", not "duck typing", meaning this all happens in compile time, not run time. Remember that Go is statically typed.

Example, please. Take a look at the following Person interface:

type Person interface {
    Name() string
    Age() int
}

A possible implementation of that interface is:

type Gamer struct {
    name  string
    age   int
}

func NewGamer(name string, age int) *Gamer {
    return &Gamer{
        name: name,
        age:  age,
    }
}

func (g *Gamer) Name() string {
    return g.name
}

func (g *Gamer) Age() int {
    return g.age
}

Does Go understand that Gamer is a Person?

func main() {
    var person Person = NewGamer("Jane Doe", 42)
    fmt.Println(person.Name())
}

The output from this program:

╰─ go run example.go
Jane Doe

Yes, Go understands that the Gamer type is a Person (or that it implements the Person interface)!

When to return structs

Alright, now that we've refreshed our understanding of interfaces in Go, let's imagine that we wish to extend the Gamer behavior. We'll also say it has a slice of games:

type Person interface {
    Name() string
    Age() int
}

type Game string
type Gamer struct {
    name  string
    age   int
    games []Game
}

func NewGamer(name string, age int) *Gamer {
    return &Gamer{
        name: name,
        age:  age,
    }
}

func (g *Gamer) Name() string {
    return g.name
}

func (g *Gamer) Age() int {
    return g.age
}

func (g *Gamer) Games() []Game {
    return g.games
}

func (g *Gamer) AddGame(game Game) {
    g.games = append(g.games, game)
}

Note that the program still works at this point as Gamer still implements Person, we just added more behavior and state to it.

Now, let's try simply modifying the main function we had before to make use of this new stuff we added:

func main() {
    var person Person = NewGamer("Jane Doe", 42)
    fmt.Println(person.Name())
    person.AddGame("Metal Gear Solid 3")
}

We'll run into:

╰─ go run example.go
# command-line-arguments
./example.go.go:46:8: person.AddGame undefined (type Person has no field or method AddGame)

This is because the person variable is of type Person, which does not have the AddGame() method. If we create a variable gamer of type *Gamer we can access those new cool and shiny methods:

func main() {
    var person Person = NewGamer("Jane Doe", 42)
    fmt.Println(person.Name())

    var gamer *Gamer = NewGamer("John Doe", 29)
    gamer.AddGame("Metal Gear Solid 3")
    fmt.Println(gamer.Games())
}

It runs fine:

╰─ go run example.go  
Jane Doe
[Metal Gear Solid 3]

Now, what if the NewGamer() function actually returned the Person interface?

func NewGamer(name string, age int) Person {
    return &Gamer{
        name: name,
        age:  age,
    }
}

In this scenario we'd never be able to make the following assignment:

var gamer *Gamer = NewGamer("John Doe", 29)

Because NewGamer returns Person, not *Gamer. And even if we change gamer's type to Person we'd be stuck with the behavior from the Person interface only. We'd lose access to the nice methods only the Gamer type has.

This is the main motivation behind returning structs instead of interfaces: it leaves it up to the caller if they want to use it as the interface or if they want to use it as the struct, and in the end that makes a lot of sense.

When to return interfaces?

Let's expand a bit on our previous Person implementations. Now we'll also have a Student:

type Student struct {
    name      string
    age       int
    knowledge int
}

func NewStudent(name string, age int) *Student {
    return &Student{
        name: name,
        age: age,
    }
}

func (s *Student) Name() string {
    return s.name
}

func (s *Student) Age() int {
    return s.age
}

func (s *Student) Study(hours int) {
    s.knowledge += hours
}

Becoming a person filled with knowledge isn't as simple as depicted here, but this model meets our requirements. Remember to KISS.

What if we needed to print the names of every people we know? In this case, an auxiliary function returning a slice of Person would make a lot of sense, because Name() is part of the Person interface:

func PrintAllNames() {
    for _, person := range getAllPeople() {
        fmt.Printf("Name: %s\n", person.Name())
    }
}

func getAllPeople() []Person {
    var people []Person
    // logic implementation
    return people
}

This is simple, but one of the cases on which it's actually ok for a method or function to return an interface instead of the struct itself.

Without a function like this, you would need to create a separate function to get all instances of each implementation of Person, another for printing the names of these instances for each implementation of Person and a final one that orchestrates all of these.

func PrintAllNames() {
    printAllGamerNames()
    printAllStudentNames()
}

func printAllGamerNames() {
    for _, gamer := range getAllGamers() {
        fmt.Printf("Name: %s\n", gamer.Name())
    }
}

func getAllGamers() []Gamer {
    var gamers []Gamer
    // logic implementation
    return gamers
}

func printAllStudentNames() {
    for _, student := range getAllStudents() {
        fmt.Printf("Name: %s\n", student.Name())
    }
}

func getAllStudents() []Student {
    var students []Student
    // logic implementation
    return students
}

See that nasty repetition of code? And worse: get ready to do this every time you add another implementation of Person. Not looking forward to it, I assume. And even worse: this sort of manual addition is error-prone and is likely to lead to bugs.

You could say "well, even if you returned a slice of Person you would still need to write the necessary logic to get all the instances of a new implementation of Person" and you would be correct. But notice how you do not need to write specific logic for printing the names, you only do that once in the previous version of the PrintAllNames() function:

func PrintAllNames() {
    for _, person := range getAllPeople() {
        fmt.Printf("Name: %s\n", person.Name())
    }
}

Summarizing: returning structures is good most of the time because you leave it up to the caller to decide how to make the assignment and how to use it, but there are cases on which using an interface just makes more sense and can save you a lot of unnecessary writing.

Thank you for reading my article! Let me know your thoughts on the points I tried to make and if you have a different view, please let me know! I'm always up for productive discussion. :-)

Top comments (2)

Collapse
 
mikeschinkel profile image
Mike Schinkel

You did not actually explain when to return an interface, even though your section heading implied you would.

Here, I'll help. One case would be when you want to disable users of your package from instantiating your type without using your instantiator function which you would typically name New() or NewWhatever().

But don't take my word for it, consider that the Go standard library does exactly that in several places, such as with http.NewFileTransport() and http.RoundTripper.

For example:

package example

type myStruct struct {}  

type Greeter interface{
   Greet() string
}

func (m *myStruct) Greet() string {  
   return "hello"  
}  

func New() Greeter {  
   return &myStruct{}  
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
cristia45971976 profile image
Cristiano

nice view, totally agree with your point