go
55 lines · 8 steps
Concurrent API fetches with errgroup in Go
Fetch a user, their repos, and orgs in parallel, cancelling the rest the moment any request fails.
Explained by
highlit
1package fetch
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/http"
8
9 "golang.org/x/sync/errgroup"
10)
11
12type Profile struct {
13 User User
14 Repos []Repo
15 Orgs []Org
16}
17
18func LoadProfile(ctx context.Context, client *http.Client, username string) (*Profile, error) {
19 g, ctx := errgroup.WithContext(ctx)
20 var profile Profile
21
22 g.Go(func() error {
23 return getJSON(ctx, client, "/users/"+username, &profile.User)
24 })
25
26 g.Go(func() error {
27 return getJSON(ctx, client, "/users/"+username+"/repos", &profile.Repos)
28 })
29
30 g.Go(func() error {
31 return getJSON(ctx, client, "/users/"+username+"/orgs", &profile.Orgs)
32 })
33
34 if err := g.Wait(); err != nil {
35 return nil, fmt.Errorf("load profile %q: %w", username, err)
36 }
37 return &profile, nil
38}
39
40func getJSON(ctx context.Context, client *http.Client, path string, dst any) error {
41 req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com"+path, nil)
42 if err != nil {
43 return err
44 }
45 resp, err := client.Do(req)
46 if err != nil {
47 return err
48 }
49 defer resp.Body.Close()
50
51 if resp.StatusCode != http.StatusOK {
52 return fmt.Errorf("%s: unexpected status %s", path, resp.Status)
53 }
54 return json.NewDecoder(resp.Body).Decode(dst)
55}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1errgroup.WithContext ties a set of goroutines to one context that cancels as soon as any of them returns an error.
- 2Each goroutine writing to a distinct field of a shared struct avoids data races without explicit locking.
- 3Wrapping the aggregated error with %w preserves the underlying cause for callers to inspect.
Related explainers
javascript
export function cloneState(state) { if (typeof structuredClone !== "function") { throw new Error("structuredClone is not available in this runtime"); }
Deep cloning with structuredClone
deep-copy
error-handling
immutability
Intermediate
7 steps
rust
use serde::Deserialize; use serde_json::Value; use std::collections::HashMap;
Modeling nested JSON with serde in Rust
deserialization
json
struct-mapping
Intermediate
9 steps
java
import java.util.concurrent.atomic.AtomicInteger; public final class RequestCounter {
A thread-safe request counter with AtomicInteger
concurrency
atomics
lock-free
Intermediate
6 steps
python
import threading import logging logger = logging.getLogger(__name__)
A self-rescheduling periodic task in Python
threading
scheduling
concurrency
Intermediate
6 steps
go
func UploadFiles(c *gin.Context) { form, err := c.MultipartForm() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid multipart form"})
Handling multi-file uploads in Gin
file-upload
multipart
validation
Intermediate
8 steps
go
package pipeline import ( "context"
A cancelable worker pool in Go
concurrency
channels
worker-pool
Advanced
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/concurrent-api-fetches-with-errgroup-in-go-explained-go-1664/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.