Go泛型语法又出“幺蛾子”:引入type set概念和移除type list中的type关键字

本文永久链接 – https://tonybai.com/2021/04/07/go-generics-use-type-sets-to-remove-type-keyword

近日,Go泛型语法负责人之一的Ian Lance Taylor发布了一个issue,说明go团队想引入新的type set概念,并去除原Go泛型方案中置于interface定义中的type list中的type关键字。

对于Go泛型来龙去脉不是很了解的童鞋,可以先去看看我看看我之前的文章:《能力越大,责任越大” – Go语言之父详解将于Go 1.18发布的Go泛型》。在那篇文章的结尾,Go设计团队对自己的Go泛型设计方案中的几个方面给出了自己的满意度评价,其中唯一让团队感觉还不是很完美的就是“Type lists in interfaces”:

1. 何为Type lists in interfaces

我们先来说说何为Type lists in interfaces!当前Go泛型方案使用interface类型用于表达对类型参数(type parameters)的约束(constraints),比如:

type MyC1 interface {
    M1()
}

func F1[T MyC1](t T) {

}

在上述代码中,我们使用interface MyC1作为类型参数(type parameters)的约束,对于F1函数而言,所有满足MyC1接口的类型都可以作为其类型参数的实参传入:

type MyT1 string
func(t1 *MyT1) M1() {}

var t1 = new(MyT1)
F1(t1)

*MyT1实现了MyC1接口,于是我们可以将其实例(t1)传给F1。Go泛型的自动类型推导会将T的实参置为*MyT1。

完整程序如下:

// https://go2goplay.golang.org/p/WPCvmwkxcEL
package main

import (
    "fmt"
)

type MyC1 interface {
    M1()
}

func F1[T MyC1](t T) {
    fmt.Printf("%T\n", t)
}

type MyT1 string

func (t1 *MyT1) M1() {

}

func main() {
    var t1 = new(MyT1)
    F1(t1) // *main.MyT1
}

对于自定义类型,通过实现接口的方法集合即可满足接口,对于类型参数可以是原生类型的情况,我们无法通过这种方式实现,于是Go团队将type list加入到interface接口中,仅用作泛型类型参数的约束检查

type MyC2 interface {
    type int, int32, int64
}

func F2[T MyC2](t T) {
    fmt.Printf("%T\n", t)
}

func main() {
    var t2 string
    F2(t2) // string
}

而MyMC2中的:

    type int, int32, int64

就是所谓的”type list”。

如果一个interface定义中既有method也有type list,那么要满足这个interface类型,则作为类型参数实参的类型既必须在type list中(或其underlying type在type list中),又必须实现接口类型的所有方法:

// https://go2goplay.golang.org/p/rE8mGH0lHWm
package main

import (
    "fmt"
)

type MyC3 interface {
    M3()
    type int, string, float64
}

func F3[T MyC3](t T) {
    fmt.Printf("%T\n", t)
}

type MyT3 string

func (t3 MyT3) M3() {

}

func main() {
    t3 := MyT3("hello")
    F3(t3) // main.MyT3
}

细心的童鞋会发现:拥有type list的interface仅能用于做为类型参数的约束,而不能像普通interface类型那样使用:

// https://go2goplay.golang.org/p/mJoEYrceBSL
package main

type MyC3 interface {
    M3()
    type int, string, float64
}

func main() {
    var i3 MyC3 // type checking failed for main
                    // prog.go2:9:9: interface contains type constraints (int, string, float64)
    _ = i3
}

这种gap(缝隙)始终让Go核心团队的开发人员感到“不爽”,那么能否将两者融合在一起呢?即放开对包含type list的interface类型仅能做constraint的限制,让其和普通interface一样使用。这次引入的type set应该是解决这个问题的一个前提。但在这个新proposal中,核心团队还没有将这个问题作为重点,只能算作是为以后留个作业吧。

2. 引入type set概念

Ian Lance Taylor发布的这个issue主要就是想引入type set概念,并用新语法等价替代原泛型proposal中的type list,新语法去除了原type list中的type关键字

于是go团队试图这样来做:

// 当前的type list
type SignedInteger interface {
    type int, int8, int16, int32, int64
}

// type set理念下的新语法
type SignedInteger interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

我们看到新语法中去掉了原先type list中的type关键字,类型间的间隔也由逗号改为了管道符|。按该proposal的原意,管道符(在布尔代数中也表示或)更接近于type list的原意,即可以是int,或int8或….。如果仅仅是变成了如下改进的语法:

type SignedInteger interface {
    int | int8 | int16 | int32 | int64
}

