一、简介

Golang诞生已经超过十个年头了,发展得愈发完善,其简单方便的协程并发机制使得其在爬虫领域有着一定的天赋。

首先我们来看一看,Golang相对于Python这个爬虫领域的传统强者,有哪些优点和缺点。

优点:

  • 完善简便的协程并发机制
  • 并发数量大
  • 占用资源少
  • 运行速度更快
  • 部署方便

缺点:

  • 数据处理比较繁琐
  • 成熟工具不是很多
  • 资料较少
  • 实现相同逻辑需要的代码更多

由于Golang本身静态语言的特性,和其特别的异常处理方式等等原因,在发起较复杂的请求时需要的代码量自然会比Python多不少,但是其并发的数量也是远超Python的,所以两者应用的场景并不十分相同,我们可以根据需要灵活的选择。

在刚刚接触Golang的http包时,觉得其非常的方便,发起请求只需要一行代码:

http.Get("https://www.baidu.com")
requestshttp.Get
net

所以本篇文章的目的,是为了让那些使用Golang的朋友,对如何使用Golang发起请求有一个比较全面的了解。

注1:Golang中文官网的文档版本比较低,有些地方与最新版本不同,有条件的同学可以爬爬梯子,去golang.org英文官网看文档。

注2:文中代码为了简洁,省略掉了异常处理的部分,实际使用时需要按情况加上。

二、简单请求

nethttpurl
import (
    "net/http"
    "net/url"
)
http
resp, err := http.Get("http://example.com/")
...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...
resp, err := http.PostForm("http://example.com/form",
    url.Values{"key": {"Value"}, "id": {"123"}})

可以看到,我们非常简单的就发起了请求并获得了响应,这里需要注意一点的是,获得的响应body需要我们手动关闭:

resp, err := http.Get("http://example.com/")
if err != nil {
    // 处理异常
}
defer resp.Body.Close()  // 函数结束时关闭Body
body, err := ioutil.ReadAll(resp.Body)  // 读取Body
// ...

这样的请求方式是非常方便的,但是当我们需要定制我们请求的其他参数时,就必须要使用其他组件了。

三、Client

Clienthttp
type Client struct {
    Transport     RoundTripper
    CheckRedirect func(req *Request, via []*Request) error
    Jar           CookieJar
    Timeout       time.Duration // Go 1.3
}

首先是生成Client对象:

client := &http.Client{}

Client也有一些简便的请求方法,如:

resp, err := client.Get("http://example.com")
http.GetRequestDo

3.1. 设置超时

这是一张说明Client超时的控制范围的图:

http.Client.Timeout
http.Clienthttp.Client.Timeout
client := &http.Client{
    Timeout: 15 * time.Second
}

还有一些更细粒度的超时控制:

net.Dialer.Timeouthttp.Transport.TLSHandshakeTimeouthttp.Transport.ResponseHeaderTimeouthttp.Transport.ExpectContinueTimeoutExpect: 100-continue
Transport
c := &http.Client{  
    Transport: &http.Transport{
        DialContext: (&net.Dialer{
                Timeout:   30 * time.Second,
                KeepAlive: 30 * time.Second,
        }).DialContext,
        TLSHandshakeTimeout:   10 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
    },
}
Docontext

3.2. 控制重定向

CheckRedirectdefaultCheckRedirect

默认的转发策略是最多转发10次。

AuthorizationWWW-AuthenticateCookie
http
  • 301 (Moved Permanently)
  • 302 (Found)
  • 303 (See Other)
  • 307 (Temporary Redirect)
  • 308 (Permanent Redirect)

301、302和303请求将会改用Get访问新的请求,而307和308会使用原有的请求方式。

CheckRedirectdefaultCheckRedirect
func defaultCheckRedirect(req *Request, via []*Request) error {
    if len(via) >= 10 {
        return errors.New("stopped after 10 redirects")
    }
    return nil
}
reqviaerrorerror

所以如果需要设置重定向次数,那么复制一份这个函数,修改函数名字和其中if判断的数字,然后在生成Client时设定到Client即可:

