Dev (Back & Front)ARTIGO

Go: implementando cache em APIs REST

Dando continuidade ao seu artigo sobre CRUD completo com Go e MongoDB, o autor apresenta um passo a passo ensinando a implementar cache em APIs REST.

Dando continuidade ao meu artigo sobre CRUD completo com Go e MongoDB, hoje mostrarei como adicionar o Redis em nossa estrutura.

Para pular a etapa de criação de um novo projeto, eu irei utilizar o mesmo desenvolvido do artigo anterior. Caso tenha interesse em clonar ele, segue o link no meu GitHub:

O projeto está bem simples. Nós criamos as principais rotas de um CRUD e configuramos o projeto para armazenar os nossos dados em um banco de dados MongoDB. Nosso primeiro passo será configurar o redis.

Configurando o Redis

Para não precisar instalar o Redis no meu computador, eu irei utilizar ele da mesma forma que estou utilizando o mongoDB: em um container Docker. Caso tenha interesse em saber como criar um contêiner com Redis, segue o link de um artigo onde eu demonstro esse passo em um ambiente Windows:

Agora, caso esteja em um ambiente Linux, basta executar o comando abaixo no seu terminal. Esse comando criará um container com Redis utilizando a porta default 6379.

bash
docker run --name some-redis -p 6379:6379 -d redis

Para validar o acesso eu irei utilizar o Redis Desktop Manager. Caso não conheça ele, o Redis Desktop Manager é um utilitário gratuito que nos permite gerenciar o Redis através de uma interface gráfica.

Abaixo você tem uma imagem demonstrando como configurar ele e o resultado do nosso teste de conexão:

Teste de conexão com redis (RedisDescktop Manager)

Para os próximos passos será necessário uma IDE. Eu irei utilizar o Visual Studio Code, mas você pode utilizar um de sua preferência.

Com o projeto aberto no editor de textos, importaremos os pacotes necessários para os próximos passos. Para isso, abra um terminal no seu computador e execute o comando abaixo:

bash
go get github.com/garyburd/redigo/redis

O próximo passo será criar o arquivo de configuração com o Redis. Para isso, adicione um novo arquivo chamado conn.go dentro do diretório config/redis. Abaixo você tem uma imagem demonstrando esse passo:

Arquivo de conexão em Go e redis

Agora atualize ele com o trecho de código abaixo:

package redis

import (
	"fmt"

	. "go-mongo-redis/config"

	"github.com/garyburd/redigo/redis"
)

const (
	redisExpire = 60
)

func RedisConnect() redis.Conn {
	c, err := redis.Dial("tcp", ":6379")
	HandleError(err)
	return c
}

func Set(key string, value []byte) error {

	conn := RedisConnect()
	defer conn.Close()

	_, err := conn.Do("SET", key, []byte(value))
	HandleError(err)

	conn.Do("EXPIRE", key, redisExpire) //10 Minutes

	return err
}

func Get(key string) ([]byte, error) {

	conn := RedisConnect()
	defer conn.Close()

	var data []byte
	data, err := redis.Bytes(conn.Do("GET", key))
	if err != nil {
		return data, fmt.Errorf("error getting key %s: %v", key, err)
	}
	return data, err
}

Analisando o código acima, nós temos:

  • Linha 15 a 19: conexão com o redis na porta default “6379”
  • Linha 21 a 32: estamos recebendo o valor de uma chave para armazenar os dados que irão vir em bytes, em seguida passamos para o redis expirar esses valores depois de um minuto
  • Linha 34 a 44: estamos recebendo o valor de uma chave e buscando no redis os valores armazenados nela

Agora, para testarmos o nosso código, abra o arquivo movie_router.go e atualize o método GetAll com o trecho de código abaixo:

func GetAll(w http.ResponseWriter, r *http.Request) {
	movies := []Movie{}
	reply, err := redis.Get("movies")

	if err != nil {
		fmt.Println("Buscando no mongoDB")
		movies, err := dao.GetAll()
		HandleError(err)
		m, err := json.Marshal(movies)
		HandleError(err)
		redis.Set("movies", []byte(m))
		respondWithJson(w, http.StatusOK, movies)

	} else {
		fmt.Println("Buscando no redis")
		json.Unmarshal(reply, &movies)
		respondWithJson(w, http.StatusOK, movies)
	}

}

Em seguida, adicione o método no final do arquivo “movie_router.go”:

javascript
func handleError(err error) {
  if err != nil {
     panic(err)
  }
}

Testando o código

Para testar o código abra um terminal e execute o comando go run main.go. Esse comando executará nosso código na porta 3000.

Para cadastrarmos um novo registro e validar o cache, irei utilizar o Postman. Para quem não conhece, o Postman é uma ferramenta gratuita que nos auxilia nos testes a requisições HTTP.

Com ele aberto, preencha os campos conforme está na imagem abaixo e clique em Send:

Testando post no Postman

Corpo do post:

