
Interroger une API réelle, ce n'est pas un GET naïf : il faut authentifier la requête, décoder le JSON, paginer les résultats et pouvoir annuler un appel qui traîne. Ce guide construit un collecteur qui parcourt une API paginée avec net/http, un token d'authentification et un contexte de timeout, puis montre l'alternative des clients générés comme go-github. Public visé : intermédiaire en Go. Tous les exemples sont testés avec Go 1.26.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Construire une requête authentifiée avec
net/httpet un contexte. - Décoder une réponse JSON dans des structures Go.
- Parcourir toutes les pages d'une API paginée.
- Recourir à un client généré (go-github) quand il existe.
Décoder une réponse dans une struct
Section intitulée « Décoder une réponse dans une struct »On ne mappe que les champs utiles de la réponse JSON. Les tags json:"..." relient chaque champ de la struct à sa clé dans le document.
// Repo est le sous-ensemble des champs qui nous intéressent.type Repo struct { Name string `json:"name"` Stars int `json:"stars"`}Les champs non listés dans la struct sont simplement ignorés au décodage : inutile de modéliser toute la réponse.
Une requête authentifiée avec contexte
Section intitulée « Une requête authentifiée avec contexte »La bonne pratique est http.NewRequestWithContext : le contexte permet d'annuler l'appel (timeout, arrêt du programme). On ajoute l'en-tête d'autorisation, on exécute, et on vérifie le code de statut avant de décoder.
func fetchPage(ctx context.Context, base, token string, page int) ([]Repo, error) { url := fmt.Sprintf("%s/repos?page=%d", base, page) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // toujours fermer le corps de réponse
if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("statut inattendu : %s", resp.Status) } var repos []Repo if err := json.NewDecoder(resp.Body).Decode(&repos); err != nil { return nil, err } return repos, nil}Deux réflexes non négociables : defer resp.Body.Close() pour ne pas fuir de connexions, et vérifier resp.StatusCode car une erreur HTTP (401, 500) n'est pas une erreur Go, client.Do réussit quand même.
Paginer jusqu'au bout
Section intitulée « Paginer jusqu'au bout »Une API ne renvoie qu'une page de résultats à la fois. Le collecteur boucle en incrémentant le numéro de page et s'arrête quand une page revient vide.
// fetchAll parcourt les pages jusqu'à en recevoir une vide.func fetchAll(ctx context.Context, base, token string) ([]Repo, error) { var all []Repo for page := 1; ; page++ { batch, err := fetchPage(ctx, base, token, page) if err != nil { return nil, err } if len(batch) == 0 { break } all = append(all, batch...) } return all, nil}Le main fixe un contexte avec délai maximal, puis lance la collecte. Ici, un petit serveur de test sert deux pages, la troisième étant vide :
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()
repos, err := fetchAll(ctx, apiURL, "secret-token")if err != nil { fmt.Println("erreur:", err) return}for _, rp := range repos { fmt.Printf("- %s (%d stars)\n", rp.Name, rp.Stars)}fmt.Printf("total: %d dépôts\n", len(repos))À l'exécution, les trois dépôts répartis sur deux pages sont bien tous collectés :
- pitstop (128 stars)- confkit (64 stars)- plumber (512 stars)total: 3 dépôtsQuand un client généré existe
Section intitulée « Quand un client généré existe »Réécrire l'authentification, la pagination et la gestion d'erreurs pour une grande API serait fastidieux. Pour GitHub et GitLab, des clients officiels encapsulent tout cela. Voici la même collecte avec go-github : le client gère l'auth, la pagination se pilote via resp.NextPage.
import ( "context"
"github.com/google/go-github/v89/github")
func listOrgRepos(ctx context.Context, org, token string) ([]string, error) { // v89 : NewClient prend des options fonctionnelles et renvoie une erreur. client, err := github.NewClient(github.WithAuthToken(token)) if err != nil { return nil, err } opt := &github.RepositoryListByOrgOptions{ ListOptions: github.ListOptions{PerPage: 100}, } var names []string for { repos, resp, err := client.Repositories.ListByOrg(ctx, org, opt) if err != nil { return nil, err } for _, r := range repos { names = append(names, r.GetName()) } if resp.NextPage == 0 { // plus de page suivante break } opt.Page = resp.NextPage } return names, nil}Le client gère pour vous les en-têtes, le format de pagination de GitHub et les limites de débit. La boucle sur resp.NextPage est le motif standard de pagination avec go-github.
Bonnes pratiques
Section intitulée « Bonnes pratiques »- Passez toujours un contexte : sans lui, un appel bloqué fige votre programme.
- Fermez le corps de réponse avec
defer resp.Body.Close(), sinon vous fuyez des connexions. - Vérifiez
resp.StatusCode: une réponse 4xx/5xx n'est pas une erreur Go. - Fixez un
Timeoutsur lehttp.Clientpour borner chaque appel. - Préférez un client généré (go-github, gitlab) quand il existe : moins de code, moins de bugs.
À retenir
Section intitulée « À retenir »http.NewRequestWithContextest la base : une requête annulable et authentifiée.- Le décodage JSON ne modélise que les champs utiles via des tags
json. - La pagination consiste à suivre la page suivante jusqu'à l'épuisement.
defer Body.Close()et la vérification du statut sont non négociables.- Pour GitHub/GitLab, un client généré encapsule auth, pagination et limites de débit.