DEV Community

Lucas Caparelli
Lucas Caparelli

Posted on • Updated on

Meus métodos devem retornar interfaces ou structs em Go?

English version


Como a maioria das coisas no design de software, depende. E não posso enfatizar isso o suficiente: realmente depende da situação e de que resultado você está tentando alcançar.

Já posso até ouvir alguns de vocês gritando uma regra muito útil (ou até mesmo provérbio, se preferir) "Accept interfaces, return structs" ("Aceite interfaces, retorne structs"). É um conselho muito bom, realmente, mas sempre há exceções. Nenhum padrão é tão flexível que se adapta a todas as situações.

Com isso dito, eu apostaria que essa regra faz sentido a maioria das vezes, então vamos dar uma olhada no raciocínio que nos leva a retornar structs.

Mas primeiro, algumas noções básicas sobre como as interfaces funcionam no Go. Se você já as conhece bem, sinta-se livre para pular para a próxima seção.

Interfaces no Go

É importante ter em mente que as interfaces em Go não estão estritamente vinculadas a tipos. O que quero dizer com isso é que você nunca diz explicitamente que um tipo implementa uma interface específica. Tudo o que você precisa fazer para implementar uma interface no Go é implementar os métodos que satisfazem essa interface.

Pense em uma interface como um conjunto de comportamentos que são esperados de um tipo. Você normalmente não vê patos com um rótulo escrito "Pato", mas sabe que é um porque parece um pato, grasna como um pato, tem penas como um pato, etc. Você deduz o que é a partir de seu comportamento, e é exatamente isso que o Go faz em tempo de compilação.

Caso esteja se perguntando, o Go usa "structural typing", não "Duck Typing", o que significa que tudo isso acontece em tempo de compilação, não em tempo de execução. Lembre-se de que Go é estaticamente tipado.

Exemplo, por favor. Dê uma olhada na seguinte interface Pessoa:

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

Uma possível implementação dessa interface é:

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
}

A Go entende que Gamer é uma Person?

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

A saída deste programa:

╰─ go run example.go
Jane Doe

Sim, o Go entende que o tipo Gamer é umaPerson (ou que implementa a interface Person)!

Quando retornar structs

Tudo bem, agora que compreendemos o funcionamento das interfaces em Go, vamos imaginar que desejamos estender o comportamento do Gamer. Também vamos dizer que tem um slice de jogos (Game):

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

Observe que o programa ainda funciona neste momento, pois o tipo Gamer ainda implementa Person, apenas adicionamos mais comportamento e estado a ele.

Agora, vamos tentar simplesmente modificar a função main que tínhamos antes para fazer uso dessas novas coisas que adicionamos:

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

Nos damos de cara com:

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

Isso ocorre porque a variável person é do tipoPerson, que não possui o método AddGame(). Se criarmos uma variável gamer do tipo *Gamer, podemos acessar esses novos métodos legais e brilhantes:

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())
}

A execução ocorre sem problemas:

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

Agora, e se a função NewGamer() na verdade retornasse a interface Person?

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

Nesse cenário, nunca poderíamos fazer a seguinte atribuição:

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

Porque NewGamer retorna Person, não *Gamer. E mesmo se mudarmos o tipo de gamer para Person, ficaríamos presos ao comportamento da interface Person apenas. Perderíamos o acesso aos métodos que somente o tipo Gamer possui.

Essa é a principal motivação por trás do retorno de structs em vez de interfaces: deixa a cargo de quem está invocando a função ou método se quer seu retorno como uma interface ou se quer usá-lo como struct e, no final das contas, isso faz muito sentido.

Quando retornar interfaces?

Vamos expandir um pouco as implementações anteriores do Person. Agora também teremos um tipo 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
}

Tornar-se uma pessoa cheia de conhecimento não é tão simples como o descrito aqui, mas esse modelo atende aos nossos requisitos. Lembre-se do princípio KISS.

E se precisássemos imprimir os nomes de todas as pessoas que conhecemos? Nesse caso, uma função auxiliar retornando um slice de Person faria muito sentido, porque Name() faz parte da interface Person:

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

func getAllPeople() []Person {
    var people []Person
    // implementação da lógica
    return people
}

Isso é simples, mas um dos casos em que é realmente aceitável que um método ou função retorne uma interface em vez da própria estrutura.

Sem uma função como essa, você precisaria criar uma função separada para obter todas as instâncias de cada implementação de Person, outra para imprimir os nomes dessas instâncias para cada implementação de Person e uma última que orquestra tudo isso.

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

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

func getAllGamers() []Gamer {
    var gamers []Gamer
    // implementação da lógica
    return gamers
}

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

func getAllStudents() []Student {
    var students []Student
    // implementação da lógica
    return students
}

Vê aquela repetição desagradável de código? E pior: prepare-se para fazer isso toda vez que adicionar outra implementação do Person. Aposto que não está feliz com isso. E ainda pior: esse tipo de adição manual é suscetível a erros e aumenta a probabilidade de introduzir um bug.

Você poderia dizer "bem, mas mesmo que você retornasse um slice de Person, você ainda precisaria escrever a lógica necessária para obter todas as instâncias de uma nova implementação de Person" e você estaria correta/correto. Mas observe como você não precisa escrever uma lógica específica para imprimir os nomes, isso é feito apenas uma vez na versão anterior da função PrintAllNames():

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

Resumindo: retornar structs é uma boa ideia na maioria das vezes, porque você deixa o usuário livre para escolher como usar seu retorno, mas há casos em que o uso de uma interface faz mais sentido e pode evitar a escrita de código desnecessária.

Obrigado por ler o meu artigo! Deixe-me saber sua opinião sobre os pontos que levantei e se você tiver uma visão diferente, diga qual é! Estou sempre disponível para uma discussão produtiva. :-)

Top comments (0)