Salta al contenuto
Go

HTTP Client

Client HTTP in Go

Il pacchetto net/http di Go non include solo un server HTTP, ma anche un client completo per effettuare richieste HTTP. Il client predefinito e semplice da usare ma anche altamente configurabile per esigenze avanzate come timeout, header personalizzati e gestione del context.

http.Get

Il modo piu semplice per effettuare una richiesta GET:

package main

import (
    "fmt"
    "io"
    "net/http"
)

func main() {
    resp, err := http.Get("https://api.esempio.it/dati")
    if err != nil {
        fmt.Println("Errore nella richiesta:", err)
        return
    }
    defer resp.Body.Close() // IMPORTANTE: chiudere sempre il body

    // Leggere il body della risposta
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Errore nella lettura:", err)
        return
    }

    fmt.Println("Status:", resp.StatusCode)
    fmt.Println("Body:", string(body))
}

E fondamentale chiamare sempre defer resp.Body.Close() per evitare perdite di risorse (resource leak).

http.Post

Per effettuare richieste POST con un body:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)

type Utente struct {
    Nome  string `json:"nome"`
    Email string `json:"email"`
}

func main() {
    utente := Utente{
        Nome:  "Mario Rossi",
        Email: "mario@esempio.it",
    }

    // Serializzare i dati in JSON
    datiJSON, err := json.Marshal(utente)
    if err != nil {
        fmt.Println("Errore serializzazione:", err)
        return
    }

    // Inviare la richiesta POST
    resp, err := http.Post(
        "https://api.esempio.it/utenti",
        "application/json",
        bytes.NewBuffer(datiJSON),
    )
    if err != nil {
        fmt.Println("Errore:", err)
        return
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Println("Status:", resp.StatusCode)
    fmt.Println("Risposta:", string(body))
}

Client personalizzato

Il client predefinito (http.DefaultClient) non ha timeout, il che puo essere pericoloso in produzione. E buona pratica creare un client personalizzato:

package main

import (
    "fmt"
    "io"
    "net/http"
    "time"
)

func main() {
    // Client con timeout
    client := &http.Client{
        Timeout: 10 * time.Second,
    }

    resp, err := client.Get("https://api.esempio.it/dati")
    if err != nil {
        fmt.Println("Errore:", err)
        return
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
}

Per un controllo piu fine sui timeout:

import (
    "net"
    "net/http"
    "time"
)

func creaClient() *http.Client {
    transport := &http.Transport{
        DialContext: (&net.Dialer{
            Timeout:   5 * time.Second,  // Timeout di connessione
            KeepAlive: 30 * time.Second, // Keep-alive
        }).DialContext,
        TLSHandshakeTimeout:   5 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
        MaxIdleConns:          100,
        MaxIdleConnsPerHost:   10,
        IdleConnTimeout:       90 * time.Second,
    }

    return &http.Client{
        Transport: transport,
        Timeout:   30 * time.Second, // Timeout totale
    }
}

Impostare Header

Per personalizzare gli header della richiesta, si crea una *http.Request manualmente:

package main

import (
    "fmt"
    "io"
    "net/http"
    "time"
)

func main() {
    client := &http.Client{Timeout: 10 * time.Second}

    // Creare la richiesta manualmente
    req, err := http.NewRequest("GET", "https://api.esempio.it/dati", nil)
    if err != nil {
        fmt.Println("Errore nella creazione richiesta:", err)
        return
    }

    // Aggiungere header
    req.Header.Set("Authorization", "Bearer token-abc123")
    req.Header.Set("Accept", "application/json")
    req.Header.Set("User-Agent", "MioClient/1.0")
    req.Header.Set("X-Custom-Header", "valore")

    // Eseguire la richiesta
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Errore:", err)
        return
    }
    defer resp.Body.Close()

    // Leggere gli header della risposta
    contentType := resp.Header.Get("Content-Type")
    fmt.Println("Content-Type:", contentType)

    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
}

Gestione degli errori

Una gestione completa degli errori include il controllo dello status code:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

type RispostaErrore struct {
    Codice    int    `json:"codice"`
    Messaggio string `json:"messaggio"`
}

func chiamaAPI(url string) ([]byte, error) {
    client := &http.Client{Timeout: 10 * time.Second}

    resp, err := client.Get(url)
    if err != nil {
        return nil, fmt.Errorf("errore di rete: %w", err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("errore lettura body: %w", err)
    }

    // Verificare lo status code
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        var errResp RispostaErrore
        if err := json.Unmarshal(body, &errResp); err == nil {
            return nil, fmt.Errorf("errore API [%d]: %s",
                errResp.Codice, errResp.Messaggio)
        }
        return nil, fmt.Errorf("errore HTTP %d: %s",
            resp.StatusCode, http.StatusText(resp.StatusCode))
    }

    return body, nil
}

func main() {
    dati, err := chiamaAPI("https://api.esempio.it/utenti")
    if err != nil {
        fmt.Println("Errore:", err)
        return
    }
    fmt.Println("Dati ricevuti:", string(dati))
}

Context con le richieste

Il context permette di controllare il timeout e la cancellazione delle richieste:

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "time"
)

func ottieniDati(ctx context.Context, url string) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, fmt.Errorf("errore creazione richiesta: %w", err)
    }

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("errore richiesta: %w", err)
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

func main() {
    // Context con timeout di 5 secondi
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    dati, err := ottieniDati(ctx, "https://api.esempio.it/dati")
    if err != nil {
        fmt.Println("Errore:", err)
        return
    }

    fmt.Println("Ricevuti:", len(dati), "byte")
}

Richieste con diversi metodi HTTP

Un helper per effettuare richieste con qualsiasi metodo:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

type ClientAPI struct {
    BaseURL string
    Client  *http.Client
    Token   string
}

func NuovoClientAPI(baseURL, token string) *ClientAPI {
    return &ClientAPI{
        BaseURL: baseURL,
        Client:  &http.Client{Timeout: 15 * time.Second},
        Token:   token,
    }
}

func (c *ClientAPI) Richiesta(metodo, percorso string, body any) ([]byte, error) {
    var bodyReader *bytes.Buffer
    if body != nil {
        dati, err := json.Marshal(body)
        if err != nil {
            return nil, err
        }
        bodyReader = bytes.NewBuffer(dati)
    } else {
        bodyReader = &bytes.Buffer{}
    }

    req, err := http.NewRequest(metodo, c.BaseURL+percorso, bodyReader)
    if err != nil {
        return nil, err
    }

    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer "+c.Token)

    resp, err := c.Client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

func main() {
    api := NuovoClientAPI("https://api.esempio.it", "mio-token")

    // GET
    dati, _ := api.Richiesta("GET", "/utenti", nil)
    fmt.Println(string(dati))

    // POST
    nuovoUtente := map[string]string{"nome": "Luigi", "email": "luigi@esempio.it"}
    dati, _ = api.Richiesta("POST", "/utenti", nuovoUtente)
    fmt.Println(string(dati))
}

Conclusione

Il client HTTP di Go e potente e flessibile. Per applicazioni di produzione, create sempre un client personalizzato con timeout appropriati, usate http.NewRequestWithContext per supportare la cancellazione tramite context, e gestite sempre gli errori controllando sia l’errore di rete che lo status code HTTP. Ricordate di chiudere sempre il body della risposta con defer resp.Body.Close() per evitare perdite di risorse.

Contenuto redatto con l'assistenza di strumenti di intelligenza artificiale e rivisto dalla redazione di Codegrind, che ne è responsabile. Hai trovato un errore? Scrivici.