估计大家也没多大意见。但是偏偏引入了“~”这个前缀。~int与int有什么区别呢?要搞清楚区别就要先来看看Ian新引入的type set概念了。

什么是type set(类型集合)?Ian给出了此概念的定义:

  • 每个类型都有一个type set。
  • 非接口类型的类型的type set中仅包含其自身。比如非接口类型T,它的type set中唯一的元素就是它自身:{T};
  • 对于一个普通的、没有type list的普通接口类型来说,它的type set是一个无限集合。所有实现了该接口类型所有方法的类型都是该集合的一个元素,另外由于该接口类型本身也声明了其所有方法,因此接口类型自身也是其Type set的一员。
  • 空接口类型interface{}的type set中则是囊括了所有可能的类型;
  • 这样一来我们来试试用type set概念重新陈述一下一个类型T实现一个接口类型I:即当类型T是接口类型I的type set的一员时,T便实现了接口I;
  • 对于使用嵌入接口类型组合而成的接口类型,其type set就是其所有的嵌入的接口类型的type set的交集。proposal中的举例:type O2 interface{ E1; E2 } ,则02这个接口类型的type set是E1和E2两个接口类型的type set的交集。
  • 一个拥有一个method的接口类型,比如:
type MyInterface1 interface {
    MyMethod()
}

可以看成嵌入一个仅包含MyMethod的接口类型的接口类型:

type MyInterface interface {
    MyMethod()
}
type MyInterface1 interface {
    MyInterface
}
  • 因此,一个带有自身Method的嵌入其他接口类型的接口类型,比如:
type 03 interface {
    E1
    E2
    MyMethod03()
}

它的type set可以看成E1、E2和E3(type E3 interface { MyMethod03})的type set的交集。

3. 替换type list的新语法方案

我们再回到前面提到的新语法方案:

// type set 新语法
type SignedInteger interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

Go开发团队给那些用于作为约束或被嵌入到作为约束的接口类型中的接口类型的定义做了重新描述,称这类接口类型的定义中可以嵌入一些额外的结构,被称为interface elements,其组成如下图:

  • 图中MyInterface是一个仅用于约束或嵌入到作为约束的接口类型中的类型;
  • MyInterface除了拥有自己的方法列表(M1、M2)外,还可以嵌入额外的结构:interface elements,就是T1|T2|~T3|T4…|Tn那一行,这一行即替代了原先方案中的type list;
  • interface elements这一行有三个值得关注的事情:
    • T1、T2、T4、Tn这些仅代表type set仅为自身的类型;
    • ~T3的type set 为所有underlying type为T3的类型,~T3被称为approximation elements;
    • 管道符将这些类型连接在一起,共同构成一个union element,该union element的type set为所有这些类型的type set的并集。

好了现在一切都建立在type set这个概念上。那么当上述接口类型作为类型参数的约束时,要想满足该约束,可以作为类型参数的实参,那么传入的类型应该在作为约束的接口类型的type set中。

有了前面关于type set以及接口嵌入的type set的铺垫,作为约束的接口类型的理解就容易多了。无论是单纯的接口类型还是使用嵌入其他接口组合而成的接口类型,亦或是既包括嵌入也拥有自己的method list的接口类型。

4. 问题

Ian的issue一发出就得到了社区的重点关注,并引来的激烈的讨论,但从头看到尾,似乎大家都有些“跑题”,关于这个proposal的真正疑问在于approximation elements身上:

  • 是否有必要单独拿出approximation elements这个概念

我们回顾一下当前泛型语法作为约束的接口定义所使用的type list语法,看看当前的type list语法中各个类型是否是仅代表自身?

// https://go2goplay.golang.org/p/5VbaSCQ8-Dq
package main

import (
    "fmt"
)

type S1 struct {
    Name string
    Age  int
}

type S2 S1

type MyC4 interface {
    type struct {
        Name string
        Age  int
    }, int
}

func F4[T MyC4](t T) {
    fmt.Printf("%T\n", t)
}

type MyInt int

func main() {
    var t1 = S1{"tony", 17}
    F4(t1) // main.S1
    var t2 = S2{"tony", 17}
    F4(t2) // main.S2
    var n MyInt = 3
    F4(n) // main.MyInt
}

我们看到作为约束的接口类型MyC4的type list中有两个类型:一个匿名struct和int。之后我们分别使用S1、S2和MyInt作为类型参数的实参,居然都通过了!也就是说当前的type list中的类型按照type set的概念解释,都属于approximation element,只要是underlying type在type list中,那么就可以作为类型参数的实参,通过约束检查。

