Você tem algum endpoint que precisa processar muita coisa, consome dados de terceiros, lento, etc…. E para ajudar esse endpoint recebe muitas requisições simultâneas( algo que carrega na tua página inicial para todos users e tem o mesmo conteúdo)

Cada vez que aquele endpoint é chamado seus olhos se enchem de lágrimas, pois então isso vai mudar :) e vou te contar como. Vamos usar o pacote singleflight. Nas palavras do pacote:

“Package singleflight provides a duplicate function call suppression mechanism.” “O pacote singleflight fornece um mecanismo de supressão de chamada de função duplicado.”

A idéia do pacote é, você cria uma chave para identificar a requisição e quando houver outras requisições com a mesma chave ela vai aguardar a resposta que está em andamento de outra request. Quando a request retornar com o resultado ela compartilhará com as outras requests que estavam esperando pelo resultado, assim evitando múltiplas chamadas/processos pesados.

Chega de papo e vamos ver código, afinal é disso que gostamos :). Criei uma api para que possamos ver o pacote em ação, você pode ver o código no repositório

Criei um serviço http que consome dados vindos de uma api externa.

package main
import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"time"
)
func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Println("calling the endpoint")
		response, err := http.Get("https://jsonplaceholder.typicode.com/photos")
		if err != nil {
			fmt.Print(err.Error())
			os.Exit(1)
		}
		responseData, err := ioutil.ReadAll(response.Body)
		if err != nil {
			log.Fatal(err)
		}
		time.Sleep(2 * time.Second)
		w.Write(responseData)
	})
	http.ListenAndServe(":3000", nil)
}

Quando você acessar http://127.0.0.1:3000/ ele vai chamar pela api jsonplaceholder, para tornar mais interessante eu adicionei um sleep de 2 segundos para simular que o processo é mais lento.

Agora vamos usar o vegeta a idéia aqui é executar várias requests para ver o singleflight brilhar. Eu defino para executar 10 requests por segundo e a duração de 1 segundo.

echo "GET http://localhost:3000/" | vegeta attack -duration=1s -rate=10 | tee results.bin | vegeta report

Aqui você pode ver o resultado do Vegeta e o output do nosso serviço: Alt Text

Como podemos ver todas requests chamaram a api externa.

Agora vamos ver o singleflight brilhar, usaremos as mesmas configurações do vegeta.

Neste código eu adicionei um novo endpoint /singleflight, na chamada da função requestGroup.Do() eu defini a chave como singleflight, agora a request vai verificar se há um processo em andamento, caso sim ele aguarda o resultado. Adicionei um print no terminal para indicar quando a request aguarda pelo resultado e usa a resposta compartilhada.

// Como ele não faz parte da standard library você precisa adicionar ele no seu gopath,go mod,vendor,etc...
import "golang.org/x/sync/singleflight"
var requestGroup singleflight.Group
//para este endpoint funcionar você precisa importar o pacote singleflight e criar essa variável(eu sei global e tal, mas para este post é suficiente).
http.HandleFunc("/singleflight", func(w http.ResponseWriter, r *http.Request) {
		res, err, shared := requestGroup.Do("singleflight", func() (interface{}, error) {
			fmt.Println("calling the endpoint")
			response, err := http.Get("https://jsonplaceholder.typicode.com/photos")

			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return nil, err
			}
			responseData, err := ioutil.ReadAll(response.Body)
			if err != nil {
				log.Fatal(err)
			}
			time.Sleep(2 * time.Second)
			return string(responseData), err
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		result := res.(string)
		fmt.Println("shared = ", shared)
		fmt.Fprintf(w, "%q", result)
	})

Vegeta novamente

echo "GET http://localhost:3000/" | vegeta attack -duration=1s -rate=10 | tee results.bin | vegeta report

Alt Text

Recomendo você executar o código+vegeta e ver isso executando na sua máquina. No primeiro endpoint, você verá que os requests são executados e o log mostra as chamadas. No segundo endpoint, você verá uma request, e de repente 10 true indicando que todas requests usaram a resposta compartilhada.

Isso é um recurso incrível, pense em endpoints que tem um processo pesado/lento ou por serviços externos que você paga por requisições, neste último caso além de ajudar o serviço evitando processamento também poderá poupar dinheiro com requests duplicadas. Outro ponto é que estou usando em um serviço de http, mas poderia ser qualquer coisa, por ex. poderia ser uma consulta na base de dados , enfim cenários não faltam :).

Bom é isso o que gostaria de mostrar, espero que te ajude como me ajudou saber deste package lindão e deixo meu agradecimento ao Henrique por te me mostrado o pacote. Vale dizer que o pacote conta com uma opção para “esquecer” a chave e tbm uma opção que o resultado é retornado via channel. Você pode por ex. definir um timeout, depois de n tempo você pode por ex. cancelar a chave ou pelo channel.