# Go 프록시 스크래핑: http.Transport, 인증, 회전 및 동시성

Go에서는 전체 프록시 스토리가 하나의 필드에 존재합니다: Transport.Proxy. 이를 올바르게 설정하면 요청별 회전, SOCKS5 및 colly를 무료로 얻을 수 있습니다. 잘못 설정하면 새벽 2시에 'context canceled' 오류를 만나게 됩니다.

Go는 프록시 설정을 놀랍도록 간단하게 만듭니다: 전체 클라이언트 측 스토리가 하나의 필드, `http.Transport.Proxy`에 존재합니다. 이를 올바르게 설정하면 요청별 회전, SOCKS5 및 `colly` 통합이 동일한 설계에서 모두 해결됩니다. 잘못 설정하면 Go 프록시의 고전적인 실패 - `context canceled`, 연결 풀 소진, 그리고 사용자 정의 전송이 조용히 무시하는 `HTTP_PROXY` 변수를 만나게 됩니다. 이 가이드는 실행 가능한 코드와 함께 전체 경로를 안내합니다.

## 한 줄 요약: 프록시 URL을 가진 http.Transport

Go에서 모든 프록시 요청은 `Transport`를 통해 흐릅니다. `net/http` 패키지는 전체 클라이언트에 대해 하나의 정적 프록시를 고정하는 도우미 `http.ProxyURL`을 제공합니다. 자격 증명은 URL 사용자 정보에 직접 입력되며, Go는 이를 `Proxy-Authorization` 헤더로 변환합니다:

```go
package main

import (
  "fmt"
  "net/http"
  "net/url"
  "time"
)

func main() {
  proxyURL, _ := url.Parse("http://USER:PASS@residentialpremium.quantumproxies.io:8080")
  client := &http.Client{
    Timeout:   20 * time.Second,
    Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
  }
  resp, err := client.Get("https://httpbin.org/ip")
  if err != nil {
    panic(err)
  }
  defer resp.Body.Close()
  fmt.Println(resp.Status) // body shows the exit IP, not yours
}
```

항상 `Client.Timeout`을 설정하십시오. Go는 `http.Client`에 기본 타임아웃이 없으므로 단일 죽은 출구가 고루틴을 영원히 차단할 것입니다 - Go 스크래퍼가 멈춘 것처럼 보이는 가장 큰 이유는 느린 대상이 아니라 누락된 타임아웃입니다.

## 프록시 함수로 프록시 회전

`Proxy`는 고정된 URL에 제한되지 않습니다 - 이는 `func(*http.Request) (*url.URL, error)` 함수로, Go는 요청당 한 번 호출합니다. 이것이 회전 훅입니다. 풀에서 무작위로 엔드포인트를 선택하고 모든 요청이 다른 IP를 통해 나가도록 하며, 가져오기 루프에서 목록 관리 로직을 전혀 사용하지 않습니다:

```go
import "math/rand"

// three sticky sessions on the same gateway = three exit IPs to pick from
pool := []string{
  "http://USER-sid-pool1-ttl-30m:PASS@residentialpremium.quantumproxies.io:8080",
  "http://USER-sid-pool2-ttl-30m:PASS@residentialpremium.quantumproxies.io:8080",
  "http://USER-sid-pool3-ttl-30m:PASS@residentialpremium.quantumproxies.io:8080",
}

transport := &http.Transport{
  Proxy: func(r *http.Request) (*url.URL, error) {
    return url.Parse(pool[rand.Intn(len(pool))])
  },
  MaxIdleConnsPerHost: 32, // reuse connections across the pool
}
```