那就是说:

我们是否可以只将:

type I1 interface {
    type int, string, float64
    ... ...
}

换成:

type I1 interface {
    int | string | float64
    ... ...
}

而无需~这个符号呢?

  • 如果~符号是必要的,可否不用~符号?

Go语言中没有使用~运算符,但这个符号在其他主流语言,比如C中是位运算符,而且代表的“非”这个运算符。因此将其用在类型T前面,打眼一看,以为其含义是“不是类型T的类型”。而新proposal则将其用于表示approximation element。这让很多gopher提出异议,希望换一个符号,比如T+等。但目前尚无定论。

5. 小结

能力有限,以上一些对该proposal的理解可能有误,欢迎交流指正。

type set并没有改变什么,只是完成了对interface与实现interface的重新解释。 但是对于后续将interface element用于普通interface类型定义可能有重大的意义。当前的带有interface element的interface类型仅能用于作为泛型类型参数的约束,这与普通interface之间的gap早晚要“填上”,不过这已经不是这个proposal要解决的事情。

从泛型提出到如今,我已经感到泛型的引入极大增加了复杂性 ,即便没有滥用泛型,没有耍奇技淫巧,泛型的引入也让go复杂性陡增。就像这个proposal,认真阅读并理解还是需要花费不少时间和精力的。


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,>每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需>求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列
  • Go与eBPF系列

欢迎大家加入!

Go技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中!本专栏主要满足>广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订
阅!

img{512x368}

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖>中,欢迎小伙伴们订阅学习!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

http.Client的连接行为控制详解

1. http包默认客户端

Go语言以“自带电池”闻名,很多开发者对Go自带的功能丰富的标准库喜爱有加。而在Go标准库中,net/http包又是最受欢迎和最常用的包之一,我们用几行代码就能生成一个支持大并发、性能中上的http server。而http.Client也是用途最为广泛的http客户端,其性能也可以满足多数情况下的需求。知名女gopherJaana Dogan开源的类apache abhttp性能测试工具hey也是直接使用的http.Client,而没有用一些性能更好的第三方库(比如:fasthttp)。

使用http包实现http客户端的最简单方法如下(来自http包的官方文档):

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

注:别忘了在Get或Post成功后,调用defer resp.Body.Close()。

在http包的Get和Post函数背后,真正完成http客户端操作的是http包原生内置的DefaultClient:

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

下面是一个使用DefaultClient的例子,我们先来创建一个特殊的http server:

// 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()
}

我们看到这个http server的“不同之处”在于它不急于回复http应答,而是在接收请求10秒后再回复应答。下面是我们的http client端的代码:

// 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()
}

上面的客户端创建了256个goroutine,每个goroutine向server建立一条连接,我们先启动server,然后再运行一下上面的这个客户端程序:

$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

我们看到上面的客户端抛出了一个panic,提示:打开文件描述符过多。

上面演示环境的ulimit -n的值为256

我们用一幅示意图来描述上面例子中的情况:

尽管根据《通过实例理解Go标准库http包是如何处理keep-alive连接的》一文我们知道,默认情况下,http客户端是会保持连接并复用到同一主机的服务的连接的。但由于上述示例中server的延迟10s回应答的上下文,客户端在默认情况下不会等待应答回来,而是尝试建立新的连接去发送新的http请求。由于示例运行环境最大允许每个进程打开256个文件描述符,因此在客户端后期向服务端建立连接时,就会出现“socket: too many open files”的错误。

2. 定义在小范围应用的http客户端实例

那么我们该如何控制客户端的行为以避免在资源受限的上下文情况下完成客户端的发送任务呢?我们通过设置http.DefaultClient的相关属性来实现这一点,但DefaultClient是包级变量,在整个程序中是共享的,一旦修改其属性,其他使用http默认客户端的包也会受到影响。因此更好的方案是定义一个在小范围应用的http客户端实例。

代码:

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

等价于如下代码:

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

不同的是我们自定义的http.Client实例的应用范围仅限于上述特定范围,不会对其他使用http默认客户端的包产生任何影响。不过此时我们自定义的http.Client实例client的行为与DefaultClient的无异,要想解决上面示例panic的问题,我们还需对自定义的新客户端实例做一进步行为定制。

3. 定制到某一host的最大连接数

上述示例的最大问题在于向server端建立的连接数不受控制,即便将每个进程可以打开的最大文件描述符个数调大,客户端还可能会遇到最大向外建立的65535个连接的极限瓶颈(客户端socket端口用尽),因此一个严谨的客户端需要设置到某个host的最大连接数限制。

