Entendendo Interfaces Em Go

Interfaces são um conceito a comum a programadores de linguagens estaticamente tipadas e orientadas a objetos, que baseia-se na noção mais abstrata de type-class, e são um artifício poderoso que confere uma grande flexibilidade ao sistema de tipos, pois se usado corretamente, pode melhorar drasticamente a modularidade do software ao reduzir a dependências entre seus componentes; então não é à toa que foi incluído na linguagem Go, mesmo com sua forte filosofia minimalista.

No entanto, pode não parecer tão simples para quem já está acostumado com linguagens como Javascript ou Python, que tendem a ser bastante permissivas no que diz respeito à checagem de tipos, e nisso permitir erros da parte do programador. Aqui vamos abordá-las na perspectiva de quem usa Go, mas ainda assim as noções apresentadas são aplicáveis a quase linguagem com interfaces.

Uma Interface É…

Nada mais nada menos que um tipo que inclui todos os tipos que satisfazem a determinadas condições, no caso, implementam certos métodos. Por exemplo é mais fácil de entender: chamamos de “falante” tudo aquilo que pode fala algo:

type Falante interface {
    Falar(algo string)
}

Se uma pessoa pode falar algo, então ela é Falante. Se um cachorro pode falar algo, também é Falante:

type Pessoa struct {/*...*/}
type Cachorro struct {/*...*/}

func (p Pessoa) Falar(algo string) {/*...*/}
func (c Cachorro) Falar(algo string) {/*...*/}

Observe que “Falar” deve ser implementado como um método desse tipo, ou seja, uma função associada a esse tipo. Uma vez que ambos Cachorro e Pessoa podem falar, podemos reuni-los em um mesmo conjunto (slice no caso) de falantes:

p := Pessoa{/*...*/}
c := Cachorro{/*...*/}
falantes := []Falante{p, c}

Suponhamos que queremos fazer uma chamada, do tipo que se fazem (ou faziam?) em escolas. Poderiamos representar esse procedimento da seguinte forma:

func chamada(falante Falante) {
    falante.Falar("presente")
}

Observe que claramente só podemos fazer uma chamada desse tipo com quem é Falante, com quem pode falar, e não nos importamos, como esse Falante consegue falar, e sim com o fato de que ele consegue falar, mas não sabemos nada além disso. Dessa forma, algo como:

func (p Pessoa) Escrever(algo string) {/*...*/}

func chamada(falante Falante) {
	falante.Falar("presente")
	falante.Escrever("presente")
}

resultaria em um erro de tipagem.

Um Exemplo Real

Você ainda pode estar se perguntando como isso pode ser útil, visto que foi um exemplo bastante teórico. Algo mais prático deve clarificar o poder das interfaces. O seguinte código é tirado do pacote io/fs da stdlib do Go, que especifica interfaces para operações com sistemas de arquivos:

type FS interface {
	// Open opens the named file.
	//
	// When Open returns an error, it should be of type *PathError
	// with the Op field set to "open", the Path field set to name,
	// and the Err field describing the problem.
	//
	// Open should reject attempts to open names that do not satisfy
	// ValidPath(name), returning a *PathError with Err set to
	// ErrInvalid or ErrNotExist.
	Open(name string) (File, error)
}

Um FS, que representa um sistema de arquivos, é qualquer tipo que implemente Open() como método, isto é, qualquer coisa que possa abrir um arquivo a partir de uma string, seu nome. O própio tipo File também é uma interface definida por io/fs por sinal, que especifica um conjunto de operações em arquivos. Então nesse contexto um FS poderia ser uma adaptação de um filesystem do Windows, ou do MacOS, ou de um filesystem em rede, ou até um filesystem trivial implementado apenas para testar uma função que recebe um FS, e que pode receber qualquer um dos anteriores como parâmetro.

É isso que eu quis dizer quando afirmei que interfaces podem reduzir a dependência entre componentes de software: um programa que realiza determinada operação em um sistema de arquivos usando a interface FS não se prende à API do sistema operacional, e sim de uma especificação que qualquer um pode implementar, tornando-o mais fácil de portar para outros sistemas, e mais fácil de testar.

Interface Vazia

A interface vazia é um caso à parte, pois ela não especifica nada, significando que qualquer tipo a implementa. Tomando como exemplo a função Marshal() do pacote encoding/json da stdlib, usada para converter valores para JSON:

func Marshal(v interface{}) ([]byte, error)

O parâmetro v pode ser qualquer coisa, pois qualquer coisa segue a especificação inexistente de interface{}. O que faz sentido, pois o propósito da função é codificar qualquer valor nativo do Go para JSON. Note, porém, que isso introduz a seguinte limitação: a variável do tipo interface{} pode ser usada apenas como um tipo interface{}. Isto é, mesmo que represente, por baixo dos panos, uma string, ela não pode ser usada em nenhuma função que receba um parâmetro do tipo string; da mesma forma, não pode passar o seu valor pra uma variável do tipo string, e isso vale pra qualquer outro tipo além de string, claro.

Mesmo consideradas um conceito típico de programação orientada a objetos (mesmo que erroneamente), as interfaces foram incluídas no Go, que rejeita fortemente esse paradigma em função do procedural, e é fácil entender o porquê dessa decisão. A flexibilidade que elas conferem, como foi visto no exemplo do filesystem, é um perfeito suplemento à linguagem, possibilitando uma, e apenas uma, forma de polimorfismo.

Use, porém, com sabedoria: definir uma interface para cada struct, por exemplo, não vai magicamente tornar seu programa mais “modular”. Todo excesso faz mal.