대부분의 스크래핑에서는 풀을 전혀 관리하지 않아야 합니다. `Proxy` 함수를 단일 [회전 주거 게이트웨이](https://quantumproxies.io/rotating-proxies)로 지정하면 게이트웨이가 200개 이상의 국가에서 90M+ 주소에 걸쳐 모든 요청에 대해 새로운 IP를 제공합니다 - 하나의 엔드포인트, 목록 없음, 몇 분 동안 동일한 출구가 필요한 흐름에는 고정 세션.

![Proxy 함수가 요청마다 실행되는 Transport 하나를 가진 Go http.Client 하나로, 요청 1, 2, 3이 서로 다른 출구 IP를 거쳐 같은 대상으로 나갑니다. 카드 요약: 이 함수는 요청마다 한 번 호출되어 풀에서 고르거나 하나의 회전 게이트웨이를 가리키며, MaxIdleConnsPerHost: 32가 연결 재사용을 유지합니다.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335148/blog/generated/qp-b9-009-diag1.png)
*하나의 Transport, 하나의 Proxy 함수: 함수는 요청당 실행되므로, 단일 클라이언트가 전체 풀을 회전합니다.*

## Go에서의 프록시 인증

프록시 URL에 `USER:PASS@`를 넣는 것이 깔끔한 경로이며, `ProxyURL` 및 사용자 정의 `Proxy` 함수 모두에 작동합니다. URL에서 자격 증명을 유지하지 않으려면 `Transport.ProxyConnectHeader`를 통해 CONNECT 터널에 헤더를 직접 설정하십시오. 공급자가 IP-화이트리스트 인증을 사용하는 경우, 자격 증명을 완전히 제거하고 대시보드에서 서버의 IP를 인증하십시오 - QuantumProxies는 둘 다 지원합니다. `407` 또는 `proxyconnect tcp`로 거부된 CONNECT는 거의 항상 누락되거나 잘못된 자격 증명 때문이지, 대상 측 차단이 아닙니다.

## HTTP_PROXY 환경 변수 (그리고 Go가 이를 무시할 때)

Go의 `http.DefaultTransport`는 `http.ProxyFromEnvironment`를 사용하여 `HTTP_PROXY`, `HTTPS_PROXY` 및 `NO_PROXY`를 읽습니다 (`golang.org/x/net/http/httpproxy` 패키지에 따라). 두 가지 동작이 사람들을 혼란스럽게 합니다. 첫째, 이러한 변수는 전체 URL 또는 단순 `host:port`를 수용하며, 여기서 `http` 스킴이 가정됩니다. 둘째, `localhost` 또는 루프백 주소로의 요청은 항상 프록시를 우회하여 nil URL을 반환합니다. 더 큰 문제는 반대입니다: 자신만의 `&http.Transport{Proxy: ...}`를 구축하는 순간, 환경 변수 동작을 *대체*한 것입니다 - 명시적인 함수가 우선하며 `HTTP_PROXY`는 무시됩니다. 둘 다 원한다면 직접 래핑하십시오.

## Go에서의 SOCKS5 프록시

표준 라이브러리에는 SOCKS5 클라이언트가 없으므로 `golang.org/x/net/proxy`를 가져와서 이를 통해 다이얼하십시오. 다이얼러를 `Transport.DialContext`에 입력하여 DNS가 로컬에서가 아니라 프록시에서 해결되도록 하십시오 (다른 스택에서 `socks5h`가 해결하는 동일한 호스트 이름 누출 문제). 두 스킴을 비교 중이라면, [SOCKS5 vs HTTP 프록시](https://quantumproxies.io/blog/socks5-vs-http-proxy-difference)에 대한 우리의 노트는 각 스킴이 언제 유리한지를 다룹니다:

```bash
go get golang.org/x/net/proxy
```

```go
import (
  "context"
  "net"
  "net/http"
  "golang.org/x/net/proxy"
)

auth := &proxy.Auth{User: "USER", Password: "PASS"}
dialer, err := proxy.SOCKS5("tcp", "residentialpremium.quantumproxies.io:1080", auth, proxy.Direct)
if err != nil {
  panic(err)
}

transport := &http.Transport{
  DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
    return dialer.Dial(network, addr)
  },
}
client := &http.Client{Transport: transport}
```

## colly에서의 프록시

`colly`로 스크래핑하면 전송을 건드리지 않고도 회전을 얻을 수 있습니다. `colly/proxy` 패키지는 요청당 엔드포인트를 순환하는 `RoundRobinProxySwitcher`를 제공하며, `LimitRule`은 페이싱을 처리합니다. 이는 Go에서 예의 바르고 회전하는 크롤러로 가는 가장 빠른 방법입니다:

```go
import (
  "time"
  "github.com/gocolly/colly/v2"
  "github.com/gocolly/colly/v2/proxy"
)

c := colly.NewCollector(colly.Async(true))

rp, err := proxy.RoundRobinProxySwitcher(
  "http://USER-sid-crawl1-ttl-30m:PASS@residentialpremium.quantumproxies.io:8080",
  "http://USER-sid-crawl2-ttl-30m:PASS@residentialpremium.quantumproxies.io:8080",
)
if err != nil {
  panic(err)
}
c.SetProxyFunc(rp)

c.Limit(&colly.LimitRule{
  DomainGlob:  "*",
  Parallelism: 8,
  RandomDelay: 2 * time.Second,
})
```

## 올바르게 수행된 동시성 (및 context-canceled 함정)

Go의 동시성은 팀들이 스크래핑을 위해 이를 선택하는 이유이며, 또한 `context canceled` 오류가 발생하는 곳입니다. 이는 요청의 컨텍스트가 본문이 완전히 읽히기 전에 취소될 때 발생합니다 - 일반적으로 만료된 요청별 타임아웃이나 잘못된 `defer`로 인해 너무 일찍 실행된 `cancel()` 때문입니다. 세마포어로 팬아웃을 제한하고, 각 요청에 자체 타임아웃 컨텍스트를 부여하며, 취소가 실행되기 전에 본문을 읽고 닫으십시오:

```go
sem := make(chan struct{}, 20) // cap concurrency
var wg sync.WaitGroup

for _, u := range urls {
  wg.Add(1)
  sem <- struct{}{}
  go func(u string) {
    defer wg.Done()
    defer func() { <-sem }()

    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancel()

    req, _ := http.NewRequestWithContext(ctx, "GET", u, nil)
    resp, err := client.Do(req)
    if err != nil {
      return // rotate/log; do not retry the same burned exit
    }
    io.Copy(io.Discard, resp.Body) // drain BEFORE cancel fires
    resp.Body.Close()
  }(u)
}
wg.Wait()
```

생산 노트 두 가지. `MaxIdleConnsPerHost`를 충분히 높게 설정하여 큰 풀이 파일 디스크립터를 소진하는 대신 연결을 재사용하도록 하십시오 - 몇 백 개의 요청 후 `EOF`가 발생하는 일반적인 원인입니다. 그리고 실패 시 동일한 출구를 다시 시도하는 대신 출구를 회전하십시오; 죽은 주거 IP는 다음 시도에서 복구되지 않습니다. 수백만 페이지의 더 넓은 설계를 위해, 우리의 [대규모 스크래핑 아키텍처](https://quantumproxies.io/blog/large-scale-web-scraping-architecture) 가이드는 큐, 중복 제거 및 프록시 계층화를 다룹니다.

## 하나의 클라이언트를 재사용하고 전송을 조정하십시오

`http.Client`를 한 번 생성하고 모든 고루틴에 걸쳐 공유하십시오 - 이는 동시 사용에 안전하며, 연결 풀이 Go 스크래퍼를 빠르게 만드는 요소입니다. 요청당 새로운 클라이언트는 keep-alive를 버리고 매번 새로운 TLS 핸드셰이크를 강제합니다. 세 가지 `Transport` 필드가 부하 시 중요합니다: `MaxIdleConns` 및 `MaxIdleConnsPerHost`는 풀의 크기를 조정하고, `IdleConnTimeout`은 회전 프록시 풀이 죽은 출구를 고정하지 않도록 오래된 연결을 은퇴시킵니다. 이를 명시적으로 설정하십시오 - 표준 라이브러리 기본값은 브라우저에 맞춰져 있으며, 수천 개의 요청을 분당 실행하는 크롤러에는 맞지 않습니다.

![원인 열이 강조된 Go 프록시 오류 표. 카드는 각 오류를 원인과 연결합니다: proxyconnect나 407은 잘못된 자격 증명, context canceled는 본문을 읽기 전에 cancel()이 실행된 것, EOF는 MaxIdleConnsPerHost가 너무 낮은 것, HTTP_PROXY가 무시되면 사용자 정의 Transport, 멈춤은 타임아웃이 없는 것입니다.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335148/blog/generated/qp-b9-009-diag2.png)
*거의 모든 Go 프록시 실패는 이 다섯 가지 중 하나에 매핑됩니다 - 동일한 출구를 다시 시도하기 전에 원인 열을 확인하십시오.*

## 전송을 수작업으로 롤링하는 것을 언제 멈출지

위의 코드는 깨끗한 대상에 충분합니다. 사이트가 Cloudflare, TLS 지문 인식 또는 JavaScript 렌더링을 추가하면, 원시 Go TLS 핸드셰이크는 Chrome의 것과 전혀 다르게 보이며, 그 불일치를 수정할 수 있는 프록시는 없습니다. 그 시점에서는 실제 브라우저 지문을 가지고 IP를 회전하고 JS를 렌더링하는 [Scraper API](https://quantumproxies.io/scraper-api)가 스택을 수작업으로 유지하는 것보다 더 적은 코드와 더 높은 성공률을 제공합니다. 새로운 스크래퍼를 위한 언어를 선택 중이라면, 우리의 [웹 스크래핑을 위한 최고의 프록시](https://quantumproxies.io/blog/best-proxies-for-web-scraping) 게시물은 스택 간의 트레이드오프를 매핑합니다.

[렌더링 및 회전을 Scraper API에 오프로드하십시오](https://quantumproxies.io/scraper-api)

## 자주 묻는 질문

### Go HTTP 클라이언트에 프록시를 어떻게 설정하나요?

`http.Transport`를 `Proxy` 필드와 함께 빌드하고 `http.Client`에 전달하십시오. 단일 정적 프록시에는 `http.ProxyURL(u)`을 사용하거나, 요청당 출구를 선택하려면 `func(*http.Request) (*url.URL, error)`을 사용하십시오. 자격 증명을 `http://user:pass@host:port` 형식으로 URL에 넣고 항상 `Client.Timeout`을 설정하십시오.

### Go가 내 HTTP_PROXY 변수를 무시하는 이유는 무엇인가요?

명시적인 `Proxy` 함수를 가진 사용자 정의 `Transport`를 빌드했기 때문입니다. 이는 기본 `ProxyFromEnvironment` 동작을 대체합니다. 환경 변수는 `http.DefaultTransport`를 사용하거나 `Proxy: http.ProxyFromEnvironment`를 직접 설정할 때만 적용됩니다. 또한 루프백 및 localhost 요청은 항상 프록시를 우회한다는 점도 유의하십시오.

### Go 프록시에서 'context canceled' 오류의 원인은 무엇인가요?

요청의 컨텍스트가 응답 본문이 읽히기 전에 취소되었습니다 - 일반적으로 만료된 요청별 타임아웃이나 잘못된 `defer`로 인해 너무 일찍 실행된 `cancel()` 때문입니다. 각 요청에 자체 타임아웃 컨텍스트를 부여하고, 연기된 취소가 실행되기 전에 본문을 소진하고 닫으십시오. 하나의 취소 가능한 컨텍스트를 여러 고루틴에 걸쳐 공유하지 마십시오.

### Go는 SOCKS5 프록시를 지원하나요?

표준 라이브러리에는 없지만, `golang.org/x/net/proxy`가 SOCKS5 다이얼러를 추가합니다. `proxy.SOCKS5`로 생성하고 `Transport.DialContext`에 연결하여 호스트 이름이 로컬 DNS 조회를 통해 누출되지 않고 프록시에서 해결되도록 하십시오.

이것이 전체 클라이언트 측 그림입니다: 설정을 위한 하나의 `Transport` 필드, 회전을 위한 함수, SOCKS5를 위한 `x/net/proxy`, 예의 바른 크롤링을 위한 `colly`, 그리고 동시성을 위한 세마포어와 요청별 컨텍스트입니다. 깨끗한 회전 풀로 시작하면 대부분의 오류 목록을 발생하기 전에 건너뛸 수 있습니다.

[QuantumProxies 주거 프록시로 시작하십시오](https://quantumproxies.io/residential-proxies)
