---
title: "Golang HTTP Proxy: net/http über einen Proxy nutzen"
description: "Proxy in Go: http.Transport.Proxy mit http.ProxyURL oder ProxyFromEnvironment setzen, user:pass in die URL. SOCKS5, Timeouts, Retries und Rotation, Go 1.26."
url: https://proxynet.io/de/blog/golang-proxy
date: 2026-10-28
author: "Acar Diveroli"
category: "Anleitungen, Integration"
lang: de
---

# Golang HTTP Proxy: net/http über einen Proxy nutzen

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](/de/blog/nodejs-proxy).

> **Hinweis: Kurze Antwort**
>
> Bauen Sie einen `http.Transport` mit `Proxy: http.ProxyURL(u)`, wobei `u` die geparste Adresse `http://user:pass@pr.proxynet.io:8000` ist, und geben Sie ihn einem `http.Client` mit `Timeout`; Benutzername und Passwort sendet Go von selbst als `Proxy-Authorization`. Für SOCKS5 nutzen Sie in derselben URL das Schema `socks5://`. `http.Get` verwendet `http.ProxyFromEnvironment`, ein gewöhnliches Programm beachtet also bereits `HTTPS_PROXY`, `HTTP_PROXY` und `NO_PROXY`. Für einen anderen Proxy pro Anfrage schreiben Sie Ihre eigene `Proxy`-Funktion und schalten Keep-Alive ab, denn eine wiederverwendete Verbindung fragt sie nicht noch einmal.

## 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](https://pkg.go.dev/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:

1. **Der Transport verbindet sich mit dem Proxy**, nicht mit dem Ziel; ein fehlender Port fällt auf 80 für `http`, 443 für `https` und 1080 für SOCKS zurück.
2. **Unverschlüsseltes `http://`-Ziel.** Die Anfrage geht mit der vollständigen URL in der Anfragezeile an den Proxy, plus `Proxy-Authorization` bei jeder Anfrage.
3. **`https://`-Ziel.** Go sendet einmal `CONNECT host:443` mit dem Header; nach dem `200` des Proxys läuft der TLS-Handshake im Tunnel, und der Proxy sieht nur Host und Port. Die [CONNECT-Methode](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.6) ist in RFC 9110 definiert.
4. **`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.
5. **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 `Proxy` nicht 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:

```go
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)
	}
}
```

```text
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:

```text
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 -> forward
```

Zwei 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](https://pkg.go.dev/golang.org/x/net/http/httpproxy), der Implementierung, die net/http verwendet:

- `HTTP_PROXY` gilt für `http://`-Ziele, `HTTPS_PROXY` für `https://`-Ziele: Das Schema des Ziels entscheidet, nicht das des Proxys, eine `http://`-Proxy-Adresse in `HTTPS_PROXY` ist also normal.
- Ein Wert ist eine vollständige URL oder ein bloßes `host[:port]`, das als `http` gilt; alles andere lässt die Anfrage scheitern.
- Existieren die groß- und die kleingeschriebene Variable, gewinnt die kleingeschriebene.
- `NO_PROXY` listet IP-Adressen, CIDR-Bereiche wie `10.0.0.0/8` und 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.
- `localhost` und Loopback-Adressen laufen nie über den Proxy.

Dieses Programm fragt `ProxyFromEnvironment` nach fünf Zielen, ohne etwas zu senden:

```go
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`:

```text
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/               direct
```

Mit nur `HTTP_PROXY` gehen die HTTPS-Ziele direkt hinaus, der häufigste Grund, warum ein Go-Programm „den Proxy ignoriert":

```text
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/               direct
```

`Redacted()` 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:

```text
before Setenv: <nil>
after Setenv, net/http: <nil>
after Setenv, httpproxy: http://user:xxxxx@pr.proxynet.io:8000
```

Um 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 `GET`s 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:

```go
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](/de/blog/proxy-authentication-methods).

## 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.

```go
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)
}
```

```text
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](https://www.rfc-editor.org/rfc/rfc1929), und die Verbindungsanfrage trug den Domainnamen, keine IP:

```text
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:443
```

Auf 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](https://proxynet.io/de/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:

```go
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 `GET`s 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](https://proxynet.io/de/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:

```go
// 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:

```text
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 failed
```

Drei 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.

```go
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](https://go-colly.org/docs/examples/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 Required`** bei einem HTTPS-Ziel, oder **Status `407`** als normale Antwort bei einem unverschlüsselten HTTP-Ziel: falsches `user:pass`, nicht kodierte Sonderzeichen oder eine IP, die nicht auf der Whitelist steht. Die Ursachen sind in [HTTP 407 Proxy Authentication Required beheben](/de/blog/http-407-proxy-authentication-required) gesammelt.
- **`proxyconnect tcp: dial tcp pr.proxynet.io:8000: …`** plus der Text des Betriebssystems für eine abgewiesene Verbindung (`connect: connection refused` unter Linux und macOS, `connectex:` und ein lokalisierter Satz unter Windows): Dort hört nichts zu. `proxyconnect` sagt, 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`**: Eine `socks5://`-URL zeigte auf den HTTP-Port; 72 ist der Bytewert des `H` in `HTTP/1.1`.
- **`socks connect tcp …: username/password authentication failed`**: Der SOCKS5-Server hat die Zugangsdaten abgelehnt.
- **`unexpected EOF`**: Eine `http://`-URL am SOCKS5-Port, der die Verbindung beim `CONNECT` geschlossen hat; ein unbekanntes Schema wie `socks4://` wird wie `http` behandelt und endet genauso.
- **`proxyconnect tcp: tls: first record does not look like a TLS handshake`**: Ein `https://`-Proxy-Schema an einem einfachen HTTP-Port; nutzen Sie `http://`, sofern der Anbieter nicht sagt, dass der Port TLS spricht.
- **Der Proxy wird schlicht nicht genutzt:** `HTTP_PROXY` ohne `HTTPS_PROXY`, ein Ziel, das `localhost` ist oder in `NO_PROXY` steht, Variablen, die nach der ersten Anfrage gesetzt wurden, oder ein Transport ohne `Proxy`-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_PROXY` setzen.** HTTPS-Ziele gehen direkt hinaus; setzen Sie auch `HTTPS_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 `Proxy` nie 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 mit `url.UserPassword`.
- **`Transport.Proxy` und einen SOCKS5-`DialContext` zugleich setzen.** Wählen Sie eins.
- **Einen `407` wiederholen.** 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](/de/proxy).