client := &http.Client{
    CheckRedirect: yourCheckRedirect,
}

或者:

client := &http.Client{}
client.CheckRedirect = yourCheckRedirect

禁止重定向则可以把判断数字修改为0。最好相应地修改errors中提示的信息。

3.3. CookieJar管理

JarCookieJar
nil
options := cookiejar.Options{
    PublicSuffixList: publicsuffix.List
}
jar, err := cookiejar.New(&options)
client := &http.Client{
    Jar: jar,
}
publicsuffix.Listnil
PublicSuffixListnil
jar, err := cookiejar.New(nil)
client := &http.Client{
    Jar: jar,
}
publicsuffix.List
import "golang.org/x/net/publicsuffix"

四、 Request

这是Golang源码中Request定义的字段,可以看到非常的多,有兴趣的可以去源码或者官方文档看有注释的版本,本文只介绍一些比较重要的字段。

type Request struct {
    Method           string
    URL              *url.URL
    Proto            string // "HTTP/1.0"
    ProtoMajor       int    // 1
    ProtoMinor       int    // 0
    Header           Header
    Body             io.ReadCloser
    GetBody          func() (io.ReadCloser, error)
    ContentLength    int64
    TransferEncoding []string
    Close            bool
    Host             string
    Form             url.Values
    PostForm         url.Values
    MultipartForm    *multipart.Form
    Trailer          Header
    RemoteAddr       string
    RequestURI       string
    TLS              *tls.ConnectionState
    Cancel           <-chan struct{}
    Response         *Response
}
NewRequestNewRequest
func NewRequest(method, url string, body io.Reader) (*Request, error)
methodurlbodynil
Do
req, err := NewRequest("GET", "https://www.baidu.com", nil)
resp, err := client.Do(req)

4.1. Method

请求方法,必备的参数,如果为空字符则表示Get请求。

CONNECT

4.2. URL

一个被解析过的url结构体。

4.3. Proto

HTTP协议版本。

HTTP1.1HTTP2.0HTTP1.1
HTTP2.0golang.org/x/net/http2

4.4. 发起Post请求

PostFormurl.Values
req, err := NewRequest("Post", "https://www.baidu.com", nil)
req.PostForm.Add("key", "value")
io.ReaderNewRequest

4.4. 设置Header

http.Header

可以使用这种方式设置Header:

req, err := NewRequest("Get", "https://www.baidu.com", nil)
req.Header.Add("key", "value")
SetDel

4.5. 添加Cookie

前文我们已经介绍了如何在Client中启用一直使用的CookieJar,使用它可以自动管理获得的Cookie。

AddCookie
func (r *Request) AddCookie(c *Cookie)

要注意的是,其传入的参数是Cookie类型,,以下是此类型包含的属性:

type Cookie struct {
    Name       string
    Value      string
    Path       string
    Domain     string
    Expires    time.Time
    RawExpires string
    MaxAge     int
    Secure     bool
    HttpOnly   bool
    Raw        string
    Unparsed   []string
}
NameValue
c := &http.Cookie{
    Name:  "key",
    Value: "value",
}
req.AddCookie(c)

五、Transport

TransportClientDefaultTransport

Transport承担起了Client中连接池的功能,它会将建立的连接缓存下来,这可能会在访问大量不同网站时,留下太多打开的连接,这可以使用Transport中的方法进行关闭。

Transport
type Transport struct {
    Proxy                  func(*Request) (*url.URL, error)
    DialContext            func(ctx context.Context, network, addr string) (net.Conn, error) // Go 1.7
    Dial                   func(network, addr string) (net.Conn, error)
    DialTLS                func(network, addr string) (net.Conn, error) // Go 1.4
    TLSClientConfig        *tls.Config
    TLSHandshakeTimeout    time.Duration // Go 1.3
    DisableKeepAlives      bool
    DisableCompression     bool
    MaxIdleConns           int // Go 1.7
    MaxIdleConnsPerHost    int
    MaxConnsPerHost        int                                                         // Go 1.11
    IdleConnTimeout        time.Duration                                               // Go 1.7
    ResponseHeaderTimeout  time.Duration                                               // Go 1.1
    ExpectContinueTimeout  time.Duration                                               // Go 1.6
    TLSNextProto           map[string]func(authority string, c *tls.Conn) RoundTripper // Go 1.6
    ProxyConnectHeader     Header                                                      // Go 1.8
    MaxResponseHeaderBytes int64                                                       // Go 1.7
}
TransportClient

