Quay lại danh sách
Cấu hình proxy cho Go (Golang) net/http để scraping an toàn

Cấu hình proxy cho Go (Golang) net/http để scraping an toàn

20 tháng 7, 2026

Giới thiệu

Khi xây dựng trình thu thập dữ liệu (scraper) bằng Go, việc sử dụng proxy không chỉ giúp ẩn địa chỉ IP thực mà còn là yếu tố then chốt để vượt qua các giới hạn tốc độ, chặn IP và cơ chế phát hiện bot. Bài viết này sẽ hướng dẫn bạn cách cấu hình proxy cho gói chuẩn net/http của Go, xoay proxy tự động, xử lý xác thực và kiểm tra kết nối, đồng thời giải thích tại sao mỗi bước lại quan trọng.

1. Tại sao cần proxy trong scraping Go

  • Ẩn IP thực: Giảm nguy cơ bị chặn bởi website mục tiêu.
  • Xoay IP: Tránh kích hoạt cơ chế rate limit dựa trên IP.
  • Truy cập nội dung địa lý: Một số trang chỉ hiển thị nội dung choIP từ определенных стран.
  • Bảo vệ hệ thống: Khi proxy bị tấn công, máy chủ gốc của bạn vẫn an toàn.

Go có thư viện chuẩn net/http mạnh mẽ và linh hoạt. Thay vì dựa vào thư viện bên thứ ba, chúng ta có thể tùy chỉnh http.Transport để chỉ định proxy cho mỗi yêu cầu.

2. Cơ bản về http.Transport và Proxy

http.Transport chịu trách nhiệm quản lý kết nối TCP, TLS và việc truyền dữ liệu qua proxy. Để thiết lập proxy, chúng ta tạo một hàm trả về *url.URL biểu diễn địa chỉ proxy và gán nó vào trường Proxy của Transport.

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

func getProxyFunc(proxyURL string) func(*http.Request) (*url.URL, error) {
    return func(_ *http.Request) (*url.URL, error) {
        return url.Parse(proxyURL)
    }
}

