go http/client

Go is known for its “self contained battery” and many developers are fond of the feature-rich standard library that comes with Go. Among the Go standard libraries, the net/http package is one of the most popular and commonly used packages, allowing us to generate a medium-performance http server with a few lines of code that supports large concurrency. http.Client is also the most widely used http client, and its performance can meet the needs of most cases. The well-known female gopher Jaana Dogan open source class apache ab http performance testing tool hey is also the direct use of http.Client, rather than using some better performance of third-party libraries (for example: fasthttp).

The simplest way to implement an http client using the http package is as follows (from the official documentation of the http package).

1
2
3
4
resp, err := http.Get("http://example.com/")
...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...

Note: Don’t forget to call defer resp.Body.Close() after a successful Get or Post.

Behind the Get and Post functions of the http package, it is the http package’s native built-in DefaultClient that really completes the http client operation.

1
2
3
// $GOROOT/src/net/http/client.go
// DefaultClient is the default Client and is used by Get, Head, and Post.
var DefaultClient = &Client{}

The following is an example of using DefaultClient. Let’s start by creating a special http server.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// github.com/bigwhite/experiments/blob/master/http-client/default-client/server.go

package main

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

func Index(w http.ResponseWriter, r *http.Request) {
    log.Println("receive a request from:", r.RemoteAddr, r.Header)
    time.Sleep(10 * time.Second)
    w.Write([]byte("ok"))
}

func main() {
    var s = http.Server{
        Addr:    ":8080",
        Handler: http.HandlerFunc(Index),
    }
    s.ListenAndServe()
}

We see that the “difference” of the http server is that it does not reply to the http answer in a hurry, but 10 seconds after receiving the request. Here is the code of our http client side.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// github.com/bigwhite/experiments/blob/master/http-client/default-client/client.go

package main

import (
    "fmt"
    "io"
    "net/http"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(256)
    for i := 0; i < 256; i++ {
        go func() {
            defer wg.Done()
            resp, err := http.Get("http://localhost:8080")
            if err != nil {
                panic(err)
            }
            defer resp.Body.Close()
            body, err := io.ReadAll(resp.Body)
            fmt.Println(string(body))
        }()
    }
    wg.Wait()
}

The above client creates 256 goroutines, each of which establishes a connection to the server, and we start the server and then run the above client program.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$go run server.go
$$go run client.go
panic: Get "http://localhost:8080": dial tcp [::1]:8080: socket: too many open files

goroutine 25 [running]:
main.main.func1(0xc000128280)
    /Users/tonybai/Go/src/github.com/bigwhite/experiments/http-client/default-client/client.go:18 +0x1c7
created by main.main
    /Users/tonybai/Go/src/github.com/bigwhite/experiments/http-client/default-client/client.go:14 +0x78
exit status 2

We see that the client above throws a panic, prompting: too many open file descriptors.

The value of ulimit -n in the above demo environment is 256

Let’s use a diagram to describe the situation in the above example.

go http client

We know that by default, http clients maintain connections and reuse connections to services on the same host. However, due to the context of the server’s 10s delayed reply in the above example, the client does not wait for the reply to come back by default, but tries to establish a new connection to send a new http request. Since the sample runtime environment allows a maximum of 256 open file descriptors per process, a “socket: too many open files” error occurs when the client establishes a connection to the server at a later stage.

2. Defining http client instances for small-scale applications

So how do we control the behavior of the client to avoid completing client-sending tasks in resource-constrained contexts? We do this by setting the relevant properties of http.DefaultClient, but DefaultClient is a package-level variable that is shared throughout the application, and once its properties are modified, other packages that use the http default client will also be affected. Therefore a better solution would be to define an instance of the http client for a small range of applications.

Code.

1
2
3
4
resp, err := http.Get("http://example.com/")
...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...

Equivalent to the following code.

1
2
3
4
5
client := &http.Client{} // 自定义一个http客户端实例
resp, err := client.Get("http://example.com/")
...
resp, err := client.Post("http://example.com/upload", "image/jpeg", &buf)
...

