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.