Richieste HTTP in Go
Go offre nel package standard net/http tutto il necessario per effettuare richieste HTTP sia semplici che avanzate. A differenza di molti altri linguaggi, non occorre installare librerie esterne per gestire GET, POST, autenticazione, timeout e molto altro. In questo articolo analizziamo in dettaglio come costruire client HTTP in Go, partendo dai casi base fino ad arrivare a pattern più sofisticati come middleware, pooling delle connessioni e gestione degli errori.
Il package net/http
Il package net/http è parte della libreria standard di Go e fornisce implementazioni complete sia del lato client che del lato server del protocollo HTTP. Per effettuare richieste è sufficiente importarlo senza dipendenze esterne.
I tipi principali con cui si lavora lato client sono:
http.Client: il client HTTP configurabile.http.Request: la rappresentazione di una richiesta HTTP.http.Response: la rappresentazione di una risposta HTTP.http.Transport: il meccanismo di basso livello che gestisce le connessioni.
Richiesta GET semplice
La forma più immediata per effettuare una richiesta GET è usare la funzione http.Get(), che utilizza il client predefinito del package.
package main
import (
"fmt"
"io"
"log"
"net/http"
)
func main() {
// Eseguiamo una richiesta GET verso un endpoint pubblico
response, err := http.Get("https://jsonplaceholder.typicode.com/posts/1")
if err != nil {
log.Fatalf("errore nella richiesta: %v", err)
}
// Il body deve essere sempre chiuso per liberare la connessione
defer response.Body.Close()
// Leggiamo il corpo della risposta
body, err := io.ReadAll(response.Body)
if err != nil {
log.Fatalf("errore nella lettura del body: %v", err)
}
fmt.Printf("Status: %s\n", response.Status)
fmt.Printf("Body: %s\n", body)
}
L'uso di defer response.Body.Close() è fondamentale: se il body non viene chiuso, le connessioni TCP sottostanti non vengono restituite al pool e si esauriscono rapidamente.
Richiesta POST con corpo JSON
Per inviare dati in formato JSON occorre costruire la richiesta manualmente usando http.NewRequest(), impostare l'header Content-Type e fornire un reader per il corpo.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
type Post struct {
Title string `json:"title"`
Body string `json:"body"`
UserID int `json:"userId"`
}
func main() {
// Costruiamo il payload da inviare
payload := Post{
Title: "Articolo di test",
Body: "Contenuto dell'articolo",
UserID: 1,
}
// Serializziamo la struct in JSON
data, err := json.Marshal(payload)
if err != nil {
log.Fatalf("errore nella serializzazione JSON: %v", err)
}
// Creiamo la richiesta POST con il body JSON
request, err := http.NewRequest(
http.MethodPost,
"https://jsonplaceholder.typicode.com/posts",
bytes.NewBuffer(data),
)
if err != nil {
log.Fatalf("errore nella creazione della richiesta: %v", err)
}
// Impostiamo gli header necessari
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept", "application/json")
// Eseguiamo la richiesta con il client predefinito
client := &http.Client{}
response, err := client.Do(request)
if err != nil {
log.Fatalf("errore nell'esecuzione della richiesta: %v", err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
log.Fatalf("errore nella lettura del body: %v", err)
}
fmt.Printf("Status: %d\n", response.StatusCode)
fmt.Printf("Risposta: %s\n", body)
}
Configurare un client HTTP personalizzato
Il client predefinito http.DefaultClient non ha timeout configurato, il che lo rende inadatto alla produzione. È sempre consigliabile creare un http.Client esplicito con timeout e transport personalizzati.
package main
import (
"fmt"
"io"
"log"
"net/http"
"time"
)
func buildClient() *http.Client {
// Configuriamo il transport con parametri ottimizzati per la produzione
transport := &http.Transport{
MaxIdleConns: 100, // Numero massimo di connessioni inattive nel pool
MaxIdleConnsPerHost: 10, // Connessioni inattive per singolo host
IdleConnTimeout: 90 * time.Second, // Timeout per le connessioni inattive
DisableCompression: false, // Lasciamo la compressione abilitata
}
// Costruiamo il client con timeout globale sulla richiesta
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second, // Timeout totale della richiesta inclusa la lettura del body
}
return client
}
func main() {
client := buildClient()
response, err := client.Get("https://jsonplaceholder.typicode.com/users")
if err != nil {
log.Fatalf("errore nella richiesta: %v", err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
log.Fatalf("errore nella lettura del body: %v", err)
}
fmt.Printf("Ricevuti %d byte\n", len(body))
}
Il campo Timeout di http.Client copre l'intera operazione: connessione, invio della richiesta, ricezione degli header e lettura del body. Il Transport offre invece un controllo più granulare sui singoli passi della connessione TCP/TLS.
Timeout granulari con http.Transport
Per scenari avanzati dove occorre differenziare il timeout di connessione da quello di lettura, si usa net.Dialer insieme a http.Transport.
package main
import (
"context"
"fmt"
"io"
"log"
"net"
"net/http"
"time"
)
func buildAdvancedClient() *http.Client {
dialer := &net.Dialer{
Timeout: 5 * time.Second, // Timeout per la fase di connessione TCP
KeepAlive: 30 * time.Second, // Intervallo dei pacchetti keep-alive
}
transport := &http.Transport{
DialContext: dialer.DialContext,
TLSHandshakeTimeout: 5 * time.Second, // Timeout per la handshake TLS
ResponseHeaderTimeout: 10 * time.Second, // Tempo massimo per ricevere gli header
ExpectContinueTimeout: 1 * time.Second, // Timeout per il meccanismo Expect: 100-continue
MaxIdleConns: 50,
MaxIdleConnsPerHost: 5,
IdleConnTimeout: 60 * time.Second,
}
return &http.Client{
Transport: transport,
}
}
func main() {
client := buildAdvancedClient()
// Usiamo un context per un timeout ulteriore a livello di chiamata
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
request, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
"https://jsonplaceholder.typicode.com/comments?postId=1",
nil,
)
if err != nil {
log.Fatalf("errore nella creazione della richiesta: %v", err)
}
response, err := client.Do(request)
if err != nil {
log.Fatalf("errore nella richiesta: %v", err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
log.Fatalf("errore nella lettura del body: %v", err)
}
fmt.Printf("Ricevuti %d byte con status %s\n", len(body), response.Status)
}
Gestione degli errori e dei codici di stato
Una richiesta HTTP completata senza errori di rete non significa necessariamente che sia andata a buon fine: occorre sempre verificare il codice di stato HTTP. In Go, err è nil anche se il server restituisce un 404 o un 500.
package main
import (
"errors"
"fmt"
"io"
"log"
"net/http"
)
// HTTPError rappresenta un errore HTTP con il relativo codice di stato
type HTTPError struct {
StatusCode int
Status string
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("errore HTTP %d: %s", e.StatusCode, e.Status)
}
// fetchURL esegue una GET e restituisce il body o un errore tipizzato
func fetchURL(client *http.Client, url string) ([]byte, error) {
response, err := client.Get(url)
if err != nil {
// Errore di rete, DNS, timeout ecc.
return nil, fmt.Errorf("errore di rete: %w", err)
}
defer response.Body.Close()
// Verifichiamo il codice di stato HTTP
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, &HTTPError{
StatusCode: response.StatusCode,
Status: response.Status,
}
}
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("errore nella lettura del body: %w", err)
}
return body, nil
}
func main() {
client := &http.Client{}
// Proviamo un URL inesistente per vedere la gestione dell'errore
body, err := fetchURL(client, "https://jsonplaceholder.typicode.com/posts/9999")
if err != nil {
var httpErr *HTTPError
// Distinguiamo tra errore HTTP ed errore di rete usando errors.As
if errors.As(err, &httpErr) {
fmt.Printf("Il server ha risposto con errore: %v\n", httpErr)
} else {
log.Fatalf("errore imprevisto: %v", err)
}
return
}
fmt.Printf("Risposta: %s\n", body)
}
Deserializzare la risposta JSON
In Go è buona pratica decodificare il JSON direttamente dal reader del body usando json.NewDecoder, senza leggere prima tutti i byte in memoria. Questo approccio è più efficiente con risposte di grandi dimensioni.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"`
}
func fetchUsers(client *http.Client) ([]User, error) {
response, err := client.Get("https://jsonplaceholder.typicode.com/users")
if err != nil {
return nil, fmt.Errorf("errore nella richiesta: %w", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status inatteso: %s", response.Status)
}
var users []User
// Decodifichiamo il JSON direttamente dal body senza bufferizzarlo completamente
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&users); err != nil {
return nil, fmt.Errorf("errore nella decodifica JSON: %w", err)
}
return users, nil
}
func main() {
client := &http.Client{}
users, err := fetchUsers(client)
if err != nil {
log.Fatalf("impossibile recuperare gli utenti: %v", err)
}
for _, user := range users {
fmt.Printf("ID: %d | Nome: %s | Email: %s\n", user.ID, user.Name, user.Email)
}
}
Autenticazione: Bearer Token e Basic Auth
Le API moderne richiedono quasi sempre un qualche meccanismo di autenticazione. I due schemi più diffusi sono Basic Auth e Bearer Token, entrambi trasportati nell'header Authorization.
package main
import (
"fmt"
"io"
"log"
"net/http"
)
// withBearerToken aggiunge il token Bearer alla richiesta
func withBearerToken(request *http.Request, token string) *http.Request {
request.Header.Set("Authorization", "Bearer "+token)
return request
}
// withBasicAuth aggiunge le credenziali Basic Auth alla richiesta
func withBasicAuth(request *http.Request, username, password string) *http.Request {
// Il metodo SetBasicAuth gestisce internamente la codifica Base64
request.SetBasicAuth(username, password)
return request
}
func main() {
client := &http.Client{}
// Esempio con Bearer Token
bearerRequest, err := http.NewRequest(
http.MethodGet,
"https://jsonplaceholder.typicode.com/posts/1",
nil,
)
if err != nil {
log.Fatalf("errore nella creazione della richiesta: %v", err)
}
// Aggiungiamo il token Bearer (in produzione viene letto da configurazione)
withBearerToken(bearerRequest, "il-mio-token-segreto")
response, err := client.Do(bearerRequest)
if err != nil {
log.Fatalf("errore nella richiesta: %v", err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
log.Fatalf("errore nella lettura: %v", err)
}
fmt.Printf("Status: %s\nBody: %s\n", response.Status, body)
}
Query string e parametri URL
Costruire URL con parametri di query in modo sicuro richiede l'uso di url.Values per evitare problemi di encoding. Non è mai consigliabile concatenare le stringhe manualmente.
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/url"
)
func buildURLWithParams(baseURL string, params map[string]string) (string, error) {
parsedURL, err := url.Parse(baseURL)
if err != nil {
return "", fmt.Errorf("URL non valido: %w", err)
}
// url.Values gestisce automaticamente l'encoding dei caratteri speciali
queryParams := url.Values{}
for key, value := range params {
queryParams.Set(key, value)
}
parsedURL.RawQuery = queryParams.Encode()
return parsedURL.String(), nil
}
func main() {
client := &http.Client{}
// Costruiamo l'URL con i parametri in modo sicuro
targetURL, err := buildURLWithParams(
"https://jsonplaceholder.typicode.com/posts",
map[string]string{
"userId": "1",
"_limit": "5",
},
)
if err != nil {
log.Fatalf("errore nella costruzione dell'URL: %v", err)
}
fmt.Printf("URL costruito: %s\n", targetURL)
response, err := client.Get(targetURL)
if err != nil {
log.Fatalf("errore nella richiesta: %v", err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
log.Fatalf("errore nella lettura: %v", err)
}
fmt.Printf("Risposta (%d byte): %s\n", len(body), body)
}
Middleware e RoundTripper
L'interfaccia http.RoundTripper è il meccanismo ufficiale di Go per intercettare e modificare le richieste HTTP a livello di transport. Implementandola si possono costruire middleware riutilizzabili per logging, retry, autenticazione automatica e molto altro.
package main
import (
"fmt"
"io"
"log"
"net/http"
"time"
)
// LoggingTransport è un middleware che logga ogni richiesta e la sua durata
type LoggingTransport struct {
base http.RoundTripper
}
// RoundTrip implementa http.RoundTripper e intercetta ogni richiesta
func (t *LoggingTransport) RoundTrip(request *http.Request) (*http.Response, error) {
start := time.Now()
log.Printf("[HTTP] %s %s", request.Method, request.URL)
// Eseguiamo la richiesta reale usando il transport sottostante
response, err := t.base.RoundTrip(request)
if err != nil {
log.Printf("[HTTP] errore dopo %v: %v", time.Since(start), err)
return nil, err
}
log.Printf("[HTTP] %s %s -> %d (%v)", request.Method, request.URL, response.StatusCode, time.Since(start))
return response, nil
}
// AuthTransport aggiunge automaticamente il Bearer Token a ogni richiesta
type AuthTransport struct {
base http.RoundTripper
token string
}
func (t *AuthTransport) RoundTrip(request *http.Request) (*http.Response, error) {
// Cloniamo la richiesta per evitare di modificare quella originale
clonedRequest := request.Clone(request.Context())
clonedRequest.Header.Set("Authorization", "Bearer "+t.token)
return t.base.RoundTrip(clonedRequest)
}
func buildClientWithMiddleware(token string) *http.Client {
// Componiamo i middleware in catena: AuthTransport -> LoggingTransport -> DefaultTransport
transport := &AuthTransport{
base: &LoggingTransport{
base: http.DefaultTransport,
},
token: token,
}
return &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}
}
func main() {
client := buildClientWithMiddleware("token-di-esempio")
response, err := client.Get("https://jsonplaceholder.typicode.com/todos/1")
if err != nil {
log.Fatalf("errore nella richiesta: %v", err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
log.Fatalf("errore nella lettura: %v", err)
}
fmt.Printf("Risposta: %s\n", body)
}
Retry automatico con backoff esponenziale
Un pattern fondamentale nei client HTTP resilienti è il retry con backoff esponenziale: in caso di errori temporanei o risposte 5xx, la richiesta viene ripetuta con un'attesa crescente tra un tentativo e l'altro.
package main
import (
"fmt"
"io"
"log"
"net/http"
"time"
)
// RetryConfig contiene i parametri per la politica di retry
type RetryConfig struct {
MaxAttempts int
BaseDelay time.Duration
MaxDelay time.Duration
}
// shouldRetry determina se la risposta giustifica un nuovo tentativo
func shouldRetry(statusCode int, err error) bool {
if err != nil {
// Errori di rete sono sempre candidati al retry
return true
}
// Ripetiamo solo per errori lato server (5xx) e per 429 Too Many Requests
return statusCode == http.StatusTooManyRequests ||
statusCode == http.StatusInternalServerError ||
statusCode == http.StatusBadGateway ||
statusCode == http.StatusServiceUnavailable ||
statusCode == http.StatusGatewayTimeout
}
// doWithRetry esegue la richiesta con logica di retry e backoff esponenziale
func doWithRetry(client *http.Client, request *http.Request, config RetryConfig) (*http.Response, error) {
var lastErr error
var response *http.Response
for attempt := 0; attempt < config.MaxAttempts; attempt++ {
if attempt > 0 {
// Calcoliamo il delay con backoff esponenziale: BaseDelay * 2^(attempt-1)
delay := config.BaseDelay * time.Duration(1< config.MaxDelay {
delay = config.MaxDelay
}
log.Printf("tentativo %d/%d tra %v...", attempt+1, config.MaxAttempts, delay)
time.Sleep(delay)
// Dobbiamo ricreare il body perché è già stato consumato al tentativo precedente
if request.GetBody != nil {
newBody, err := request.GetBody()
if err != nil {
return nil, fmt.Errorf("impossibile rileggere il body: %w", err)
}
request.Body = newBody
}
}
response, lastErr = client.Do(request)
if lastErr != nil {
if shouldRetry(0, lastErr) {
continue
}
return nil, lastErr
}
if !shouldRetry(response.StatusCode, nil) {
// La risposta è accettabile, usciamo dal loop
return response, nil
}
// Chiudiamo il body della risposta non valida prima del prossimo tentativo
response.Body.Close()
}
if lastErr != nil {
return nil, fmt.Errorf("tutti i tentativi falliti: %w", lastErr)
}
return response, nil
}
func main() {
client := &http.Client{Timeout: 10 * time.Second}
request, err := http.NewRequest(
http.MethodGet,
"https://jsonplaceholder.typicode.com/posts/1",
nil,
)
if err != nil {
log.Fatalf("errore nella creazione della richiesta: %v", err)
}
config := RetryConfig{
MaxAttempts: 3,
BaseDelay: 500 * time.Millisecond,
MaxDelay: 5 * time.Second,
}
response, err := doWithRetry(client, request, config)
if err != nil {
log.Fatalf("richiesta fallita: %v", err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
log.Fatalf("errore nella lettura: %v", err)
}
fmt.Printf("Status: %s\nBody: %s\n", response.Status, body)
}
Richieste concorrenti con goroutine
Uno dei punti di forza di Go è la concorrenza nativa. Quando occorre effettuare molte richieste HTTP indipendenti, le goroutine permettono di parallelizzarle in modo semplice ed efficiente. L'http.Client è sicuro per l'uso concorrente.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
)
type Post struct {
ID int `json:"id"`
Title string `json:"title"`
}
// FetchResult contiene il risultato o l'errore di una singola fetch
type FetchResult struct {
PostID int
Post *Post
Err error
}
// fetchPost recupera un singolo post e invia il risultato sul canale
func fetchPost(client *http.Client, postID int, results chan<- FetchResult, wg *sync.WaitGroup) {
defer wg.Done()
url := fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", postID)
response, err := client.Get(url)
if err != nil {
results <- FetchResult{PostID: postID, Err: err}
return
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
results <- FetchResult{
PostID: postID,
Err: fmt.Errorf("status inatteso: %s", response.Status),
}
return
}
var post Post
if err := json.NewDecoder(response.Body).Decode(&post); err != nil {
results <- FetchResult{PostID: postID, Err: err}
return
}
results <- FetchResult{PostID: postID, Post: &post}
}
func main() {
// Un client condiviso tra tutte le goroutine garantisce il pool di connessioni
client := &http.Client{Timeout: 10 * time.Second}
postIDs := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// Creiamo un canale per raccogliere i risultati
results := make(chan FetchResult, len(postIDs))
var wg sync.WaitGroup
for _, id := range postIDs {
wg.Add(1)
go fetchPost(client, id, results, &wg)
}
// Chiudiamo il canale quando tutte le goroutine hanno terminato
go func() {
wg.Wait()
close(results)
}()
// Raccogliamo i risultati dal canale
for result := range results {
if result.Err != nil {
log.Printf("errore per post %d: %v", result.PostID, result.Err)
continue
}
fmt.Printf("Post %d: %s\n", result.Post.ID, result.Post.Title)
}
}
Limitare la concorrenza con un semaforo
Lanciare centinaia di goroutine simultanee può sovraccaricare il server remoto o esaurire le connessioni disponibili. Il pattern del semaforo tramite canale bufferizzato permette di limitare il numero massimo di richieste concorrenti.
package main
import (
"fmt"
"io"
"log"
"net/http"
"sync"
"time"
)
// Semaphore è un semaforo implementato come canale bufferizzato
type Semaphore chan struct{}
func NewSemaphore(maxConcurrent int) Semaphore {
return make(Semaphore, maxConcurrent)
}
// Acquire occupa uno slot del semaforo (blocca se il canale è pieno)
func (s Semaphore) Acquire() {
s <- struct{}{}
}
// Release libera uno slot del semaforo
func (s Semaphore) Release() {
<-s
}
func fetchWithSemaphore(
client *http.Client,
url string,
sem Semaphore,
wg *sync.WaitGroup,
) {
defer wg.Done()
// Acquistiamo il semaforo prima di procedere con la richiesta
sem.Acquire()
defer sem.Release()
response, err := client.Get(url)
if err != nil {
log.Printf("errore per %s: %v", url, err)
return
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
log.Printf("errore nella lettura per %s: %v", url, err)
return
}
fmt.Printf("URL: %s | Byte ricevuti: %d\n", url, len(body))
}
func main() {
client := &http.Client{Timeout: 10 * time.Second}
// Permettiamo al massimo 3 richieste simultanee
sem := NewSemaphore(3)
urls := []string{
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/posts/2",
"https://jsonplaceholder.typicode.com/posts/3",
"https://jsonplaceholder.typicode.com/posts/4",
"https://jsonplaceholder.typicode.com/posts/5",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go fetchWithSemaphore(client, url, sem, &wg)
}
wg.Wait()
fmt.Println("Tutte le richieste completate.")
}
Caricare un file con richiesta multipart
Per caricare file su un server tramite HTTP occorre costruire un body multipart. Il package mime/multipart e il tipo bytes.Buffer sono gli strumenti standard per questo scopo.
package main
import (
"bytes"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
// uploadFile carica un file verso un endpoint multipart
func uploadFile(client *http.Client, targetURL, filePath string) error {
// Apriamo il file da caricare
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("impossibile aprire il file: %w", err)
}
defer file.Close()
// Creiamo un buffer e un writer multipart
var buffer bytes.Buffer
writer := multipart.NewWriter(&buffer)
// Creiamo il campo file nel form multipart
part, err := writer.CreateFormFile("file", filepath.Base(filePath))
if err != nil {
return fmt.Errorf("errore nella creazione del campo form: %w", err)
}
// Copiamo il contenuto del file nel campo multipart
if _, err := io.Copy(part, file); err != nil {
return fmt.Errorf("errore nella copia del file: %w", err)
}
// Aggiungiamo eventuali campi testuali aggiuntivi
if err := writer.WriteField("description", "File di esempio"); err != nil {
return fmt.Errorf("errore nella scrittura del campo: %w", err)
}
// Chiudiamo il writer per finalizzare il body multipart
writer.Close()
request, err := http.NewRequest(http.MethodPost, targetURL, &buffer)
if err != nil {
return fmt.Errorf("errore nella creazione della richiesta: %w", err)
}
// L'header Content-Type deve includere il boundary generato dal writer
request.Header.Set("Content-Type", writer.FormDataContentType())
response, err := client.Do(request)
if err != nil {
return fmt.Errorf("errore nella richiesta: %w", err)
}
defer response.Body.Close()
fmt.Printf("Upload completato con status: %s\n", response.Status)
return nil
}
func main() {
client := &http.Client{}
// Creiamo un file temporaneo per la dimostrazione
tmpFile, err := os.CreateTemp("", "upload-*.txt")
if err != nil {
log.Fatalf("impossibile creare il file temporaneo: %v", err)
}
defer os.Remove(tmpFile.Name())
tmpFile.WriteString("Contenuto di esempio per il caricamento.")
tmpFile.Close()
// In un caso reale useremmo un endpoint che accetta upload
err = uploadFile(client, "https://httpbin.org/post", tmpFile.Name())
if err != nil {
log.Fatalf("upload fallito: %v", err)
}
}
Leggere gli header della risposta
Gli header HTTP della risposta contengono informazioni preziose: rate limit, tipo di contenuto, caching, tracciamento. In Go si accede tramite la mappa response.Header, con metodi dedicati per i casi comuni.
package main
import (
"fmt"
"log"
"net/http"
)
func inspectResponseHeaders(response *http.Response) {
// Leggiamo il Content-Type con il metodo Get (case-insensitive)
contentType := response.Header.Get("Content-Type")
fmt.Printf("Content-Type: %s\n", contentType)
// Leggiamo tutti i valori per un header che può averne multipli
setCookies := response.Header["Set-Cookie"]
for _, cookie := range setCookies {
fmt.Printf("Set-Cookie: %s\n", cookie)
}
// Stampiamo tutti gli header ricevuti per scopi di debug
fmt.Println("\nTutti gli header:")
for name, values := range response.Header {
for _, value := range values {
fmt.Printf(" %s: %s\n", name, value)
}
}
}
func main() {
client := &http.Client{}
response, err := client.Get("https://jsonplaceholder.typicode.com/posts/1")
if err != nil {
log.Fatalf("errore nella richiesta: %v", err)
}
defer response.Body.Close()
fmt.Printf("Status: %s\n", response.Status)
fmt.Printf("Protocollo: %s\n", response.Proto)
inspectResponseHeaders(response)
}
Usare un context per la cancellazione
Il context.Context è il meccanismo idiomatico in Go per propagare la cancellazione attraverso la call stack. Usato con http.NewRequestWithContext permette di annullare una richiesta HTTP in corso quando, ad esempio, l'utente chiude la connessione o un timeout superiore scade.
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"time"
)
func fetchWithContext(ctx context.Context, client *http.Client, url string) ([]byte, error) {
// Leghiamo il context alla richiesta: se il context viene cancellato,
// la richiesta HTTP viene interrotta immediatamente
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("errore nella creazione della richiesta: %w", err)
}
response, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("errore nella richiesta: %w", err)
}
defer response.Body.Close()
return io.ReadAll(response.Body)
}
func main() {
client := &http.Client{}
// Creiamo un context con timeout di 2 secondi
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // Assicuriamoci di rilasciare le risorse del context
body, err := fetchWithContext(ctx, client, "https://jsonplaceholder.typicode.com/posts")
if err != nil {
// Distinguiamo tra timeout del context ed errori di rete
if ctx.Err() != nil {
log.Fatalf("richiesta annullata o scaduta: %v", ctx.Err())
}
log.Fatalf("errore di rete: %v", err)
}
fmt.Printf("Ricevuti %d byte\n", len(body))
}
Considerazioni finali
Il package net/http di Go offre una base solida e performante per costruire qualsiasi tipo di client HTTP. I principi fondamentali da tenere sempre presenti sono: chiudere sempre il body della risposta con defer, non usare mai http.DefaultClient in produzione senza configurare un timeout, condividere un singolo http.Client tra le goroutine per beneficiare del connection pooling, e usare context.Context per propagare la cancellazione in modo corretto.
Per scenari più complessi come OAuth 2.0, circuit breaker o metriche Prometheus, esistono librerie dedicate che si integrano perfettamente con il pattern http.RoundTripper illustrato in questo articolo, mantenendo piena compatibilità con la libreria standard.