Construindo Sua Primeira API em Go do Zero - Entendendo o servidor http da nossa aplicação ToDo

Buenas para você que está lendo mais um artigo do meu blog, e vamos continuar no artigo de hoje com nossa aplicação ToDo! Hoje vamos nos aprofundar um pouco mais no servidor do pacote padrão `http` da linguagem Go, aprender algumas curiosidades a respeito dele e adicionar novas rotas ao nosso projeto.
Caso não lembre, no artigo passado desta série, criamos nossa aplicação do zero, estruturando as pastas, nomeando arquivos até expliquei algumas curiosidades sobre criação de projetos em Go, tudo isso você pode encontrar aqui caso não tenha lido o artigo ainda: Construindo Sua Primeira API em Go do Zero - Inicializando o projeto ToDo.
Vamos começar?
Pacote http do Go: Um poderoso servidor de fábrica na linguagem
Ah, um aviso, não estarei explicando aqui como funciona uma requisição HTTP neste artigo, mas se tiver alguma dúvida, se cadastre aqui no site para receber a minha newsletter e poder interagir com artigos comentando pois assim consigo tirar suas dúvidas.
Antes de colocarmos a mão na massa, digo, no código do projeto, vamos entender um pouco como funciona o pacote `http`do Go. O pacote `http` funciona de duas formas em Go:
- Para fazer requisições para outro servidor/aplicação, funcionando como um client
- Para criar servidores que irão receber requisições de outro servidor/aplicação, funcionando como um server (vamos focar neste segundo caso primeiramente, ok?)
Agora que sabemos no que devemos focar, vou colocar aqui um exemplo bem simples de como criar um servidor, lembrando que no artigo anterior já criamos um servidor com uma rota, mas como tem outras partes com importações de pacotes acho que um exemplo mais simples vai nos ajudar.
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Aqui vamos indicar que estamos retornado um status http 200
w.WriteHeader(200)
// Aqui nós estamos dizendo o que vamos devolver no conteúdo
// só enfatizando que está não é a melhor forma de devolver uma resposta
// no nosso projeto vamos utilizar o Encoder e Decodor do pacote json
fmt.Fprint(w, "Olá, aqui é o seu servidor http em Go")
})
http.ListenAndServe(":8080", nil)
}
O código é bem simples, dentro da função main nós já chamamos o pacote http e invocamos a função que vai manipular a requisição. Para a HandleFunc, vamos passar dois parâmetros: a rota que queremos e a função que queremos que está rota execute, pode reparar que iniciamos o segundo parâmetro declarando uma função seguindo a assinatura do tipo de função que a HandleFunc espera.
Dentro da função que criamos vamos fazer algo bem simples, vamos dizer que queremos que a resposta tenha um status http 200 (ok) e que ela escreva o texto "Olá, aqui é o seu servidor http em Go". Se você copiar o código e executá-lo, o seu servidor em localhost:8080, vai devolver a resposta que acabamos de codificar. Tenta ai e me diga como foi.
Mas como eu sei que meu servidor está configurado corretamente?
Um detalhe bem interessante de se dizer sobre o pacote `http` é que se você não criar sua struct específica de servidor e invocar diretamente as funções do pacote, por debaixo dos panos a linguagem já criou um servidor default e é ele quem vamos utilizar. O pessoal que criou a linguagem pensou até nesse detalhe! Didaticamente isto é bem interessante pois facilita e diminui a barreira de entrada na linguagem, entretanto uma atenção aqui:
Não devemos utilizar o servidor padrão do pacote `http`em produção! Por mais que ele seja configurado corretamente, ainda faltam algumas configurações de segurança nele que iremos aprender em outro artigo.
Com o aviso de cima, mostrarei agora um outro exemplo simplificado de como podemos ter a nossa struct de servidor de uma forma mais manualmente controlada. Vamos ao exemplo abaixo:
package main
import (
"fmt"
"net"
"net/http"
)
func main() {
// Aqui criamos o nosso multiplexador de requisições http. A função desse multiplexador é
// gerenciar todas as rotas que vamos ter em nosso servidor.
mux := http.NewServeMux()
// Como por exemplo, recriando a mesma rota do exemplo anterior, ficaria assim.
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprint(w, "Olá, aqui é o seu servidor http em Go")
})
// Agora precisamos criar o nosso servidor http, passando
// ele qual vai ser o multiplexador que ele deve usar (quais rotas o servidor terá)
server := &http.Server{
Handler: mux,
}
// E por final precisamos criar a interface que o nosso servidor deve usar
// para se comunicar na rede e aqui entra o pacote net que gera essa
// interface pegando protocolo TCP como exemplo
netListener, err := net.Listen("tcp", ":80")
if err != nil {
panic(err)
}
// Agora só mandar nosso servidor levantar e disponibilizar as rotas!
server.Serve(netListener)
}
Bem no exemplo acima eu mostro algumas configurações básicas para ter um servidor executando, onde configuramos manualmente. Nós criamos o multiplexador de requisições http, também conhecido apenas como mux dentro do ecossistema Go. Mas espera, você sabe o que é o multiplexador de requisições http? Bem, vou resumir agora: um multiplexador de requisições é um componente de software que atua como um roteador de requisições http. Ele recebe requisições e as encaminha para o manipulador (handler) correto com base em regras de correspondência.
Depois de criar o mux, criamos a nossa struct http.Server passando para ela o mux que acabamos de criar. Em seguida precisamos também criar uma interface de comunicação de rede para o nosso servidor e é aqui que o pacote `net` entra! Nele podemos criar a interface para diversos protocolos, como TCP, UDP, entre outros. Interessante né? Mas lembra que eu falei que não ia me aprofundar em como funciona requisições http, e se você está com um pouco de dificuldade de entender essa parte, deixo como tarefa você pesquisar e estudar um pouco mais sobre!
Progredindo o nosso projeto - Completando nosso CRUD de ToDos
Agora que tendemos um pouco mais sobre um servidor em Go, vamos continuar evoluindo nosso projeto, vamos seguir os passos de: adicionar novas funções no modelo para:
- Criar uma nova ToDo
- Atualizar uma ToDo
- Deletar uma ToDo
Modelo com funções completas - CRUD em ação
Para você que não sabe o que é CRUD, lhe digo agora o que é: é a sigla que representa as operações básicas de manipulação de dados, onde o C é o Create, R é o Read, U é o Update e D é o Delete.
Vamos para o nosso arquivo atualizado do modelo agora?
package todo
import (
"context"
"errors"
)
// Todo é a nossa estrutura de tarefas a fazer, por hora ela vai ter apenas ID inteiro, titulo e se está completa
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
// atualizamos agora o slice para o map, desta forma podemos ter um melhor controle da nossa base temporária
// de tal forma que podemos validar, por exemplo, se estamos tentando inserir um todo de ID n, porém o ID n já
// existe na base, algo similar ao que aconteceria em uma Base de Dados real.
var todos = map[int]Todo{
1: {
ID: 1,
Title: "Meu primeiro todo!",
Completed: true,
},
2: {
ID: 2,
Title: "Da minha primeira api Go!",
Completed: false,
},
}
// com a visão de pacotes do Go, podemos criar uma função sem precisar de um objeto (struct) para ter o método
// entretanto não é considerado uma boa prática na visão de POO, mas quis trazer que também é possível, diferente da service e do
// handler.
func GetAllTodos(ctx context.Context) []Todo {
// Aqui atualizamos a nossa função para que possamos converter o mapa em um slice com todos os Todos
result := make([]Todo, len(todos))
for k, v := range todos {
result[k-1] = v
}
return result
}
// agora temos uma função para adicionar o Todo na nossa base, e ainda vamos prevenir que acidentalmente
// se salva um todo novo em cima de um todo já existente
func AddTodo(ctx context.Context, newTodo Todo) error {
// toda vez que vamos acessar um item (valor) dentro de um map em go, temos dois campos que são retornados
// o primeiro campo é o valor em si, mas aqui tem uma pegadinha, e é por isso que vem o segundo campo to tipo bool (booleano)
// esse segundo retorno, que chamei de 'ok' vai indicar se item existe no mapa (true) ou não (false).
_, ok := todos[newTodo.ID]
if ok {
return errors.New("todo already exists")
}
todos[newTodo.ID] = newTodo
return nil
}
// agora temos uma função para atualizar o Todo na nossa base, e ainda vamos prevenir que acidentalmente
// se tente atualizar um todo novo em cima de um todo que não existe
func UpdateTodo(ctx context.Context, todo Todo) error {
_, ok := todos[todo.ID]
if !ok {
return errors.New("todo not found")
}
todos[todo.ID] = todo
return nil
}
// e agora temos uma função para deletar o Todo na nossa base, e ainda vamos prevenir que acidentalmente
// se tente deletar um todo novo em cima de um todo que não existe
func DeleteTodo(ctx context.Context, id int) error {
_, ok := todos[id]
if !ok {
return errors.New("todo not found")
}
delete(todos, id)
return nil
}
Eu já deixei bastante comentário no código que já ajuda a entendê-lo, tanto no repositório quanto aqui no artigo, legal né? Mas tem dois pontos que gostaria de destacar aqui que acho bem legal na linguagem Go!
O seguinte trecho abaixo, onde usamos a palavra make para criar um slice com o mesmo tamanho do map. O legal do make é justamente isso, ele serve para criarmos tipos de go com algumas facilidades, no caso do slice eu digo qual o tipo do slice, eu digo qual o tamanho do slice e qual a capacidade do slice respectivamente como parametros. No código abaixo eu passei só o tipo e o tamanho do slice, e sabe o que isso impacta na lógica?
// criando o slice dessa forma eu falo para ele criar um slice do tamanho do mapa
// e já preencher o slice com structs com valores zeros, no caso o Todo vai vir com
// o id = 0, o title = “” e o completed = false.
result := make([]Todo, len(todos))
for k, v := range todos {
// e é por isso que aqui eu posso fazer a atribuição direta do valor em um índice
// do slice, mas deixo aqui o desafio para você entender porque faço k-1 como
// índice.
result[k-1] = v
}
Caso eu queira criar um slice com um tamanho determinado, mas não sei exatamente quantos itens vão estar presente nele, a melhor forma é passar o tamanho como 0 e a capacidade como o valor que você quer, desta forma você informa que o slice deve ser iniciado sem nenhum item, mas deve suportar até n itens. Legal né? Nesse caso, o código acima ficaria assim:
result := make([]Todo, 0, len(todos))
for _, v := range todos {
result = append(result, v)
}
O outro ponto até expliquei no comentário, mas vou reforçar aqui: quando vamos acessar um valor em um map em Go, nós temos dois retornos, o valor do item (que é o item em si) e um sinalizador bool (booleano) dizendo se encontrou o item no map (true) ou não (false). Isso eu acho bem interessante pois fica mais direto entender se um item existe ou não sem tentar lidar com valores zeros ou ponteiros nulos! O trecho em questão é este:
// aqui estou tentando acessar um todo de id no mapa de todos
todo, ok := todos[id]
// aqui eu valido de forma direta e mais simples se o item está ok, ou não
if !ok {
return errors.New("todo not found")
}
// agora eu posso manipular o todo da forma que quiser
Serviço e manipuladores recebendo as funções e métodos restante do CRUD
Bem, agora que alteramos o nosso modelo, vamos prosseguir para a nossa camada de serviço. Bem, aqui ressalto que a camada ainda é bem simples, pois não temos lógica de negócio ainda para aplicar. Sem mais delongas, o nosso arquivo de service fica assim:
package todo
import (
"context"
)
// A service aqui que criamos vai conter a logica, ela é um objet (struct) pois mais para frente iremos injetar a dependencia
// nela, calma que vai fazer sentido em outro artigo. Também adiciono aqui neste artigo que por hora não temos validações
// de regra de negócio, ainda, mas em outro artigo já vamos adicionar algumas.
type Service struct {
}
func NewService() Service {
return Service{}
}
// Dessa forma aque fizemos, basta que o método da service chame a função da model para que list todos os todos.
func (s Service) ListTodos(ctx context.Context) []Todo {
return GetAllTodos(ctx)
}
// igualmente para inserir um novo todos
func (s Service) InsertTodo(ctx context.Context, todo Todo) error {
return AddTodo(ctx, todo)
}
// igualmente para atualizar um todos
func (s Service) UpdateTodo(ctx context.Context, todo Todo) error {
return UpdateTodo(ctx, todo)
}
// e para deletar um todos
func (s Service) DeleteTodo(ctx context.Context, id int) error {
return DeleteTodo(ctx, id)
}
E os nossos manipuladores também ficam parecidos, entretanto neles já adicionei algumas validações simples, para que desta forma você possa entender que sempre devemos fazer algumas validações nos manipuladores, mas que não sejam validações de regra de negócio e sim validações de lógica para evitar bug e para ter segurança na aplicação. Vamos ao handler agora?
package todo
import (
"encoding/json"
"net/http"
"strconv"
"strings"
)
type Handler struct {
service Service
}
func NewHandler(service Service) Handler {
return Handler{service: service}
}
// ListTodos lista todos os Todos que temos através da service injetada como dependencia
// de novo, calma que vai fazer sentido mais para frente.
func (h Handler) ListTodos(w http.ResponseWriter, r *http.Request) {
// aqui ele chama a service que foi injetada como dependencia
// (depois veremos melhor essa parte de injeção e como fazer de uma forma mais própria para isso e os seus benefícios)
allTodos := h.service.ListTodos(r.Context())
// com o resultado em mãos já podemos dizer que vamos retornar uma resposta do tipo json
w.Header().Set("Content-Type", "application/json")
// com status http 200 (OK)
w.WriteHeader(http.StatusOK)
// e por final pegamos nosso slice e convertemos em um json para ser respondido.
_ = json.NewEncoder(w).Encode(allTodos)
}
// PostTodo insere o novo todos que enviamos através da service injetada como dependencia
func (h Handler) PostTodo(w http.ResponseWriter, r *http.Request) {
var todo Todo
if errDecode := json.NewDecoder(r.Body).Decode(&todo); errDecode != nil {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error": errDecode.Error()})
return
}
if err := h.service.InsertTodo(r.Context(), todo); err != nil {
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusCreated)
}
// PostTodo atualiza o um todos que enviamos através da service injetada como dependencia
func (h Handler) PutTodo(w http.ResponseWriter, r *http.Request) {
id, convErr := strconv.Atoi(strings.TrimSpace(r.PathValue("id")))
if convErr != nil {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error": convErr.Error()})
return
}
var todo Todo
if errDecode := json.NewDecoder(r.Body).Decode(&todo); errDecode != nil {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error": errDecode.Error()})
return
}
if id != todo.ID {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "invalid todo to update"})
return
}
if err := h.service.UpdateTodo(r.Context(), todo); err != nil {
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusOK)
}
// PostTodo deleta o todos que enviamos através da service injetada como dependencia
func (h Handler) DeleteTodo(w http.ResponseWriter, r *http.Request) {
id, convErr := strconv.Atoi(strings.TrimSpace(r.PathValue("id")))
if convErr != nil {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error": convErr.Error()})
return
}
if err := h.service.DeleteTodo(r.Context(), id); err != nil {
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.WriteHeader(http.StatusOK)
}
Bem, eu trouxe outro desafio aqui para o artigo nessa parte, eu não comentei parte do código explicando as regras de validações pois quero que você pense um pouco para entender cada validação. Caso queira, pode até comentar aqui no artigo o que pensou, ou pode me chamar nas redes sociais, ou até mesmo lá no Github, beleza?
Atualizando nosso arquivo main com o nosso servidor
Pois bem, já atualizamos quase tudo e o artigo está bem longo, mas já posso dizer que o nosso arquivo main continua bem simples ainda, mas trago algo interessante nele. E que venha o main.go!
package main
import (
"fmt"
"net/http"
"github.com/augustoasilva/todo-go-app-tutorial/internal/todo"
)
func main() {
service := todo.NewService()
handler := todo.NewHandler(service)
// agora que temos mais rotas, e as rotas acabam se duplicando, para identificar qual rota combina com qual verbo http
// e assim poder escolher o handler correto, é desta forma que fica usando o pacote padrão http do Go
http.HandleFunc("GET /todos", handler.ListTodos)
http.HandleFunc("POST /todos", handler.PostTodo)
// e no caso de querermos passar um parametro ao final da url, que geralmente fazemos passando algum id, usamos um
// wildcard na url, que é como chamamos o {} aqui. para este caso estamos usando {id}
http.HandleFunc("PUT /todos/{id}", handler.PutTodo)
http.HandleFunc("DELETE /todos/{id}", handler.DeleteTodo)
fmt.Println("Servidor iniciado em local host na porta 8080")
_ = http.ListenAndServe(":8080", nil)
}
Deixei uma curiosidade para o final, claro! Não entreguei tudo sobre o mux lá no começo do artigo. Inclusive, se você for uma pessoa atenta e curiosa pode ter percebido que em momento nenhum eu tinha declarado qual seria o verbo http que a rota iria receber. Hoje, por default, desde o go 1.22, podemos declarar o verbo junto da rota! Antes, para definirmos o verbo tínhamos que fazer as validações nos manipuladores (handlers) de cada rota, o que era bem trabalhoso. Mas os desenvolvedores da linguagem ouviram a comunidade, se inspiraram em outros pacotes mux bem famosos e trouxeram essa novidade. Por isso que venho gostando bastante dessa linguagem, a comunidade faz acontecer junto aos desenvolvedores da linguagem!
E agora? Os próximos passos
Bem, agora encerramos aqui o nosso artigo de hoje, onde implementamos outros endpoints para a nossa aplicação,e por curiosidade eu adicionei uma coleção do Postman (Postman Collection) com os quatro endpoints no repositório no Github para que possa servir de referência caso queira testá-los!
Também, como de costume, estarei deixando o link do repositório aqui para que possa acompanhar mais do projeto (e queira spoilers antes dos artigos) e deixarei o link da branch correspondente a este artigo aqui também para que veja todos os arquivos iniciais juntos.
No próximo artigo da série estaremos escrevendo as primeiras regras de negócio e estaremos também adicionando logs em nossa aplicação, entre outras coisinhas mais. E também já aviso que, assim como foi com o artigo dessa série anterior a este, o próximo artigo não será continuação deste, será outro.
Nos vemos até o próximo artigo, até!