golang长连接和短连接的那些事儿
Posted 杰哥的技术杂货铺
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang长连接和短连接的那些事儿相关的知识,希望对你有一定的参考价值。
文章目录
- 一、TCP长连接和短连接区别
- 二、TCP长连接和短连接应用场景
- 三、Golang HTTP连接池
- 四、初始化HTTP长连接池
- 五、为什么需要response.Body.Close()
- 六、golang短连接使用
- 七、总结
TCP/IP是个协议组,可分为三个层次:网络层、传输层和应用层。
在网络层有IP协议、ICMP协议、ARP协议、RARP协议和BOOTP协议。
在传输层中有TCP协议与UDP协议。
在应用层有FTP、HTTP、TELNET、SMTP、DNS等协议。
一、TCP长连接和短连接区别
1.1 长连接、短连接概念
-
所谓长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持。
-
短连接(short connnection)是相对于长连接而言的概念,指的是在数据传送过程中,只在需要发送数据时,才去建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。
比如http的,只是连接、请求、关闭,过程时间较短,服务器若是一段时间内没有收到请求即可关闭连接。
其实长连接是相对于通常的短连接而说的,也就是长时间保持客户端与服务端的连接状态。
1.2 长连接、短连接的传输过程区别
通常的短连接操作步骤是: 连接→数据传输→关闭连接;
而长连接通常就是:
连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接;
这就要求长连接在没有数据通信时,定时发送数据包(心跳),以维持连接状态,短连接在没有数据传输时直接关闭就行了。
1.3 长连接与短连接的优缺点
长连接
- 优点
- 省去了较多的TCP的建立与关闭的时间
- 性能比较好(因为一直保持连接的状态)
- 缺点
- 当连接越来越多会压垮服务器
- 连接管理难度较大
- 安全性能差
短连接
- 优点
- 管理服务简单,存在的连接都是有效连接,不需要额外的控制手段
- 缺点
- 如果请求频繁,不断的连接以及关闭连接,浪费时间
二、TCP长连接和短连接应用场景
2.1 长连接应用场景
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个TCP连接都需要三次握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,再次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接,如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
2.2 短连接应用场景
像WEB网站的http服务一般都用短连接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连接好。
三、Golang HTTP连接池
参考文章:
详解golang net之transport:https://www.cnblogs.com/charlieroro/p/11409153.html
3.1 问题引入
作为一名Golang开发者,你可能会遇到线上环境遇到连接数暴增问题。
纠其原因,Golang作为常驻进程,请求第三方服务或者资源完毕后,需要手动关闭连接,否则连接会一直存在。而很多时候,开发者不一定记得关闭这个连接。
那么你可能会问,我可以在程序中defer主动关闭连接啊!需要知道的是,连接相对于其他对象,创建成本较高,资源也有限。如果没有连接池,在高并发场景下,连接关闭又新建,很快就会因为过多的TIME_WAIT(连接主动关闭方)导致无法创建更多连接了,程序被压死。
那么这样是不是很麻烦很头疼?于是有了连接池。顾名思义,连接池就是管理连接的;我们从连接池获取连接,请求完毕后再将连接还给连接池;连接池帮我们做了连接的建立、复用以及回收工作。
在设计与实现连接池时,我们通常需要考虑以下几个问题:
- 连接池的连接数目是否有限制,最大可以建立多少个连接?
- 当连接长时间没有使用,需要回收该连接吗?
- 业务请求需要获取连接时,此时若连接池无空闲连接且无法新建连接,业务需要排队等待吗?
- 排队的话又存在另外的问题,队列长度有无限制,排队时间呢?
3.2 golang连接池原理
Transport:为http.RoundTripper接口,定义功能为负责http的请求分发。实际功能由结构体net/http/transport.go中的Transport struct继承并实现,除了请求发分还实现了对空闲连接的管理。如果创建client时不定义,就用系统默认配置。
Transport结构定义如下:
type Transport struct
//操作空闲连接需要获取锁
idleMu sync.Mutex
//空闲连接池,key为协议目标地址等组合
idleConn map[connectMethodKey][]*persistConn // most recently used at end
//等待空闲连接的队列,基于切片实现,队列大小无限制
idleConnWait map[connectMethodKey]wantConnQueue // waiting getConns
//排队等待建立连接需要获取锁
connsPerHostMu sync.Mutex
//每个host建立的连接数
connsPerHost map[connectMethodKey]int
//等待建立连接的队列,同样基于切片实现,队列大小无限制
connsPerHostWait map[connectMethodKey]wantConnQueue // waiting getConns
//tls client用于tls协商的配置
TLSClientConfig *tls.Config
//tls协商的超时时间
TLSHandshakeTimeout time.Duration
//是否取消长连接,默认使用长连接
DisableKeepAlives bool
//是否取消HTTP压缩
DisableCompression bool
//所有host的连接池最大连接数量,默认无穷大
MaxIdleConns int
//每个目标host最大空闲连接数;默认为2(注意默认值)
MaxIdleConnsPerHost int
//对每个host可建立的最大连接数量,0表示不限制
MaxConnsPerHost int
//连接多少时间没有使用则被关闭
IdleConnTimeout time.Duration
//发送完request后等待serve response的时间
ResponseHeaderTimeout time.Duration
//限制客户端在发送一个包含:100-continue的http报文头后,等待收到一个go-ahead响应报文所用的时间。
ExpectContinueTimeout time.Duration
//在tls协商带NPN/ALPN的扩展后,transport如何切换到其他协议。指tls之上的协议(next指的就是tls之上的意思)
TLSNextProto map[string]func(authority string, c *tls.Conn) RoundTripper
//在CONNECT请求时,配置request的首部信息,可选
ProxyConnectHeader Header
//指定server响应首部的最大字节数
MaxResponseHeaderBytes int64
//写bufffer的大小,默认为4096。
WriteBufferSize int
//读bufffer的大小,默认为4096。
ReadBufferSize int
//是否启用HTTP/2,默认为启用
ForceAttemptHTTP2 bool
需要特别注意的是,MaxIdleConnsPerHost
默认等于2,即与目标主机最多只维护两个空闲连接。这会导致什么呢?
如果遇到突发流量,瞬间建立大量连接,但是回收连接时,由于最大空闲连接数的限制,该联机不能进入空闲连接池,只能直接关闭。结果是,一直新建大量连接,又关闭大量连,业务机器的TIME_WAIT连接数随之突增。
最后,Transport也提供了配置DisableKeepAlives,禁用长连接,使用短连接访问第三方资源或者服务。
3.3 连接获取与回收
Transport结构提供下面两个方法实现连接的获取与回收操作。
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error)
func (t *Transport) tryPutIdleConn(pconn *persistConn) error
连接的获取主要分为两步走:
1)尝试获取空闲连接;
2)尝试新建连接:
//getConn方法内部实现
if delivered := t.queueForIdleConn(w); delivered
return pc, nil
t.queueForDial(w)
当然,可能获取不到连接而需要排队,此时怎么办呢?当前会阻塞当前协程了,直到获取连接为止,或者httpclient超时取消请求:
select
case <-w.ready:
return w.pc, w.err
//超时被取消
case <-req.Cancel:
return nil, errRequestCanceledConn
……
var errRequestCanceledConn = errors.New("net/http: request canceled while waiting for connection") // TODO: unify?
排队等待空闲连接的逻辑如下:
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool)
//如果配置了空闲超时时间,获取到连接需要检测,超时则关闭连接
if t.IdleConnTimeout > 0
oldTime = time.Now().Add(-t.IdleConnTimeout)
if list, ok := t.idleConn[w.key]; ok
for len(list) > 0 && !stop
pconn := list[len(list)-1]
tooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)
//超时了,关闭连接
if tooOld
go pconn.closeConnIfStillIdle()
//分发连接到wantConn
delivered = w.tryDeliver(pconn, nil)
//排队等待空闲连接
q := t.idleConnWait[w.key]
q.pushBack(w)
t.idleConnWait[w.key] = q
排队等待新建连接的逻辑如下:
func (t *Transport) queueForDial(w *wantConn)
//如果没有限制最大连接数,直接建立连接
if t.MaxConnsPerHost <= 0
go t.dialConnFor(w)
return
//如果没超过连接数限制,直接建立连接
if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost
go t.dialConnFor(w)
return
//排队等待连接建立
q := t.connsPerHostWait[w.key]
q.pushBack(w)
t.connsPerHostWait[w.key] = q
连接建立完成后,同样会调用tryDeliver分发连接到wantConn,同时关闭通道w.ready,这样主协程就接触阻塞了。
func (w *wantConn) tryDeliver(pc *persistConn, err error) bool
w.pc = pc
close(w.ready)
请求处理完成后,通过tryPutIdleConn将连接放回连接池;这时候如果存在等待空闲连接的协程,则需要分发复用该连接。另外,在回收连接时,还需要校验空闲连接数目是否超过限制:
func (t *Transport) tryPutIdleConn(pconn *persistConn) error
//禁用长连接;或者最大空闲连接数不合法
if t.DisableKeepAlives || t.MaxIdleConnsPerHost < 0
return errKeepAlivesDisabled
if q, ok := t.idleConnWait[key]; ok
//如果等待队列不为空,分发连接
for q.len() > 0
w := q.popFront()
if w.tryDeliver(pconn, nil)
done = true
break
//空闲连接数目超过限制,默认为DefaultMaxIdleConnsPerHost=2
idles := t.idleConn[key]
if len(idles) >= t.maxIdleConnsPerHost()
return errTooManyIdleHost
3.4 空闲连接超时关闭
Golang HTTP连接池如何实现空闲连接的超时关闭逻辑呢?从上述queueForIdleConn逻辑可以看到,每次在获取到空闲连接时,都会检测是否已经超时,超时则关闭连接。
那如果没有业务请求到达,一直不需要获取连接,空闲连接就不会超时关闭吗?其实在将空闲连接添加到连接池时,Golang同时还设置了定时器,定时器到期后,自然会关闭该连接。
pconn.idleTimer = time.AfterFunc(t.IdleConnTimeout, pconn.closeConnIfStillIdle)
3.5 排队队列怎么实现
怎么实现队列模型呢?很简单,可以基于切片:
queue []*wantConn
//入队
queue = append(queue, w)
//出队
v := queue[0]
queue[0] = nil
queue = queue[1:]
这样有什么问题吗?随着频繁的入队与出队操作,切片queue的底层数组,会有大量空间无法复用而造成浪费。除非该切片执行了扩容操作。
Golang在实现队列时,使用了两个切片head和tail;head切片用于出队操作,tail切片用于入队操作;出队时,如果head切片为空,则交换head与tail。通过这种方式,Golang实现了底层数组空间的复用。
func (q *wantConnQueue) pushBack(w *wantConn)
q.tail = append(q.tail, w)
func (q *wantConnQueue) popFront() *wantConn
if q.headPos >= len(q.head)
if len(q.tail) == 0
return nil
// Pick up tail as new head, clear tail.
q.head, q.headPos, q.tail = q.tail, 0, q.head[:0]
w := q.head[q.headPos]
q.head[q.headPos] = nil
q.headPos++
return w
3.6 tranport连接池总结
以上便是整个流程,其实还是很清晰的,最后总结一下:
tranport用来建立一个连接,其中维护了一个空闲连接池idleConn map[connectMethodKey][]*persistConn
,其中的每个成员都是一个persistConn
对象,persistConn
是个具体的连接实例,包含了连接的上下文,会启动两个groutine
分别执行readLoop
和writeLoop
, 每当transport
调用roundTrip
的时候,就会从连接池中选择一个空闲的persistConn
,然后调用其roundTrip
方法,将读写请求通过channel
分别发送到readLoop
和writeLoop
中,然后会进行select
各个channel
的信息,包括连接关闭,请求超时,writeLoop
出错, readLoop
返回读取结果等。在writeLoop
中发送请求,在readLoop
中获取response
并通过channe
返回给roundTrip
函数中,并再次将自己加入到idleConn
中,等待下次请求到来。
四、初始化HTTP长连接池
对于golang的net/http库其使用通常有两种方式:
- 使用DefaultClient;
- 使用自定义Client。下面来看看两种方式的用法
4.1 net/http client使用
1.使用DefalutClient
对于没有高并发的场景下,使用DefaultClient十分简单,能够快速达到目的。下面看一个示例:
resp, err := http.Get("http://www.baidu.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. 使用自定义Clien
实际应用中,为应对各种不同的场景通常需要自定以http Client来满足要求,实现目的。
下面看看自定义client的常见用法:
- 超时设置
client := http.Client
Timeout: 10 * time.Second,
- 结构体定义
type Client struct
url string
client *http.Client
- 连接池设置
// NewClient 初始化客户端
func NewClient(url string) *Client
return &Client
url: url,
client: &http.Client
Transport: &http.Transport
MaxIdleConnsPerHost: 10, // 每台主机保持的最大空闲连接
MaxConnsPerHost: 10, // 限制每个主机的连接总数
TLSClientConfig: &tls.ConfigInsecureSkipVerify: true,
,
,
实际应用中client通常是作为全局变量来使用,初始化一次即可。无需每次请求都重新定义一次,
因为client中底层使用的transport是一个连接池,不同请求会取用一条不同的连接。
4.2 Transport连接池使用
上面说到,http client中transport是一个连接池,其无论是DefaultClient还是自定义Client都是用到的。
默认的transport定义如下:
路径:go\\src\\net\\http\\transport.go
// 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,通常根据应用场景需要调整配置参数。
自定义transport定义如下:
// Client
type Client struct
url string
client *http.Client
// NewClient 初始化客户端
func NewClient(url string) *Client
return &Client
url: url,
client: &http.Client
Transport: &http.Transport
DialContext: (&net.Dialer
Timeout: 30 * time.Millisecond, //限制建立TCP连接的时间
KeepAlive: 10 * time.Millisecond, //指定 TCP keep-alive 探测发送到对等方的频率。
).DialContext,
ForceAttemptHTTP2: true, //是否启用HTTP/2
IdleConnTimeout: 90 * time.Millisecond, //连接多少时间没有使用则被关闭
TLSHandshakeTimeout: 10 * time.Second, //tls协商的超时时间
ExpectContinueTimeout: 1 * time.Second, //等待收到一个go-ahead响应报文所用的时间
MaxIdleConns: 100, //最大空闲连接数
MaxIdleConnsPerHost: 10, // 每台主机保持的最大空闲连接
MaxConnsPerHost: 10, // 限制每个主机的连接总数
TLSClientConfig: &tls.ConfigInsecureSkipVerify: true,
,
Timeout: 20 * time.Second, //从发起连接到接收响应报文结束
,
// httpRequest http请求
func (c *Client) HttpRequest(methed, params string) (resBody []byte, err error)
// 初始化请求
body := strings.NewReader(params)
fmt.Println(body)
req, _ := http.NewRequest("POST", c.url+methed, body)
fmt.Println("req===============",req)
if err != nil
return nil, errors.Wrap(err, "Http NewRequest")
// 执行请求
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
res, err := c.client.Do(req)
if err != nil
return nil, errors.Wrap(err, "Client Do")
defer res.Body.Close()
// 接收返回结果
resBody, err = ioutil.ReadAll(res.Body)
fmt.Println(string(resBody))
if err != nil
return nil, errors.Wrap(err, "ioutil.ReadAll")
return resBody, nil
// httpRequest http请求
func (c *Client) HttpGetRequest(methed, params string) (resBody []byte, err error)
// 初始化请求
req, err := http.NewRequest("GET", c.url+methed+params, nil)
if err != nil
return nil, errors.Wrap(err, "Http NewRequest")
// 执行请求
req.Header.Add("Content-Type", "application/json")
res, err := c.client.Do(req)
if err != nil
return nil, errors.Wrap(err, "Client Do")
defer res.Body.Close()
// 接收返回结果
resBody, err = ioutil.ReadAll(res.Body)
if err != nil
return nil, errors.Wrap(err, "ioutil.ReadAll")
return resBody, nil
一般比较关心的参数分两部分:DialContext参数和连接参数
-
context参数是指建立连接时涉及的参数包括超时,保活参数
-
连接参数是指与pool相关的参数包括:最大空闲连接数,最大连接数和空闲超时时间。
4.3 各个超时时间设置
1. http.Client
首先client结构中有一个Timeout
Timeout: 10 * time.Second, //从发起连接到接收响应报文结束
这个 timeout使用比较简单,它涵盖整个交互过程,从发起连接到接收响应报文结束,该超时限制包括连接时间、重定向和读取回复主体的时间。
Timeout 为零值表示不设置超时。
2. Transport
而Transport中的超时能让你更精确的控制超时
-
net.Dialer.Timeout: 限制创建一个TCP连接使用的时间(如果需要一个新的链接)
-
http.Transport.TLSHandshakeTimeout: 限制TLS握手使用的时间
当使用golang http.client尝试用https协议访问目标网站的时候,如果目标网站不是https网站,则有可能出现函数response结果后tcp连接依然establish的情况。此时需要设置:http.Transport.TLSHandshakeTimeout
-
http.Transport.ResponseHeaderTimeout: 限制读取响应报文头使用的时间
-
http.Transport.ExpectContinueTimeout: 限制客户端在发送一个包含:100-continue的http报文头后,等待收到一个go-ahead响应报文所用的时间。在1.6中,此设置对HTTP/2无效。(在1.6.2中提供了一个特定的封装DefaultTransport)
-
http.Transport.IdleConnTimeout:连接最大空闲时间,超过这个时间就会被关闭
-
http.Transport.ExpectContinueTimeout:等待服务器的第一个响应headers的时间,0表示没有超时,则body会立刻发送,无需等待服务器批准,这个时间不包括发送请求header的时间
五、为什么需要response.Body.Close()
仔细观察可以看到,在上文中,POST和GET 请求中,都出现了以下代码:
// 初始化请求
req, err := http.NewRequest("GET", c.url+methed+params, nil)
if err != nil
return nil, errors.Wrap(err, "Http NewRequest")
req.Header.Add("Content-Type", "application/json")
// 执行请求
res, err := c.client.Do(req)
if err != nil
return nil, errors.Wrap(err, "Client Do")
// 关闭请求
defer res.Body.Close()
5.1 Do 方法
Do 方法发送请求,返回 HTTP 回复。它会遵守客户端 client 设置的策略(如重定向、cookie、认证)。
如果客户端的策略(如重定向)返回错误或存在 HTTP协议错误时,本方法将返回该错误;如果回应的状态码不是 2xx,本方法并不会返回错误。
如果返回值 err 为 nil,res.Body
总是非 nil 的,调用者应该在读取完 res.Body
后关闭它。
请求的主体,如果非 nil,会在执行后被 c.Transport 关闭,即使出现错误。
一般应使用 Get 、 Post 或 PostForm 方法就可以代替 Do 方法,其实它们最终执行的也是 Do ,只不过做了一些包装。
当有一些比较特殊的,上面的三种方式不能满足时,就要自己初始化 Request ,然后调用 Do 方法。
- Do语法:
func (c *Client) Do(req *Request) (resp *Response, err error)
- 参数:
*Request 可以通过这个参数来自定义 Request
- 返回值:
*Response 如果获取到了数据,会将数据保存在 Response 中
error 如果请求数据的时候出现错误,会返回一个 error ,并将具体的错误记录到 error 中
5.2 为什么需要response.Body.Close()
resp.Body.Close() 做了什么?
如果返回值res
的主体未关闭,client
下层的 RoundTripper
接口(一般为 Transport
类型)可能无法重用 res
主体下层保持的 TCP 连接去执行之后的请求。所以它的作用就是用来确保body读干净,释放出该连接
为什么这样做?
连接复用
如果不这么做会发生什么?
-
第一则是:无法重新使用与服务器的持久 TCP 连接来进行后续的“保持活动”请求,在下次发起HTTP请求的时候,就会重新建立TCP连接
-
第二如果不关闭当前请求,readLoop 和 writeLoop 两个 goroutine 在 写入请求并获取 response 返回后,并没有跳出 for 循环,而继续阻塞在下一次 for 循环的 select 语句里面,goroutine 一直无法被回收,cpu 和 memory 全部打满。发生goroutine内存泄漏
-
第三如果请求完成后,对端关闭了连接(对端的HTTP服务器向我发送了FIN),如果这边不调用
response.Body.Close()
,那么可以看到与这个请求相关的TCP连接的状态一直处于CLOSE_WAIT
状态,态,不会被系统回收,则文件描述符不会被释放,出现资源泄漏。
六、golang短连接使用
有时候我们因为一些业务场景,需要使用短连接,那么应该如何实现呢?
6.1 关闭连接
直接在请求后关闭连接
httpReq.Close = true
代码如图所示
httpReq, err := http.NewRequest("POST", url, bodyReader)
if err != nil
return nil, exception.Build("http NewRequest error", err)
httpReq.Close = true
httpReq.Header.Set("Content-Type", "application/json")
client := httpClient()
resp, err := client.Do(httpReq)
6.2 使用 Transport 取消 HTTP利用连接
使用DisableKeepAlives
参数取消连接复用
// 短连接池
func httpClient() http.Client
return http.Client
Transport: &http.Transport
DisableKeepAlives: true,
,
七、总结
长连接与短连接的应用场景已在上文中进行讲解,长连接多用于点对点的通讯,而且连接数不能太多的情况,而并发量大,但每个用户无需频繁操作情况下需用短连接好。
简而言之,与两种情况有关:数据传输的长短、并发量
- 数据传输长,并发量较小,使用长连接
- 数据传输短,并发量较大,使用短连接
长连接和短连接的产生在于client和server采取的关闭策略,具体的应用场景采用具体的策略,没有十全十美的选择,只有合适的选择。
总之,长连接和短连接的选择要视情况而定。
参考资料:
以上是关于golang长连接和短连接的那些事儿的主要内容,如果未能解决你的问题,请参考以下文章