Como Usar Interfaces em Go

Introdução

Escrever código flexível, reutilizável e modular é fundamental para o desenvolvimento de programas versáteis. Trabalhando dessa forma garante que o código seja mais fácil de manter, evitando a necessidade de fazer a mesma mudança em vários lugares. Como você consegue isso varia de linguagem para linguagem. Por exemplo, herança é uma abordagem comum usada em linguagens como Java, C++, C# e mais.

Desenvolvedores também conseguem esses mesmos objetivos de projeto através de composição. Composição é uma maneira de combinar objetos ou tipos de dados em mais complexos. Esta é a abordagem que o Go usa para promover a reutilização de código, modularidade e flexibilidade. Interfaces no Go fornecem um meio de organizar compostos complexos, e aprender a usá-las permitirá que você crie código comum e reutilizável.

Neste artigo, vamos aprender a compor tipos personalizados que têm comportamentos comuns, o que nos permitirá reutilizar o nosso código. Também vamos aprender a implementar interfaces para nossos próprios tipos personalizados que satisfazem interfaces definidas em outro pacote.

Definição de um Comportamento

Um dos aspectos fundamentais da composição é o uso de interfaces. Uma interface definirá o comportamento de um tipo. Uma das interfaces mais comumente utilizadas na biblioteca padrão do Go é a fmt.Stringer:

type Stringer interface {
    String() string
}

A primeira linha de código define uma tipo chamada Stringer. Ela então declara que é uma interface. Mesmo como definindo estruturas, o Go usa parênteses curvas ({}) para envolver a definição da interface. Na diferença de definir estruturas, nós somente definimos o comportamento dessa interface; ou seja, “o que este tipo pode fazer”.

No caso da Stringer interface, a única função é a String(). A função não recebe argumentos e retorna uma string.

Agora, vejamos algum 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())
}

O primeiro passo é criar um novo tipo chamado Article. Este tipo tem um campo Title e um campo Author e ambos são do tipo string dado:

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

Aproximadamente, definimos uma método chamada String no tipo Article. O método String irá retornar uma string que represente 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, na nossa função main , criamos uma instância do tipo Article e a associamos à variável chamada a. Nós passamos os valores de "Understanding Interfaces in Go" para o campo Título e "Sammy Shark" para o campo Autor:

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

Então, imprimimos o resultado da função String chamando fmt.Println e passando o resultado da chamada da função a.String():

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

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

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

Até agora, não usamos uma interface, mas sim criamos um tipo que tinha um comportamento desejado. Esse comportamento correspondeu ao interface fmt.Stringer. A próxima etapa é ver como podemos usar esse comportamento para fazer nosso código mais reusável.

Definindo uma Interface

Agora que temos o tipo definido com o comportamento desejado, vamos ver como usar esse comportamento.

Antes disso, porém, vejamos o que precisaria fazer se quissemos chamar o método String da classe 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())
}

Na código que vem a seguir, adicionamos uma nova função chamada Print que recebe um Artigo como argumento. Nota-se que o único que faz a função Print é chamar o método String. Por essa razão, podemos definir uma interface para ser passada à 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 definida em Go que tem escopo para um tipo específico. Diferente de uma função, um método pode ser chamado somente da instância do tipo em que foi definido.

Nós atualizamos a assinatura do método Print para aceitar uma Stringer, e não um tipo concreto de Artigo. Porque o compilador sabe que uma interface Stringer define o método String, ele aceita apenas tipos que também possuam 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 Livro. Ele também tem o método String definido. Isso significa que também satisfez a interface Stringer. Por causa disso, podemos enviar-lo para a 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, mostramos como usar apenas uma interface de vez. No entanto, um interface pode ter mais de um comportamento definido. A seguir, veremos como tornar nossas interfaces mais flexíveis declarando métodos adicionais.

Múltiplos Comportamentos em uma Interface

Uma das principais tenências de escrever código Go é escrever tipos pequenos e concisos e compor-los para tipos maiores e mais complexos. O mesmo é verdade quando compondo interfaces. Para ver como construir uma interface, vamos começar definindo primeiro uma interface com apenas um método chamado Área. Neste exemplo, definiremos duas formas, Circulo e Quadrado, que ambas definirão o método Área. Este método retornará o área geométrica de suas respectivas formas:

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
}

Porque cada tipo declara o método Área, podemos criar uma interface que defina esse comportamento. Definimos a seguinte Sizer:

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

Agora definimos uma função chamada Menor que aceita dois argumentos Sizer e retorna o menor entre eles:

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

Observe que não somente aceitemos ambos os argumentos como o tipo Sizer, mas também retornamos o resultado como Sizer também. Isso significa que não retornamos um Quadrado ou um Circulo, mas sim o comportamento da interface de Sizer.

Finalmente, imprimimos o que tinha o área mais pequena:

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

Agora, vamos adicionar outto comportamento a cada tipo. Desta vez, vamos adicionar o método String() que retorna uma string. Isto 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 ambos os tipos Circle e Square implementam ambos os métodos Area e String, agora podemos criar outra interface para descrever esse conjunto mais amplo de comportamento. Para fazer isso, vamos criar uma interface chamada Shaper. Vamos compor isso das interfaces Sizer e fmt.Stringer:

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

Nota: É considerado idiomático tentar nomear sua interface terminando em er, como fmt.Stringer, io.Writer, etc. É por isso que nós chamamos nossa interface Shaper, e não Shape.

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

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

Se executarmos o programa, receberá a seguinte saída:

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 vemos como podemos criar interfaces menores e construí-las em interfaces maiores conforme necessário. Enquanto poderíamos ter começado com a interface maior e passá-la para todas as nossas funções, é considerado melhor prática enviar apenas a interface mais pequena para uma função que é necessária. Isso normalmente resulta em código mais claro, pois qualquer coisa que aceita uma interface específica menor só pretende trabalhar com esse comportamento definido.

Por exemplo, se passarmos Shaper para a função Less, podemos assumir que vai chamar ambas as funções Area e String. No entanto, porque só pretendemos chamar o método Area, faz a função Less mais clara, pois sabemos que somente poderemos chamar o método Area de qualquer argumento passado para ela.

Conclusão

Veremos como criar interfaces menores e construí-las para formas maiores permitem nós compartilhar apenas o que precisamos com uma função ou método. Também veremos que podemos compor nossas interfaces de outras interfaces definidas, incluindo aquelas definidas em pacotes diferentes, não apenas nas nossas próprias.

Se você gostou de saber mais sobre a linguagem de programação Go, consulte a série Como Códigar em Go.

Source:
https://www.digitalocean.com/community/tutorials/how-to-use-interfaces-in-go