bash
{
  "name": "The Equalizer",
  "description": "Robert McCall (Denzel Washington), a man of mysterious origin who believes he has put the past behind him, dedicates himself to creating a quiet new life. However, when he meets Teri (Chloë Grace Moretz), a teenager who has been manhandled by violent Russian mobsters, he simply cannot walk away. With his set of formidable skills, McCall comes out of self-imposed retirement and emerges as an avenging angel, ready to take down anyone who brutalizes the helpless.",
  "thumb_image": "http://t2.gstatic.com/images?q=tbn:ANd9GcQkGfxoavBEp4fR6P-yi2mIkUl1aZHHFIietLK4GriI5YyvGSJ7",
  "active": true
}

Agora abra o endereço abaixo no seu navegador.

bash
http://localhost:3000/api/v1/movies

Em seguida, atualize essa o seu navegador com essa rota “f5”. Caso tudo esteja funcionando corretamente, você irá receber uma mensagem na sua console informando quando os dados vierem do banco Mongo ou do Redis:

api em Go, mongoDB e redis

Organizando o código

Com o nosso código funcionando, vamos refatorar o nosso pacote movie_router.go.

Crie um novo diretório chamado helper dentro da sua aplicação. Em seguida, adicione um novo arquivo chamado comum.go dentro dele e atualize-o com o trecho de código abaixo:

package comum

import (
	"encoding/json"
	"net/http"
)

func RespondWithError(w http.ResponseWriter, code int, msg string) {
	RespondWithJson(w, code, map[string]string{"error": msg})
}

func RespondWithJson(w http.ResponseWriter, code int, payload interface{}) {
	response, _ := json.Marshal(payload)
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	w.Write(response)
}

func HandleError(err error) {
	if err != nil {
		panic(err)
	}
}

Em seguida, atualize o arquivo movie_router.go, com o trecho de código abaixo:

package movierouter

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/gorilla/mux"
	. "github.com/programadriano/go-restapi/config/dao"
	helper "github.com/programadriano/go-restapi/config/helper"
	redis "github.com/programadriano/go-restapi/config/redis"
	. "github.com/programadriano/go-restapi/models"
	"gopkg.in/mgo.v2/bson"
)

var dao = MoviesDAO{}

func GetAll(w http.ResponseWriter, r *http.Request) {
	movies := []Movie{}
	reply, err := redis.Get("movies")

	if err != nil {
		fmt.Println("Buscando no mongoDB")
		movies, err := dao.GetAll()
		helper.HandleError(err)
		m, err := json.Marshal(movies)
		helper.HandleError(err)
		redis.Set("movies", []byte(m))
		helper.RespondWithJson(w, http.StatusOK, movies)

	} else {
		fmt.Println("Buscando no redis")
		json.Unmarshal(reply, &movies)
		helper.RespondWithJson(w, http.StatusOK, movies)
	}

}

func GetByID(w http.ResponseWriter, r *http.Request) {
	params := mux.Vars(r)
	movie, err := dao.GetByID(params["id"])
	if err != nil {
		helper.RespondWithError(w, http.StatusBadRequest, "Invalid Movie ID")
		return
	}
	helper.RespondWithJson(w, http.StatusOK, movie)
}

func Create(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	var movie Movie
	if err := json.NewDecoder(r.Body).Decode(&movie); err != nil {
		helper.RespondWithError(w, http.StatusBadRequest, "Invalid request payload")
		return
	}
	movie.ID = bson.NewObjectId()
	if err := dao.Create(movie); err != nil {
		helper.RespondWithError(w, http.StatusInternalServerError, err.Error())
		return
	}
	helper.RespondWithJson(w, http.StatusCreated, movie)
}

func Update(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	params := mux.Vars(r)
	var movie Movie
	if err := json.NewDecoder(r.Body).Decode(&movie); err != nil {
		helper.RespondWithError(w, http.StatusBadRequest, "Invalid request payload")
		return
	}
	if err := dao.Update(params["id"], movie); err != nil {
		helper.RespondWithError(w, http.StatusInternalServerError, err.Error())
		return
	}
	helper.RespondWithJson(w, http.StatusOK, map[string]string{"result": movie.Name + " atualizado com sucesso!"})
}

func Delete(w http.ResponseWriter, r *http.Request) {
	defer r.Body.Close()
	params := mux.Vars(r)
	if err := dao.Delete(params["id"]); err != nil {
		helper.RespondWithError(w, http.StatusInternalServerError, err.Error())
		return
	}
	helper.RespondWithJson(w, http.StatusOK, map[string]string{"result": "success"})
}

Em resumo nós retiramos de dentro do pacote movie_router.go os métodos ResponseWithError,RespondeWithJson e HandlerError e passamos para um outro pacote que pode ser utilizado nas nossas outras rotas.

Caso tenha interesse em baixar a versão final do código desenvolvido neste artigo, segue o seu link no meu GitHub:

Com isso finalizo mais esse artigo. Espero que tenham gostado e que ele possa ajudar!

Microsoft (MVP), atualmente trabalha como Arquiteto de Software para TV Bandeirantes. Nesses últimos anos, focou nas tecnologias criadas pela Microsoft, mas sempre esteve antenado para as novas tecnologias que estão surgindo no mercado. Em um breve resumo, é uma pessoa apaixonada pelo que faz, tem a sua profissão como hobby.

Ver perfil