Tipos de interface
em Go expressam generalizações ou abstrações acerca de outros tipos. Em geral, interfaces
nos permitem escrever funções mais flexiveis e adaptaveis porque ela não se determina aos detalhes de uma implementação particular.
Em muitas linguagens orientadas a objetos existe uma noção sobre interfaces
, entretanto, o diferencial em Go se trata da simplicidade.
Interfaces como contrato
Em Go, um concrete type
(ou tipo consistente) especifica sua exata representação, assim como seus valores e suas operações intrinsícas para atender a essa forma, por exemplo, para números temos a aritmética, para slices
temos indexing
, append
e range
.
Um concrete type
também pode oferecer outros comportamentos através dos seus métodos, quando você tem um valor de tipo consistente, você sabe exatamente aquilo que ele é, assim como você sabe exatamente oque você pode fazer com ele.
Todavia existe um tipo diferente em go chamado interface type
. Uma interface por definição é um abstract type
, que não expõe sua representação, sua estrutura interna, seus valores e as operações que suporta, relevando apenas alguns de seus métodos. Quando você tem um valor de tipo interface
, você não sabe nada sobre oque é, você só sabe aquilo que você pode fazer com as opções fornecidas pelos seus métodos.
Interface como tipo
Um tipo interface
especifica um conjunto de métodos que os tipos consistentes precisam possuir para ser uma instância dessa interface
.
A implementação io.Writer
, por exemplo, é uma das que mais utilizou de interfaces
uma vez que proporciona uma abstração de todos os tipos aos quais os bytes
podem ser escritos, isso inclui arquivos, buffers de memória, conexões de rede, clientes http
, arquivos, hashers
e muito mais. Um Reader
também representa qualquer tipo aos quais bytes
possam ser lidos, mostrando a quantidade de interfaces que o pacote io
implementa.
package io
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
Olhando mais a frente, encontraremos novas declarações de interfaces porém agora combinando com algumas já existentes
type ReadWriter interface {
Read
Writer
}
type ReadWriterClose interface {
Read
Writer
Closer
}
A syntax usada acima se chama interface embedding
e é apenas uma maneira mais curta de juntar esses pedaços.
Interface como valor
Conceitualmente, um valor de uma interface, ou interface value
, contém dois componentes, um concrete type
e um valor correspondente a este tipo. Essas são chamadas interfaces dynamic type
e dynamic value
. Em linguagens estáticamente tipadas como Go, tipos pertencem ao conceito de tempo de compilação, logo um tipo não pode ser um valor, no nosso modelo conceitual, um conjunto de valores são chamados type descriptors
os quais fornecem informações sobre cada tipo específico, como nome e métodos. Em um valor de interface
o tipo do componente é representado pelo type descriptor
apropriado. Nos três exemplos abaixo, a variavel w
recebe três valores diferentes.
var w io.Writer
w = os.Stdout
w = nil
Olhando mais mais a fundo na dinamica de cada declaração
Primeira declaração de w
var w io.Writer
Em Go, variaveis sempre são inicializadas com valores bem definidos, interfaces
não são exceção. A declaração zerada para uma interface
possui ambos os valores e tipos como valor nil
. O valor nil
da interface
depende se o tipo é dynamic type
ou não, caso seja, logo essa interface contém um nil interface value
, isso pode ser testado tentando invocar um método de uma nil interface
w.Write([]byte("Hello")) // panic: nil pointer deference
Segunda declaração de w
w = os.Stdout
A atribuição à variavel w
relaciona uma conversão implícita de um tipo consistente para um tipo interface, e isso é equivalente a conversão explícita io.Writer(os.Stdout)
. Uma conversão desse tipo, independente de ser implicita ou explícita captura o tipo e o valor de seu operador.
Nesse caso, o interface dynamic type
é atribuido ao type descriptor
com um ponteiro do tipo *os.File
e seu valor contém uma cópia para os.Stdout
, que é um ponteiro para representar o processo de output padrão os.File
.
Por exemplo, chamar o método Write em uma interface que contém um ponteiro para *os.File
resultará no método (*os.File).Write
.
Normalmente, nós não sabemos em tempo de compilação qual será o dynamic type
do valor de uma interface, portanto, a chamada através desse contexto deve utilizar dynamic dispatch
. Ao invés de uma chamada direta, o compilador precisa gerar um código para obter o endereço de um método chamado Write
, vindo do type descriptor
, e depois fazer uma chamada indireta para o endereço, quem recebe o argumento para a chamada é uma copia do interface dynamic value
ou nesse caso os.Stdout
.
Terceira declaração de w
w = nil
Essa declaração reinicia nossa variavel w
para o ponto de partida, sendo nil
seu valor.
Valores arbitrários
O valor dinâmico de uma interface pode ser arbitrariamente diverso e grande, por exemplo, o tipo do método time.Time
que representa o tempo no momento instântaneo é uma struct type
com muitos campos não exportados, vamos criar um valor de interface com isso
var x interface{} = time.Now()
Conceitualmente, o valor dinâmico sempre irá se adequar dentro do valor da estrutura, não importa seu tamanhou ou tipo.
Esses valores, podem ser comparados por operadores lógicos, ainda que dinâmicos, com a exceção dos valores aos quais seus tipos atribuídos são incomparáveis, como por exemplo um slice
var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int