Wissen

Residential Proxies in Go mit net/http und Colly verwenden

Proxies in Go: Transport-Setup, die Body-Drain- und MaxIdleConnsPerHost-Fallstricke, die Verbindungs-Wiederverwendung zerstören, Geo-Rotation pro Anfrage, und Collys SetProxyFunc.

Chris Collins

Chris Collins

24. Juli 2026 · 8 Min. Lesezeit

Go passt natürlich zum Scraping: billige Nebenläufigkeit, eine solide Standardbibliothek, und ein einzelnes statisches Binary zum Deployen. Einen Residential Proxy in net/http einzubinden, sind wirklich drei Zeilen. Was Leute einen Tag kostet, ist alles nach diesen drei Zeilen, denn Gos HTTP-Client hat einige scharfe Kanten, die die Verbindungs-Wiederverwendung still zerstören, und über einen Proxy wird daraus Latenz und Timeouts, die Sie kaum erklären können.

Dies ist der Go-Beitrag derselben Serie wie Residential Proxies mit Python und mit Playwright: der funktionierende Code, plus die spezifischen Fallen, die in Go zählen.

Alles unten nutzt das Shifter Residential-Gateway: ein Endpunkt, p.shifter.io:443, mit dem gesamten Targeting im Benutzernamen kodiert. Tauschen Sie Host und Zugangsdaten für einen anderen Anbieter; die Form bleibt gleich.

Das Gateway-Modell in einem Absatz

Der Proxy-Benutzername trägt Ihre Authentifizierung und Ihr Targeting. Sie wechseln nicht den Endpunkt, um Land oder Session zu ändern, Sie ändern die Benutzernamen-Zeichenfolge:

customer-USERNAME-country-us-sid-abc123-ttl-600

country-us zielt auf die USA, sid fixiert eine Sticky Session, ttl hält diese IP für N Sekunden. Lassen Sie sid/ttl weg, und jede neue Verbindung rotiert. Das Passwort ist konstant.

Anders als Chromium (das inline Proxy-Zugangsdaten ignoriert), verarbeitet Gos net/http Zugangsdaten in der Proxy-URL korrekt und sendet Proxy-Authorization für Sie. Also funktioniert http://user:pass@host:port einfach.

Die minimale Version

package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
user, pass := os.Getenv("SHIFTER_USER"), os.Getenv("SHIFTER_PASS")
proxyURL, err := url.Parse(fmt.Sprintf("http://%s-country-us:%s@p.shifter.io:443", user, pass))
if err != nil {
panic(err)
}
client := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
Timeout: 30 * time.Second,
}
resp, err := client.Get("https://api.ipify.org")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body)) // eine US-Residential-IP
}

Das ist die gesamte Integration. Jetzt die Teile, die über die Performance entscheiden.

Fallstrick 1: den Client wiederverwenden, nie einen pro Anfrage erstellen

Das ist der häufigste und teuerste Go-Fehler, und ein Proxy verschlimmert ihn.

http.Client und http.Transport sind für lange Lebensdauer und gemeinsame Nutzung ausgelegt. Sie sind für die nebenläufige Nutzung durch mehrere Goroutinen sicher, und der Verbindungspool lebt im Transport. Erstellen Sie einen neuen Client pro Anfrage, und jede Anfrage zahlt einen frischen TCP-Handshake plus einen TLS-Handshake durch den Proxy, genau der Overhead, den der Latenz-Leitfaden zu eliminieren empfiehlt. Schlimmer noch, Sie leaken Idle-Verbindungen.

// SCHLECHT: ein neuer Client pro Anfrage. Null Wiederverwendung, leakt Verbindungen.
func fetch(u string) (*http.Response, error) {
client := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}}
return client.Get(u)
}
// GUT: ein Client, paketweit oder injiziert, über Goroutinen geteilt.
var client = &http.Client{ /* einmal konfiguriert, siehe unten */ }

Bauen Sie ihn einmal beim Start und reichen Sie ihn herum.

Fallstrick 2: den Body leeren und schließen, sonst keine Wiederverwendung

Go gibt eine Verbindung nur dann an den Pool zurück, wenn der Response-Body vollständig gelesen und geschlossen wurde. Schließen Sie ihn ohne zu lesen, wird die Verbindung verworfen, und Sie verlieren still das Pooling, obwohl Ihr Client geteilt ist.

resp, err := client.Get(u)
if err != nil {
return err
}
defer resp.Body.Close()
// Brauchen Sie den Body nicht, leeren Sie ihn trotzdem, damit die Verbindung
// wiederverwendet werden kann. Deferred-Aufrufe laufen last-in-first-out, dieses
// Leeren läuft also *vor* dem Close oben, was die gewünschte Reihenfolge ist.
defer io.Copy(io.Discard, resp.Body)

