Acortar una URL en Go consiste en un POST con un cuerpo JSON. Serializa el destino, envíalo al endpoint de enlaces del acortador con net/http, pasa tu clave de API como token Bearer y decodifica short_url de la respuesta. La biblioteca estándar se encarga de todo, y la única dependencia que merece la pena añadir después es errgroup para trabajos masivos.
Los resultados de búsqueda están dominados por proyectos de "construye tu propio acortador en Go", que son otro ejercicio: esos proyectos almacenan enlaces; este llama a un servicio que ya lo hace. Si quieres la parte de almacenamiento, cómo construir un acortador de URL cubre las decisiones de diseño. Si quieres un enlace corto en los próximos cinco minutos, sigue leyendo.
Esta es la entrega sobre Go de una serie que también cubre Python, JavaScript y PHP. La forma del endpoint, el modelo de autenticación y los límites del plan gratuito asumidos aquí están documentados en la descripción general de la API gratuita de acortamiento de URL.
La forma más rápida: un POST con net/http
Dos estructuras, una solicitud, una decodificación:
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
}
Ejecuta go run main.go con ELIDO_API_KEY exportada y tendrás un enlace corto. Los panic están bien en un programa de quince líneas y mal en cualquier otro contexto; la siguiente sección convierte esto en una función que devuelve errores.
La línea client := &http.Client{Timeout: ...} es la que debes conservar. http.DefaultClient no tiene tiempo de espera, así que http.Post contra un host que no responde bloquea ese goroutine hasta que la conexión muere por sí sola, lo que en una red deficiente puede tardar minutos.
Da un plazo a cada solicitud y comparte un cliente
La forma de producción: un cliente a nivel de paquete, un contexto en cada solicitud y errores en lugar de 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
}
Tres detalles justifican su lugar. Reutilizar un único http.Client mantiene vivo el grupo de conexiones entre llamadas; construir un cliente por solicitud desecha cada conexión persistente. Vaciar el cuerpo antes de cerrarlo es lo que realmente permite que la conexión vuelva al grupo, y es la razón habitual por la que un trabajo por lotes abre muchos más sockets de los necesarios. Además, context en la solicitud significa que quien llama y se da por vencido - una CLI cancelada o un usuario que se ha desconectado - detiene la llamada HTTP en lugar de dejarla en ejecución.
MaxIdleConnsPerHost merece ese comentario. El valor predeterminado es 2, algo invisible en un script y costoso cuando ocho goroutines acceden al mismo host.
Reintenta ante 429 y 5xx sin crear duplicados
Hay un modo de fallo que un bucle de reintento sencillo empeora. El POST llega a la API, se crea el enlace y la respuesta se pierde de camino de vuelta. Tu código ve un tiempo de espera agotado, reintenta y ahora hay dos enlaces cortos que apuntan al mismo destino. Una Idempotency-Key lo evita: misma clave, misma solicitud lógica, y la API devuelve el enlace original en lugar de crear otro.
Deriva la clave del destino y se mantendrá estable entre reintentos y ejecuciones repetidas del mismo lote.
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)
}
Observa el select en lugar de time.Sleep. Dormir ignora la cancelación, así que un trabajo al que se le ha ordenado apagarse sigue detenido durante cuatro segundos por URL. Esperar a ctx.Done() y time.After a la vez hace que la espera creciente se pueda interrumpir, que es la diferencia entre un apagado limpio y un SIGKILL. El análisis detallado de los límites de frecuencia y la idempotencia explica la semántica de los encabezados y por qué los reintentos ciegos convierten una interrupción en dos.
¿Quieres ejecutar esto contra un endpoint activo? Crea una clave en el plan gratuito, exporta ELIDO_API_KEY y cada fragmento de esta guía compilará y se ejecutará tal cual.
Acorta en masa con errgroup y SetLimit
La versión concurrente ingenua lanza un goroutine por URL. Con 5.000 URL son 5.000 solicitudes simultáneas, y la API responde a la mayoría con 429. errgroup junto con SetLimit te da un fan-out con un límite:
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()
}
En Go 1.22 y versiones posteriores, la variable del bucle es propia de cada iteración, así que ha desaparecido la antigua línea u := u dentro del bucle. El mutex sigue siendo necesario: un mapa al que escriben varios goroutines sin uno presenta una condición de carrera de datos, y go test -race lo indicará.
Hay un comportamiento que debes conocer antes de ponerlo en producción: errgroup.WithContext cancela el contexto compartido en cuanto cualquier goroutine devuelve un error, así que el primer fallo grave detiene el resto del lote. Eso es lo que quieres para un paso de compilación que debe ser todo o nada. Para un importador que deba acortar todo lo posible e informar de los fallos al final, recopila los errores en un slice, devuelve nil desde cada goroutine y conserva el grupo únicamente como limitador de concurrencia.
Cliente tipado o net/http sin intermediarios
Para un endpoint, el código anterior es toda la integración y una dependencia no aporta nada. El cálculo cambia cuando empiezas a listar enlaces con paginación, filtrar por etiqueta, leer totales de clics y gestionar media docena de formas de respuesta: ahí los modelos generados y un cliente que ya conoce las reglas de reintento ahorran más de lo que cuestan. La página de la API y los SDK contiene la lista actual, y el inicio rápido de los SDK muestra la versión tipada de esta misma llamada.
En cualquier caso, los hábitos son idénticos: obtener la clave del entorno, poner un plazo en cada solicitud, comprobar el código de estado antes del cuerpo, usar una clave de idempotencia estable en todo lo que pueda ejecutarse dos veces y limitar la concurrencia. Cuando los enlaces se crean desde un servicio y no desde un portátil, los webhooks para eventos de enlaces son mejores que consultar periódicamente para saber qué les ha ocurrido, y lo que la plataforma ofrece a los desarrolladores cubre el resto de la superficie.
Lee la serie principal
Este artículo pertenece al clúster de ingeniería. Empieza con la guía de la API gratuita de acortamiento de URL para conocer la forma del endpoint y la autenticación, y sigue con el artículo sobre límites de frecuencia e idempotencia para el comportamiento bajo carga. La referencia activa son los documentos de la API, y acortadores de URL para desarrolladores explica qué comprobar en una API antes de basarte en ella.
Relacionado en el blog
- Cómo acortar una URL en JavaScript con fetch y Node
- Cómo acortar una URL en Python con la biblioteca requests
- Cómo acortar una URL en PHP con curl, Guzzle o WordPress
- Límites de frecuencia e idempotencia de la API del acortador de URL
- Cómo acortar una URL en Ruby con Net::HTTP y Faraday
- Cómo acortar una URL en Java con el HttpClient integrado
Preguntas frecuentes
¿Cómo acorto una URL en Go?
Serializa la URL larga en un cuerpo JSON, envíala mediante POST al endpoint de enlaces de un acortador con net/http, establece tu clave de API como encabezado Bearer y decodifica short_url de la respuesta. La biblioteca estándar cubre todo, así que la función completa ocupa unas veinte líneas y no necesita dependencias de terceros.
¿Necesito una biblioteca para acortar URL en Go?
No. net/http y encoding/json bastan para un POST individual, y el único paquete externo que merece la pena añadir es golang.org/x/sync/errgroup para la concurrencia acotada en trabajos masivos. Un SDK generado compensa cuando usas muchos endpoints y quieres modelos tipados y paginación en lugar de una sola llamada.
¿Cómo establezco un tiempo de espera en una solicitud HTTP de Go?
Establece Timeout en tu propio http.Client y construye las solicitudes con http.NewRequestWithContext y context.WithTimeout. http.DefaultClient no tiene ningún tiempo de espera, así que un servidor bloqueado puede mantener un goroutine bloqueado para siempre. El tiempo de espera del cliente cubre todo el intercambio; el contexto también permite que quien llama cancele antes.
¿Cómo acorto muchas URL de forma concurrente en Go?
Usa errgroup con SetLimit para que solo haya un número fijo de solicitudes en vuelo, en lugar de lanzar un goroutine por URL y recibir límites de frecuencia. Protege el mapa de resultados con un mutex y envía una Idempotency-Key estable por URL para que los reintentos nunca creen enlaces duplicados.
¿Por qué mi cliente HTTP de Go no reutiliza las conexiones?
Normalmente porque el cuerpo de la respuesta nunca se lee por completo antes de cerrarlo, así que la conexión no puede volver al grupo de conexiones inactivas. Vacíalo con io.Copy(io.Discard, resp.Body) antes de Close. La otra causa habitual es el valor predeterminado del transporte de dos conexiones inactivas por host, que es bajo para un trabajo por lotes concurrente.
¿Por qué mi solicitud de Go a un acortador de URL devuelve 401?
Falta el encabezado Authorization, está mal escrito o contiene una variable de entorno vacía. Imprime os.Getenv antes de la solicitud para confirmar que la clave se ha cargado y comprueba que el encabezado lea 'Bearer ' con el espacio final antes de la clave. Un 403 significa que la clave es válida, pero no tiene el alcance necesario para ese endpoint.
Prueba Elido
Pega una URL, obtén un enlace corto
Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.
Gratis, sin registro · 2 por día