Dev (Back & Front)ARTIGO

Interface vazia

No seguinte artigo, o autor apresenta um passo a passo ensinando a usar a interface vazia e explica quais são os riscos de usar esse tipo.

Interface vazia é um tipo que aceita qualquer coisa; você pode passar o que quiser como parâmetros de função ou variáveis do tipo interface{}.

Parece muito prático à primeira vista, mas quando usamos interface{} estamos jogando pela janela a validação de tipos feita em tempo de compilação e perdemos uma das grandes vantagens de uma linguagem compilada de tipagem forte e estática.

E como a checagem de tipo não vai acontecer em tempo de compilação, é sua responsabilidade checar se está recebendo o tipo certo em tempo de execução.

Identificando o tipo

Quando queremos saber um determinado tipo, principalmente quando estamos depurando e tentando ver se o tipo que chegou foi o tipo esperado, podemos usar a função fmt.Printf passando o formato %T como no exemplo:

var value interface{}
value = 1
fmt.Printf("tipo de value: %T\n", value)

No nosso exemplo declaramos a variável value como interface{} e agora podemos passar qualquer valor pra ela, como passamos um inteiro à função Printf com o formato %T vai informar que a variável é do tipo int.

Vamos ver um exemplo completo

package main

import (
	"fmt"
)

func main() {
	var value interface{}
	value = 1
	fmt.Printf("tipo de value: %T\n", value)
	value = 3.14
	fmt.Printf("tipo de value: %T\n", value)
	value = "isso é uma string"
	fmt.Printf("tipo de value: %T\n", value)
}

Esse pequeno programa deve mostrar o seguinte resultado:

tipo de value: int
tipo de value: float64
tipo de value: string

Switch

Inspecionar o tipo de uma variável como vimos é bem simples, ajuda bastante a depurar o código e saber se estamos recebendo o que esperamos. Agora vamos fazer nosso código fazer esse trabalho sozinho.

Vamos usar a combinação do switch com o cast (type) que vai retornar o tipo da variável, veja o exemplo:

package main

import (
	"fmt"
)

func main() {
	var value interface{}
	value = 1
	switch value.(type) {
	case int:
		fmt.Println("Value é do tipo int")
	case string:
		fmt.Println("Value é do tipo string")
	default:
		fmt.Printf("Tipo %T não implementado\n", value)

	}
}

Se a variável value tiver um int, nosso programa vai retornar Value do tipo int. Se passarmos uma string para value, o retorno vai ser Value do tipo string e se passarmos qualquer outro valor o programa vai avisar que o suporte para aquele tipo ainda não está implementado.

Uma vez que já sabemos o tipo da variável, podemos fazer cast para o tipo correto e a partir daí usar sem problemas.

switch value.(type) {
case int:
fmt.Println(value.(int))
case string:
fmt.Println(value.(string))

Se não fizermos essa validação e simplesmente usarmos o cast para tentar converter a variável para o tipo que queremos, corremos o risco do programa quebrar com um panic caso o tipo passado não seja o que estamos esperando.

map[string]interface{}

Um uso muito comum é usar interfaces vazias em conjunto com mapas para converter strings JSON para estruturas que não conhecemos com certeza o formato. Como não sabemos quais campos e nem os tipos dos campos que a estrutura contém, podemos usar um map[string]interface{}, que basicamente casa com qualquer estrutura.

package main

import (
	"fmt"
	"encoding/json"
)

func main() {
	b := []byte(`{"Name":"Cesar","Value":10}`)
	
	var m map[string]interface{}
	m = make(map[string]interface{})
	
	err := json.Unmarshal(b, &m)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("%#v\n", m)
}

Neste exemplo primeiro declaramos um array de bytes com uma estrutura JSON, depois declaramos um mapa de strings e interfaces vazias e instanciamos na memória. Daí usamos json.Unmarshal para ler os array de bytes e popular o mapa com os campos e valores encontrados.

Verificamos se houve algum erro porque sempre tem a possibilidade do JSON ser inválido e então usamos mais um pequeno truque para mostrar a estrutura. O formato %#v na função fmt.Printf para mostrar mais dados e não apenas o valor como aconteceria se usássemos apenas o formato %v.

Vamos ver um exemplo mais completo usando range para percorrer os campos do JSON e switch para desviar o código para o tipo correto.

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	b := []byte(`{"Name":"Banana","Value":2.10}`)

	var m map[string]interface{}
	m = make(map[string]interface{})

	err := json.Unmarshal(b, &m)
	if err != nil {
		fmt.Println(err)
		return
	}
	for k, v := range m {
		switch v.(type) {
		case float64:
			fmt.Printf("%v %v\n", k, v.(float64))
		case string:
			fmt.Printf("%v %v\n", k, v.(string))
		default:
			fmt.Printf("Tipo %T não implementado\n", v)

		}
	}
}

Uma outra forma de validar se a interface vazia tem o tipo que você quer, é fazendo como no exemplo abaixo:

package main

import "fmt"

func main() {
	var value interface{}
	value = 1

	str, ok := value.(string)
	if !ok {
		fmt.Println("Value não é string")
	}
	fmt.Println(str)
}

Aqui usamos str, ok := value.(string) de forma que se value for do tipo string ok, será true e str conterá o valor propriamente dito já como string; é uma forma simples de testar o tipo sem escrever o switch case.

Espero ter esclarecido um pouco mais sobre interface vazia, seus usos e porque pode ser arriscado.

Links úteis

Código fonte dos exemplos de hoje:

Cesar GimenesAutor

Trabalha com tecnologia desde a década de 90. Já atuou na área de educação e participou de projetos de mobilidade de grande volume para laboratórios farmacêuticos. Criou games tanto para PC, como para iOS. Hoje está direcionando seus esforços em plataformas de Sistemas Embarcados, IoT, microservices e cloud computing. É um entusiasta de tecnologias como Golang e Docker.

Ver perfil