那么,http.Client是如何控制到某个host的最大连接数的呢?http包的Client结构如下:

//$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

Client结构体一共四个字段,能控制Client连接行为的是Transport字段。如果Transport的值为nil,那么Client的连接行为遵守DefaultTransport的设置:

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

不过在这份DefaultTransport的“配置”中,并没有有关向某个host建立最大连接数的设置,因为在Transport结构体中,起到这个作用的字段是MaxConnsPerHost:

// $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
    ... ...
}

我们来改造一下上面的示例:

// 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()
}

上面的代码不再使用DefaultClient,而是自定义了一个新Client实例,并设置该实例的Transport字段为我们新建的设置了MaxConsPerHost字段的Transport实例。将server启动,并执行上面client.go,我们从server端看到如下结果:

$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]]

我们看到:客户端一共向server端建立了5条连接(客户端端口号从63673到63677),并且每隔10s,客户端复用这5条连接发送下一批请求。

http.Transport维护了到每个server host的计数器connsPerHost和请求等待队列:

// $GOROOT/src/net/http/transport.go
type Transport struct {
    ... ...
        connsPerHostMu   sync.Mutex
        connsPerHost     map[connectMethodKey]int
        connsPerHostWait map[connectMethodKey]wantConnQueue // waiting getConns
    ... ...
}

Transport结构体使用了一个connectMethodKey结构作为key:

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

我们看到connectMethodKey使用一个四元组(proxy,scheme,addr, onlyH1)来唯一标识一个“host”。通常对一个Client实例而言,proxy,scheme和onlyH1都是相同的,不同的是addr(ip+port),因此实际上也就是按addr区分host。我们同样用一幅示意图描示意一下这种情况:

4. 设定idle池的大小

不知道大家是否想到这点:当上面示例中的到某一个host的五个链接没那么繁忙时,依旧保持这个五个链接是不是有些浪费资源呢?至少占用着客户端端口以及服务端的文件描述符资源。我们是否能让客户端在闲时减少保持的到服务端的链接数量呢?我们可以通过Transport结构体类型中的MaxIdleConnsPerHost字段实现这一点。

其实如果你不显式设置MaxIdleConnsPerHost,http包也会使用其默认值(2):

// $GOROOT/src/net/http/transport.go

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

我们用一个例子来验证http.Client的这一行为!

首先我们改变一下server端的行为,将原先的“等待10s”改为立即返回应答:

// 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()
}

而对于client,我们需要精心设计一下:

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

我们首先制造一次忙碌的发送行为(21~32行),使得client端建满5个连接;然后等待10s,即让client闲下来;之后再建立5个groutine,以每秒一条的速度向server端发送请求(不忙的节奏),我们来看看程序运行后服务端的输出:

$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]]

... ...

我们来分析一下:
- 第一部分的五行输出是“忙时”client端建立的5条不同的连接,客户端端口号从56242到56246;
- 第二部分的五行输出是“非忙时”client利用idle池中的连接发送的请求,关键点就在于这5个请求的源端口号:56242、56243和56244,五个请求使用了三个早已建立好的alive的连接;
- 后面的几部分使用的也是这三个早已建立好的alive的连接。

这就是MaxIdleConnsPerHost的作用:最初“忙时”建立的5条连接,在client进入闲时时要进入idle状态。但MaxIdleConnsPerHost的值为3,也就是说只有3条连接可以进入idle池,而另外两个会被close掉。于是源端口号为56242、56243和56244的三条连接被保留了下来。

下面是这节例子的示意图:

Transport结构体还有一个字段与idle池有关,那就是MaxIdleConns,不同于MaxIdleConnsPerHost只针对某个host,MaxIdleConns是针对整个Client的所有idle池中的连接数的和,这个和不能超过MaxIdleConns。

5. 清理idle池中的连接

如果没有其他设定,那么一个Client到一个host在闲时至少会保持DefaultMaxIdleConnsPerHost个idle连接(前提是之前已经建立了2条或2条以上的连接),但如果Client针对这个host一直就保持无流量的状态,那么idle池中的连接也是一种资源浪费。于是Transport又提供了IdleConnTimeout字段用于超时清理idle池中的长连接。下面的示例复用上面的server,但client.go改为如下形式:

