在 Go 中使用代理,需要为 http.Client 的 http.Transport 配置一个 Proxy 函数:静态代理通常使用 http.ProxyURL,动态选择代理则使用自定义函数。
为什么在 Go 中使用代理?
在网络通信中,尤其是在 Go 应用中发起 HTTP 请求时,代理有多种用途:
- 匿名性: 隐藏客户端的原始 IP 地址。
- 地域定向: 通过位于目标地区的代理转发请求,访问仅限特定地理区域的内容或服务。
- 速率限制: 将请求分散到多个 IP 地址上,避免触发目标服务器的速率限制。
- 网页抓取: 通过轮换 IP 地址、绕过检测和控制请求量,实现大规模数据采集。
- 安全与过滤: 将流量经由企业代理转发,用于安全扫描、内容过滤或访问控制。
- 调试: 拦截并检查流量。
为 HTTP 客户端配置代理
Go 标准库的 net/http 包对代理配置提供了完善的支持。核心机制是修改与 http.Client 关联的 http.Transport。
使用 http.ProxyFromEnvironment
使用代理最简单的方式是借助环境变量。Go 的 http.ProxyFromEnvironment 函数会检查 HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY 环境变量。
HTTP_PROXY:用于http://请求。HTTPS_PROXY:用于https://请求。NO_PROXY:以逗号分隔的主机名列表,这些主机名将绕过代理。
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
// Set environment variables (for demonstration)
os.Setenv("HTTP_PROXY", "http://your_proxy_ip:port")
os.Setenv("HTTPS_PROXY", "http://your_proxy_ip:port") // Note: HTTPS_PROXY can be an HTTP proxy
// os.Setenv("NO_PROXY", "localhost,127.0.0.1")
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
Timeout: 10 * time.Second,
}
resp, err := client.Get("http://httpbin.org/ip")
if err != nil {
fmt.Printf("Error making request: %v\n", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response body: %v\n", err)
return
}
fmt.Printf("Response from httpbin.org/ip:\n%s\n", body)
os.Unsetenv("HTTP_PROXY")
os.Unsetenv("HTTPS_PROXY")
}
静态代理使用 http.ProxyURL
如需显式配置代理,可使用 http.ProxyURL。该函数接收一个表示代理地址的 *url.URL。
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"time"
)
func main() {
proxyStr := "http://your_proxy_ip:port" // Replace with your proxy address
proxyURL, err := url.Parse(proxyStr)
if err != nil {
fmt.Printf("Error parsing proxy URL: %v\n", err)
return
}
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
Timeout: 10 * time.Second,
}
resp, err := client.Get("http://httpbin.org/ip")
if err != nil {
fmt.Printf("Error making request: %v\n", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response body: %v\n", err)
return
}
fmt.Printf("Response from httpbin.org/ip:\n%s\n", body)
}
代理认证
许多代理需要认证。对于使用 Basic Authentication 的 HTTP/HTTPS 代理,可将凭据直接嵌入代理 URL 中:
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"time"
)
func main() {
// Format: "http://username:password@proxy_ip:port"
proxyStr := "http://user:password@your_auth_proxy_ip:port" // Replace with your authenticated proxy
proxyURL, err := url.Parse(proxyStr)
if err != nil {
fmt.Printf("Error parsing proxy URL: %v\n", err)
return
}
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
Timeout: 10 * time.Second,
}
resp, err := client.Get("http://httpbin.org/ip")
if err != nil {
fmt.Printf("Error making request with authenticated proxy: %v\n", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response body: %v\n", err)
return
}
fmt.Printf("Response from httpbin.org/ip via authenticated proxy:\n%s\n", body)
}
使用 SOCKS5 代理
net/http 直接支持 HTTP/HTTPS 代理,而 SOCKS5 代理需要额外的包:golang.org/x/net/proxy。SOCKS5 工作在比 HTTP 代理更低的层级,可支持多种协议。
要使用 SOCKS5 代理,需要用 proxy 包创建一个 Dialer,然后把它赋给 http.Transport 的 DialContext 字段。
package main
import (
"context"
"fmt"
"io"
"net"
"net/http"
"time"
"golang.org/x/net/proxy" // Make sure to 'go get golang.org/x/net/proxy'
)
func main() {
socks5Proxy := "socks5://user:password@your_socks5_proxy_ip:port" // Replace with your SOCKS5 proxy
dialer, err := proxy.SOCKS5("tcp", socks5Proxy[len("socks5://"):], nil, proxy.Direct) // Basic auth is handled by the dialer
if err != nil {
fmt.Printf("Error creating SOCKS5 dialer: %v\n", err)
return
}
// For authenticated SOCKS5, the proxy.SOCKS5 function handles it if the URL contains credentials.
// If the proxy.SOCKS5 function doesn't parse credentials directly, you might need to
// extract them and pass a proxy.Auth struct. Example:
// auth := &proxy.Auth{User: "user", Password: "password"}
// dialer, err := proxy.SOCKS5("tcp", "your_socks5_proxy_ip:port", auth, proxy.Direct)
client := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
},
// Optionally, set TLSClientConfig for HTTPS over SOCKS5
// TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // Use with caution
},
Timeout: 10 * time.Second,
}
resp, err := client.Get("http://httpbin.org/ip")
if err != nil {
fmt.Printf("Error making request with SOCKS5 proxy: %v\n", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response body: %v\n", err)
return
}
fmt.Printf("Response from httpbin.org/ip via SOCKS5 proxy:\n%s\n", body)
}
动态代理选择与轮换
对于网页抓取或大规模数据采集这类应用,代理轮换是避免 IP 封禁、控制请求量的关键。这需要为 http.Transport 编写自定义的 Proxy 函数。
http.Transport 的 Proxy 字段需要一个签名为 func(*http.Request) (*url.URL, error) 的函数。该函数在每次请求前被调用,可根据请求属性或轮换策略动态选择代理。
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"sync"
"time"
)
// ProxyRotator manages a list of proxies and provides a round-robin selection.
type ProxyRotator struct {
proxies []*url.URL
current int
mu sync.Mutex
}
// NewProxyRotator creates a new ProxyRotator from a slice of proxy URLs.
func NewProxyRotator(proxyStrings []string) (*ProxyRotator, error) {
proxies := make([]*url.URL, len(proxyStrings))
for i, s := range proxyStrings {
u, err := url.Parse(s)
if err != nil {
return nil, fmt.Errorf("invalid proxy URL '%s': %w", s, err)
}
proxies[i] = u
}
return &ProxyRotator{
proxies: proxies,
current: 0,
}, nil
}
// GetProxy returns the next proxy in a round-robin fashion.
func (pr *ProxyRotator) GetProxy(_ *http.Request) (*url.URL, error) {
if len(pr.proxies) == 0 {
return nil, nil // No proxy
}
pr.mu.Lock()
defer pr.mu.Unlock()
proxy := pr.proxies[pr.current]
pr.current = (pr.current + 1) % len(pr.proxies)
return proxy, nil
}
func main() {
proxyList := []string{
"http://user1:[email protected]:8080",
"http://user2:[email protected]:8080",
"http://user3:[email protected]:8080",
}
rotator, err := NewProxyRotator(proxyList)
if err != nil {
fmt.Printf("Error creating proxy rotator: %v\n", err)
return
}
client := &http.Client{
Transport: &http.Transport{
Proxy: rotator.GetProxy, // Assign the rotator's GetProxy method
},
Timeout: 15 * time.Second,
}
for i := 0; i < 5; i++ {
resp, err := client.Get("http://httpbin.org/ip")
if err != nil {
fmt.Printf("Request %d error: %v\n", i+1, err)
continue
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Request %d error reading body: %v\n", i+1, err)
continue
}
fmt.Printf("Request %d IP: %s\n", i+1, body)
time.Sleep(1 * time.Second) // Simulate some work
}
}
这个示例实现了基础的轮询(round-robin)轮换器。更高级的轮换器还可以:
* 跟踪代理健康状态并剔除失效代理。
* 实现不同的轮换策略(例如加权、随机)。
* 与外部代理管理服务集成。
通过代理传输 HTTPS 流量
当 http.Client 通过 HTTP 代理发起 https:// 请求时,http.Transport 会与代理执行 CONNECT 方法握手。客户端请求代理建立到目标主机和端口(通常为 443)的 TCP 隧道。隧道建立后,客户端通过该代理隧道直接与目标服务器完成 TLS 握手。代理本身不会解密 HTTPS 流量(除非它是"中间人"代理,那是另一种场景,需要特定的信任配置)。
对于 https:// 请求,Proxy 函数照常返回代理 URL 即可。Go 的 net/http 会自动处理 CONNECT 方法。
TLS/SSL 验证
默认情况下,Go 客户端会执行严格的 TLS 证书验证。如果目标服务器的证书无法通过验证(例如自签名、已过期或 CA 不受信任),请求将失败。
在特定场景下(例如在受控环境中使用已知的自签名证书进行测试),可以通过 tls.Config 中的 InsecureSkipVerify 关闭证书验证。
package main
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
func main() {
proxyStr := "http://your_proxy_ip:port"
proxyURL, err := url.Parse(proxyStr)
if err != nil {
fmt.Printf("Error parsing proxy URL: %v\n", err)
return
}
// Configure a custom Transport for TLS settings
tr := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{
// WARNING: InsecureSkipVerify should not be used in production environments.
// It disables certificate chain and hostname verification.
InsecureSkipVerify: true,
},
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := &http.Client{
Transport: tr,
Timeout: 15 * time.Second, // Overall request timeout
}
// Example target for HTTPS
resp, err := client.Get("https://example.com")
if err != nil {
fmt.Printf("Error making HTTPS request with proxy: %v\n", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response body: %v\n", err)
return
}
fmt.Printf("Response status from https://example.com: %s\n", resp.Status)
// fmt.Printf("Response body: %s\n", body) // Omitted for brevity
_ = body
}
对比:HTTP/HTTPS 代理与 SOCKS5 代理
| 特性 | HTTP/HTTPS 代理 | SOCKS5 代理 |
|---|---|---|
| 协议层 | 应用层(第 7 层) | 会话层(第 5 层) |
| 支持的流量 | 主要是 HTTP/HTTPS 请求 | 任意 TCP/UDP 流量(HTTP、FTP、SMTP、DNS 等) |
| 流量过滤 | 可检查 HTTP 头、URL 和内容 | 通常不检查流量内容 |
| 认证方式 | HTTP Basic、Digest(Go 仅支持 URL 中的 Basic) | 用户名/密码(SOCKS5 AUTH 方法) |
| 易用性(Go) | net/http 原生支持(http.ProxyURL) |
需要 golang.org/x/net/proxy 包 |
| 性能 | 因解析 HTTP 可能带来更高开销 | 通常开销更低,相当于简单转发 |
| 适用场景 | 网页抓取、API 交互、网页过滤 | 通用网络隧道、绕过防火墙 |
最佳实践
- 设置超时: 始终为
http.Client和http.Transport配置超时(例如DialContext、TLSHandshakeTimeout),避免请求无限期挂起,处理不稳定代理时尤其重要。 - 完善的错误处理: 代理相关错误多种多样(例如连接问题的
net.OpError、意外断开的io.EOF)。请实现全面的错误处理与重试逻辑。 - 代理管理: 在动态场景中,构建健壮的代理管理系统,监控代理健康状况、有效轮换代理并妥善处理故障。
- 资源管理: 确保用
defer调用resp.Body.Close(),以释放网络资源。 - 用 Context 实现取消: 将
context.Context与http.Request配合使用,实现跨函数的请求取消与超时管理。 - 避免
InsecureSkipVerify: 除非您完全理解其安全影响并有补偿性控制措施,否则不要在生产环境中使用InsecureSkipVerify = true。
