Skrócenie URL-a w Go to jedno żądanie POST z treścią JSON. Zserializuj miejsce docelowe, wyślij je do endpointu linków skracacza za pomocą net/http, przekaż klucz API jako token Bearer i zdekoduj short_url z odpowiedzi. Biblioteka standardowa robi to wszystko, a jedyną zależnością wartą dodania później jest errgroup do pracy masowej.
Wyniki wyszukiwania dla tego tematu zdominowały projekty "zbuduj własny skracacz w Go", ale to inne ćwiczenie: tamte projekty przechowują linki, a ten artykuł wywołuje usługę, która już to robi. Jeśli interesuje Cię warstwa przechowywania, artykuł jak zbudować skracacz URL omawia decyzje projektowe. Jeśli chcesz mieć krótki link w ciągu pięciu minut, czytaj dalej.
To artykuł o Go w serii, która obejmuje też Python, JavaScript i PHP. Kształt endpointu, model uwierzytelniania i przyjęte tu limity darmowego planu opisano w przeglądzie bezpłatnego API skracacza URL.
Najszybszy sposób: jedno żądanie POST z net/http
Dwie struktury, jedno żądanie, jedno dekodowanie:
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
}
Uruchom go run main.go po wyeksportowaniu ELIDO_API_KEY, a otrzymasz krótki link. Paniki są dopuszczalne w programie mającym piętnaście wierszy, ale wszędzie indziej są błędem; w następnej sekcji zamienimy to na funkcję zwracającą błędy.
Warto zachować wiersz client := &http.Client{Timeout: ...}. http.DefaultClient nie ma limitu czasu, więc http.Post do nieodpowiadającego hosta blokuje tę goroutine, dopóki połączenie samo nie umrze, co przy kiepskiej sieci może potrwać kilka minut.
Nadaj każdemu żądaniu termin wykonania i współdziel jeden klient
Kształt produkcyjny: klient na poziomie pakietu, kontekst przy każdym żądaniu i błędy zamiast panik.
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
}
Trzy szczegóły zasługują na uwagę. Ponowne używanie jednego http.Client utrzymuje pulę połączeń między wywołaniami; tworzenie klienta dla każdego żądania wyrzuca wszystkie połączenia keep-alive. Opróżnienie treści przed zamknięciem faktycznie pozwala połączeniu wrócić do puli i zwykle właśnie dlatego zadanie masowe otwiera znacznie więcej gniazd, niż powinno. A context przy żądaniu oznacza, że wywołujący, który rezygnuje - anulowana aplikacja CLI albo żądanie użytkownika, który się rozłączył - zatrzymuje wywołanie HTTP zamiast pozostawiać je w toku.
Komentarz przy MaxIdleConnsPerHost jest uzasadniony. Wartość domyślna to 2, co jest niewidoczne w skrypcie i kosztowne, gdy osiem goroutine uderza w tego samego hosta.
Ponawiaj po 429 i 5xx bez tworzenia duplikatów
Istnieje tryb awarii, który zwykła pętla ponowień pogarsza. Żądanie POST dociera do API, link zostaje utworzony, a odpowiedź gubi się po drodze powrotnej. Kod widzi limit czasu, ponawia żądanie i teraz dwa krótkie linki wskazują jedno miejsce docelowe. Idempotency-Key zamyka tę lukę: ten sam klucz oznacza to samo logiczne żądanie, a API zwraca pierwotny link zamiast tworzyć kolejny.
Wyprowadź klucz z miejsca docelowego, a pozostanie stabilny między ponowieniami i kolejnymi uruchomieniami tej samej partii.
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)
}
Zwróć uwagę na select zamiast time.Sleep. Sen ignoruje anulowanie, więc zadanie, któremu kazano się zamknąć, nadal czeka cztery sekundy na każdy URL. Oczekiwanie jednocześnie na ctx.Done() i time.After oznacza, że wycofywanie jest przerywalne, co odróżnia czyste zamknięcie od SIGKILL. Szczegółowy artykuł o limitach szybkości i idempotentności omawia semantykę nagłówków oraz to, dlaczego bezmyślne ponowienia zamieniają jedną awarię w dwie.
Chcesz uruchomić to przeciwko działającemu endpointowi? Utwórz klucz w darmowym planie, wyeksportuj ELIDO_API_KEY, a każdy fragment tutaj skompiluje się i uruchomi dokładnie tak, jak został napisany.
Skracaj masowo za pomocą errgroup i SetLimit
Naiwna wersja współbieżna uruchamia jedną goroutine na każdy URL. Przy 5000 URL-i oznacza to 5000 równoczesnych żądań, a API odpowiada na większość z nich kodem 429. errgroup wraz z SetLimit daje rozgałęzienie z limitem:
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()
}
W Go 1.22 i nowszych zmienna pętli jest przypisywana osobno dla każdej iteracji, więc stary wiersz u := u wewnątrz pętli zniknął. Mutex nadal jest wymagany - mapa zapisywana z kilku goroutine bez niego zawiera wyścig danych, a go test -race to zgłosi.
Przed wdrożeniem musisz znać jeszcze jedno zachowanie: errgroup.WithContext anuluje współdzielony kontekst natychmiast, gdy którakolwiek goroutine zwróci błąd, więc pierwsza poważna awaria zatrzymuje resztę partii. Tego właśnie chcesz w kroku budowania, który musi być wykonany w całości albo wcale. W przypadku importera, który powinien skrócić wszystko, co się da, i zgłosić awarie na końcu, zbierz błędy w wycinku, zwracaj nil z każdej goroutine i pozostaw grupę wyłącznie jako ogranicznik współbieżności.
Klient typowany czy czysty net/http
Dla jednego endpointu powyższy kod wyczerpuje całą integrację, a zależność niczego nie wnosi. Rachunek zmienia się, gdy zaczynasz wyświetlać listę linków z paginacją, filtrować po tagu, odczytywać sumy kliknięć i obsługiwać pół tuzina kształtów odpowiedzi: wtedy wygenerowane modele i klient, który zna już reguły ponowień, oszczędzają więcej, niż kosztują. Strona API i SDK zawiera aktualną listę, a szybki start SDK pokazuje typowaną wersję tego samego wywołania.
Tak czy inaczej nawyki są identyczne: klucz ze środowiska, termin wykonania przy każdym żądaniu, sprawdzenie kodu stanu przed treścią, stabilny klucz idempotentności dla wszystkiego, co może uruchomić się dwa razy, oraz limit współbieżności. Gdy linki są już tworzone z usługi, a nie z laptopa, webhooki dla zdarzeń linków są lepsze od odpytywania do sprawdzania, co się z nimi stało, a to, co platforma udostępnia deweloperom obejmuje resztę powierzchni.
Przeczytaj serię główną
Ten artykuł należy do klastra engineering. Zacznij od przewodnika po bezpłatnym API skracacza URL, który omawia kształt endpointu i uwierzytelnianie, a następnie przeczytaj artykuł o limitach szybkości i idempotentności, poświęcony zachowaniu przy obciążeniu. Aktualną referencją są dokumenty API, a artykuł Skracacze URL dla deweloperów opisuje, co sprawdzić w API, zanim zaczniesz na nim budować.
Powiązane na blogu
- Jak skrócić URL w JavaScript za pomocą fetch i Node
- Jak skrócić URL w Pythonie za pomocą biblioteki requests
- Jak skrócić URL w PHP za pomocą curl, Guzzle lub WordPressa
- Limity szybkości i idempotentność API skracacza URL
- Jak skrócić URL w Ruby za pomocą Net::HTTP i Faraday
- Jak skrócić URL w Javie za pomocą wbudowanego HttpClient
Najczęściej zadawane pytania
Jak skrócić URL w Go?
Zserializuj długi URL do treści JSON, wyślij go metodą POST do endpointu linków skracacza za pomocą net/http, ustaw klucz API jako nagłówek Bearer i zdekoduj short_url z odpowiedzi. Biblioteka standardowa wystarcza do całości, więc cała funkcja ma około dwudziestu wierszy i nie wymaga zależności zewnętrznej.
Czy potrzebuję biblioteki do skracania URL-i w Go?
Nie. net/http i encoding/json wystarczą do pojedynczego żądania POST, a jedynym zewnętrznym pakietem wartym dodania jest golang.org/x/sync/errgroup do ograniczania współbieżności w zadaniach masowych. Wygenerowany SDK opłaca się, gdy używasz wielu endpointów i chcesz mieć typowane modele oraz paginację zamiast jednego wywołania.
Jak ustawić limit czasu żądania HTTP w Go?
Ustaw Timeout we własnym http.Client i twórz żądania za pomocą http.NewRequestWithContext oraz context.WithTimeout. http.DefaultClient w ogóle nie ma limitu czasu, więc zawieszony serwer zablokuje goroutine na zawsze. Limit czasu klienta obejmuje całą wymianę; kontekst pozwala też wywołującemu anulować ją wcześniej.
Jak skrócić wiele URL-i współbieżnie w Go?
Użyj errgroup z SetLimit, aby tylko stała liczba żądań była jednocześnie w toku, zamiast uruchamiać jedną goroutine na każdy URL i doprowadzać do ograniczania przez limit szybkości. Zabezpiecz mapę wyników mutexem i wysyłaj stabilny Idempotency-Key dla każdego URL-a, aby ponowienia nigdy nie tworzyły duplikatów linków.
Dlaczego mój klient HTTP w Go nie ponownie wykorzystuje połączeń?
Zwykle dlatego, że treść odpowiedzi nie jest w pełni odczytywana przed jej zamknięciem, więc połączenie nie może wrócić do puli bezczynnej. Opróżnij ją za pomocą io.Copy(io.Discard, resp.Body) przed Close. Inną częstą przyczyną jest domyślna wartość transportu wynosząca dwa bezczynne połączenia na hosta, co jest małą liczbą dla współbieżnego zadania masowego.
Dlaczego moje żądanie Go do skracacza URL zwraca 401?
Brakuje nagłówka Authorization, jest on zapisany z błędem albo zawiera pustą zmienną środowiskową. Wyświetl os.Getenv przed żądaniem, aby potwierdzić, że klucz został wczytany, i sprawdź, czy nagłówek ma postać 'Bearer ' ze spacją na końcu przed kluczem. Kod 403 oznacza natomiast, że klucz jest prawidłowy, ale nie ma zakresu wymaganego przez ten endpoint.
Wypróbuj Elido
Wklej URL, otrzymaj krótki link
Bez rejestracji. Link działa 30 dni. Zarejestruj się, aby zachować go na zawsze.
Za darmo, bez rejestracji · 2 dziennie