Criando uma API Rest com Fiber - Uma história pessoal de aprendizado
Gustavo Henrique
Posted on November 22, 2022
Todos temos jornadas diferentes quando nos propomos a aprender algo novo, e eu tive um pouco de dificuldade quando comecei a aprender Go.
Passei bons anos trabalhando com Node.js, e seu ecossistema me deixou mal acostumado com a facilidade e disponibilidade de bibliotecas de terceiros.
Depois de ler a documentação e estudar alguns snippets a fim de entender como a linguagem funcionava, meu cérebro automaticamente me fez abrir o Google e pesquisar "best go frameworks", e eu fui (felizmente) bombardeado com várias opções. Tentei me aventurar com os frameworks MVC, com a ideia de acelerar o desenvolvimento com ferramentas que já "fizessem tudo por mim"... Sem sucesso.
Comecei a esbarrar em conceitos básicos que não havia entendido muito bem, e estruturas de pastas que não significavam nada pra mim, então precisei dar alguns passos para trás, pois estava há um tempo considerável na zona de conforto de uma linguagem que já conhecia.
Antes de continuar, precisamos esclarecer algumas coisas:
- O artigo diz única e exclusivamente sobre a minha jornada pessoal com Go, e não tem intenção nenhuma de ser científico ou a palavra final sobre o assunto.
- Se quiser ir direto ao ponto onde eu mostro qual meu roadmap de estudos, pode pular pro final do artigo.
- Go não é orientado a objetos, e nem se propõe a garantir um suporte completo a hierarquia ou classes (temos formas de mimetizar esse comportamento, mas em casos específicos).
- Sim, você vai precisar lidar com todos os erros que ocorrerem.
- Go é uma linguagem focada em performance, ela se propõe a atingir o desempenho de linguagens como C, segundo a própria doc: "One of Go's design goals is to approach the performance of C for comparable programs [...]".
- O mascote é super fofo.
Voltando ao assunto, a zona de conforto me fez esperar (por algum motivo) que Go funcionasse da mesma forma, e que eu poderia achar tudo que eu precisava dentro de um gerenciador de pacotes. Em resumo: eu estava errado, e nesse meio tempo um colega de trabalho (que acabou se tornando um amigo) que já usava Go há bastante tempo entrou na minha equipe. Ele me esclareceu muita coisa e foi o responsável por acelerar meu aprendizado, e o que mudou meu ponto de vista foi entender que a linguagem foca na simplicidade ao invés de possuir várias maneiras de fazer a mesma coisa, um exemplo disso são loops: Go só possui o loop for
:
For?
sum := 0
for i := 1; i < 5; i++ {
sum += i
}
fmt.Println(sum) // 10 (1+2+3+4)
For-each?
strings := []string{"hello", "world"}
for i, s := range strings {
fmt.Println(i, s)
}
While?
n := 1
for n < 5 {
n *= 2
}
fmt.Println(n) // 8 (1*2*2*2)
Loop infinito?
sum := 0
for {
sum++ // repete para sempre
}
fmt.Println(sum) // nunca é alcançado
Esse conceito se estende também às convenções da linguagem como:
Formatação - O pacote gofmt
é responsável por toda a formatação do código. Ele é o padrão utilizado, e em alguns casos um erro de indentação pode até impedir seu programa de ser compilado.
Um exemplo de formatação errada:
type T struct {
name string // name of the object
value int // its value
}
O mesmo trecho após rodar o gofmt
:
type T struct {
name string // name of the object
value int // its value
}
Nomes de pacotes - Quando um pacote é importado em Go, o nome que foi importado é o que vai ser utilizado no código para acessar suas funções:
package main
import (
"fmt"
"net/http"
)
func main() {
fmt.Println(http.StatusOK)
}
Isso significa que os nomes dos pacotes devem ser curtos, concisos e evocativos. Por convenção, os nomes de pacotes devem ser lower-case e uma única palavra, sem a necessidade de underlines ou traços. O que isso quer dizer? Quer dizer que os nomes devem ser memoráveis e auto explicativos o suficiente, para serem reduzidos a uma palavra.
Exemplos: net/http
, bytes
, log
, user
, routes
, etc.
Em alguns casos pode ser difícil reduzir um nome complexo a uma só palavra, nesses casos podemos abrir exceção, mas sempre usando do bom senso.
E como fica a questão de colisão de nomes? Bom, o nome do pacote pode ser alterado no momento do import
, dessa forma poderiamos chamar o pacote net/http
de httpzinho
:
package main
import (
"fmt"
httpzinho "net/http"
)
func main() {
fmt.Println(httpzinho.StatusOK)
}
Nomes de funções - Os nomes de funções em Go possuem uma peculiaridade: se começarem com a letra maiúscula a função é exportada pelo pacote, caso o nome comece com uma letra minúscula, ele não é exportado:
a.go
package a
func Exported() string {
return "Hi, I'm exported"
}
func unexported() string {
return "Hi, I'm unexported"
}
main.go
package main
import "a"
func main() {
a.Exported()
a.unexported() // vai retornar um erro
}
Esses são só alguns pontos de convenções da linguagem, o restante pode ser encontrado num guia oficial da linguagem.
Além de simples Go também é "batteries included", que traduzindo livremente seria: "pilhas inclusas". Esse termo faz referência aos brinquedos que tinham um aviso na caixa, sinalizando que não vinham com pilhas (batteries not included) e elas precisavam ser compradas separadamente.
Adaptando para a programação, linguagens com "pilhas inclusas" proporcionam um leque de ferramentas e bibliotecas nativas para resolver problemas comuns, como servidores HTTP, testes, etc.
Go possui essas bibliotecas e podem ser encontradas aqui
Porém algumas "pilhas" não estão inclusas:
Logging - Assim como muitas linguagens, Go tem funções para mostrar informações no console e até um pacote, mas as soluções são muito simples e insuficientes. Uma alternativa que gosto muito de utilizar é o logrus.
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.Info("Hello World! Info")
log.Warn("Hello World! Warn")
log.Fatal("Hello World! Fatal")
}
Test assertions - O pacote nativo do Go para testes é muito bom e poderia render um artigo inteiro só sobre ele (mas um resumo pode ser encontrado no guia oficial). O pacote atendeu todos os casos que precisei, mas uma coisa que começa a fazer falta depois de um tempo são funções de comparação ou "assertions".
Normalmente, você faria algo do tipo:
package main
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
// Hello returns a greeting for the named person.
func Hello(name string) string {
// Return a greeting that embeds the name in a message.
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message
}
// TestHelloEmpty calls greetings.Hello with an empty string,
// checking for an error.
func TestHelloEmpty(t *testing.T) {
msg := Hello("")
if msg != "" {
t.Fatalf(`Hello("") = %q, want "", error`, msg)
}
}
OUTPUT
=== RUN TestHelloEmpty
prog.go:20: Hello("") = "Hi, . Welcome!", want "", error
--- FAIL: TestHelloEmpty (0.00s)
FAIL
Nesse caso, estamos usando o pacote testing
, porém quando checamos nosso caso de erro, fazemos um if explícito if msg != "" {
, e então retornamos um erro com a mensagem informando a diferença entre o resultado atual e o esperado, depois de algum tempo essa estrutura fica gigantesca, e em casos de teste muito específicos acaba dificultando sua manutenção.
A alternativa nesse caso, é utilizar um pacote de assertion, e nossa sintaxe muda pra isso:
package main
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
// Hello returns a greeting for the named person.
func Hello(name string) string {
// Return a greeting that embeds the name in a message.
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message
}
// TestHelloEmpty calls greetings.Hello with an empty string,
// checking for an error.
func TestHelloEmpty(t *testing.T) {
msg := Hello("")
assert.Equalf(t, "", msg, "Empty string Hello")
}
OUTPUT
=== RUN TestHelloEmpty
prog.go:21:
Error Trace: /prog.go:21
Error: Not equal:
expected: ""
actual : "Hi, . Welcome!"
Diff:
--- Expected
+++ Actual
@@ -1 +1 @@
-
+Hi, . Welcome!
Test: TestHelloEmpty
Messages: Empty string Hello
--- FAIL: TestHelloEmpty (0.00s)
FAIL
Muito mais simples né!? E além de tudo, ainda temos um retorno muito mais explícito e explicativo no console, agilizando a correção do erro.
Outras "pilhas" estão inclusas, mas podem ser trocadas por outras mais potentes, como é o caso do pacote net/http
. Um exemplo de um servidor HTTP com pacotes nativos:
package main
import (
"fmt"
"net/http"
)
func hello(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "hello\n")
}
func main() {
http.HandleFunc("/hello", hello)
http.ListenAndServe(":8090", nil)
}
Bem direto ao ponto. Mas e se quisermos adicionar uma leitura dos headers da requisição?
package main
import (
"fmt"
"net/http"
"github.com/sirupsen/logrus"
)
func hello(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "hello\n")
}
func headers(w http.ResponseWriter, req *http.Request) {
for name, headers := range req.Header {
for _, h := range headers {
fmt.Fprintf(w, "%v: %v\n", name, h)
}
}
}
func main() {
http.HandleFunc("/hello", hello)
http.HandleFunc("/headers", headers)
http.ListenAndServe(":8090", nil)
}
Agora ficou um pouco mais complicado, e isso tem tendência a escalar cada vez mais quando começamos a adicionar tempo de processamento nas nossas requisições, manipulando channels
e o Context
da requisição.
Em casos como esses, é prudente utilizar pacotes que abstraem essa complexidade, e um desses pacotes vai ser o tema da continuação dessa série de artigos.
Conclusão
Levando tudo isso em consideração, comecei a estudar diretamente as bibliotecas nativas da linguagem, e tentando fazer tudo o mais simples possível. A recompensa de optar pelo simples em Go é muito grande por alguns motivos:
- Reforça as particularidades e convenções da linguagem.
- Quanto menos dependências forem usadas, melhor — Go é uma linguagem compilada, e binários pequenos são sempre bem-vindos.
- Os pacotes utilizados tendem a ser importados para resolver problemas necessários, e não para qualquer fim, tornando as dependências fáceis de serem gerenciadas.
- Capacidade de debugar bibliotecas de terceiros. Os pacotes open-source tem a tendência de seguir a convenção oficial e o padrão de código dos pacotes nativos, uma vez que esses padrões estão memorizados, fica fácil entender o que códigos de pacotes complexos estão fazendo no seu código.
A forma que consegui atingir esse conhecimento foi inicialmente fazendo um curso na Udemy. Nesse curso o professor Otávio Augusto Gallego faz um tour pelas estruturas mais básicas da linguagem, escalando gradativamente até chegar na construção de uma rede social 100% em Go com o padrão MVC.
Enquanto seguia o curso, eu criei um repositório próprio, documentando e organizando cada conceito da linguagem em pastas, que pode ser encontrado aqui. A estrutura do repositório é parecida com a criada pelo professor no curso.
Depois disso fiz alguns projetos de exemplo, como esse, que é um CRUD básico em Go, utilizando 3 abordagens diferentes de acesso ao banco de dados. E também utilizei esse projeto para estudos, cujo objetivo é automatizar o envio de fotos de gatinhos fofos pelo Whatsapp.
Além desses pontos, a leitura dos materiais oficiais da linguagem é imprescindível.
Um resumo do roadmap de estudos:
- Curso da Udemy 100% em português
- Links oficiais pra aprender Go gratuitamente:
- Criar um repositório de estudos
- Criar projetos open-source como treino.
- Perguntar e trocar código com outras pessoas que já saibam ou estejam aprendendo Go.
Nos próximos artigos vou mostrar como fazer uma API usando o Fiber!
Se tiver algum código de estudo pra compartilhar, só mandar na dm!
Posted on November 22, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.