The difference is that the application of our custom http.Client instance is limited to the above specific scope and will not have any effect on other packages using the http default client. However, at this time the behavior of our custom http.Client instance client is no different from that of DefaultClient, to solve the problem of the above example panic, we also need to do a progressive behavior customization of the new custom client instance.

3. customize the maximum number of connections to a particular host

The biggest problem with the above example is that the number of connections to the server is not controlled. Even if the maximum number of file descriptors that can be opened per process is increased, the client may still encounter a bottleneck of 65535 maximum outgoing connections (client socket port exhaustion), so a strict client needs to set a limit on the maximum number of connections to a particular host.

So, how does http.Client control the maximum number of connections to a particular host? The Client structure of the http package is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//$GOROOT/src/net/http/client.go

type Client struct {
        // Transport specifies the mechanism by which individual
        // HTTP requests are made.
        // If nil, DefaultTransport is used.
        Transport RoundTripper

    CheckRedirect func(req *Request, via []*Request) error
    Jar CookieJar
    Timeout time.Duration

There are four fields in the Client structure, and the one that controls the Client’s connection behavior is the Transport field. If the value of Transport is nil, then the Client’s connection behavior follows the DefaultTransport setting.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// $GOROOT/src/net/http/transport.go

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,
}

However, in this DefaultTransport “configuration”, there is no setting for the maximum number of connections to a particular host, because in the Transport structure, the field that plays this role is MaxConnsPerHost.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// $GOROOT/src/net/http/transport.go

type Transport struct {
    ... ...

    // MaxConnsPerHost optionally limits the total number of
        // connections per host, including connections in the dialing,
        // active, and idle states. On limit violation, dials will block.
        //
        // Zero means no limit.
        MaxConnsPerHost int
    ... ...
}

Let’s transform the above example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// github.com/bigwhite/experiments/blob/master/http-client/client-with-maxconnsperhost/client.go

package main

import (
    "fmt"
    "io"
    "net/http"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(256)
    tr := &http.Transport{
        MaxConnsPerHost: 5,
    }
    client := http.Client{
        Transport: tr,
    }
    for i := 0; i < 256; i++ {
        go func(i int) {
            defer wg.Done()
            resp, err := client.Get("http://localhost:8080")
            if err != nil {
                panic(err)
            }
            defer resp.Body.Close()
            body, err := io.ReadAll(resp.Body)
            fmt.Printf("g-%d: %s\n", i, string(body))
        }(i)
    }
    wg.Wait()
}

Instead of using DefaultClient, the above code customizes a new Client instance and sets the Transport field of that instance to our new Transport instance with the MaxConsPerHost field set. Start the server and execute the above client.go, we see the following result from the server side.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$go run server.go

receive a request from: [::1]:63677 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63675 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63676 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63673 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63674 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:63673 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63675 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63674 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63676 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63677 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:63677 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63674 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63676 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63675 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:63673 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

We see: the client establishes a total of 5 connections to the server (client port numbers from 63673 to 63677), and every 10s the client reuses these 5 connections to send the next batch of requests.

The http.Transport maintains a counter to each server host connsPerHost and a request waiting queue.

1
2
3
4
5
6
7
8
// $GOROOT/src/net/http/transport.go
type Transport struct {
    ... ...
        connsPerHostMu   sync.Mutex
        connsPerHost     map[connectMethodKey]int
        connsPerHostWait map[connectMethodKey]wantConnQueue // waiting getConns
    ... ...
}

The Transport structure uses a connectMethodKey structure as the key.

1
2
3
4
5
// $GOROOT/src/net/http/transport.go
type connectMethodKey struct {
        proxy, scheme, addr string
        onlyH1              bool
}

We see that connectMethodKey uses a quadruplet (proxy, scheme, addr, onlyH1) to uniquely identify a “host”. Usually for a Client instance, proxy, scheme and onlyH1 are the same, but the difference is the addr (ip+port), so in effect it is the host that is differentiated by addr. Let’s also use a diagram to illustrate this situation.

http client

4. set the size of the idle pool

