Nos últimos meses, tenho conduzido um
estudo perguntando às pessoas o que é difícil para elas entenderem no Go. E notei que o conceito de interfaces era mencionado regularmente nas respostas. Go foi a primeira linguagem de interface que usei, e lembro que naquele momento esse conceito parecia muito confuso. E neste guia, quero fazer o seguinte:
- Explicar na linguagem humana o que são interfaces.
- Explique como eles são úteis e como você pode usá-los no seu código.
- Fale sobre o que é a
interface{}
(uma interface vazia). - E percorra alguns tipos de interface úteis que você pode encontrar na biblioteca padrão.
Então, o que é uma interface?
O tipo de interface no Go é um tipo de
definição . Ele define e descreve os métodos específicos que
algum outro tipo deve ter .
Um dos tipos de interface da biblioteca padrão é a interface
fmt.Stringer :
type Stringer interface { String() string }
Dizemos que algo
satisfaz essa interface (ou
implementa essa interface ) se esse "algo" tiver um método com um valor específico de string de assinatura
String()
.
Por exemplo, o tipo
Book
satisfaz a interface porque possui o método
String()
:
type Book struct { Title string Author string } func (b Book) String() string { return fmt.Sprintf("Book: %s - %s", b.Title, b.Author) }
Não importa que tipo de
Book
ou o que faz. Tudo o que importa é que ele possui um método chamado
String()
que retorna um valor de string.
Aqui está outro exemplo. O tipo
Count
também
satisfaz a interface fmt.Stringer
, pois possui um método com o mesmo valor da string de assinatura
String()
.
type Count int func (c Count) String() string { return strconv.Itoa(int(c)) }
É importante entender aqui que temos dois tipos diferentes de
Book
e
Count
, que agem de maneira diferente. Mas eles estão unidos pelo fato de que ambos satisfazem a interface
fmt.Stringer
.
Você pode olhar do outro lado. Se você souber que o objeto atende à interface
fmt.Stringer
, pode assumir que ele possui um método com o valor da cadeia de assinatura
String()
que você pode chamar.
E agora a coisa mais importante.
Quando você vê uma declaração em Go (de uma variável, parâmetro de função ou campo de estrutura) que possui um tipo de interface, você pode usar um objeto de qualquer tipo, desde que satisfaça a interface.Digamos que temos uma função:
func WriteLog(s fmt.Stringer) { log.Println(s.String()) }
Como
WriteLog()
usa o tipo de interface
fmt.Stringer
na
fmt.Stringer
parâmetro, podemos passar qualquer objeto que satisfaça a interface
fmt.Stringer
. Por exemplo, podemos passar os tipos
Book
e
Count
que criamos anteriormente no método
WriteLog()
, e o código funcionará bem.
Além disso, como o objeto passado satisfaz a interface
fmt.Stringer
,
sabemos que ele possui um método
String()
, que pode ser chamado com segurança pela função
WriteLog()
.
Vamos juntar tudo em um exemplo, demonstrando o poder das interfaces.
package main import ( "fmt" "strconv" "log" )
Isso é demais. Na função principal, criamos diferentes tipos de
Book
e
Count
, mas os passamos para a
mesma função
WriteLog()
. E ela chamou as funções apropriadas
String()
e escreveu os resultados no log.
Se você
executar o código , obterá um resultado semelhante:
2009/11/10 23:00:00 Book: Alice in Wonderland - Lewis Carrol 2009/11/10 23:00:00 3
Não vamos nos deter sobre isso em detalhes. O principal a lembrar: usando o tipo de interface na declaração da função
WriteLog()
, tornamos a função indiferente (ou flexível) ao
tipo do objeto recebido. O que importa é
que métodos ele tem .
O que são interfaces úteis?
Existem várias razões pelas quais você pode começar a usar interfaces no Go. E, na minha experiência, os mais importantes são:
- As interfaces ajudam a reduzir a duplicação, ou seja, a quantidade de código padrão.
- Eles facilitam o uso de stubs em testes de unidade, em vez de objetos reais.
- Sendo uma ferramenta de arquitetura, as interfaces ajudam a desatar partes de sua base de código.
Vamos dar uma olhada mais de perto nessas maneiras de usar interfaces.
Reduza a quantidade de código padrão
Suponha que tenhamos uma estrutura de
Customer
contendo algum tipo de dados do cliente. Em uma parte do código, queremos gravar essas informações em
bytes.Buffer , e na outra parte, queremos gravar dados do cliente no
os.File no disco. Mas, nos dois casos, queremos primeiro serializar a estrutura
ustomer
para JSON.
Nesse cenário, podemos reduzir a quantidade de código padrão usando as interfaces Go.
Go tem um tipo de interface
io.Writer :
type Writer interface { Write(p []byte) (n int, err error) }
E podemos aproveitar o fato de que
bytes.Buffer e o tipo
os.File satisfazem essa interface, porque eles possuem os
métodos bytes.Buffer.Write () e
os.File.Write () , respectivamente.
Implementação simples:
package main import ( "encoding/json" "io" "log" "os" )
Obviamente, este é apenas um exemplo fictício (podemos estruturar o código de maneira diferente para obter o mesmo resultado). Mas ilustra bem as vantagens do uso de interfaces: podemos criar o método
Customer.WriteJSON()
uma vez e chamá-lo toda vez que precisamos gravar em algo que satisfaça a interface
io.Writer
.
Mas se você é novo no Go, terá algumas perguntas: “
Como sei se a interface io.Writer existe? E como você sabe de antemão que ele está satisfeito com bytes.Buffer
e os.File
? "
Receio que não haja uma solução simples. Você só precisa adquirir experiência, se familiarizar com as interfaces e os diferentes tipos da biblioteca padrão. Isso ajudará a ler a documentação desta biblioteca e a visualizar o código de outra pessoa. E para referência rápida, adicionei os tipos mais úteis de tipos de interface ao final do artigo.
Mas mesmo se você não usar interfaces da biblioteca padrão, nada o impede de criar e usar
seus próprios tipos de interface . Falaremos sobre isso abaixo.
Teste de unidade e stubs
Para entender como as interfaces ajudam nos testes de unidade, vejamos um exemplo mais complexo.
Suponha que você tenha uma loja e informações sobre vendas e o número de clientes no PostgreSQL. Você deseja escrever um código que calcule o compartilhamento de vendas (número específico de vendas por cliente) do último dia, arredondado para duas casas decimais.
Uma implementação mínima seria assim:
Agora, queremos criar um teste de unidade para a função
calculateSalesRate()
para verificar se os cálculos estão corretos.
Agora é problemático. Precisamos configurar uma instância de teste do PostgreSQL, além de criar e excluir scripts para preencher o banco de dados com dados falsos. Temos muito trabalho a fazer se realmente queremos testar nossos cálculos.
E as interfaces vêm em socorro!
CountSales()
nosso próprio tipo de interface que descreve os
CountSales()
e
CountCustomers()
, nos quais a função
CountCustomers()
baseia. Em seguida, atualize a assinatura
*ShopDB
calculateSalesRate()
para usar esse tipo de interface como parâmetro, em vez do tipo
*ShopDB
prescrito.
Assim:
Depois de fazer isso, apenas criaremos um esboço que satisfaça a interface do
ShopModel
. Em seguida, você pode usá-lo durante o teste de unidade da operação correta da lógica matemática na função
calculateSalesRate()
. Assim:
Agora execute o teste e tudo funciona bem.
Arquitetura de aplicativos
No exemplo anterior, vimos como você pode usar interfaces para desacoplar determinadas partes do código de tipos específicos. Por exemplo, a função
ShopModel
calculateSalesRate()
não importa o que você passa para ela, desde que satisfaça a interface
ShopModel
.
Você pode expandir essa ideia e criar níveis "desatados" inteiros em grandes projetos.
Suponha que você esteja criando um aplicativo Web que interaja com um banco de dados. Se você criar uma interface que descreva certos métodos para interagir com o banco de dados, poderá consultá-la em vez de um tipo específico através de manipuladores HTTP. Como os manipuladores HTTP se referem apenas à interface, isso ajudará a dissociar o nível HTTP e o nível de interação com o banco de dados. Será mais fácil trabalhar com níveis de forma independente e, no futuro, você poderá substituir alguns níveis sem afetar o trabalho de outros.
Eu escrevi sobre esse padrão em
um dos posts anteriores , há mais detalhes e exemplos práticos.
O que é uma interface vazia?
Se você está programando o Go há algum tempo, provavelmente encontrou uma
interface do tipo de interface vazia interface{}
. Vou tentar explicar o que é. No começo deste artigo, escrevi:
O tipo de interface no Go é um tipo de definição . Ele define e descreve os métodos específicos que algum outro tipo deve ter .
Um tipo de interface vazio
não descreve métodos . Ele não tem regras. E assim, qualquer objeto satisfaz uma interface vazia.
Em essência, o tipo de interface vazia
interface{}
é um tipo de curinga. Se você o encontrou em uma declaração (variável, parâmetro de função ou campo de estrutura), poderá usar um objeto de
qualquer tipo .
Considere o código:
package main import "fmt" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 fmt.Printf("%+v", person) }
Aqui inicializamos o mapa para
person
, que usa um tipo de string para chaves e uma interface vazia do tipo de
interface{}
para valores. Atribuímos três tipos diferentes como valores de mapa (string, número inteiro e float32), e não há problema. Como objetos de qualquer tipo atendem à interface vazia, o código funciona muito bem.
Você pode
executar este código aqui , verá um resultado semelhante:
map[age:21 height:167.64 name:Alice]
Quando se trata de extrair e usar valores de um mapa, é importante ter isso em mente. Suponha que você queira obter o valor da
age
e aumentá-lo em 1. Se você escrever um código semelhante, ele não será compilado:
package main import "log" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 person["age"] = person["age"] + 1 fmt.Printf("%+v", person) }
Você receberá uma mensagem de erro:
invalid operation: person["age"] + 1 (mismatched types interface {} and int)
O motivo é que o valor armazenado no mapa pega o tipo de
interface{}
e perde seu tipo int base original. E como o valor não é mais inteiro, não podemos adicionar 1 a ele.
Para contornar isso, você precisa tornar o valor inteiro novamente e só então usá-lo:
package main import "log" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 age, ok := person["age"].(int) if !ok { log.Fatal("could not assert value to int") return } person["age"] = age + 1 log.Printf("%+v", person) }
Se você
executar isso , tudo funcionará conforme o esperado:
2009/11/10 23:00:00 map[age:22 height:167.64 name:Alice]
Então, quando você deve usar um tipo de interface vazia?
Talvez
não com muita frequência . Se você chegar a esse ponto, pare e pense se é correto usar a
interface{}
. Como conselho geral, posso dizer que será mais claro, mais seguro e mais produtivo usar tipos específicos, ou seja, tipos de interface não vazios. No exemplo acima, era melhor definir uma estrutura de
Person
com campos adequadamente digitados:
type Person struct { Name string Age int Height float32 }
Uma interface vazia, por outro lado, é útil quando você precisa acessar e trabalhar com tipos imprevisíveis ou definidos pelo usuário. Por algum motivo, essas interfaces são usadas em locais diferentes da biblioteca padrão, por exemplo, nas
funções gob.Encode ,
fmt.Print e
template.Execute .
Tipos de interface úteis
Aqui está uma pequena lista dos tipos de interface mais solicitados e úteis da biblioteca padrão. Se você ainda não estiver familiarizado com eles, recomendo a leitura da documentação relevante.
Uma lista mais longa de bibliotecas padrão também está disponível
aqui .