Introdução

Escrever um código flexível, reutilizável e modular é vital para o desenvolvimento de programas versáteis. Trabalhar dessa maneira garante que o código seja mais fácil de manter, evitando assim a necessidade de fazer a mesma mudança em vários locais. A maneira de se conseguir isso irá variar de uma linguagem para outra. Por exemplo, a herança é uma abordagem comum usada em linguagens como Java, C++, C#, entre outras.

Os desenvolvimentos também podem atingir esses mesmos objetivos de design através da composição. A composição é uma maneira de combinar objetos ou tipos de dados em um ambiente mais complexo. Esta é a abordagem que a linguagem Go usa para promover a reutilização, modularidade e flexibilidade dos códigos. As interfaces em Go proporcionam um método de organizar composições complexas. Aprender como usá-las permitirá que você crie um código comum e reutilizável.

Neste artigo, vamos aprender como compor tipos personalizados que tenham comportamentos comuns, os quais nos permitirão reutilizar o nosso código. Também vamos aprender como implementar interfaces para nossos próprios tipos personalizados que irão atender interfaces definidas de outro pacote.

Definindo um comportamento

Uma das implementações principais da composição é o uso das interfaces. Uma interface define um comportamento de um tipo. Uma das interfaces mais comuns usadas na biblioteca padrão do Go é a interface fmt.Stringer:

type Stringer interface {
    String() string
}

A primeira linha de código define um type chamado Stringer. Em seguida, ele declara que ele é uma interface. Assim como definir uma struct, o Go usa chaves ({}) para cercar a definição da interface. Em comparação com a definição das structs, definimos apenas o comportamento da interface; ou seja, “o que esse tipo pode fazer”.

No caso da interface Stringer, o único comportamento é o método String(). O método não aceita argumentos e retorna uma string.

Em seguida, vamos examinar um código que tem o comportamento fmt.Stringer:

main.go
package main

import "fmt"

