Interface
A interface é um tipo que descreve um comportamento esperado.
Ela não guarda dados, ela não diz quais campos uma struct precisa ter, ela diz apenas quais métodos um tipo precisa ter para ser aceito em determinado lugar.
Exemplo simples:
type Notificador interface {
Notificar(mensagem string)
}
Isso significa: qualquer tipo que tiver um método Notificar(mensagem string) pode ser usado como Notificador.
Imagine que um sistema precisa enviar notificações. No começo, ele só envia por email:
package main
import "fmt"
type Email struct {
Endereco string
}
func (e Email) Notificar(mensagem string) {
fmt.Println("Enviando email para", e.Endereco+":", mensagem)
}
func enviarEmail(e Email, mensagem string) {
e.Notificar(mensagem)
}
func main() {
email := Email{Endereco: "ana@email.com"}
enviarEmail(email, "Pedido aprovado")
}
No código acima, criamos uma struct chamada Email, que define um tipo próprio. Esse tipo tem um campo chamado Endereco, do tipo string.
Depois criamos duas funções, Notificar e enviarEmail. A função Notificar é na verdade um método do tipo Email. Isso acontece porque ele tem um receiver:
func (e Email) Notificar(mensagem string)
O receiver (e Email) significa que esse método pertence ao tipo Email. Dentro do método, e representa o valor de Email que chamou o método. O método também recebe um argumento chamado mensagem, do tipo string.
A função enviarEmail recebe dois argumentos:
func enviarEmail(e Email, mensagem string)
O primeiro argumento é e Email, ou seja, ela precisa receber um valor do tipo Email. Dentro da função, esse valor será chamado de e. O segundo argumento é mensagem string, que representa o texto que será enviado.
Dentro de main, criamos uma variável chamada email:
email := Email{Endereco: "ana@email.com"}
Essa variável é do tipo Email, porque foi criada usando a struct Email. Então agora temos um valor do tipo Email, chamado email, contendo as regras definidas pela struct, que neste caso é ter um Endereco.
Depois chamamos a função enviarEmail:
enviarEmail(email, "Pedido aprovado")
Aqui passamos a variável email, que é do tipo Email, e também passamos a mensagem "Pedido aprovado". Dentro da função enviarEmail, esse valor chega como e, e a mensagem chega como mensagem. Então a função chama:
e.Notificar(mensagem)
Isso executa o método Notificar daquele email, usando o endereço guardado na struct e a mensagem recebida pela função.
Isso funciona, mas a função enviarEmail está presa ao tipo Email.
func enviarEmail(e Email, mensagem string)
Se amanhã o sistema também precisar enviar SMS, WhatsApp, push notification ou apenas imprimir no terminal para teste, essa função não serve mais. Ela só aceita Email.
Melhorando com interface
Agora podemos pensar no comportamento, não no tipo específico. O comportamento esperado é:
Notificar(mensagem string)
Então criamos uma interface:
type Notificador interface {
Notificar(mensagem string)
}
Agora a função não precisa receber um Email. Ela pode receber qualquer coisa que saiba notificar.
func enviarNotificacao(n Notificador, mensagem string) {
n.Notificar(mensagem)
}
O exemplo completo fica assim:
package main
import "fmt"
type Notificador interface {
Notificar(mensagem string)
}
type Email struct {
Endereco string
}
type SMS struct {
Numero string
}
type Console struct{}
func (e Email) Notificar(mensagem string) {
fmt.Println("Enviando email para", e.Endereco+":", mensagem)
}
func (s SMS) Notificar(mensagem string) {
fmt.Println("Enviando SMS para", s.Numero+":", mensagem)
}
func (c Console) Notificar(mensagem string) {
fmt.Println("Mensagem no console:", mensagem)
}
func enviarNotificacao(n Notificador, mensagem string) {
n.Notificar(mensagem)
}
func main() {
email := Email{Endereco: "ana@email.com"}
sms := SMS{Numero: "11999999999"}
console := Console{}
enviarNotificacao(email, "Pedido aprovado")
enviarNotificacao(sms, "Pedido aprovado")
enviarNotificacao(console, "Pedido aprovado")
}
Resultado:
Enviando email para ana@email.com: Pedido aprovado
Enviando SMS para 11999999999: Pedido aprovado
Mensagem no console: Pedido aprovado
Explicação
Primeiro, criamos uma interface chamada Notificador:
type Notificador interface {
Notificar(mensagem string)
}
Essa interface define uma regra de comportamento, ela diz que qualquer tipo que quiser ser considerado um Notificador precisa ter um método chamado Notificar, recebendo uma string chamada mensagem.
Ela não está dizendo que o tipo precisa ter campo Endereco, Numero ou qualquer outro dado. Ela só exige o método Notificar:
Notificar(mensagem string)
Depois criamos três tipos diferentes, começando por Email, que é uma struct que tem um campo Endereco.
type Email struct {
Endereco string
}
Depois criamos SMS, que é uma struct que tem um campo Numero.
type SMS struct {
Numero string
}
E por fim, criamos Console, que é uma struct vazia. Ela não tem campos, porque nesse exemplo não precisamos guardar nenhum dado. Ela só vai imprimir a mensagem no terminal.
type Console struct{}
Agora vamos criar um método Notificar para cada tipo, já que eles vão usar a mesma interface.
// Método pertencente ao tipo `Email`, porque tem o receiver:
func (e Email) Notificar(mensagem string) {
fmt.Println("Enviando email para", e.Endereco+":", mensagem)
}
Dentro do método Notificar para o tipo Email, e representa o valor do tipo Email que chamou esse método. Então conseguimos acessar:
e.Endereco
Agora vamos criar o método do SMS:
// Método pertencente ao tipo `SMS`, porque tem o receiver:
func (s SMS) Notificar(mensagem string) {
fmt.Println("Enviando SMS para", s.Numero+":", mensagem)
}
Dentro do método, s representa o valor do tipo SMS. Então conseguimos acessar:
s.Numero
Por fim, vamos criar o método do Console:
// Método pertencente ao tipo `Console`, porque tem o receiver:
func (c Console) Notificar(mensagem string) {
fmt.Println("Mensagem no console:", mensagem)
}
Como Console não tem campos, não acessamos nada como c.AlgumaCoisa. Ele apenas recebe a mensagem e imprime no terminal.
Agora vem a parte mais importante:
func enviarNotificacao(n Notificador, mensagem string) {
n.Notificar(mensagem)
}
A função enviarNotificacao recebe dois argumentos, o primeiro é: n Notificador. Isso significa que ela não exige um Email, nem um SMS, nem um Console especificamente, ela exige qualquer valor que atenda à interface Notificador.
E atender à interface Notificador significa ter este método:
Notificar(mensagem string)
O segundo argumento é:
mensagem string
Ele representa o texto que será enviado. Já, dentro da função, chamamos:
n.Notificar(mensagem)
Como n é um Notificador, o Go sabe que ele obrigatoriamente tem o método Notificar. Depois chamamos a mesma função três vezes:
enviarNotificacao(email, "Pedido aprovado")
enviarNotificacao(sms, "Pedido aprovado")
enviarNotificacao(console, "Pedido aprovado")
Mesmo a função esperando um Notificador (func enviarNotificacao(n Notificador, mensagem string) {), isso funciona porque Email tem o método chamado Notificar(mensagem string), então Email atende à interface Notificador.
O mesmo vale para o restante!
O Go tem algumas ideias que lembram orientação a objetos, principalmente porque existem tipos, métodos e interfaces, mas o modelo é diferente de linguagens baseadas em classes. Aqui não existe classe, herança tradicional nem a necessidade de declarar explicitamente que um tipo implementa uma interface.
- A
structdefine um tipo com dados Ela representa a forma daquele valor, ou seja, quais campos ele possui. - O método adiciona comportamento a esse tipo Ele permite que valores daquele tipo executem ações ou retornem informações relacionadas aos próprios dados.
- A interface entra como uma regra de comportamento Ela não guarda dados e não pertence diretamente a uma struct. Ela apenas diz quais métodos um tipo precisa ter para ser aceito em determinado lugar.
A parte que parece estranha no começo é justamente passar um valor concreto, como Email, para uma função que pede uma interface, como Notificador. Isso funciona porque, para o Go, uma interface não exige que o valor seja "daquele tipo" no sentido tradicional. Ela exige que o valor tenha os métodos definidos pela interface.
Então, quando uma função pede um Notificador, ela está dizendo: "eu aceito qualquer valor que saiba notificar". Se Email tem o método necessário para notificar, então Email pode ser usado ali. O mesmo valeria para SMS, Console ou qualquer outro tipo que tivesse o mesmo método com a mesma assinatura.
O Go faz essa relação automaticamente, ele olha para o tipo concreto e verifica se os métodos dele batem com os métodos exigidos pela interface. Essa verificação não é baseada só no nome do método, mas na assinatura completa, ou seja, nome, parâmetros e retornos. Se tudo for compatível, o tipo atende à interface.
Por isso, é mais correto pensar que a interface fica separada, descrevendo um comportamento. A struct pode acabar atendendo essa interface se tiver os métodos necessários. Essa relação é implícita, e isso é uma característica importante.
Interface com retorno
Uma interface também pode exigir métodos com retorno.
type Descritivel interface {
Descricao() string
}
Isso significa que: para atender essa interface, o tipo precisa ter um método chamado Descricao, que retorna uma string.
package main
import "fmt"
type Descritivel interface {
Descricao() string
}
type Produto struct {
Nome string
Preco float64
}
func (p Produto) Descricao() string {
return fmt.Sprintf("%s custa R$ %.2f", p.Nome, p.Preco)
}
func imprimirDescricao(d Descritivel) {
fmt.Println(d.Descricao())
}
func main() {
produto := Produto{
Nome: "Mouse",
Preco: 89.90,
}
imprimirDescricao(produto)
}
Resultado:
Mouse custa R$ 89.90
Interface com mais de um método
Uma interface pode exigir mais de um método.
type Autenticavel interface {
Login() string
Senha() string
}
Para um tipo atender essa interface, ele precisa ter os dois métodos, exemplo:
package main
import "fmt"
type Autenticavel interface {
Login() string
Senha() string
}
type Usuario struct {
Email string
Hash string
}
func (u Usuario) Login() string {
return u.Email
}
func (u Usuario) Senha() string {
return u.Hash
}
func autenticar(a Autenticavel) {
fmt.Println("Login:", a.Login())
fmt.Println("Senha:", a.Senha())
}
func main() {
usuario := Usuario{
Email: "ana@email.com",
Hash: "abc123",
}
autenticar(usuario)
}
Resultado:
Login: ana@email.com
Senha: abc123
Se Usuario tivesse apenas Login() e não tivesse Senha(), ele não atenderia a interface Autenticavel.
Interface pequena é comum em Go
Em Go, interfaces pequenas são muito comuns, uma interface com um método só é normal.
type Leitor interface {
Ler() string
}
type Salvavel interface {
Salvar() error
}
type Validador interface {
Validar() error
}
Isso combina com o estilo da linguagem, em vez de criar interfaces enormes, Go costuma preferir interfaces pequenas e específicas. Uma regra prática é: interface boa normalmente descreve um comportamento pequeno e claro.
Interface vazia e any
Existe uma interface especial:
interface{}
Ela representa qualquer valor. Hoje em dia, não usamos mais any. Ele é um apelido para interface{}, exemplo:
package main
import "fmt"
func mostrar(valor any) {
fmt.Println(valor)
}
func main() {
mostrar("Ana")
mostrar(20)
mostrar(1.75)
mostrar(true)
}
Resultado:
Ana
20
1.75
true
Isso funciona porque qualquer valor atende a uma interface vazia. Mas existe um cuidado que precisamos ter, quando usamos any, perdemos a informação específica do tipo naquele ponto do código.
Por isso, any deve ser usado quando o valor realmente pode ser de vários tipos, como em dados genéricos, JSON dinâmico ou logs.
Type assertion
Quando temos um valor any ou uma interface mais genérica, às vezes precisamos recuperar o tipo real. Para isso, usamos type assertion.
valor, ok := x.(string)
Exemplo:
package main
import "fmt"
func mostrarTamanho(valor any) {
texto, ok := valor.(string)
if !ok {
fmt.Println("O valor não é uma string")
return
}
fmt.Println("Tamanho:", len(texto))
}
func main() {
mostrarTamanho("Go")
mostrarTamanho(123)
}
Resultado:
Tamanho: 2
O valor não é uma string
Type switch
Quando existem vários tipos possíveis, usamos type switch.
package main
import "fmt"
func mostrarTipo(valor any) {
switch v := valor.(type) {
case string:
fmt.Println("string:", v)
case int:
fmt.Println("int:", v)
case float64:
fmt.Println("float64:", v)
case bool:
fmt.Println("bool:", v)
default:
fmt.Println("tipo desconhecido")
}
}
func main() {
mostrarTipo("Ana")
mostrarTipo(20)
mostrarTipo(1.75)
mostrarTipo(true)
}
Resultado:
string: Ana
int: 20
float64: 1.75
bool: true
Isso é útil quando estamos lidando com valores genéricos.