Ihr Go-Dienst muss seine Anfragen über einen Proxy senden: ein Ausgang in einem anderen Land, eine feste IP, die ein Partner auf seine Whitelist gesetzt hat, oder ein Pool, damit nicht eine einzige Adresse jede Anfrage trägt. Go braucht dafür kein zusätzliches Paket; net/http bringt die Proxy-Unterstützung mit, SOCKS5 und die Standard-Umgebungsvariablen eingeschlossen. Die Überraschungen stecken in den Voreinstellungen: http.Get liest HTTPS_PROXY bereits, localhost läuft nie über den Proxy, die Variablen werden pro Prozess nur einmal gelesen, und eine HTTP/2-Verbindung kann die Proxy-Funktion, die Sie für die Rotation geschrieben haben, stillschweigend überspringen.
Dieser Artikel behandelt http.ProxyURL, ProxyFromEnvironment und die NO_PROXY-Regeln, den Header Proxy-Authorization, SOCKS5 in beiden Formen, die Go kennt, einen anderen Proxy pro Anfrage, Timeouts, Wiederholungen, die genauen Fehlertexte und einen vollständigen Fetcher. Jeder Codeausschnitt lief unter Go 1.26.6 (golang.org/x/net v0.62.0) gegen einen lokalen HTTP-Proxy, der user:pass verlangt, und einen lokalen SOCKS5-Server; Ausgaben und Protokollzeilen sind echt, nur der Proxy-Host wurde durch pr.proxynet.io:8000 ersetzt. Die Node.js-Seite derselben Aufgabe steht in Proxys in Node.js verwenden.
Wie entscheidet net/http, welcher Proxy verwendet wird?
Jede Anfrage läuft über einen http.Transport, dessen Feld Proxy eine Funktion ist: Sie erhält die Anfrage und gibt die Proxy-URL zurück, oder nil für eine direkte Verbindung. Das Schema der URL bestimmt den Proxy-Typ: Die Dokumentation zu net/http Transport nennt http, https, socks5 und socks5h, ein leeres Schema bedeutet http, und Zugangsdaten in der URL werden zum Header Proxy-Authorization. http.DefaultTransport, das hinter http.Get steht, setzt Proxy auf http.ProxyFromEnvironment; ein selbst gebauter Transport nutzt keinen Proxy, bis Sie einen setzen.
Danach hängt es vom Ziel ab:
- Der Transport verbindet sich mit dem Proxy, nicht mit dem Ziel; ein fehlender Port fällt auf 80 für
http, 443 fürhttpsund 1080 für SOCKS zurück. - Unverschlüsseltes
http://-Ziel. Die Anfrage geht mit der vollständigen URL in der Anfragezeile an den Proxy, plusProxy-Authorizationbei jeder Anfrage. https://-Ziel. Go sendet einmalCONNECT host:443mit dem Header; nach dem200des Proxys läuft der TLS-Handshake im Tunnel, und der Proxy sieht nur Host und Port. Die CONNECT-Methode ist in RFC 9110 definiert.socks5://-Proxy. Go führt den SOCKS-Handshake aus, meldet sich an, wenn die URL Zugangsdaten enthält, und sendet den Hostnamen des Ziels, sodass der Proxy den Namen per DNS auflöst.- Die Verbindung bleibt im Pool für die nächste Anfrage an dasselbe Ziel über denselben Proxy, per HTTP/1.1-Keep-Alive oder HTTP/2, und eine wiederverwendete Verbindung ruft
Proxynicht noch einmal auf.
Wie setzen Sie einen Proxy mit http.ProxyURL?
http.ProxyURL verpackt eine feste URL in eine Funktion mit der passenden Signatur. Parsen Sie die Adresse einmal, legen Sie sie in einen Transport und geben Sie den Transport einem Client, der auch ein Timeout trägt:
package main
import (
"cmp"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
// Diese Zeichenkette liefert der Endpoint-Generator im Panel. Legen Sie sie in eine Umgebungsvariable, nicht in Git.
raw := cmp.Or(os.Getenv("PROXY_URL"), "http://user:pass@pr.proxynet.io:8000")
proxyURL, err := url.Parse(raw)
if err != nil {
panic(err)
}
client := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
Timeout: 20 * time.Second,
}
for _, target := range []string{"https://httpbin.org/ip", "http://httpbin.org/ip"} {
resp, err := client.Get(target)
if err != nil {
fmt.Println("error:", err)
continue
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%s -> %d %s", target, resp.StatusCode, body)
}
}https://httpbin.org/ip -> 200 {
"origin": "203.0.113.42"
}
http://httpbin.org/ip -> 200 {
"origin": "203.0.113.42"
}Auf der Proxy-Seite unterscheiden sich die beiden: ein CONNECT für das HTTPS-Ziel, ein weitergeleitetes GET mit der vollständigen URL für das HTTP-Ziel, beide mit Zugangsdaten:
13:25:51.052 [http-proxy] CONNECT httpbin.org:443 proxy-authorization=Basic user -> 200 (tunnel #1)
13:25:51.563 [http-proxy] GET http://httpbin.org/ip proxy-authorization=Basic user user-agent=Go-http-client/1.1 -> forwardZwei Gewohnheiten: resp.Body.Close() nach jeder Antwort, sonst kann die Verbindung nicht in den Pool zurück, und ein Timeout am Client, sonst hält ein Ausgang, der nicht mehr antwortet, eine Goroutine für immer fest. Der Standard-User-Agent ist Go-http-client/1.1; setzen Sie einen eigenen, damit das Ziel Ihr Programm erkennen kann.
Wie liest Go HTTP_PROXY, HTTPS_PROXY und NO_PROXY?
http.ProxyFromEnvironment liest die drei Variablen, oder ihre kleingeschriebenen Formen, pro Anfrage; die Regeln stehen in der Dokumentation des Pakets httpproxy, der Implementierung, die net/http verwendet:
HTTP_PROXYgilt fürhttp://-Ziele,HTTPS_PROXYfürhttps://-Ziele: Das Schema des Ziels entscheidet, nicht das des Proxys, einehttp://-Proxy-Adresse inHTTPS_PROXYist also normal.- Ein Wert ist eine vollständige URL oder ein bloßes
host[:port], das alshttpgilt; alles andere lässt die Anfrage scheitern. - Existieren die groß- und die kleingeschriebene Variable, gewinnt die kleingeschriebene.
NO_PROXYlistet IP-Adressen, CIDR-Bereiche wie10.0.0.0/8und Domainnamen, jeweils optional mit Port. Eine Domain passt auf sich selbst und ihre Subdomains; ein Name, der mit einem Punkt beginnt, passt nur auf Subdomains; ein einzelnes*schaltet den Proxy ganz ab.localhostund Loopback-Adressen laufen nie über den Proxy.
Dieses Programm fragt ProxyFromEnvironment nach fünf Zielen, ohne etwas zu senden:
package main
import (
"fmt"
"net/http"
)
func main() {
targets := []string{
"https://httpbin.org/ip",
"http://httpbin.org/ip",
"https://api.internal.example.com/v1",
"http://10.0.0.7:8080/health",
"http://localhost:9000/",
}
for _, target := range targets {
req, _ := http.NewRequest(http.MethodGet, target, nil)
proxyURL, err := http.ProxyFromEnvironment(req)
switch {
case err != nil:
fmt.Printf("%-36s error: %v\n", target, err)
case proxyURL == nil:
fmt.Printf("%-36s direct\n", target)
default:
fmt.Printf("%-36s via %s\n", target, proxyURL.Redacted())
}
}
}Mit gesetztem HTTPS_PROXY und HTTP_PROXY sowie NO_PROXY=.example.com,10.0.0.0/8:
https://httpbin.org/ip via http://user:xxxxx@pr.proxynet.io:8000
http://httpbin.org/ip via http://user:xxxxx@pr.proxynet.io:8000
https://api.internal.example.com/v1 direct
http://10.0.0.7:8080/health direct
http://localhost:9000/ directMit nur HTTP_PROXY gehen die HTTPS-Ziele direkt hinaus, der häufigste Grund, warum ein Go-Programm „den Proxy ignoriert":
https://httpbin.org/ip direct
http://httpbin.org/ip via http://user:xxxxx@pr.proxynet.io:8000
https://api.internal.example.com/v1 direct
http://10.0.0.7:8080/health via http://user:xxxxx@pr.proxynet.io:8000
http://localhost:9000/ directRedacted() maskiert das Passwort als xxxxx; nutzen Sie es in Protokollen.
Eine Regel steht nicht in der Variablensemantik: net/http liest die Umgebung einmal, bei der ersten Anfrage, und behält sie für die Lebensdauer des Prozesses, ein os.Setenv danach hat also keine Wirkung:
before Setenv: <nil>
after Setenv, net/http: <nil>
after Setenv, httpproxy: http://user:xxxxx@pr.proxynet.io:8000Um eine geänderte Umgebung zur Laufzeit zu übernehmen, legen Sie httpproxy.FromEnvironment().ProxyFunc(), das die Variablen bei jedem Aufruf liest, in Transport.Proxy.
Wie sendet Go Benutzername und Passwort für den Proxy?
Enthält die Proxy-URL Benutzer und Passwort, sendet Go Proxy-Authorization: Basic … von selbst. Bei einem unverschlüsselten http://-Ziel reist der Header mit jeder Anfrage, auch über eine Keep-Alive-Verbindung: In unserem Wiederholungstest trug ihn jedes der vier GETs auf einem einzigen Socket. Bei einem https://-Ziel wird er einmal gesendet, im CONNECT, und der Tunnel wird wiederverwendet, solange die Verbindung lebt.
Zeichen wie @, : oder / im Passwort zerstören die URL, wenn sie nicht prozentkodiert sind; bauen Sie die URL daher aus ihren Teilen:
proxyURL := &url.URL{
Scheme: "http",
Host: "pr.proxynet.io:8000",
User: url.UserPassword("user", "p@ss:word"), // auf der Leitung als p%40ss%3Aword kodiert
}Basic ist das einzige Schema, das Go an einen Proxy sendet; NTLM oder Kerberos braucht ein Paket von Dritten. Das Gateway eines Anbieters nimmt Basic oder eine IP-Whitelist, bei der kein Passwort in der URL steht und die zu einem Server mit fester Adresse passt; beide vergleichen wir in Proxy-Authentifizierung: User:Pass oder IP-Whitelist.
Wie nutzen Sie einen SOCKS5-Proxy in Go?
Zwei Wege. Transport.Proxy akzeptiert eine socks5://-URL und führt den Handshake selbst aus; Go behandelt socks5 genau wie socks5h, der Proxy löst den Hostnamen des Ziels also auf. Das Paket golang.org/x/net/proxy gibt Ihnen stattdessen den SOCKS5-Dialer als Wert, den Sie in Transport.DialContext einhängen und auch für Verbindungen nutzen können, die kein HTTP sind.
package main
import (
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"time"
"golang.org/x/net/proxy"
)
func main() {
// SOCKS_URL=socks5://user:pass@pr.proxynet.io:<der SOCKS5-Port, den der Endpoint-Generator anzeigt>
socksURL, err := url.Parse(os.Getenv("SOCKS_URL"))
if err != nil || socksURL.Host == "" {
log.Fatal("set SOCKS_URL to socks5://user:pass@host:port")
}
// 1. net/http verarbeitet socks5:// selbst. Der Proxy löst den Hostnamen auf.
viaTransport := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(socksURL)},
Timeout: 20 * time.Second,
}
show("Transport.Proxy", viaTransport)
// 2. golang.org/x/net/proxy liefert den SOCKS5-Dialer selbst, nutzbar für jede TCP-Verbindung.
password, _ := socksURL.User.Password()
auth := &proxy.Auth{User: socksURL.User.Username(), Password: password}
dialer, err := proxy.SOCKS5("tcp", socksURL.Host, auth, &net.Dialer{Timeout: 10 * time.Second})
if err != nil {
log.Fatal(err)
}
viaDialer := &http.Client{
Transport: &http.Transport{DialContext: dialer.(proxy.ContextDialer).DialContext},
Timeout: 20 * time.Second,
}
show("x/net/proxy dialer", viaDialer)
}
func show(label string, client *http.Client) {
resp, err := client.Get("https://httpbin.org/ip")
if err != nil {
fmt.Printf("%-19s error: %v\n", label, err)
return
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%-19s %d %s", label, resp.StatusCode, body)
}Transport.Proxy 200 {
"origin": "203.0.113.42"
}
x/net/proxy dialer 200 {
"origin": "203.0.113.42"
}Unser SOCKS5-Server protokollierte für beide Clients dieselben drei Schritte: Go bot die Methoden „keine Authentifizierung" und „Benutzername/Passwort" an, der Server wählte Benutzername/Passwort nach RFC 1929, und die Verbindungsanfrage trug den Domainnamen, keine IP:
13:26:07.490 [socks5] greeting methods=[0,2]
13:26:07.490 [socks5] auth user=user -> OK
13:26:07.490 [socks5] CONNECT domain httpbin.org:443Auf dem Dialer-Weg lassen Sie Transport.Proxy leer; der Dialer läuft bereits über den Proxy. SOCKS5 transportiert TCP und UDP, net/http öffnet aber nur TCP-Verbindungen. Der SOCKS5-Proxy von Proxynet läuft auf einem eigenen Port neben dem HTTP-Port, mit demselben Benutzernamen und Passwort.
Welche Methode passt zu welcher Situation?
| Methode | Wo sie steht | Reichweite | Hinweis |
|---|---|---|---|
http.ProxyURL(u) | Transport.Proxy | dieser Transport | ein fester Proxy |
http.ProxyFromEnvironment | Transport.Proxy (Standard in DefaultTransport) | der Prozess | einmal gelesen; localhost ausgenommen |
eigene func(*http.Request) | Transport.Proxy | pro Anfrage | Rotation, Regeln pro Host |
socks5://-URL | jede der drei oben | dieser Transport | DNS wird beim Proxy aufgelöst |
x/net/proxy-Dialer | Transport.DialContext | dieser Transport | auch rohes TCP; Proxy leer lassen |
Wie nutzen Sie für jede Anfrage einen anderen Proxy?
Transport.Proxy ist eine gewöhnliche Funktion, sie kann also bei jedem Aufruf eine andere URL zurückgeben. Diese hier wählt reihum aus einer Liste und gibt ihre Wahl aus:
var next atomic.Uint64
transport := &http.Transport{
// Reihum über die Liste. Wird aufgerufen, wenn der Transport für eine Anfrage eine Verbindung braucht.
Proxy: func(req *http.Request) (*url.URL, error) {
u := proxies[(next.Add(1)-1)%uint64(len(proxies))]
fmt.Printf("Proxy func: %s %s via %s\n", req.Method, req.URL, u.Redacted())
return u, nil
},
DisableKeepAlives: true, // eine neue Verbindung, und damit eine neue Proxy-Wahl, für jede Anfrage
}Die letzte Zeile ist entscheidend. Wir haben vier GETs an https://httpbin.org/ip gesendet, das HTTP/2 aushandelt, mit zwei Proxys in der Liste, und Aufrufe und Tunnel gezählt:
| Transport-Einstellungen | Aufrufe der Proxy-Funktion | Geöffnete Tunnel |
|---|---|---|
| Voreinstellungen (HTTP/2 ausgehandelt) | 1 | 1, nur der erste Proxy |
Nur HTTP/1.1 (leere TLSNextProto-Map) | 4 | 2, einer pro Proxy, wiederverwendet |
DisableKeepAlives: true | 4 | 4, abwechselnd |
Mit den Voreinstellungen öffnete Go eine HTTP/2-Verbindung über den ersten Proxy und bündelte alle vier Anfragen darauf; die Funktion wurde nie wieder gefragt. Mit HTTP/1.1-Keep-Alive wurde sie jedes Mal gefragt, doch die Anfragen drei und vier nutzten die ruhende Verbindung zu ihrem Proxy wieder. Nur DisableKeepAlives, oder req.Close = true, ergibt einen Tunnel pro Anfrage. Ein rotierendes Gateway folgt derselben Regel, weil die Ausgangs-IP beim Öffnen des Tunnels gewählt wird: Ein Residential-Proxy als Gateway wechselt den Ausgang bei jedem neuen Tunnel über eine einzige Adresse, und ein Sitzungsparameter im Benutzernamen hält für Anmeldeabläufe einen Ausgang 1 bis 60 Minuten lang fest.
Timeouts, Verbindungslimits und TLS
http.Client.Timeout ist die eine Einstellung, die nie auf null bleiben darf: Sie deckt die ganze Anfrage ab, vom Verbindungsaufbau bis zum letzten Byte. Die feineren Regler liegen am Transport: DialContext mit einem net.Dialer{Timeout: …} begrenzt die Verbindung zum Proxy, TLSHandshakeTimeout den Handshake im Tunnel, ResponseHeaderTimeout das Warten auf die Header; http.NewRequestWithContext mit einer Deadline tut dasselbe für eine einzelne Anfrage.
Drei Felder entscheiden, wie viele Verbindungen Sie öffnen. MaxConnsPerHost begrenzt die Verbindungen pro Host und lässt weitere Verbindungsaufbauten warten, ein günstiger Rate-Limiter pro Ziel. MaxIdleConnsPerHost steht standardmäßig auf 2; bei mehr Workern werden fertige Verbindungen geschlossen statt in den Pool gelegt, setzen Sie den Wert also auf die Zahl der Worker. IdleConnTimeout schließt Verbindungen, die ungenutzt herumlagen.
Bei TLS greifen Sie nie zu InsecureSkipVerify. x509: certificate signed by unknown authority hinter einem Proxy bedeutet, dass ein Unternehmens-Proxy TLS mit seiner eigenen Zertifizierungsstelle inspiziert; fügen Sie diese CA zu tls.Config.RootCAs hinzu. Das Gateway eines Anbieters rührt das TLS im Tunnel nie an, dort zeigt der Fehler also auf das Zertifikat des Ziels.
Ein vollständiges Beispiel: Wiederholungen, Backoff und ein Nebenläufigkeitslimit
Der Fetcher unten setzt die Teile zusammen: die Timeouts und Pool-Limits von oben, Wiederholungen bei Netzwerkfehlern, 429 und 5xx mit exponentiellem Backoff und Jitter, Retry-After wird beachtet, kein neuer Versuch bei 407, und höchstens vier Anfragen gleichzeitig:
// fetch.go - holt eine URL-Liste per GET über einen Proxy, mit Timeouts, Wiederholungen, Backoff und Nebenläufigkeitslimit.
package main
import (
"cmp"
"context"
"errors"
"fmt"
"io"
"log"
"math/rand/v2"
"net"
"net/http"
"net/url"
"os"
"strconv"
"sync"
"sync/atomic"
"time"
)
const (
maxAttempts = 4 // erster Versuch + 3 Wiederholungen
maxInFlight = 4 // gleichzeitig laufende Anfragen
)
var (
retryStatus = map[int]bool{429: true, 500: true, 502: true, 503: true, 504: true}
errProxyAuth = errors.New("proxy rejected the credentials (407): check user:pass or the IP whitelist")
)
func newClient(proxyURL *url.URL) *http.Client {
return &http.Client{
Timeout: 30 * time.Second, // die ganze Anfrage: Verbindung, Header und Body
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
MaxConnsPerHost: maxInFlight,
MaxIdleConnsPerHost: maxInFlight,
IdleConnTimeout: 60 * time.Second,
// Ein 407 auf dem CONNECT-Tunnel wird zu einem typisierten Fehler statt zu "Proxy Authentication Required".
OnProxyConnectResponse: func(_ context.Context, _ *url.URL, _ *http.Request, res *http.Response) error {
if res.StatusCode == http.StatusProxyAuthRequired {
return errProxyAuth
}
return nil
},
},
}
}
// backoff: 1, 2, 4 s ... gedeckelt bei 30 s, plus bis zu 1 s Jitter, damit Worker nicht im Gleichschritt wiederholen.
func backoff(attempt int) time.Duration {
d := min(time.Duration(1<<(attempt-1))*time.Second, 30*time.Second)
return d + time.Duration(rand.IntN(1000))*time.Millisecond
}
func sleep(ctx context.Context, d time.Duration) {
select {
case <-ctx.Done():
case <-time.After(d):
}
}
// fetch holt eine URL per GET. Netzwerkfehler, 429 und 5xx werden wiederholt; ein 407 nicht, denn die
// Zugangsdaten sind falsch, und ein weiterer Versuch kann das nicht beheben.
func fetch(ctx context.Context, client *http.Client, target string) ([]byte, int, error) {
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
if err != nil {
return nil, 0, err
}
req.Header.Set("User-Agent", "price-monitor/1.0 (+mailto:you@example.com)")
resp, err := client.Do(req)
if err != nil {
if errors.Is(err, errProxyAuth) || ctx.Err() != nil {
return nil, 0, err
}
lastErr = err
log.Printf("attempt %d/%d %s: %v", attempt, maxAttempts, target, err)
sleep(ctx, backoff(attempt))
continue
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusProxyAuthRequired { // unverschlüsseltes http-Ziel: der 407 ist eine Antwort
return body, resp.StatusCode, errProxyAuth
}
if !retryStatus[resp.StatusCode] {
return body, resp.StatusCode, err // 200, 404, 301 ... der Aufrufer entscheidet
}
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
if attempt == maxAttempts {
break
}
// Die Seite hat uns gebeten, langsamer zu werden. Ihr eigenes Retry-After geht vor unserem Zeitplan.
wait := backoff(attempt)
if secs, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
wait = time.Duration(secs) * time.Second
}
log.Printf("attempt %d/%d %s: HTTP %d, sleeping %s", attempt, maxAttempts, target, resp.StatusCode, wait.Round(100*time.Millisecond))
sleep(ctx, wait)
}
return nil, 0, lastErr
}
func main() {
proxyURL, err := url.Parse(cmp.Or(os.Getenv("PROXY_URL"), "http://user:pass@pr.proxynet.io:8000"))
if err != nil {
log.Fatal(err)
}
targets := os.Args[1:]
if len(targets) == 0 {
for i := 1; i <= 4; i++ {
targets = append(targets, fmt.Sprintf("https://httpbin.org/ip?n=%d", i))
}
targets = append(targets, "https://httpbin.org/status/503")
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
client := newClient(proxyURL)
sem := make(chan struct{}, maxInFlight)
var wg sync.WaitGroup
var ok, failed atomic.Int32
for _, target := range targets {
wg.Add(1)
sem <- struct{}{} // blockiert, solange maxInFlight Anfragen laufen
go func() {
defer wg.Done()
defer func() { <-sem }()
started := time.Now()
body, status, err := fetch(ctx, client, target)
if err != nil {
failed.Add(1)
log.Printf("fail %s -> %v", target, err)
return
}
ok.Add(1)
log.Printf("ok %s -> %d, %d bytes in %s", target, status, len(body), time.Since(started).Round(100*time.Millisecond))
}()
}
wg.Wait()
log.Printf("done: %d ok, %d failed", ok.Load(), failed.Load())
}Fünf URLs, vier Worker, eine Adresse, die immer 503 antwortet, um den Wiederholungspfad auszuführen:
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=1 -> 200, 32 bytes in 500ms
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=2 -> 200, 32 bytes in 500ms
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=4 -> 200, 32 bytes in 500ms
2026/10/28 16:30:56 attempt 1/4 https://httpbin.org/status/503: HTTP 503, sleeping 1.3s
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=3 -> 200, 32 bytes in 700ms
2026/10/28 16:30:58 attempt 2/4 https://httpbin.org/status/503: HTTP 503, sleeping 2.8s
2026/10/28 16:31:00 attempt 3/4 https://httpbin.org/status/503: HTTP 503, sleeping 4.8s
2026/10/28 16:31:05 fail https://httpbin.org/status/503 -> HTTP 503
2026/10/28 16:31:05 done: 4 ok, 1 failedDrei Designentscheidungen. OnProxyConnectResponse läuft, wenn der Proxy das CONNECT beantwortet, bevor Go einen Status ungleich 200 in eine schlichte Fehlerzeichenkette verwandelt; ein typisierter Fehler dort lässt errors.Is die Wiederholungsschleife stoppen, und mit falschem Passwort gab das Programm für ein HTTPS- und ein unverschlüsseltes HTTP-Ziel fail … -> proxy rejected the credentials (407) aus, ohne einen einzigen neuen Versuch. Die Menge der wiederholten Status ist absichtlich klein: Ein 404 oder 403 wird unverändert zurückgegeben, weil ein anderer Ausgang nichts daran ändert, was die Seite ist. Gegen einen lokalen Server, der Retry-After: 2 sendete, wartete die Schleife zwischen den Versuchen genau 2 s statt nach ihrem eigenen Zeitplan.
Wie prüfen Sie, ob die Anfrage wirklich über den Proxy läuft?
Fragen Sie einen Echo-Dienst, welche Adresse er sieht, einmal mit Proxy: nil und einmal mit dem Proxy. Das nil ist Absicht: Ein Transport ohne Proxy-Wert liest die Umgebung nie, die erste Zeile ist also auch bei gesetztem HTTPS_PROXY Ihre eigene Adresse.
direct := &http.Client{Transport: &http.Transport{Proxy: nil}, Timeout: 20 * time.Second}
proxied := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}, Timeout: 20 * time.Second}
fmt.Println("without proxy:", exitIP(direct)) // exitIP holt https://api.ipify.org?format=json per GET und gibt das Feld "ip" zurück
fmt.Println("with proxy: ", exitIP(proxied))Mit einem echten Gateway zeigt die zweite Zeile die Adresse des Ausgangs. Mit unserem Test-Proxy gaben beide Zeilen dieselbe Adresse aus, weil der Proxy auf derselben Maschine läuft; der Beweis war die Zeile CONNECT api.ipify.org:443 in seinem Protokoll. Zwei identische Zeilen mit einem echten Proxy bedeuten meist HTTP_PROXY ohne HTTPS_PROXY, ein Ziel in NO_PROXY oder einen Transport, der ohne Proxy-Wert erzeugt wurde.
Was ist mit Colly?
Colly, das meistgenutzte Crawling-Framework für Go, baut auf http.Transport auf, alles oben gilt also auch dort. Sein Paket proxy bringt einen RoundRobinProxySwitcher mit, der http://-, https://- oder socks5://-URLs nimmt und eine colly.ProxyFunc zurückgibt, dieselbe Signatur wie Transport.Proxy; Sie hängen ihn mit c.SetProxyFunc(rp) ein, wie im Beispiel zum Proxy-Switcher in der Colly-Dokumentation. Der Switcher wird pro Anfrage aufgerufen, doch ob eine Anfrage einen neuen Tunnel öffnet, entscheidet weiterhin die Regel zur Verbindungswiederverwendung aus dem Abschnitt zur Rotation.
Häufige Fehlermeldungen und ihre Bedeutung
Die Meldungen, wie Go 1.26.6 sie in unseren Tests ausgab, mit dem Platzhalter-Host:
Get "https://httpbin.org/ip": Proxy Authentication Requiredbei einem HTTPS-Ziel, oder Status407als normale Antwort bei einem unverschlüsselten HTTP-Ziel: falschesuser:pass, nicht kodierte Sonderzeichen oder eine IP, die nicht auf der Whitelist steht. Die Ursachen sind in HTTP 407 Proxy Authentication Required beheben gesammelt.proxyconnect tcp: dial tcp pr.proxynet.io:8000: …plus der Text des Betriebssystems für eine abgewiesene Verbindung (connect: connection refusedunter Linux und macOS,connectex:und ein lokalisierter Satz unter Windows): Dort hört nichts zu.proxyconnectsagt, dass der Fehler auf dem Weg zum Proxy lag, nicht zum Ziel.socks connect tcp pr.proxynet.io:8000->httpbin.org:443: unexpected protocol version 72: Einesocks5://-URL zeigte auf den HTTP-Port; 72 ist der Bytewert desHinHTTP/1.1.socks connect tcp …: username/password authentication failed: Der SOCKS5-Server hat die Zugangsdaten abgelehnt.unexpected EOF: Einehttp://-URL am SOCKS5-Port, der die Verbindung beimCONNECTgeschlossen hat; ein unbekanntes Schema wiesocks4://wird wiehttpbehandelt und endet genauso.proxyconnect tcp: tls: first record does not look like a TLS handshake: Einhttps://-Proxy-Schema an einem einfachen HTTP-Port; nutzen Siehttp://, sofern der Anbieter nicht sagt, dass der Port TLS spricht.- Der Proxy wird schlicht nicht genutzt:
HTTP_PROXYohneHTTPS_PROXY, ein Ziel, daslocalhostist oder inNO_PROXYsteht, Variablen, die nach der ersten Anfrage gesetzt wurden, oder ein Transport ohneProxy-Wert.
Wo dieses Setup eingesetzt wird
- Preis- und Verfügbarkeitsprüfungen aus mehreren Ländern: ein Länderparameter im Proxy-Benutzernamen und Rotation pro Anfrage für voneinander unabhängige Produktseiten.
- API-Clients hinter einer freigegebenen Adresse, wenn ein Partner eine statische IP auf seine Whitelist gesetzt hat und der Dienst über genau diese hinausgehen muss.
- Monitoring von außerhalb des eigenen Netzes, damit ein Health-Check die Website so sieht wie ein Besucher in einer anderen Region.
- Worker in einem Unternehmensnetz, die das Internet nur über den Firmen-Proxy erreichen.
Häufige Fehler
- Nur
HTTP_PROXYsetzen. HTTPS-Ziele gehen direkt hinaus; setzen Sie auchHTTPS_PROXY. - Die Umgebung nach der ersten Anfrage ändern. net/http hat sie zwischengespeichert; starten Sie den Prozess neu oder nutzen Sie
httpproxy. - Rotation bei eingeschaltetem Keep-Alive erwarten. Eine wiederverwendete Verbindung ruft
Proxynie wieder auf. resp.Body.Close()vergessen. Verbindungen kehren nie in den Pool zurück.- Kein
Client.Timeout. Ein stummer Ausgang blockiert eine Goroutine für immer. - Ein Passwort mit
@oder:in die URL-Zeichenkette einfügen. Bauen Sie sie miturl.UserPassword. Transport.Proxyund einen SOCKS5-DialContextzugleich setzen. Wählen Sie eins.- Einen
407wiederholen. Der zweite Versuch sendet dieselben falschen Zugangsdaten.
Welchen Ansatz sollten Sie wählen?
| Ihre Situation | Empfehlung |
|---|---|
| Ein Proxy, keine Codeänderung | HTTPS_PROXY und HTTP_PROXY, NO_PROXY für interne Hosts |
| Ein Proxy, im Code gesetzt | Transport{Proxy: http.ProxyURL(u)} plus Client.Timeout |
| Ein neuer Ausgang für jede Anfrage | Eigene Proxy-Funktion oder ein rotierendes Gateway, DisableKeepAlives: true |
| Derselbe Ausgang für einen Anmeldeablauf | Ein Sitzungsparameter im Benutzernamen, Keep-Alive an |
| SOCKS5 mit DNS beim Proxy | socks5://user:pass@host:port in Transport.Proxy |
| SOCKS5 für eine Verbindung ohne HTTP | golang.org/x/net/proxy-Dialer |
| Viele Worker auf einer Website | MaxConnsPerHost und MaxIdleConnsPerHost = Zahl der Worker |
| Unternehmens-Proxy mit eigener CA | Die CA in tls.Config.RootCAs, nie InsecureSkipVerify |
Häufig gestellte Fragen
Nutzt http.Get in Go automatisch einen Proxy?
Ja, wenn die Umgebung es vorgibt: http.Get verwendet http.DefaultClient, dessen Transport Proxy auf http.ProxyFromEnvironment setzt. Ein selbst erzeugter Transport startet ohne Proxy.
Ist das dasselbe wie GOPROXY?
Nein. GOPROXY sagt dem Befehl go, woher er Module herunterladen soll; mit den Anfragen, die Ihr Programm zur Laufzeit sendet, hat es nichts zu tun.
Wie setze ich einen Proxy nur für eine einzelne Anfrage?
Geben Sie dieser Anfrage einen eigenen Client und Transport, oder schreiben Sie eine Proxy-Funktion, die für Anfragen, die direkt gehen sollen, nil zurückgibt. Eine gepoolte Verbindung zu demselben Host kann bereits existieren; req.Close = true erzwingt eine neue.
Unterstützt Go socks5h?
Ja, und socks5 verhält sich genauso: Go sendet den Hostnamen immer an den Proxy, der ihn auflöst; net/http kennt keinen SOCKS5-Modus mit lokaler Auflösung.
Warum läuft localhost nie über den Proxy?
ProxyFromEnvironment gibt für localhost und Loopback-Adressen keinen Proxy zurück, was auch immer die Variablen sagen; um solche Anfragen über den Proxy zu leiten, nutzen Sie stattdessen http.ProxyURL.
Wie sehe ich, welchen Proxy eine Anfrage genutzt hat?
Umhüllen Sie die Proxy-Funktion und protokollieren Sie u.Redacted(), oder setzen Sie Transport.OnProxyConnectResponse, das die Proxy-URL und die CONNECT-Antwort erhält. Bei eingeschaltetem Keep-Alive wird eine Wahl nur protokolliert, wenn eine neue Verbindung geöffnet wird.
Fazit
In Go ist der Proxy eine Funktion am http.Transport: http.ProxyURL für eine feste Adresse, http.ProxyFromEnvironment für die Standardvariablen oder Ihre eigene Funktion für die Wahl pro Anfrage. Schreiben Sie user:pass in die URL, und Go sendet Proxy-Authorization von selbst; nutzen Sie socks5:// für SOCKS5; setzen Sie Client.Timeout und die Pool-Limits; wiederholen Sie 429 und 5xx mit Backoff, aber nie einen 407; schalten Sie Keep-Alive ab, wenn jede Anfrage einen neuen Ausgang braucht. Residential-, statische ISP-, Datacenter- und SOCKS5-Endpunkte, die mit diesen Einstellungen funktionieren, finden Sie auf der Seite Proxynet-Proxys.