type Article struct {
    Title string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

func main() {
    a := Article{
        Title: "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    fmt.Println(a.String())
}

A primeira coisa que vamos fazer é criar um novo tipo chamado Article. Esse tipo tem um campo Title e um campo Author e ambos são do tipo de dados string:

main.go
...
type Article struct {
    Title string
    Author string
}
...

Em seguida, definimos um método chamado String no tipo Article. O método String retornará uma string que representa o tipo Article:

main.go
...
func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
...

Então, em nossa função main, criamos uma instância do tipo Article e a atribuímos à variável chamada a. Informamos os valores de "Understanding Interfaces in Go" para o campo Title, e "Sammy Shark" para o campo Author:

main.go
...
a := Article{
    Title: "Understanding Interfaces in Go",
    Author: "Sammy Shark",
}
...

Em seguida, imprimimos o resultado do método String, chamando fmt.Println e enviando o resultado da chamada do método a.String():

main.go
...
fmt.Println(a.String())

Após executar o programa, você verá o seguinte resultado:

Output
The "Understanding Interfaces in Go" article was written by Sammy Shark.

Até agora, não usamos uma interface, mas criamos um tipo que tinha um comportamento. Esse comportamento correspondia ao da interface fmt.Stringer. Em seguida, vamos ver como podemos usar aquele comportamento para tornar nosso código mais reutilizável.

Definindo uma interface

Agora que temos nosso tipo definido com o comportamento desejado, podemos examinar como usar tal comportamento.

No entanto, antes de fazer isso, vamos examinar o que precisaríamos fazer se quiséssemos chamar o método String do tipo Article em uma função:

main.go
package main

import "fmt"

type Article struct {
    Title string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

func main() {
    a := Article{
        Title: "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    Print(a)
}

func Print(a Article) {
    fmt.Println(a.String())
}

Nesse código, adicionamos uma nova função chamada Print, que aceita um Article como um argumento. Note que a única coisa que a função Print faz é chamar o método String. Por conta disso, podemos em vez disso, definir uma interface para enviar para função:

main.go
package main

import "fmt"

type Article struct {
    Title string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

type Stringer interface {
    String() string
}

func main() {
    a := Article{
        Title: "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    Print(a)
}

func Print(s Stringer) {
    fmt.Println(s.String())
}

Aqui, criamos uma interface chamada Stringer:

main.go
...
type Stringer interface {
    String() string
}
...

A interface Stringer tem apenas um método, chamado String() que retorna uma string. Um método é uma função especial com escopo definido para um tipo específico em Go. Ao contrário do que ocorre com uma função, um método só pode ser chamado a partir da instância do tipo em que ele foi definido.

Em seguida, atualizamos a assinatura do método Print para aceitar um Stringer e não um tipo concreto do Article. Como o compilador sabe que uma interface Stringer define o método String, ele aceitará apenas tipos que também possuem o método String.

Agora, podemos usar o método Print com qualquer coisa que satisfaça a interface Stringer. Vamos criar outro tipo para demonstrar isso:

main.go
package main

import "fmt"

type Article struct {
    Title  string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

type Book struct {
    Title  string
    Author string
    Pages  int
}

func (b Book) String() string {
    return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
}

type Stringer interface {
    String() string
}

func main() {
    a := Article{
        Title:  "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    Print(a)

    b := Book{
        Title:  "All About Go",
        Author: "Jenny Dolphin",
        Pages:  25,
    }
    Print(b)
}

func Print(s Stringer) {
    fmt.Println(s.String())
}

Agora, adicionamos um segundo tipo chamado Book. Ele também tem o método String definido. Isso significa que ele também atende a interface Stringer. Por conta disso, podemos enviá-lo para nossa função Print:

Output
The "Understanding Interfaces in Go" article was written by Sammy Shark. The "All About Go" book was written by Jenny Dolphin. It has 25 pages.

Até agora, demonstramos como usar uma única interface apenas. No entanto, uma interface pode ter mais de um comportamento definido. Em seguida, vamos ver como podemos tornar nossas interfaces mais versáteis declarando mais métodos.

Múltiplos comportamentos em uma interface

Um dos princípios norteadores para se escrever códigos em Go é escrever tipos pequenos e concisos e que possam entrar em composições com tipos maiores e mais complexos. O mesmo vale quando se compoem interfaces. Para ver como vamos criar uma interface, vamos primeiro começar definindo apenas uma interface. Vamos definir duas formas, um Circle e um Square, e elas definirão um método chamado Area. Esse método retornará a área geométrica de suas formas respectivas:

main.go
package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * math.Pow(c.Radius, 2)
}

type Square struct {
    Width  float64
    Height float64
}

func (s Square) Area() float64 {
    return s.Width * s.Height
}

type Sizer interface {
    Area() float64
}

func main() {
    c := Circle{Radius: 10}
    s := Square{Height: 10, Width: 5}

    l := Less(c, s)
    fmt.Printf("%+v is the smallest\n", l)
}

func Less(s1, s2 Sizer) Sizer {
    if s1.Area() < s2.Area() {
        return s1
    }
    return s2
}

Como cada tipo declara o método Area, podemos criar uma interface que define tal comportamento. Criamos a seguinte interface Sizer:

main.go
...
type Sizer interface {
    Area() float64
}
...

Então, definimos uma função chamada Less que aceita dois Sizer e retorna o menor deles:

main.go
...
func Less(s1, s2 Sizer) Sizer {
    if s1.Area() < s2.Area() {
        return s1
    }
    return s2
}
...

Note que aceitamos não apenas ambos argumentos como o tipo Sizer, mas que também retornamos o resultado como um Sizer também. Isso significa que não vamos mais retornar um Square ou um Circle, mas sim a interface do Sizer.

Por fim, imprimimos o que tinha a menor área:

Output
{Width:5 Height:10} is the smallest

Em seguida, vamos adicionar outro comportamento a cada tipo. Desta vez, vamos adicionar o método String() que retorna uma string. Isso irá satisfazer a interface fmt.Stringer:

main.go
package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * math.Pow(c.Radius, 2)
}

func (c Circle) String() string {
    return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
}

type Square struct {
    Width  float64
    Height float64
}

func (s Square) Area() float64 {
    return s.Width * s.Height
}

func (s Square) String() string {
    return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
}

type Sizer interface {
    Area() float64
}

type Shaper interface {
    Sizer
    fmt.Stringer
}

func main() {
    c := Circle{Radius: 10}
    PrintArea(c)

    s := Square{Height: 10, Width: 5}
    PrintArea(s)

    l := Less(c, s)
    fmt.Printf("%v is the smallest\n", l)

}

func Less(s1, s2 Sizer) Sizer {
    if s1.Area() < s2.Area() {
        return s1
    }
    return s2
}

func PrintArea(s Shaper) {
    fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}

Como o tipo Circle e o tipo Square implementam tanto os métodos Area como String, podemos agora criar outra interface para descrever aquele conjunto de comportamentos mais amplo. Para tanto, vamos criar uma interface chamada Shaper. Vamos compor isso a partir da interface Sizer e da interface fmt.Stringer:

main.go
...
type Shaper interface {
    Sizer
    fmt.Stringer
}
...

Nota: é considerada como escolha idiomática tentar nomear sua interface de modo a terminar em er, como fmtStringer, io.Writer, etc. É por esse motivo que nomeamos nossa interface como Shaper e não Shape.

Agora, podemos criar uma função chamada PrintArea, que aceita um Shaper como um argumento. Isso significa que podemos chamar ambos métodos no valor enviado para o método Area e para o método String:

main.go
...
func PrintArea(s Shaper) {
    fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}

Se executarmos o programa, vamos receber o seguinte resultado:

Output
area of Circle {Radius: 10.00} is 314.16 area of Square {Width: 5.00, Height: 10.00} is 50.00 Square {Width: 5.00, Height: 10.00} is the smallest

Agora, vimos como podemos criar interfaces menores e compilá-las em interfaces maiores, conforme necessário. Embora pudéssemos ter começado com a interface maior para depois enviá-la para todas as nossas funções, é considerada melhor prática enviar apenas a interface menor para uma função que seja necessária. Isso tipicamente resulta em um código mais claro, uma vez que qualquer um que aceite uma interface menor específica pretende trabalhar somente com aquele comportamento definido.

Por exemplo, se enviássemos o Shaper para a função Less, poderíamos presumir que ele chamaria os dois métodos: Area e String. No entanto, como pretendemos chamar somente o método Area, a função Less se torna clara, na medida em que sabemos que somente podemos chamar o método Area de um argumento que tiver sido enviado para essa função.

Conclusão

Vimos como criar interfaces menores e que compilá-las em interfaces maiores nos permite compartilhar apenas o que precisamos para uma função ou método. Também aprendemos que podemos compor nossas interfaces a partir de outras interfaces, incluindo aquelas definidas de outros pacotes, não apenas nossos pacotes.

Se quiser aprender mais sobre a linguagem de programação Go, confira toda a série sobre Como codificar em Go.

0 Comments

Creative Commons License