Wenn Sie den Body normal parsen (io.ReadAll, ein JSON-Decoder, der bis EOF liest), sind Sie fein raus. Die Falle sind frühe Returns: bei einem Nicht-200-Status auszusteigen, ohne zu leeren, macht die Verbindung unbrauchbar, und bei einem Scraper, der viele Blocks trifft, ist das der Großteil Ihres Traffics.

Fallstrick 3: MaxIdleConnsPerHost ist standardmäßig 2

Dieser beißt jeden Go-Scraper. Der Standardwert von MaxIdleConnsPerHost bei http.Transport ist 2. Fahren Sie 50 Goroutinen gegen einen Host, und 48 davon öffnen und verwerfen ständig frische Verbindungen, jede mit einem vollen Handshake durch den Proxy.

Setzen Sie ihn mindestens auf Ihre Nebenläufigkeit pro Host:

transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
MaxIdleConns: 200,
MaxIdleConnsPerHost: 50, // >= Ihre Nebenläufigkeit pro Host
IdleConnTimeout: 90 * time.Second,
// Granulare Timeouts schlagen ein stumpfes Client.Timeout.
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := &http.Client{Transport: transport}

Beachten Sie: hier steht absichtlich kein Client.Timeout. Client.Timeout ist ein einziges Gesamtbudget für Dial, TLS, Header und Body. Die granularen Transport-Timeouts lassen Sie einen langsamen Connect von einer langsamen Antwort unterscheiden, genau die Connect-versus-Read-Unterscheidung, die Timeouts diagnostizierbar macht. Nutzen Sie context.WithTimeout pro Anfrage für eine Gesamtfrist.

Geo und Sessions rotieren

Weil das Targeting im Benutzernamen lebt, bedeutet eine andere Identität eine andere Proxy-URL. Zwei Ansätze:

Pro Anfrage, über die Proxy-Funktion. Transport.Proxy wird pro Anfrage aufgerufen, Sie können die Identität also dort variieren. Go poolt Verbindungen pro Proxy-URL, das bleibt also effizient:

func proxyFor(country, sid string) (*url.URL, error) {
u := fmt.Sprintf("%s-country-%s", os.Getenv("SHIFTER_USER"), country)
if sid != "" {
u += fmt.Sprintf("-sid-%s-ttl-600", sid)
}
return url.Parse(fmt.Sprintf("http://%s:%s@p.shifter.io:443", u, os.Getenv("SHIFTER_PASS")))
}
// Targeting aus dem Request-Kontext lesen, damit jeder Job seine Identität wählt.
type ctxKey string
const identityKey ctxKey = "identity"
type identity struct{ Country, SID string }
transport := &http.Transport{
Proxy: func(r *http.Request) (*url.URL, error) {
if id, ok := r.Context().Value(identityKey).(identity); ok {
return proxyFor(id.Country, id.SID)
}
return proxyFor("us", "")
},
MaxIdleConnsPerHost: 50,
}

Dann hängen Sie pro Job eine Identität an:

ctx := context.WithValue(req.Context(), identityKey, identity{Country: "de", SID: "job-42"})
resp, err := client.Do(req.WithContext(ctx))

Oder ein Transport pro Identität, in einer Map gecacht, wenn Sie eine kleine feste Menge haben (einer pro Land, etwa). Beides geht; der Kontext-Ansatz skaliert besser, wenn die Identität pro Arbeitseinheit gilt.

Geben Sie jeder logischen Arbeitseinheit ihre eigene sid und rotieren Sie zwischen Einheiten statt mitten im Ablauf (Sticky vs Rotierend behandelt, wann was gilt, und der Lastverteilungs-Beitrag behandelt die Abbildung von Arbeit auf Identitäten).

Nebenläufigkeit, pro Host

Go macht es trivial, 10.000 Goroutinen zu starten, und ebenso trivial, blockiert zu werden. Begrenzen Sie die Nebenläufigkeit pro Zielhost, nicht global, damit ein fragiles Ziel nicht bombardiert und ein permissives nicht gedrosselt wird:

import "golang.org/x/sync/semaphore"
var limits = map[string]*semaphore.Weighted{
"tough-site.example": semaphore.NewWeighted(4),
"open-site.example": semaphore.NewWeighted(32),
}
func fetch(ctx context.Context, host, u string) error {
sem := limits[host]
if err := sem.Acquire(ctx, 1); err != nil {
return err
}
defer sem.Release(1)
// ... die Anfrage ausführen
return nil
}

Jenseits der Toleranz eines Ziels kauft mehr Parallelität Blocks, keinen Durchsatz (wie man Blocks vermeidet).

Colly

Colly ist das Standard-Scraping-Framework in Go, und es nimmt einen Proxy in einer Zeile:

c := colly.NewCollector(colly.AllowedDomains("example.com"))
proxyURL := fmt.Sprintf("http://%s-country-us:%s@p.shifter.io:443", user, pass)
if err := c.SetProxy(proxyURL); err != nil {
log.Fatal(err)
}

