Go语言http.Get()超时设置

Go by 达达 at 2014-02-22

本文中的方案是有缺陷的,本文目前只当成历史记录,完整方案请参考(这篇)[http://1234n.com/?post/mwsw2r]

Go自带的http包中提供了很完整的HTTP客户端和服务端功能。最近项目有几个需求需要从游戏服务端发起HTTP请求来调用运营商提供的接口。用Go语言实现起来超简单,http.Get()调一下就行了。

但是,http.Get()是没提供参数让调用者设置连接和读写的超时,项目在线上就遇到了永久阻塞在http.Get()不返回的情况。

上网找了一下资料,最后解决了这个问题,以下是试验代码,先贴代码再分析原理(gist连接):

//
// How to set timeout for http.Get() in golang
//
package main

import (
    "io"
    "io/ioutil"
    "log"
    "net"
    "net/http"
    "time"
)

func StartTestServer() string {
    http.HandleFunc("/normal", func(w http.ResponseWriter, req *http.Request) {
        time.Sleep(1000 * time.Millisecond)
        io.WriteString(w, "ok")
    })

    http.HandleFunc("/timeout", func(w http.ResponseWriter, req *http.Request) {
        time.Sleep(2500 * time.Millisecond)
        io.WriteString(w, "ok")
    })

    listener, err := net.Listen("tcp", ":0")

    if err != nil {
        log.Fatalf("failed to listen - %s", err.Error())
    }

    go func() {
        err = http.Serve(listener, nil)
        if err != nil {
            log.Fatalf("failed to start HTTP server - %s", err.Error())
        }
    }()

    log.Printf("start http server at http://%s/", listener.Addr())

    return listener.Addr().String()
}

func main() {
    addr := StartTestServer()

    client := &http.Client{
        Transport: &http.Transport{
            Dial: func(netw, addr string) (net.Conn, error) {
                conn, err := net.DialTimeout(netw, addr, time.Second*2)
                if err != nil {
                    return nil, err
                }
                conn.SetDeadline(time.Now().Add(time.Second * 2))
                return conn, nil
            },
            ResponseHeaderTimeout: time.Second * 2,
        },
    }

    // 1st request
    if resp, err := client.Get("http://" + addr + "/normal"); err != nil {
        log.Fatalf("1st request failed - %s", err)
    } else {
        result, err2 := ioutil.ReadAll(resp.Body)
        if err2 != nil {
            log.Fatalf("1st response read failed - %s", err2)
        }
        resp.Body.Close()
        log.Printf("1st request - %s", result)
    }

    // 2nd request
    if _, err := client.Get("http://" + addr + "/timeout"); err == nil {
        log.Fatalf("2nd not timeout")
    } else {
        log.Printf("2nd request - %s", err)
    }

    // 3rd request
    if resp, err := client.Get("http://" + addr + "/normal"); err != nil {
        log.Fatalf("3rd request - %s", err)
    } else {
        result, err2 := ioutil.ReadAll(resp.Body)
        if err2 != nil {
            log.Fatalf("3rd response read failed - %s", err2)
        }
        resp.Body.Close()
        log.Printf("3rd request - %s", result)
    }
}

代码中最主要的是创建http.Client的那一段,其中自定义了http.Client的Transport,而Transport创建时指定了一个拨号回调,在拨号回调中,使用DialTimeout来支持连接超时,当连接成功后,利用SetDeadline来让连接支持读写超时。

http包提供的http.Get实际上调用的是事先创建好的DefaultClient,而DefaultClient使用的则是默认的Transport,这一调用关系很容易从http包的代码中看出来。

默认的Client和Transport都没有做超时设置,所以我们需要自己创建http.Client来实现带超时功能的http客户。

http包还有一个比较容易坑到新手的坑点,就是请求后返回的http.Response,用完必须调用Body.Close(),文档上有写了,但是很容易被忽略,并且Close方法不是在Response类型上的,而是在Body属性上。

如果没有调用Body.Close(),http请求所用的tcp连接就不会释放,最后就会出现连数过多。