如何在Golang中为http.Get()请求设置超时时间?

178

我正在使用Go语言编写一个URL抓取程序,有一系列需要抓取的URL。我对每个URL发送http.Get()请求并获取响应。

resp,fetch_err := http.Get(url)

我该怎么为每个Get请求设置自定义超时时间? (默认时间太长了,这使得我的抓取器变得非常缓慢。) 我希望我的抓取器在约40-45秒后超时,然后返回“请求超时”,并继续下一个URL。

我该如何实现这个?


1
只是想让大家知道,我发现这种方式更方便(如果存在网络问题,拨号超时可能无法正常工作,至少对我来说是这样):https://blog.golang.org/context - Audrius
@Audrius请问为什么在网络出现问题时,拨号超时无法起作用?我觉得我也遇到了同样的问题。我以为这就是DialTimeout的功效?!? - Jordan
@Jordan 很难说,因为我没有深入研究库代码。我已经发布了我的解决方案。我现在在生产中使用它已经相当长时间了,到目前为止它“只是工作”(tm)。 - Audrius
7个回答

378

据说在 Go 1.3 中,http.Client 有一个超时字段(Timeout field)。

client := http.Client{
    Timeout: 5 * time.Second,
}
client.Get(url)

那对我起了作用。


12
好的,对我来说这就足够了。很高兴我往下滚了一点 :) - James Adam
11
每个请求是否可以设置不同的超时时间? - Arnaud Rinquin
19
当超时时间到达时会发生什么?Get方法会返回一个错误吗?我有点困惑,因为Client的Godoc文档上说:在GetHeadPostDo方法返回后,计时器仍在运行,并且会中断读取Response.Body。那么这是否意味着Get方法或读取Response.Body方法任一一个都可能被错误中断? - Avi Flax
3
http.Client.Timeouthttp.Transport.ResponseHeaderTimeout 之间的区别是什么?
  • http.Client.Timeout 控制整个请求的超时时间,包括连接建立、数据传输和响应接收。如果在此时间内没有完成请求,则出现超时错误。默认为无限制。
  • http.Transport.ResponseHeaderTimeout 控制等待服务器发送响应头的时间。如果在此时间内未收到响应头,则会出现超时错误。默认为 0,表示不设置超时限制。
- Roy Lee
4
根据文档,主要区别之一是:http.Client.Timeout 包括读取响应体所需的时间,而 http.Transport.ResponseHeaderTimeout 不包括它。 - imwill
显示剩余2条评论

57

你需要使用自己的客户端和自己的传输层,其中包含一个自定义的 Dial 函数,该函数将包装 DialTimeout

类似这样(完全未经测试):这个

var timeout = time.Duration(2 * time.Second)

func dialTimeout(network, addr string) (net.Conn, error) {
    return net.DialTimeout(network, addr, timeout)
}

func main() {
    transport := http.Transport{
        Dial: dialTimeout,
    }

    client := http.Client{
        Transport: &transport,
    }

    resp, err := client.Get("http://some.url")
}

使用net.DialTimeout相对于zzzz的回答中描述的Transport.ResponseHeaderTimeout有什么优势? - Daniele B
5
@Daniel B: 你问的问题不对。这不是关于优点而是关于每种代码的作用。如果服务器无法拨号,DialTimeouts会跳入,而其他超时则在已建立连接的情况下执行某些操作花费太长时间。如果你的目标服务器快速建立连接,但后来开始限制你的速度,那么 dial timeout 将没有帮助。 - Volker
1
@Volker,感谢您的回答。实际上我也意识到了:看起来Transport.ResponseHeaderTimeout设置了读取超时时间,即连接建立后的超时时间,而您的是拨号超时时间。dmichael的解决方案处理了拨号超时和读取超时两个问题。 - Daniele B
不需要进行“编辑”的字符:右括号需要移动 time.Duration(2 * time.Second) --> time.Duration(2) * time.Second - Jonno
2
@Jonno:Go语言中没有强制类型转换,只有类型转换。 - Volker
显示剩余3条评论

53

如果您想根据请求执行它,则由于简洁起见,错误处理被忽略:

ctx, cncl := context.WithTimeout(context.Background(), time.Second*3)
defer cncl()