// github.com/bigwhite/experiments/blob/master/http-client/client-with-idleconntimeout/client.go

     1  package main
     2
     3  import (
     4      "fmt"
     5      "io"
     6      "net/http"
     7      "sync"
     8      "time"
     9  )
    10
    11  func main() {
    12      var wg sync.WaitGroup
    13      wg.Add(5)
    14      tr := &http.Transport{
    15          MaxConnsPerHost:     5,
    16          MaxIdleConnsPerHost: 3,
    17          IdleConnTimeout:     10 * time.Second,
    18      }
    19      client := http.Client{
    20          Transport: tr,
    21      }
    22      for i := 0; i < 5; i++ {
    23          go func(i int) {
    24              defer wg.Done()
    25              resp, err := client.Get("http://localhost:8080")
    26              if err != nil {
    27                  panic(err)
    28              }
    29              defer resp.Body.Close()
    30              body, err := io.ReadAll(resp.Body)
    31              fmt.Printf("g-%d: %s\n", i, string(body))
    32          }(i)
    33      }
    34      wg.Wait()
    35
    36      time.Sleep(5 * time.Second)
    37
    38      wg.Add(5)
    39      for i := 0; i < 5; i++ {
    40          go func(i int) {
    41              defer wg.Done()
    42              for i := 0; i < 2; i++ {
    43                  resp, err := client.Get("http://localhost:8080")
    44                  if err != nil {
    45                      panic(err)
    46                  }
    47                  defer resp.Body.Close()
    48                  body, err := io.ReadAll(resp.Body)
    49                  fmt.Printf("g-%d: %s\n", i+10, string(body))
    50                  time.Sleep(time.Second)
    51              }
    52          }(i)
    53      }
    54
    55      time.Sleep(15 * time.Second)
    56      wg.Add(5)
    57
    58      for i := 0; i < 5; i++ {
    59          go func(i int) {
    60              defer wg.Done()
    61              for i := 0; i < 100; i++ {
    62                  resp, err := client.Get("http://localhost:8080")
    63                  if err != nil {
    64                      panic(err)
    65                  }
    66                  defer resp.Body.Close()
    67                  body, err := io.ReadAll(resp.Body)
    68                  fmt.Printf("g-%d: %s\n", i+20, string(body))
    69                  time.Sleep(time.Second)
    70              }
    71          }(i)
    72      }
    73      wg.Wait()
    74  }

这个client.go代码分为三部分:首先和上个示例一样,我们首先制造一次忙碌的发送行为(22~33行),使得client端建满5个连接;然后等待5s,即让client闲下来;之后再建立5个groutine,以每秒一条的速度向server端发送请求(不忙的节奏);第三部分同样是先等待15s,然后创建5个goroutine分别以不忙的节奏向server端发送请求。我们来看看程序运行后服务端的输出:

$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]]

... ...

这里摘录5段输出。和预想的一样,第一段client向server建立了5条连接(客户端的端口号从52484~52487);暂停5s后,新创建的5个goroutine通过idle池中的三条保持的连接向server发送请求(第二段和第三段,端口号:52484、52487、52488);之后暂停15s,由于设置了IdleConnTimeout,idle池中的三条连接也被close掉了。这时再发送请求,client会重新建立连接(第四段,端口号52542~52546),最后一段则又开始通过idle池中的三条保持的连接向server发送请求了(端口号:52542、52544和52545)。

6. 其他控制项

如果觉得idle池超时清理依旧会占用“资源”一小会儿,那么可以利用Transport的DisableKeepAlives使得每个请求都创建一个新连接,即不复用keep-alive连接。当然这种控制设定在忙时导致的频繁建立新连接的损耗可是要比占用一些“资源”来的更大。示例可参考 github.com/bigwhite/experiments/blob/master/http-client/client-with-disablekeepalives,这里就不贴出来了。

另外像本文开始示例中server那样等待10s才回应答的行为可不是所有client端都能接受的,为了限定应答及时返回,client端可以设定等待应答的超时时间,如果超时,client将返回失败。http.Client结构中的Timeout可以实现这一特性。示例可参考 github.com/bigwhite/experiments/blob/master/http-client/client-with-timeout,这里同样不贴出来了。

本文涉及的代码可以在这里下载:https://github.com/bigwhite/experiments/blob/master/http-client。


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,>每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需>求!部落目前虽小,但持续力很强。在2021年上半年,部落将策划两个专题系列分享,并且是部落独享哦:

  • Go技术书籍的书摘和读书体会系列
  • Go与eBPF系列

欢迎大家加入!

Go技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中!本专栏主要满足>广大gopher关于Go语言进阶的需求,围绕如何写出地道且高质量Go代码给出50条有效实践建议,上线后收到一致好评!欢迎大家订
阅!

img{512x368}

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网热卖>中,欢迎小伙伴们订阅学习!

img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats