背景
使用golang进行业务开发时,通常会遇到需要发起HTTP调用请求,用于获取业务所需的数据进行下一步处理。
golang在标准库中直接提供了net/http包,通过这个包可以很方便的去发起一个HTTP请求。
对于golang的net/http库其使用通常有两种方式:
1. 使用DefaultClient;2. 使用自定义Client。下面来看看两种方式的用法
net/http使用

1. 使用DefalutClient

对于没有高并发的场景下,使用DefaultClient十分简单,能够快速达到目的。下面看一个示例:
resp, err := http.Get("http://www.example.com")
if err != nil {
	log.Fatal(err)
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
	fmt.Println("read body err:", err)
}

fmt.Println("body:", string(body))
这里直接调用net/http封装提供的Get函数,标准库中还封装提供了Post,Head函数。
这些封装函数背后都是使用的DefaultClient。下面看看Get函数的源码:
func Get(url string) (resp *Response, err error) {
	return DefaultClient.Get(url)
}
DefaultClient 是一个全局Client结构,其定义如下:
// DefaultClient is the default Client and is used by Get, Head, and Post.
var DefaultClient = &Client{}

2. 使用自定义Client

实际应用中,为应对各种不同的场景通常需要自定以http Client来满足要求,实现目的。
下面看看自定义client的常见用法:
  • 超时设置
    client := http.Client{
        Timeout: 10 * time.Second,
    }
  • 代理设置
    proxy, _ := url.Parse(proxyUrl)
	tr := &http.Transport{
		Proxy:           http.ProxyURL(proxy),
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}

	client := &http.Client{
		Transport: tr,
		Timeout:   time.Second * 5, //超时时间
	}

	resp, err := client.Get(webUrl)
	if err != nil {
		fmt.Println("出错了", err)
		return
	}

	defer resp.Body.Close()
	body, _ := ioutil.ReadAll(resp.Body)
	fmt.Println(string(body))
  • 连接池设置
    proxy := func(_ *http.Request) (*url.URL, error) {
				return url.Parse("http://" + netproxy.GetAddr())
			}
	httpTransport = &http.Transport{
					Proxy: proxy,
					DialContext: (&net.Dialer{
						Timeout:   30 * time.Second,
						KeepAlive: 30 * time.Second,
					}).DialContext,
					ForceAttemptHTTP2:     true,
					MaxIdleConns:          100,
					IdleConnTimeout:       90 * time.Second,
					TLSHandshakeTimeout:   10 * time.Second,
					ExpectContinueTimeout: 1 * time.Second,
				}
	httpClient = &http.Client{
				Transport: httpTransport,
			}
实际应用中client通常是作为全局变量来使用,初始化一次即可。无需每次请求都重新定义一次,
因为client中底层使用的transport是一个连接池,不同请求会取用一条不同的连接。
连接池
上面说到,http client中transport是一个连接池,其无论是DefaultClient还是自定义Client都是用到的。
默认的transport定义如下:
    // DefaultTransport is the default implementation of Transport and is
    // used by DefaultClient. It establishes network connections as needed
    // and caches them for reuse by subsequent calls. It uses HTTP proxies
    // as directed by the $HTTP_PROXY and $NO_PROXY (or $http_proxy and
    // $no_proxy) environment variables.
    var DefaultTransport RoundTripper = &Transport{
    	Proxy: ProxyFromEnvironment,
    	DialContext: (&net.Dialer{
    		Timeout:   30 * time.Second,
    		KeepAlive: 30 * time.Second,
    	}).DialContext,
    	ForceAttemptHTTP2:     true,
    	MaxIdleConns:          100,
    	IdleConnTimeout:       90 * time.Second,
    	TLSHandshakeTimeout:   10 * time.Second,
    	ExpectContinueTimeout: 1 * time.Second,
    }
    
    // DefaultMaxIdleConnsPerHost is the default value of Transport's
    // MaxIdleConnsPerHost.
    const DefaultMaxIdleConnsPerHost = 2
如果在http client中没有设置transport属性,那么它就会使用默认的transport。在默认中最大的空闲连接数为100,
每个Host最大空闲数为2. 但是默认的配置中只有关于空闲连接的配置,在实际大量并发的情况下会创建很多连接,
进而导致性能急剧下降。
如果需要控制合适的连接数,就需要使用自定义的client和transport,通常根据应用场景需要调整配置参数。
    type HTTPTransportParam struct {
    	MaxIdleConnsPerHost int
    	MaxIdleConns        int
    	MaxConnsPerHost     int
    	IdleConnTimeout     int
    	DialTimeout         int
    	KeepAlive           int
    }
    var httpTrans *HTTPTransportParam
    
    httpTransport = &http.Transport{
		Proxy: proxy,
		DialContext: (&net.Dialer{
			Timeout:   time.Duration(httpTrans.DialTimeout) * time.Millisecond,
			KeepAlive: time.Duration(httpTrans.KeepAlive) * time.Millisecond,
		}).DialContext,
		ForceAttemptHTTP2:     true,
		MaxIdleConns:          httpTrans.MaxIdleConns,
		MaxIdleConnsPerHost:   httpTrans.MaxIdleConnsPerHost,
		MaxConnsPerHost:       httpTrans.MaxConnsPerHost,
		IdleConnTimeout:       time.Duration(httpTrans.IdleConnTimeout) * time.Millisecond,
		TLSHandshakeTimeout:   10 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
	}
	
	httpClient = &http.Client{
		Transport: httpTransport,
	}
一般比较关心的参数分两部分:context参数和连接参数
context参数是指建立连接时涉及的参数包括超时,保活参数
连接参数是指与pool相关的参数包括:最大空闲连接数,最大连接数和空闲超时时间。
尤其是最大连接数,默认是没有限制的,如果并发量大的时候会引起大量的连接,进而导致性能下降。
所以需要根据实际情况合理调整最大连接数参数的配置。另外,这里的最大连接数也只是针对单个host的限制,
暂时没找到限制总连接数及主机连接池的控制入口。

net/http的连接池是默认全局共用的,假如后端主机虽然只有一百多台,如果我有100个协程,
有概率会出现同时针对一主机并发访问,那么一个主机就有100个连接,100个后端主机就会产生10000个连接。 
这问题的概率在生产环境中经常出现。100台没问题,那么更多呢?  单ip在主动连接可用的端口不到65535的…  
所以,大家也要考虑到这问题。 

简单的做法可以在进程的连接数做计数,当达到一定的阈值后,进行短连接请求,  但这样带来的问题是time wait过多,
重复的建连效率也在下降。
总结
golang的标准库中提供了很多使用的库,但在实际使用时需要注意应用场景及合适的使用方式。
默认情况的配置通常只是适用与功能实现,对性能有要求的场景通常需要仔细考量和分析具体的使用方式。