req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://google.com", nil)

resp, _ := http.DefaultClient.Do(req)

2
额外信息:根据文档,Context所施加的截止日期也包括读取Body,类似于http.Client.Timeout - kubanczyk
5
对于 Go 1.7+ 版本,该答案应该被接受。对于 Go 1.13+ 版本,可以使用NewRequestWithContext进行略微缩短。 - kubanczyk

36

补充Volker的回答,如果您还想设置读写超时时间以及连接超时时间,可以像下面这样操作

package httpclient

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

func TimeoutDialer(cTimeout time.Duration, rwTimeout time.Duration) func(net, addr string) (c net.Conn, err error) {
    return func(netw, addr string) (net.Conn, error) {
        conn, err := net.DialTimeout(netw, addr, cTimeout)
        if err != nil {
            return nil, err
        }
        conn.SetDeadline(time.Now().Add(rwTimeout))
        return conn, nil
    }
}

func NewTimeoutClient(connectTimeout time.Duration, readWriteTimeout time.Duration) *http.Client {

    return &http.Client{
        Transport: &http.Transport{
            Dial: TimeoutDialer(connectTimeout, readWriteTimeout),
        },
    }
}

这段代码已经通过测试并在生产环境中运行。完整的包含测试代码的要点在此处可用: https://gist.github.com/dmichael/5710968

请注意,由于conn.SetDeadline引用了time.Now()之后的一个时间点,因此每个请求都需要创建一个新的客户端。


你难道不应该检查conn.SetDeadline的返回值吗? - Eric Urban
3
这个超时时间无法与保持连接的连接一起使用,而这是默认设置,我想大多数人都应该使用它。以下是我想出来解决此问题的方法:http://gist.github.com/seantalts/11266762 - xitrium
感谢 @xitrium 和 Eric 的额外贡献。 - dmichael
我觉得我们不需要为每个请求创建一个新的客户端,这与你所说的不同。因为 Dial 是一个函数,我认为每次在同一客户端中发送请求时它都会被调用。 - A-letubby
你确定每次都需要一个新的客户端吗?每次拨号时,它将使用TimeoutDialer构建的函数而不是net.Dial。这是一个新的连接,每次从一个新的time.Now()调用中评估截止日期。 - Blake Caldwell

22

Go的http模块有几个客户端超时,当前的答案中有一些关于这些超时的示例。

下面是一张图片,用于说明客户端超时情况,请参见完整指南:Go net/http超时 enter image description here

设置HTTP请求超时有两种方法:

  • http.Client

client := http.Client{
    Timeout: 3 * time.Second,
}
resp, err := client.Do(req)
  • 上下文
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL)

它们之间的区别

  • 使用上下文是请求特定的,而使用客户端超时可能适用于传递给Do方法的所有请求客户端具有。
  • 如果您想要为每个请求专门设置deadline/timeout,则使用上下文,否则,如果您想为每个出站请求使用1个超时,则使用客户端超时就足够了。

12

一种快速而简单的方法:

http.DefaultTransport.(*http.Transport).ResponseHeaderTimeout = time.Second * 45

这会改变全局状态而没有任何协调。但如果你的url获取器可能可以接受,否则请创建http.RoundTripper的私有实例:

var myTransport http.RoundTripper = &http.Transport{
        Proxy:                 http.ProxyFromEnvironment,
        ResponseHeaderTimeout: time.Second * 45,
}

var myClient = &http.Client{Transport: myTransport}

resp, err := myClient.Get(url)
...

以上内容均未经过测试。


请任何人纠正我,但看起来ResponseHeaderTimeout是关于读取超时的,也就是在连接建立后的超时时间。最全面的解决方案似乎是@dmichael提供的,因为它允许设置拨号超时和读取超时。 - Daniele B
http.DefaultTransport.(*http.Transport).ResponseHeaderTimeout = time.Second * 45 这句话对我在编写请求超时测试方面帮助很大。非常感谢。 - lee

0
timeout := time.Duration(5 * time.Second)
transport := &http.Transport{Proxy: http.ProxyURL(proxyUrl), ResponseHeaderTimeout:timeout}

这可能有所帮助,但请注意ResponseHeaderTimeout仅在建立连接之后开始计时。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接