Für Geo oder Sticky Sessions pro Anfrage nutzen Sie SetProxyFunc, dieselbe Idee wie die Transport.Proxy-Funktion oben:

c.SetProxyFunc(func(r *http.Request) (*url.URL, error) {
// Land/Session pro Anfrage wählen, z. B. aus der URL oder einer Job-Map.
return proxyFor("de", "job-42")
})

Colly hat einen eigenen Limiter pro Domain, nutzen Sie ihn, statt selbst einen zu bauen:

c.Limit(&colly.LimitRule{
DomainGlob: "*",
Parallelism: 8,
Delay: 200 * time.Millisecond,
RandomDelay: 200 * time.Millisecond, // Jitter, damit Anfragen kein Metronom sind
})

Und setzen Sie einen eigenen Transport, wenn Sie die Pooling- und Timeout-Einstellungen von oben wollen:

c.WithTransport(transport)

Collys OnError ist der Ort für Retry-Logik. Klassifizieren Sie vor dem Retry und wechseln Sie bei Blocks die Identität, statt denselben Exit erneut zu versuchen, die Klassifizierungstabelle im Lastverteilungs-Beitrag gilt direkt.

Verifizieren, dass Sie tatsächlich auf dem Proxy sind

Bevor Sie irgendetwas anderes benchmarken oder debuggen, bestätigen Sie die Exit-IP:

resp, _ := client.Get("http://ip-api.com/json")
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
fmt.Println(string(b)) // erwarte eine Residential-IP im Zielland

Ihre eigene IP heißt, der Client nutzt den Proxy nicht. Ein Hänger heißt, der lokale Egress ist blockiert. Beides behandelt der Timeout-Diagnoseleitfaden.

FAQ

Unterstützt Go Proxy-Zugangsdaten in der URL? Ja. Anders als Chromium-basierte Browser verarbeitet Gos net/http http://user:pass@host:port korrekt und sendet Proxy-Authorization für Sie. Da das Gateway das Targeting im Benutzernamen kodiert, ist das alles, was Sie brauchen.

Warum ist mein Go-Scraper über einen Proxy langsam, selbst mit einem Client? Höchstwahrscheinlich leeren Sie Response-Bodies nicht (Verbindungen kehren also nie in den Pool zurück), oder MaxIdleConnsPerHost steht noch auf dem Standard 2, während Sie viele Goroutinen fahren. Beheben Sie beides, und der Handshake-Overhead pro Anfrage verschwindet weitgehend.

Wie rotiere ich IPs pro Anfrage in Go? Variieren Sie den Proxy-Benutzernamen. Geben Sie entweder eine andere URL aus Transport.Proxy zurück (die Identität aus dem Request-Kontext gelesen) oder halten Sie einen Transport pro Identität. Go poolt Verbindungen pro Proxy-URL, Variation pro Anfrage bleibt also effizient.

Sollte ich Client.Timeout oder Transport-Timeouts nutzen? Beides, für verschiedene Aufgaben. Granulare Transport-Timeouts (TLSHandshakeTimeout, ResponseHeaderTimeout) lassen Sie einen langsamen Connect von einer langsamen Antwort unterscheiden; nutzen Sie context.WithTimeout pro Anfrage für die Gesamtfrist. Ein einzelnes stumpfes Client.Timeout verbirgt, welche Phase scheiterte.

Kann ich stattdessen http.ProxyFromEnvironment nutzen? Ja, es liest HTTP_PROXY/HTTPS_PROXY/NO_PROXY, was in Containern praktisch ist. Es ist ein guter Standard, wenn jede Anfrage dieselbe Identität nutzt, aber so können Sie Geo oder Session nicht pro Anfrage variieren, nutzen Sie also eine explizite Proxy-Funktion, wenn Sie Targeting brauchen.

Fazit

Go plus Residential Proxies ist eine starke Kombination, sobald Sie die Regeln der Standardbibliothek respektieren: teilen Sie einen langlebigen Client, leeren und schließen Sie Bodies immer, heben Sie MaxIdleConnsPerHost auf Ihre Nebenläufigkeit an, und bevorzugen Sie granulare Transport-Timeouts gegenüber einem stumpfen Gesamtwert. Variieren Sie den Proxy-Benutzernamen für Geo oder Session, begrenzen Sie die Nebenläufigkeit pro Host, und lassen Sie Collys Limiter und SetProxyFunc dieselbe Arbeit auf Framework-Ebene tun.

Machen Sie das richtig, und die Drei-Zeilen-Integration performt so, wie Go es sollte. Richten Sie sie auf das Residential-Gateway, und denken Sie daran, dass die Poolqualität bestimmt, wie oft Sie überhaupt retryen (IP-Reputation). Die Preisseite hat die Pro-GB-Tarife, um es gegen Ihre eigenen Ziele zu testen.

Bereit, loszulegen?

Testen Sie Shifters Residential-Proxys, 205M+ IPs, 195+ Länder, ab $0.75/GB.

Jetzt starten