Meu nome é Elton Minetto

Usando as interfaces da stdlib de Go

Neste post vou mostrar como usar duas das features mais interessantes da linguagem Go: sua biblioteca padrão (a stdlib do título) e interfaces.

Go é famosa por prover uma grande quantidade de funcionalidades nativamente, graças a sua biblioteca padrão poderosa. Cobrindo desde conversões de texto e json até bancos de dados e servidores HTTP, podemos desenvolver aplicações complexas sem a necessidade de importar pacotes de terceiros.

Outra característica importante da linguagem é o poder das suas interfaces. Diferente de linguagens orientadas a objetos, Go não possui a palavra-chave extends e permite que uma interface seja implementada por uma variável, struct, slice, etc. Basta que sejam implementadas as mesmas assinaturas de função definidas na interface e pronto.

Vamos usar estas duas features para incrementar o código de nossas aplicações.

Implementando a interface error

A primeira interface que vamos explorar é a error:

type error interface {
	Error() string
}

Qualquer estrutura ou variável que implementar essa interface pode ser reconhecida como um erro em funções e testes:

package main

import (
	"fmt"
)

type MyError struct {
	Message string
}

func (m MyError) Error() string {
	return fmt.Sprintf("Message: %s", m.Message)
}

func main() {
	_, err := divide(10, 0)
	if err != nil {
		fmt.Println(err)
	}
}

func divide(x, y int) (float64, error) {
	if y <= 0 {
		return 0.0, MyError{
			Message: "error in divide function",
		}
	}
	return float64(x / y), nil
}

Neste exemplo criei a struct MyError e implementei a função Error, conforme a interface. Fazendo isso, a struct pode ser retornada como um erro na função divide. Graças a essa feature podemos criar erros customizados para nossas aplicações, com informações extras, logs e outras funcionalidades.

Implementando as interfaces fmt.Stringer e fmt.Formatter

Para o próximo exemplo eu criei um tipo chamado Level, que é um int. Ele pode ser usado em uma biblioteca que gera logs de uma aplicação e o fato de ser um inteiro permite fazermos lógicas como if os.Getenv('ENV') == "prod" && level < INFO para controlarmos quais mensagens devem ser processadas ou não.

Mas apesar de ser bem prático para usarmos esse tipo em lógicas como a descrita acima, pode ser útil convertermos esse valor em uma string em alguns cenários. É o que vamos fazer implementando as interfaces fmt.Stringer e fmt.Formatter:

type Stringer interface {
	String() string
}
type Formatter interface {
	Format(f State, c rune)
}

O código do nosso exemplo é:

package main

import (
	"fmt"
	"strings"
)

type Level int

const (
	DEBUG Level = iota + 1
	INFO
	NOTICE
	ALERT
	WARN
	ERROR
	CRITICAL
	FATAL
	DISASTER
)

var toString = map[Level]string{
	DEBUG:    "DEBUG",
	INFO:     "INFO",
	NOTICE:   "NOTICE",
	ALERT:    "ALERT",
	WARN:     "WARN",
	ERROR:    "ERROR",
	CRITICAL: "CRITICAL",
	FATAL:    "FATAL",
	DISASTER: "DISASTER",
}

func (l Level) String() string {
	return toString[l]
}

func (l Level) Format(f fmt.State, c rune) {
	switch c {
	case 'l':
		fmt.Fprint(f, strings.ToLower(toString[l]))
	default:
		fmt.Fprintf(f, toString[l])
	}
}
func main() {
	l := DEBUG
	fmt.Println(l)
	fmt.Printf("Level: %l\n", l)
}

A função String() é usada pela função fmt.Println(l) e também pela fmt.Printf. Neste exemplo, a função Format foi implementada apenas para demonstrar como podemos criar formatações especiais, neste caso o %l, que eu defini como sendo responsável por transformar o valor em letras minúsculas.

Implementando a interface json.Marshaler

Vamos agora criar uma nova struct, Log, que contém um Level:

type Log struct {
	Message string `json:"message"`
	Level   Level  `json:"level"`
}

Um recurso comum em um pacote de logs é a conversão em JSON:

log := Log{
		Message: "Message log",
		Level:   ERROR,
}
j, _ := json.Marshal(log)
fmt.Println(string(j))

Mas o resultado não é exatamente o esperado, pois o Level foi gerado como um inteiro:

{"message":"Message log","level":6}

Para resolver isso de maneira fácil podemos implementar a interface json.Marshaler:

type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

A implementação ficou desta forma:

func (l Level) MarshalJSON() ([]byte, error) {
	buffer := bytes.NewBufferString(`"`)
	buffer.WriteString(toString[l])
	buffer.WriteString(`"`)
	return buffer.Bytes(), nil
}

E agora o resultado da impressão ficou como esperávamos:

{"message":"Message log","level":"ERROR"}

Implementando a interface sort.Interface

Para o próximo exemplo vamos ordenar um slice de structs, uma lógica que aparece em vários cenários. Primeiro vamos criar os dados que serão ordenados:

package main

import (
	"fmt"
)

type Movie struct {
	ReleaseYear int
	Title       string
}

func main() {
	movies := []*Movie{
		&Movie{
			ReleaseYear: 2022,
			Title:       "The Northman",
		},
		&Movie{
			ReleaseYear: 1994,
			Title:       "Pulp Fiction",
		},
		&Movie{
			ReleaseYear: 1999,
			Title:       "Matrix",
		},
	}
	for _, m := range movies {
		fmt.Println(m)
	}
}

Vamos agora ordenar o nosso slice, primeiro por ordem de lançamento. Para isso precisamos implementar a interface sort.Interface:

type Interface interface {
	Len() int
	Less(i, j int) bool
	Swap(i, j int)
}

Para isso adicionei o seguinte trecho de código:

type byReleaseDate []*Movie

func (e byReleaseDate) Len() int           { return len(e) }
func (e byReleaseDate) Swap(i, j int)      { e[i], e[j] = e[j], e[i] }
func (e byReleaseDate) Less(i, j int) bool { return e[i].ReleaseYear < e[j].ReleaseYear }

E na função main, antes do loop que faz a impressão dos filmes:

sort.Sort(byReleaseDate(movies))

Podemos fazer o mesmo com outras ordenações. O código a seguir é o exemplo completo, com mais de uma ordenação e também a implementação da interface fmt.Stringer para facilitar a impressão dos filmes:

package main

import (
	"fmt"
	"sort"
)

type Movie struct {
	ReleaseYear int
	Title       string
}

type byReleaseDate []*Movie

func (e byReleaseDate) Len() int           { return len(e) }
func (e byReleaseDate) Swap(i, j int)      { e[i], e[j] = e[j], e[i] }
func (e byReleaseDate) Less(i, j int) bool { return e[i].ReleaseYear < e[j].ReleaseYear }

type byTitle []*Movie

func (e byTitle) Len() int           { return len(e) }
func (e byTitle) Swap(i, j int)      { e[i], e[j] = e[j], e[i] }
func (e byTitle) Less(i, j int) bool { return e[i].Title < e[j].Title }

func (m Movie) String() string {
	return fmt.Sprintf("%s was released at %d", m.Title, m.ReleaseYear)
}

func main() {
	movies := []*Movie{
		&Movie{
			ReleaseYear: 2022,
			Title:       "The Northman",
		},
		&Movie{
			ReleaseYear: 1994,
			Title:       "Pulp Fiction",
		},
		&Movie{
			ReleaseYear: 1999,
			Title:       "Matrix",
		},
	}
	sort.Sort(byReleaseDate(movies))
	for _, m := range movies {
		fmt.Println(m)
	}
	fmt.Println("====")
	sort.Sort(byTitle(movies))
	for _, m := range movies {
		fmt.Println(m)
	}
}

O resultado da execução foi:

Pulp Fiction was released at 1994
Matrix was released at 1999
The Northman was released at 2022
====
Matrix was released at 1999
Pulp Fiction was released at 1994
The Northman was released at 2022

E mais…

Além dos exemplos que mostrei aqui, talvez o mais conhecido é a implementação da interface http.Handler, que é usada para o desenvolvimento de APIs Rest. A interface:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

E a implementação mais simples:

package main

import (
	"fmt"
	"net/http"
)

type helloHandler struct{}

func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "HeloWorld")
}

func main() {
	http.Handle("/hello", helloHandler{})
	http.ListenAndServe(":8090", nil)
}

Mas como esse exemplo é muito conhecido não vou me aprofundar nele.

A stdlib possui uma grande quantidade de pacotes e interfaces que podem ser implementadas e extendidas para o desenvolvimento de aplicações complexas. Recomendo a investigação na documentação para encontrar mais destas funcionalidades interessantes e úteis.