5.1. 拨号

Dial
DialDialContextnet.Dialer
type Dialer struct {
    Timeout time.Duration
    Deadline time.Time
    LocalAddr Addr
    DualStack bool
    FallbackDelay time.Duration
    KeepAlive time.Duration
    Resolver *Resolver
    Cancel <-chan struct{}
    Control func(network, address string, c syscall.RawConn) error
}

这其中需要我们设置的并不多,主要是Timeout和KeepAlive。Timeout是Dial这个过程的超时时间,而KeepAlive是连接池中连接的超时时间,如下所示:

trans := &http.Transport{
    DialContext: (&net.Dialer{
        Timeout: 30 * time.Second,
        KeepAlive: 30 * time.Second,
    }).DialContext,
}

5.2. 设置代理

Proxy
package main

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

func main() {
    proxyURL, _ := url.Parse("https://127.0.0.1:1080")
    trans := &http.Transport{
        Proxy: http.ProxyURL(proxyURL),
    }
    client := &http.Client{
        Transport: trans,
    }
    client.Get("https://www.google.com")
}
golang.org/x/net/proxy
package main

import (
    "net/url"
    "net/http"
    "golang.org/x/net/proxy"
)

func main() {
    dialer, err := proxy.SOCKS5("tcp", "127.0.0.1:8080",
        &proxy.Auth{User:"username", Password:"password"},
        &net.Dialer {
            Timeout: 30 * time.Second,
            KeepAlive: 30 * time.Second,
        },
    )
    trans := &http.Transport{
        DialContext: dialer.DialContext
    }
    client := &http.Client{
        Transport: trans,
    }
    client.Get("https://www.google.com")
}
proxy.SOCKS5DialerDialernil

5.3. 连接控制

众所周知,HTTP1.0协议使用的是短连接,而HTTP1.1默认使用的是长连接,使用长连接则可以复用连接,减少建立连接的开销。

Transport
DisableKeepAlives
trans := &http.Transport{
    ...
    DisableKeepAlives: true,
}
MaxConnsPerHost intMaxIdleConns intMaxIdleConnsPerHost intIdleConnTimeout time.Duration

由于Transport负担起了连接池的功能,所以在并发使用时,最好将Transport与Client一起复用,不然可能会造成发起过量的长连接,浪费系统资源。

六、其他

6.1. 设置url参数

在Go的请求方式中,没有给我们提供可以直接设置url参数的方法,所以需要我们自己在url地址中进行拼接。

urlurl.Valuesmap[string][]string
URL := "http://httpbin.org/get"
params := url.Values{
    "key1": {"value"},
    "key2": {"value2", "value3"},
}
URL = URL + "&" + params.Encode()
fmt.Println(URL)
// 输出为:http://httpbin.org/get&key1=value&key2=value2&key2=value3

七、总结

总的来说,Go语言中内置的标准库功能是比较完善的,如果要写一个客户端的话,基本不需要用到标准库之外的内容,其可以控制的请求细节也比较多。

但相较于Python的Requests这类库,需要写的代码依然要多非常多,再加上特别的异常处理机制,在请求过程中要写大量的异常检查语句。需要使用的朋友可以考虑先将请求和异常处理的部分封装以后使用。

八、示例

以下是发起Get请求的一个例子:

// 生成client客户端
client := &http.Client{}
// 生成Request对象
req, err := http.NewRequest("Get", "http://httpbin.org/get", nil)
if err != nil {
    fmt.Println(err)
}
// 添加Header
req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36")
// 发起请求
resp, err := client.Do(req)
if err != nil {
    fmt.Println(err)
}
// 设定关闭响应体
defer resp.Body.Close()
// 读取响应体
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
    fmt.Println(err)
}
fmt.Println(string(body))

参考: