Aprendendo Go: Interfaces

Aprendendo Go: Interfaces

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