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.
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
:
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:
...
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
:
...
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 mai
n, 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
:
...
a := Article{
Title: "Understanding Interfaces in Go",
Author: "Sammy Shark",
}
...
Em seguida, imprimimos o resultado do método String
, chamando fmt.Printl
n e enviando o resultado da chamada do método a.String()
:
...
fmt.Println(a.String())
Após executar o programa, você verá o seguinte resultado:
OutputThe "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.
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:
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:
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
:
...
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:
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
:
OutputThe "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.
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:
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
:
...
type Sizer interface {
Area() float64
}
...
Então, definimos uma função chamada Less
que aceita dois Sizer
e retorna o menor deles:
...
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
:
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
:
...
type Shaper interface {
Sizer
fmt.Stringer
}
...
Nota: é considerada como escolha idiomática tentar nomear sua interface de modo a ter
minar 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
:
...
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:
Outputarea 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.
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!