El pasado domingo 29 de marzo, nuestro querido Joan estaba de speaker en la StayAtHomeConf, y nos explicaba con ejemplos muy claros como utilizar los contextos.
Si tenéis ganas de echarle un ojo podéis seguir el siguiente link para ver la charla en cuestión.
Pero os voy a contar un secreto, Joan, decidió esta vez no enseñarme la charla para que fuera una sorpresa para mí. Y en cuanto la vi y vi todo el pifostio que había montado, obviamente a sabiendas para el buen explicamiento del paquete context pues se me encendió una bombilla, ¿por qué no profundizar un poco más en el manejo de múltiples peticiones y aprovecharnos también del paquete context?
Así que hoy os voy explicar como mejorar vuestra gestión de procesos con el paquete errgroup
Antes de empezar
Si no estás familiarizado con la concurrencia y los waitGroups te recomendamos que eches primero un vistazo a los artículos respectivos, pues puede que sino te sea un poco complicado de seguir este artículo.
Concurrencia en estado puro
Para poder ponernos en situación os voy a explicar un caso, imaginad que queremos montar una página que obtenga resultados de diferentes motores de búsqueda, pero no queremos esperar a que todas acaben sino que queremos quedarnos con los resultados de las que acaben en un determinado tiempo.
Podríamos solucionar la papeleta de la siguiente manera:
type response struct {
url string
response *http.Response
}
func main() {
var urls = []string{
"https://www.bing.com/search?q=friends+of+go",
"https://www.google.com/search?q=friends+of+go",
"https://duckduckgo.com/?q=friends+of+go",
}
respCh := make(chan response)
ctx, _ := context.WithTimeout(context.Background(), 300*time.Millisecond)
for _, url := range urls {
go func(url string) {
// Fetch the URL.
resp, err := http.Get(url)
if err == nil {
respCh <- response{url: url, response: resp}
resp.Body.Close()
}
if err != nil {
log.Println(err)
}
}(url)
}
var responses []response
loop:
for {
select {
case resp := <-respCh:
responses = append(responses, resp)
case <-ctx.Done():
break loop
}
}
fmt.Println(responses)
}
Lo que haríamos es crear un contexto que dado un tiempo determinado, en este caso 300 milisegundos
cancele nuestras rutinas. Si quieres más información sobre contexto en concurrencia te recomiendo ver el artículo respectivo.
ctx, _ := context.WithTimeout(context.Background(), 300*time.Millisecond)
Por cada url
invocaremos una gorrutina
la cual se encargará de hacer la petición al servidor y obtener los resultados, los cuales serán publicados en un canal.
for _, url := range urls {
go func(url string) {
// Fetch the URL.
resp, err := http.Get(url)
if err == nil {
respCh <- response{url: url, response: resp}
resp.Body.Close()
}
if err != nil {
log.Println(err)
}
}(url)
}
Finalmente lo que haremos es esperar con un bucle infinito a recoger esos datos que publicamos en el canal, y si el contexto se termina acabar nuestra ejecución y mostrar los resultados.
loop:
for {
select {
case resp := <-respCh:
responses = append(responses, resp)
case <-ctx.Done():
break loop
}
}
fmt.Println(responses)
}
Esta sería una manera de ejecutar el programa presentado, y sería completamente válido pero, podríamos hacer uso de otra herramienta proporcionada por Go, el paquete errgroup
Usando errgroup
Como hemos visto en el ejemplo anterior deberemos de preocuparnos por el contexto para poder cancelar los procesos, pero tenemos una alternativa que encapsula esta lógica, el paquete el paquete errgroup.
Vamos a ver como queda nuestro código anterior y explicarlo paso a paso.
type response struct {
url string
response *http.Response
}
func main() {
var urls = []string{
"https://www.bing.com/search?q=friends+of+go",
"https://www.google.com/search?q=friends+of+go",
"https://duckduckgo.com/?q=friends+of+go",
}
ctx, _ := context.WithTimeout(context.Background(), 300*time.Millisecond)
g, ctx := errgroup.WithContext(ctx)
respCh := make(chan response)
for _, url := range urls {
url := url
g.Go(func() error {
client := http.DefaultClient
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := client.Do(request)
if err != nil {
return err
}
defer resp.Body.Close()
respCh <- response{url: url, response: resp}
return nil
})
}
go func() {
g.Wait()
close(respCh)
}()
var responses []response
for r := range respCh {
responses = append(responses, r)
}
fmt.Println(responses)
}
Lo primero que deberemos hacer es crear un nuevo errgroup
g, ctx := errgroup.WithContext(ctx)
Para inicializarlo nos basaremos en la función del mismo paquete WithContext(context.Context)
en la cual pasaremos nuestro contexto creado anteriormente, ¿por qué? El errgroup
funciona como un waitGroup
es decir iremos añadiendo rutinas a nuestra cola, y esperaremos (Wait()
) a recibir la señal de Done()
de cada una de ellas para terminar nuestra ejecución, pero el errgroup
también añade una cláusula más y es que cancelará el contexto si devolvemos un error
.
Entendido esto vemos que nuestro bucle ha cambiado un poco, lo primero de todo nos encontramos con una línea que lo mismo os sorpende un poco.
url := url
A esto se le conoce como shadowed variable, y en este caso lo usamos para crear una nueva variable url
dentro del contexto del bucle con el valor de url
que nos envía el bucle para poder enviarlo a la rutina.
A continuación cambiamos de utilizar una simple petición Get
del package http, a utilizar una request
con contexto.
client := http.DefaultClient
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := client.Do(request)
if err != nil {
return err
}
defer resp.Body.Close()
Básicamente gracias a esto, la request
sabrá si tiene que ejecutarse o no una vez alcanzado el tiempo que indicamos en el contexto, y por ello devolveremos un error y cortaremos la ejecución como hemos explicado anteriormente.
A continuación seguiremos escribiendo en nuestro canal, pues a pesar de utilizar errgroup
, el append
, sigue sin ser trhead-safe.
respCh <- response{url: url, response: resp}
return nil
Luego, que se encargará de llamar al método Wait()
que nos proporciona el errgroup
, si bien aquí no hemos querido controlar los errores por simplicidad sería una buena práctica tenerlos en cuenta. Además cerraremos el canal de respuestas ya que no queremos continuar recibiendo más respuestas.
go func() {
g.Wait()
close(respCh)
}()
Así pues por último tendremos un bucle escuchando sobre nuestro canal, para recoger las respuestas, hasta que el mismo acabe cerrado, y escribiéndolas en nuestro slice
de respuestas.
var responses []response
for r := range respCh {
responses = append(responses, r)
}
fmt.Println(responses)
Conclusión
Hoy hemos visto dos maneras de las que podemos valernos de la concurrencia y los contextos para realizar acciones como realizar múltiples llamadas a servidores y quedarnos con aquellos datos necesarios. Gracias al paquete errgroup
podemos abstraernos un poco del control de ese contexto facilitándonos un poco más el manejo y la legibilidad de nuestro código.
Recordar que si tenéis cualquier duda podéis dejarlo en los comentarios o escribirnos por Twitter a @FriendsOfGoTech