Especialização em Backend com Go & Gin • Demonstrações Práticas Compiláveis


💻 Código de Exemplo 01 — Cliente Redis e Operações Set/Get com TTL

package cache

import (
	"context"
	"encoding/json"
	"time"

	"github.com/redis/go-redis/v9"
)

var Ctx = context.Background()

func NovoClienteRedis() *redis.Client {
	return redis.NewClient(&redis.Options{
		Addr:         "localhost:6379",
		PoolSize:     10,
		DialTimeout:  5 * time.Second,
		ReadTimeout:  2 * time.Second,
		WriteTimeout: 2 * time.Second,
	})
}

// SalvarJSON serializa qualquer struct em JSON e grava com TTL.
func SalvarJSON(rdb *redis.Client, chave string, valor interface{}, ttl time.Duration) error {
	bytes, err := json.Marshal(valor)
	if err != nil {
		return err
	}
	return rdb.Set(Ctx, chave, bytes, ttl).Err()
}

// ObterJSON busca a chave e desserializa no destino apontado por dest.
// Retorna redis.Nil quando a chave não existe (cache miss).
func ObterJSON(rdb *redis.Client, chave string, dest interface{}) error {
	val, err := rdb.Get(Ctx, chave).Result()
	if err != nil {
		return err
	}
	return json.Unmarshal([]byte(val), dest)
}

💻 Código de Exemplo 02 — Middleware de Cache de Resposta HTTP (Cache-Aside)

package middlewares

import (
	"errors"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/redis/go-redis/v9"
)

// bodyCapturer intercepta o corpo escrito pelo handler para poder gravá-lo
// no cache depois que a rota real for executada (em caso de miss).
type bodyCapturer struct {
	gin.ResponseWriter
	corpo []byte
}

func (b *bodyCapturer) Write(dados []byte) (int, error) {
	b.corpo = append(b.corpo, dados...)
	return b.ResponseWriter.Write(dados)
}

// CacheDeResposta serve GETs diretamente do Redis quando a chave (o path)
// já está cacheada; em cache miss, deixa o handler real responder e grava
// o resultado no Redis com TTL curto antes de retornar.
func CacheDeResposta(rdb *redis.Client, ttl time.Duration) gin.HandlerFunc {
	ctx := Ctx
	return func(c *gin.Context) {
		if c.Request.Method != http.MethodGet {
			c.Next()
			return
		}

		chave := "http-cache:" + c.Request.URL.String()
		if val, err := rdb.Get(ctx, chave).Result(); err == nil {
			c.Data(http.StatusOK, "application/json", []byte(val))
			c.Abort()
			return
		} else if !errors.Is(err, redis.Nil) {
			c.Next() // Redis indisponível: degrada sem cache em vez de falhar a request
			return
		}

		writer := &bodyCapturer{ResponseWriter: c.Writer}
		c.Writer = writer
		c.Next()

		if c.Writer.Status() == http.StatusOK {
			rdb.Set(ctx, chave, writer.corpo, ttl)
		}
	}
}

💻 Código de Exemplo 03 — Invalidação por Padrão (SCAN + DEL em Lote)

package cache

import "github.com/redis/go-redis/v9"

// PurgarPorPadrao remove todas as chaves que casam com um padrão glob
// (ex.: "produtos:*") usando SCAN em vez de KEYS, que evita bloquear o
// servidor Redis inteiro em keyspaces grandes (KEYS é O(N) e não paginado).
func PurgarPorPadrao(rdb *redis.Client, padrao string) (int64, error) {
	var removidas int64
	iter := rdb.Scan(Ctx, 0, padrao, 100).Iterator()

	var lote []string
	for iter.Next(Ctx) {
		lote = append(lote, iter.Val())
		if len(lote) >= 100 {
			n, err := rdb.Del(Ctx, lote...).Result()
			if err != nil {
				return removidas, err
			}
			removidas += n
			lote = lote[:0]
		}
	}
	if err := iter.Err(); err != nil {
		return removidas, err
	}
	if len(lote) > 0 {
		n, err := rdb.Del(Ctx, lote...).Result()
		if err != nil {
			return removidas, err
		}
		removidas += n
	}
	return removidas, nil
}