I wonder if you think about this point: when the five links to a host in the above example are not so busy, is it not a waste of resources to keep the five links? At least the client port and the server’s file descriptor resources are used up. Can we make the client reduce the number of links it keeps to the server during idle time? We can do this with the MaxIdleConnsPerHost field in the Transport structure type.

In fact, if you do not explicitly set MaxIdleConnsPerHost, the http package will also use its default value (2).

1
2
3
4
5
// $GOROOT/src/net/http/transport.go

// DefaultMaxIdleConnsPerHost is the default value of Transport's
// MaxIdleConnsPerHost.
const DefaultMaxIdleConnsPerHost = 2

Let’s use an example to verify this behavior of http.Client!

First, we change the behavior of the server side from “wait 10s” to an immediate answer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// github.com/bigwhite/experiments/blob/master/http-client/client-with-maxidleconnsperhost/server.go

package main

import (
    "fmt"
    "net/http"
)

func Index(w http.ResponseWriter, r *http.Request) {
    fmt.Println("receive a request from:", r.RemoteAddr, r.Header)
    w.Write([]byte("ok"))
}

func main() {
    var s = http.Server{
        Addr:    ":8080",
        Handler: http.HandlerFunc(Index),
    }
    s.ListenAndServe()
}

As for the client, we need to elaborate a bit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// github.com/bigwhite/experiments/blob/master/http-client/client-with-maxidleconnsperhost/client.go
package main
import (
    "fmt"
    "io"
    "net/http"
    "sync"
    "time"
)
func main() {
    var wg sync.WaitGroup
    wg.Add(5)
    tr := &http.Transport{
        MaxConnsPerHost:     5,
        MaxIdleConnsPerHost: 3,
    }
    client := http.Client{
        Transport: tr,
    }
    for i := 0; i < 5; i++ {
        go func(i int) {
            defer wg.Done()
            resp, err := client.Get("http://localhost:8080")
            if err != nil {
                panic(err)
            }
            defer resp.Body.Close()
            body, err := io.ReadAll(resp.Body)
            fmt.Printf("g-%d: %s\n", i, string(body))
        }(i)
    }
    wg.Wait()
   time.Sleep(10 * time.Second)
   wg.Add(5)
    for i := 0; i < 5; i++ {
    go func(i int) {
            defer wg.Done()
            for i := 0; i < 100; i++ {
                resp, err := client.Get("http://localhost:8080")
                if err != nil {
                    panic(err)
                }
                defer resp.Body.Close()
                body, err := io.ReadAll(resp.Body)
                fmt.Printf("g-%d: %s\n", i+10, string(body))
                time.Sleep(time.Second)
            }
        }(i)
    }
    wg.Wait()
}

We first create a busy sending behavior (21~32 lines), so that the client side to build a full 5 connections; then wait for 10s, that is, let the client idle; after that, then build 5 groutines to send requests to the server side at the rate of one per second (not busy rhythm), let’s see the output of the server side after the program runs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$go run server.go
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56246 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56245 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

... ...

Let’s analyze it.

  • The first five lines of output are the five different connections established on the “busy” client side, with client port numbers ranging from 56242 to 56246.
  • The second part of the five lines of output is the “non-busy time” client using the idle pool of connections sent to the request, the key point is the source port number of these five requests: 56242, 56243 and 56244, five requests using the three long-established alias connection.
  • The later parts use the same three long-established active connections.

This is the role of MaxIdleConnsPerHost: the initial five connections established in “busy time” are put into idle state when the client enters idle time. But the value of MaxIdleConnsPerHost is 3, which means that only 3 connections can enter the idle pool, while the other two will be closed out. So the three connections with source port numbers 56242, 56243 and 56244 are kept.

The following is a diagram of the example in this section.

golang http client

Unlike MaxIdleConnsPerHost, which is only for a particular host, MaxIdleConns is the sum of all connections in the idle pool for the entire Client, and this sum cannot exceed MaxIdleConns.

5. Clear connections in the idle pool

If there is no other setting, then a Client to a host will keep at least DefaultMaxIdleConnsPerHost idle connections (provided that 2 or more connections have been established before), but if the Client keeps no traffic against the host, then the connections in the idle pool is also a waste of resources. So Transport provides the IdleConnTimeout field to time out the long connections in the idle pool. The following example replicates the server above, but with client.go in the following form.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// github.com/bigwhite/experiments/blob/master/http-client/client-with-idleconntimeout/client.go
package main
import (
    "fmt"
    "io"
    "net/http"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(5)
    tr := &http.Transport{
        MaxConnsPerHost:     5,
        MaxIdleConnsPerHost: 3,
        IdleConnTimeout:     10 * time.Second,
    }
    client := http.Client{
        Transport: tr,
    }
    for i := 0; i < 5; i++ {
        go func(i int) {
            defer wg.Done()
            resp, err := client.Get("http://localhost:8080")
            if err != nil {
                panic(err)
            }
            defer resp.Body.Close()
            body, err := io.ReadAll(resp.Body)
            fmt.Printf("g-%d: %s\n", i, string(body))
       }(i)
    }
    wg.Wait()
   time.Sleep(5 * time.Second)
   wg.Add(5)
    for i := 0; i < 5; i++ {
        go func(i int) {
            defer wg.Done()
            for i := 0; i < 2; i++ {
                resp, err := client.Get("http://localhost:8080")
                if err != nil {
                    panic(err)
                }
                defer resp.Body.Close()
                body, err := io.ReadAll(resp.Body)
                fmt.Printf("g-%d: %s\n", i+10, string(body))
                 time.Sleep(time.Second)
            }
       }(i)
    }
    time.Sleep(15 * time.Second)
    wg.Add(5)
    for i := 0; i < 5; i++ {
        go func(i int) {
            defer wg.Done()
            for i := 0; i < 100; i++ {
                resp, err := client.Get("http://localhost:8080")
                if err != nil {
                    panic(err)
                }
                defer resp.Body.Close()
                body, err := io.ReadAll(resp.Body)
                fmt.Printf("g-%d: %s\n", i+20, string(body))
                time.Sleep(time.Second)
            }
        }(i)
    }
    wg.Wait()
}

This client.go code is divided into three parts: first, as in the previous example, we first create a busy sending behavior (lines 22~33), so that the client side builds 5 connections; then wait for 5s, i.e., let the client idle; after that, we create 5 goroutines to send requests to the server side at the rate of one per second (not busy rhythm ); the third part is also to wait for 15s first, and then create 5 goroutines to send requests to the server side at a non-busy pace. Let’s see the output of the server side after the program runs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$go run server.go

receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52488 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52486 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52485 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52488 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52488 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:52542 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52544 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52545 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52543 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52546 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

receive a request from: [::1]:52542 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52544 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52545 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52542 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52544 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]

... ...

Here are 5 excerpts of the output. As expected, the first segment client establishes 5 connections to the server (client’s port number from 52484 to 52487); after a pause of 5s, the newly created 5 goroutines send requests to the server through the three held connections in the idle pool (second and third segments, port numbers: 52484, 52487, 52488 ); after that it pauses for 15s, and the three connections in the idle pool are also closed out due to the IdleConnTimeout set. At this point, the client will re-establish the connection (fourth segment, port number 52542~52546) and the last segment will start sending requests to the server again through the three maintained connections in the idle pool (port numbers: 52542, 52544 and 52545).

6. Other controls

If you feel that the idle pool timeout cleanup will still take up “resources” for a little while, you can use Transport’s DisableKeepAlives to make each request create a new connection, i.e., not reuse the keep-alive connection. Of course, the loss of frequent new connections when the control is set to busy is greater than taking up some “resources”. For an example, see http://github.com/bigwhite/experiments/blob/master/http-client/client-with-disablekeepalives, which I won’t post here.

In addition, the server’s behavior of waiting 10s for an answer as in the example at the beginning of this article is not acceptable to all clients. In order to limit the answer to return in time, the client can set a timeout to wait for an answer, and if the timeout expires, the client will return a failure. http. Examples can be found at http://github.com/bigwhite/experiments/blob/master/http-client/client-with-timeout, which is also not posted here.