Accorciare un URL in Go significa fare una sola richiesta POST con un corpo JSON. Serializza la destinazione, inviala all'endpoint links dell'URL shortener con net/http, passa la tua chiave API come token Bearer e decodifica short_url dalla risposta. La libreria standard fa tutto, e l'unica dipendenza che vale la pena aggiungere in seguito è errgroup per i lavori bulk.
I risultati di ricerca su questo argomento sono dominati dai progetti "costruisci il tuo URL shortener in Go", che sono un esercizio diverso: quelli archiviano i link, questo articolo chiama un servizio che lo fa già. Se ti interessa la parte di archiviazione, come costruire un URL shortener illustra le decisioni di progettazione. Se vuoi uno short link nei prossimi cinque minuti, continua a leggere.
Questo è l'articolo su Go di una serie che tratta anche Python, JavaScript e PHP. La struttura dell'endpoint, il modello di autenticazione e i limiti del piano gratuito qui utilizzati sono documentati nella panoramica dell'API gratuita per URL shortener.
Il modo più rapido: una richiesta POST con net/http
Due struct, una richiesta, una decodifica:
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
type linkRequest struct {
DestinationURL string `json:"destination_url"`
}
type linkResponse struct {
ID string `json:"id"`
ShortURL string `json:"short_url"`
}
func main() {
body, err := json.Marshal(linkRequest{
DestinationURL: "https://example.com/spring-sale?utm_source=newsletter",
})
if err != nil {
panic(err)
}
req, err := http.NewRequest(http.MethodPost, "https://api.elido.app/v1/links", bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("ELIDO_API_KEY"))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
panic("shorten failed: " + resp.Status)
}
var link linkResponse
if err := json.NewDecoder(resp.Body).Decode(&link); err != nil {
panic(err)
}
fmt.Println(link.ShortURL) // https://s.elido.me/ab12cd
}
Esegui go run main.go con ELIDO_API_KEY esportata e avrai uno short link. I panic vanno bene in un programma di quindici righe e sono sbagliati in qualsiasi altro contesto; la sezione successiva trasforma tutto in una funzione che restituisce errori.
La riga client := &http.Client{Timeout: ...} è quella da conservare. http.DefaultClient non ha alcun timeout, quindi http.Post verso un host che non risponde blocca quella goroutine finché la connessione non muore da sola, cosa che su una rete problematica può richiedere minuti.
Dai una scadenza a ogni richiesta e condividi un client
La forma adatta alla produzione: un client a livello di package, un contesto su ogni richiesta ed errori invece di panic.
var client = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 64,
MaxIdleConnsPerHost: 16, // default is 2, too low for a concurrent batch
IdleConnTimeout: 90 * time.Second,
},
}
const endpoint = "https://api.elido.app/v1/links"
func shorten(ctx context.Context, destination string) (string, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
body, err := json.Marshal(linkRequest{DestinationURL: destination})
if err != nil {
return "", fmt.Errorf("marshal: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return "", fmt.Errorf("new request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("ELIDO_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("post %s: %w", endpoint, err)
}
defer func() {
io.Copy(io.Discard, resp.Body) // drain so the connection can be reused
resp.Body.Close()
}()
if resp.StatusCode >= 400 {
return "", fmt.Errorf("shorten: %s", resp.Status)
}
var link linkResponse
if err := json.NewDecoder(resp.Body).Decode(&link); err != nil {
return "", fmt.Errorf("decode: %w", err)
}
return link.ShortURL, nil
}
Tre dettagli meritano di esserci. Riutilizzare un solo http.Client mantiene attivo il pool di connessioni tra una chiamata e l'altra; creare un client per richiesta elimina ogni keep-alive. Svuotare il corpo prima di chiuderlo è ciò che permette davvero alla connessione di tornare nel pool, ed è il motivo più comune per cui un job batch apre molti più socket del necessario. Inoltre context sulla richiesta fa sì che chi chiama, quando rinuncia - una CLI annullata o una richiesta il cui utente si è disconnesso -, interrompa la chiamata HTTP invece di lasciarla in esecuzione.
MaxIdleConnsPerHost merita il suo commento. Il valore predefinito è 2, cosa invisibile in uno script ma costosa quando otto goroutine chiamano lo stesso host.
Fai retry su 429 e 5xx senza creare duplicati
C'è un errore che un semplice ciclo di retry peggiora. La richiesta POST raggiunge l'API, il link viene creato e la risposta si perde durante il ritorno. Il tuo codice vede un timeout, riprova e ora due short link puntano alla stessa destinazione. Un Idempotency-Key risolve il problema: stessa chiave, stessa richiesta logica, e l'API restituisce il link originale invece di crearne un altro.
Deriva la chiave dalla destinazione e resterà stabile tra i retry e tra le nuove esecuzioni dello stesso batch.
var (
errRateLimited = errors.New("rate limited")
errServer = errors.New("server error")
)
func shortenWithRetry(ctx context.Context, destination string, attempts int) (string, error) {
key := fmt.Sprintf("%x", sha256.Sum256([]byte(destination)))
var lastErr error
for attempt := 0; attempt < attempts; attempt++ {
short, retryAfter, err := postLink(ctx, destination, key)
if err == nil {
return short, nil
}
var wait time.Duration
switch {
case errors.Is(err, errRateLimited):
wait = retryAfter // from the Retry-After header
case errors.Is(err, errServer):
wait = time.Duration(1<<attempt) * time.Second // 1s, 2s, 4s
default:
return "", err // 401, 403, 422: retrying will not help
}
lastErr = err
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(wait):
}
}
return "", fmt.Errorf("shorten %q: %w", destination, lastErr)
}
Nota il select al posto di time.Sleep. Dormire ignora l'annullamento, quindi un job a cui è stato detto di arrestarsi resta comunque fermo quattro secondi per URL. Attendere contemporaneamente ctx.Done() e time.After rende interrompibile il backoff, ed è la differenza tra un arresto ordinato e un SIGKILL. L'approfondimento su limiti di velocità e idempotenza tratta la semantica degli header e spiega perché i retry ciechi trasformano un'interruzione in due.
Vuoi eseguire questo codice contro un endpoint reale? Crea una chiave sul piano gratuito, esporta ELIDO_API_KEY e ogni snippet qui compila ed esegue il codice così com'è.
Accorcia in bulk con errgroup e SetLimit
La versione concorrente ingenua avvia una goroutine per URL. Con 5.000 URL sono 5.000 richieste simultanee, e l'API risponde alla maggior parte con 429. errgroup insieme a SetLimit offre un fan-out con un limite massimo:
func shortenAll(ctx context.Context, urls []string) (map[string]string, error) {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(8) // at most 8 requests in flight, whatever len(urls) is
var mu sync.Mutex
out := make(map[string]string, len(urls))
for _, u := range urls {
g.Go(func() error {
short, err := shortenWithRetry(ctx, u, 3)
if err != nil {
return fmt.Errorf("%s: %w", u, err)
}
mu.Lock()
defer mu.Unlock()
out[u] = short
return nil
})
}
return out, g.Wait()
}
In Go 1.22 e versioni successive la variabile del ciclo è locale a ogni iterazione, quindi la vecchia riga u := u dentro il ciclo non serve più. Il mutex è comunque necessario: una mappa scritta da più goroutine senza mutex è un data race, e go test -race lo segnalerà.
Un comportamento da conoscere prima del rilascio: errgroup.WithContext annulla il contesto condiviso non appena una goroutine restituisce un errore, quindi il primo errore grave interrompe il resto del batch. È ciò che vuoi per un passaggio di build che deve riuscire interamente o fallire interamente. Per un importer che deve accorciare tutto ciò che può e riportare gli errori alla fine, raccogli gli errori in uno slice, restituisci nil da ogni goroutine e mantieni il gruppo esclusivamente come limitatore della concorrenza.
Client tipizzato o net/http puro
Per un solo endpoint, il codice qui sopra è l'intera integrazione e una dipendenza non offre alcun vantaggio. Il calcolo cambia quando inizi a elencare link con paginazione, filtrare per tag, leggere i totali dei clic e gestire una mezza dozzina di forme di risposta: è allora che i modelli generati e un client che conosce già le regole di retry fanno risparmiare più di quanto costino. La pagina API e SDK contiene l'elenco aggiornato, mentre il quickstart dell'SDK mostra la versione tipizzata di questa stessa chiamata.
In entrambi i casi le abitudini sono identiche: la chiave dall'ambiente, una scadenza su ogni richiesta, il codice di stato verificato prima del corpo, una chiave di idempotenza stabile su tutto ciò che può essere eseguito due volte e un limite alla concorrenza. Quando i link vengono creati da un servizio invece che da un laptop, i webhook per gli eventi dei link sono migliori del polling per scoprire cosa è successo, e ciò che la piattaforma espone agli sviluppatori copre il resto della superficie.
Leggi la serie fondamentale
Questo articolo appartiene al cluster engineering. Inizia dalla guida all'API gratuita per URL shortener per la struttura dell'endpoint e l'autenticazione, poi passa all'articolo su limiti di velocità e idempotenza per il comportamento sotto carico. Il riferimento aggiornato è la documentazione API, mentre URL shortener per sviluppatori spiega cosa verificare in un'API prima di basarci il tuo progetto.
Correlati nel blog
- Come accorciare un URL in JavaScript con fetch e Node
- Come accorciare un URL in Python con la libreria requests
- Come accorciare un URL in PHP con curl, Guzzle o WordPress
- Limiti di velocità e idempotenza dell'API per URL shortener
- Come accorciare un URL in Ruby con Net::HTTP e Faraday
- Come accorciare un URL in Java con l'HttpClient integrato
Domande frequenti
Come accorcio un URL in Go?
Serializza l'URL lungo in un corpo JSON, invialo con una richiesta POST all'endpoint links di un URL shortener usando net/http, imposta la tua chiave API come header Bearer e decodifica short_url dalla risposta. La libreria standard copre tutto, quindi l'intera funzione è di circa venti righe e non richiede dipendenze di terze parti.
Mi serve una libreria per accorciare gli URL in Go?
No. net/http ed encoding/json bastano per una singola richiesta POST, e l'unico pacchetto esterno che vale la pena aggiungere è golang.org/x/sync/errgroup per la concorrenza limitata nei job bulk. Un SDK generato conviene quando usi molti endpoint e vuoi modelli tipizzati e paginazione invece di una sola chiamata.
Come imposto un timeout su una richiesta HTTP in Go?
Imposta Timeout sul tuo http.Client e costruisci le richieste con http.NewRequestWithContext insieme a context.WithTimeout. http.DefaultClient non ha alcun timeout, quindi un server bloccato terrà una goroutine occupata per sempre. Il timeout del client copre l'intero scambio; il contesto permette anche a chi chiama di annullare prima.
Come accorcio molti URL in parallelo in Go?
Usa errgroup con SetLimit in modo che solo un numero fisso di richieste sia in corso, invece di avviare una goroutine per URL e incorrere nel rate limiting. Proteggi la mappa dei risultati con un mutex e invia un Idempotency-Key stabile per URL, così i retry non creano mai link duplicati.
Perché il mio client HTTP Go non riutilizza le connessioni?
Di solito perché il corpo della risposta non viene letto completamente prima di essere chiuso, quindi la connessione non può tornare nel pool inattivo. Svuotalo con io.Copy(io.Discard, resp.Body) prima di Close. L'altra causa comune è il valore predefinito del transport di due connessioni inattive per host, basso per un job batch concorrente.
Perché la mia richiesta Go a un URL shortener restituisce 401?
L'header Authorization manca, è scritto in modo errato oppure contiene una variabile d'ambiente vuota. Stampa os.Getenv prima della richiesta per verificare che la chiave sia stata caricata e controlla che l'header contenga 'Bearer ' con lo spazio finale prima della chiave. Un 403 invece significa che la chiave è valida ma non dispone dello scope per quell'endpoint.
Prova Elido
Incolla un URL, ottieni un link breve
Senza registrazione. Il link vive 30 giorni. Iscriviti per conservarlo.
Gratis, nessuna registrazione richiesta · 2 al giorno