transport := &http.Transport{
    Proxy: getProxyFunc("http://user:[email protected]:3128\)),
}
client := &http.Client{Transport: transport}

2.1 Proxy HTTP vs HTTPS

  • HTTP proxy: Xử lý yêu cầu CONNECT để tạo tunnel qua HTTPS, hoặc forwarding trực tiếp cho HTTP.
  • HTTPS proxy (CONNECT): Yêu cầu client gửi lệnh CONNECT host:port\r\n\r\n để tạo kênh TLS end‑to‑end.

Go's net/http tự động xử lý việc CONNECT khi scheme của URL là httpsProxy trả về một proxy HTTP.

2.2 Xác thực proxy

Nếu proxy yêu cầu xác thực, bạn có thể embed username và password vào URL như trên, hoặc sử dụng ProxyConnectHeader để truyền header Proxy-Authorization.

transport := &http.Transport{
    Proxy: getProxyFunc("http://proxy.example.com:3128\)),
    ProxyConnectHeader: http.Header{
        "Proxy-Authorization": {"Basic " + basicAuth("user", "pass\))},
    },
}

3. Xây dựng danh sách proxy xoay

Để tránh bị chặn, chúng ta cần một danh sách proxy và chọn ngẫu nhiên (hoặc theo vòng quay) cho mỗi request.

3.1 Lưu trữ proxy

Giả sử chúng ta có danh sách ở dạng []string chứa các URL proxy (có thể bao gồm xác thực).

var proxyList = []string{
    "http://proxy1.example.com:3128",
    "http://user:[email protected]:3128",
    "http://proxy3.example.com:3128",
}

3.2 Hàm chọn proxy

Một cách đơn giản là sử dụng rand để chọn một phần tử. Để đảm bảo sự đồng đều, chúng ta có thể sử dụng chỉ số tăng dần modulo độ dài danh sách (round‑robin).

import (
    "math/rand"
    "time"
)

var proxyIndex int

func nextProxy() string {
    proxyIndex = (proxyIndex + 1) % len(proxyList)
    return proxyList[proxyIndex]
}

3.3 Tạo Transport động cho mỗi request

Thay vì sử dụng một Transport固定, chúng ta sẽ tạo một RoundTripper tùy chỉnh gọi nextProxy mỗi lần.

type proxyRoundTripper struct{}

func (p *proxyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
    transport := &http.Transport{
        Proxy: func(_ *http.Request) (*url.URL, error) {
            return url.Parse(nextProxy())
        },
    }
    return transport.RoundTrip(req)
}

client := &http.Client{Transport: &proxyRoundTripper{}}

4. Xử lý lỗi và thử lại

Khi một proxy chết (kết nối timeout, phản hồi 4xx/5xx), chúng ta cần đánh dấu proxy đó là xấu và thử lại với proxy khác.

4.1 Định nghĩa loại lỗi

type badProxyError struct{ proxy string }
func (e *badProxyError) Error() string { return "bad proxy: " + e.proxy }

4.2 Hàm request với redo

func doRequest(client *http.Client, req *http.Request, maxRetries int) (*http.Response, error) {
    var resp *http.Response
    var err error
    for i := 0; i <= maxRetries; i++ {
        resp, err = client.Do(req)
        if err == nil {
            // Kiểm tra mã trạng thái
            if resp.StatusCode >= 200 && resp.StatusCode < 400 {
                return resp, nil
            }
            // Nếu là 429 hoặc 503, coi như proxy bị chặn
            if resp.StatusCode == 429 || resp.StatusCode >= 500 {
                // Đánh dấu proxy hiện tại là xấu (cần lưu trữ ở nơi khác)
                // Để đơn giản, chúng ta chỉ đóng kết nối và thử lại
                resp.Body.Close()
                continue
            }
            // Các lỗi khác (404, 403…) không do proxy => trả về ngay
            return resp, nil
        }
        // Lỗi kết nối => thử proxy khác
        if i == maxRetries {
            return nil, err
        }
        // Đợi ngắn trước khi thử lại
        time.Sleep(200 * time.Millisecond)
    }
    return nil, err
}

5. Ví dụ thực tế: Scraping danh sách sản phẩm từ trang demo

Giả sử chúng ta muốn lấy tiêu đề sản phẩm từ https://example.com/products. Dưới đây là mã hoàn chỉnh sử dụng các thành phần trên.

package main

import (
    "context"
    "fmt"
    "io/ioutil"
    "math/rand"
    "net/http"
    "net/url"
    "time"
)

var proxyList = []string{
    "http://proxy1.example.com:3128",
    "http://user:[email protected]:3128",
    "http://proxy3.example.com:3128",
)

var proxyIndex int

func nextProxy() string {
    proxyIndex = (proxyIndex + 1) % len(proxyList)
    return proxyList[proxyIndex]
}

type proxyRoundTripper struct{}

func (p *proxyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
    transport := &http.Transport{
        Proxy: func(_ *http.Request) (*url.URL, error) {
            return url.Parse(nextProxy())
        },
    }
    return transport.RoundTrip(req)
}

func doRequest(client *http.Client, req *http.Request, maxRetries int) (*http.Response, error) {
    var resp *http.Response
    var err error
    for i := 0; i <= maxRetries; i++ {
        resp, err = client.Do(req)
        if err == nil {
            if resp.StatusCode >= 200 && resp.StatusCode < 400 {
                return resp, nil
            }
            if resp.StatusCode == 429 || resp.StatusCode >= 500 {
                resp.Body.Close()
                time.Sleep(200 * time.Millisecond)
                continue
            }
            return resp, nil
        }
        if i == maxRetries {
            return nil, err
        }
        time.Sleep(200 * time.Millisecond)
    }
    return nil, err
}

func main() {
    rand.Seed(time.Now().UnixNano())
    client := &http.Client{Transport: &proxyRoundTripper{}}

    req, err := http.NewRequest("GET", "https://example.com/products", nil)
    if err != nil {
        panic(err)
    }
    // Thêm User‑Agent để tránh bị chặn ngay
    req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Go‑Scraper/1.0)" )

    resp, err := doRequest(client, req, 3)
    if err != nil {
        fmt.Printf("Lỗi sau khi thử lại: %v\n", err)
        return
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Không thể đọc body: %v\n", err)
        return
    }

    fmt.Printf("Trạng thái: %d\n", resp.StatusCode)
    fmt.Printf("Đługo body: %d bytes\n", len(body))
    // Ở đây bạn có thể parse body với goquery, encoding/xml, etc.
    fmt.Printf("500 ký tự đầu: %.500s\n", string(body))
}

Giải thích code

  • proxyRoundTripper: Tạo mới Transport cho mỗi request, lấy proxy tiếp theo từ danh sách.
  • doRequest: Thực hiện request với tối đa 3 lần thử lại; nếu nhận được 429 hoặc 5xx, coi như proxy bị chặn và thử proxy khác.
  • User‑Agent: Thiết lập header để mô phỏng trình duyệt thực tế, giảm khả năng bị chặn dựa trên đặc điểm header.

6. Tích hợp với RoProxy

Nếu bạn đang sử dụng dịch vụ proxy trả phí như RoProxy, việc chỉ cần thay đổi danh sách proxyList thành các endpoint mà RoProxy cung cấp (kèm username/password hoặc token). RoProxy thường cung cấp:

  • Proxy xoay tự động: Mỗi kết nối sẽ được分配 một IP mới từ pool.
  • Proxy sticky (session): Giữ cùng IP trong một khoảng thời gian, hữu ích khi cần duy trì phiên đăng nhập.
  • Geo‑targeting: Chọn IP từ quốc gia cụ thể để truy cập nội dung địa phương.

Ví dụ cấu hình cho RoProxy (xoay):

var roproxyList = []string{
    "http://user:[email protected]:8000",
    "http://user:[email protected]:8000",
}

Bạn chỉ cần thay proxyList bằng roproxyList và phần xoay sẽ vẫn hoạt động.

7. Những lưu ý quan trọng

  1. Kiểm tra sức khỏe proxy: Định kỳ (mỗi 5‑10 phút) gửi một request nhẹ (ví dụ HEAD tới http://ipinfo.io/ip) để xác định proxy còn sống.
  2. Giới hạn kết nối đồng thời: Quá nhiều goroutine sử dụng cùng một proxy có thể khiến proxy quá tải. Điều chỉnh MaxIdleConnsPerHost trong http.Transport hoặc sử dụng pool proxy riêng cho mỗi worker.
  3. Xử lý TLS: Nếu bạn cần kết nối tới các website chỉ cho phép TLS 1.2+, hãy để TLSHandshakeTimeout và cấu hình TLSClientConfig nếu cần.
  4. Log và監控: Ghi lại哪個 proxy được dùng cho mỗi request, mã trạng thái và thời gian phản hồi để phát hiệnproxy xấu sớm.
  5. Tuân thủ pháp luật: Đảm bảo hoạt động scraping tuân thủ điều khoản dịch vụ của trang web và quy định về bảo vệ dữ liệu cá nhân.

8. Kết luận

Việc cấu hình proxy trong Go bằng net/http là một phương pháp linh hoạt, không cần thư viện bên thứ ba và cho phép bạn kiểm soát chính xác cách mỗi request được gửi qua proxy. Kết hợp với danh sách proxy xoay, cơ chế thử lại và việc xử lý lỗi, bạn có thể xây dựng một trình scraper ổn định, giảm khả năng bị chặn và duy trì hiệu suất cao.

Hãy bắt đầu với một danh sách proxy nhỏ, kiểm tra kỹ các trường hợp lỗi, sau đó mở rộng quy mô khi cảm thấy tự tin. Nếu bạn cần một nguồn proxy chất lượng cao với hỗ trợ kỹ thuật và các tính năng như geo‑targeting hoặc sticky sessions, RoProxy là một lựa chọn đáng xem xét.

Chúc bạn thành công trong việc xây dựng các giải pháp scraping mạnh mẽ và bền vững!