Back-End

11 out, 2018

Go: implementando cache em APIs REST

Publicidade

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.

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:

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”:

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:

{
  "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.

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!