一句话总结就是用来传递请求上下文(比如取消信号,超时时间,traceid)等消息的机制,可以在多goroutine之间传递信号
- 控制并发任务的生命周期,多个goroutine之间共享一个ctx的话,可以实现一处取消,处处取消
- 传递请求范围内的数据,在处理HTTP请求的时候,context可以用来传递一些请求相关的数据,比如认证,token,用户信息等
- 设置超时时间,可以通过contexy来设置某个操作的超时时间,一定那超时,相关的goroutine就会取消
context.Context接口中提供了若干方法
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
- deadline是用在需要对某个操作设置超时时间的场景
- done是实现取消操作的核心方法
- err是用于在上层代码中检查和处理不同的取消情况
- value,用于传递跨api边界的请求范围数据,例如身份验证令牌,请求标识符等
超时控制
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
resultChan := make(chan string)
go func() {
time.Sleep(3 * time.Second) // 模拟耗时任务
resultChan <- "done"
}()
select {
case <-ctx.Done():
fmt.Println("超时取消:", ctx.Err())
case res := <-resultChan:
fmt.Println("任务结果:", res)
}
取消goroutine
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("子任务已取消")
return
default:
fmt.Println("工作中...")
time.Sleep(500 * time.Millisecond)
}
}
}()
time.Sleep(2 * time.Second)
cancel() // 触发取消
time.Sleep(1 * time.Second)
传递traceid/user_id
func WithTraceID(ctx context.Context, traceID string) context.Context {
return context.WithValue(ctx, "trace_id", traceID)
}
func GetTraceID(ctx context.Context) string {
if v := ctx.Value("trace_id"); v != nil {
return v.(string)
}
return ""
}
func Handler(ctx context.Context) {
fmt.Println("Trace ID:", GetTraceID(ctx))
}
func main() {
ctx := WithTraceID(context.Background(), "abc-123")
Handler(ctx)
}
go语言context value的查找过程是怎样的
go会沿着context链从最内层向外逐层查找关键字匹配的值,具体来说
如果当前context的关键词与目标关键词匹配,则返回对应的值
否则,继续向外查找父context,直到匹配到关键字或到达根context,通常是context background
context如何取消
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println("Context was canceled")
case <-time.After(5 * time.Second):
fmt.Println("Context was not canceled within 5 seconds")
}
}(ctx)
cancel()
time.Slee p(1 * time.Second) // 确保查看结果之前协程有时间执行
}
需要注意的是,当一个ctx被cancel后它所有的子ctx都会被cancel
什么是反射
略
如何利用unsafe获取slice和map的长度
net/http包中client如何实现长连接
长连接实现的原理是HTTP/1.1通过在HTTP头部设置Connection:keep-alive来告诉服务器希望复用TCP连接以发送和接收后续的HTTP请求与响应,而不是每进行一次HTTP请求就建立一个新连接
在go语言中,要实现长连接可以通过net/http包中的客户端(http.client)
需要关注以下几个参数:
- tarnsport参数:这个字段允许你自定义HTTP传输机制,可以配置连接池,代理,tls等,设置最大连接数,代理地址,keepalive等
- Timeout参数:这个参数不是直接控制长连接的字段,但它设置了整个请求(抱愧哦连接建立,请求发送,响应接收)的超时时间。合理的超时设置可以避免因为某些原因导致的连接长时间占用
怎么关闭连接
如果要主动关闭连接的话,可以这样
transport, ok := client.Transport.(*http.Transport)
if ok {
transport.CloseIdleConnections() // 关闭所有空闲连接。但是
}
但是不建议直接关闭连接(net.Conn)
在go的http客户端(http.client)中,你一般拿不到底层的连接对象(net.Conn),除非你自己去实现Transport,而标准库的设计就是为了屏蔽底层连接细节,交给连接池自动管理
go的连接复用是自动管理的,go的http.transport会维护连接池(Connection Pool),它通过复用连接来提升性能。
- 首次请求时:创建新连接(net.Conn),放入连接池中(按照目标主机地址分)
- 后续请求:检查连接池中是否已有空闲连接->复用它(避免TCP/TLS握手),如果没有空闲连接就再新建连接
- 请求完成,如果设置了Keep-Alive,连接不会关闭而是放回池子
- 主动关闭连接池:transport.CloseIdConnections()
什么是连接复用
连接复用是指多个http请求公用同一个TCP连接(在Keep-Alive模式上)
避免每次都三次握手(TCP),握手(TLS)。减少延迟,提升吞吐量。节省系统资源
net/http如何做连接池
在net/http包中,连接池是通过http.Transport来实现的。http.Transport默认提供了一个内置的连接池,它会为每个主机保存多个空闲连接,并在发送请求时复用这些连接。这样可以避免每次请求都建立新的TCP连接,提升性能
连接池的工作原理
- http.Transport会为每个主机维护一个连接池
- 当发送请求时,Transport会产生复用已有的连接,如果有空闲连接则复用它
- 如果没有空闲连接,Transport会建立新的连接,并将其放入连接池中
核心参数
- MaxIdleConns:设置最大空闲数,控制连接池的大小
- MaxIdleConnsPerHost:设置每个主机的最大空闲连接数
- IdleConnTimeout:设置空闲连接的最大保持时间,如果连接在指定时间内没有使用,它会被关闭
http.Transport和连接池的实现实例
http.Transport负责管理与远程服务器的连接。它会在内部为每个主机维护一个连接池,避免频繁的建立与关闭连接
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
// 配置自定义 Transport 以启用连接池
transport := &http.Transport{
MaxIdleConns: 100, // 最大空闲连接数
MaxIdleConnsPerHost: 10, // 每个主机的最大空闲连接数
IdleConnTimeout: 30 * time.Second, // 空闲连接最大保持时间
}
client := &http.Client{
Transport: transport,
}
// 发送请求
resp, err := client.Get("http://example.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println("Response status:", resp.Status)
}
连接池是按照主机划分的,即协议+ip+端口只要有一个不同就是不同主机,就会建立不同连接池
部分实现原理的源码解析
http.Transport的源代码主要作用是
- 处理HTTP请求的发送
- 管理连接池
- 支持持久连接
- 支持并发请求复用连接
并发请求复用连接
package main
import (
"fmt"
"io"
"net/http"
"sync"
"time"
)
func main() {
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 10,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
},
Timeout: 10 * time.Second,
}
var wg sync.WaitGroup
url := "https://httpbin.org/get"
concurrency := 5
wg.Add(concurrency)
start := time.Now()
for i := 0; i < concurrency; i++ {
go func(id int) {
defer wg.Done()
resp, err := client.Get(url)
if err != nil {
fmt.Printf("Goroutine %d: request error: %v\n", id, err)
return
}
defer resp.Body.Close()
// 读取响应体,必须读完或关闭,否则连接无法复用
_, _ = io.Copy(io.Discard, resp.Body)
fmt.Printf("Goroutine %d: status=%s\n", id, resp.Status)
}(i + 1)
}
wg.Wait()
fmt.Printf("All requests done in %v\n", time.Since(start))
}
你会发现这些请求实际上只建立了一两条tcp连接,后续请求直接复用
连接池的结构
http.Tranport内部用coonPool(连接池)来管理所有连接,为了高效存储和查找连接,go使用了sync.Map来存储不同目标主机的空闲连接
// Transport is an HTTP handler that implements RoundTripper.
type Transport struct {
// Base settings for Transport.
// Transport implements RoundTripper
proxy func(*http.Request) (*url.URL, error)
dial func(ctx context.Context, network, addr string) (net.Conn, error)
dialTLS func(ctx context.Context, network, addr string) (net.Conn, error)
tlsClientConfig *tls.Config
disableCompression bool
// The connPool is a map of host -> connection pool.
connPool *connPool
}
connPool是一个自定义的结构体,用于存储和管理不同主机之间的连接池
connPool的结构与管理
connPool负责管理空闲连接的复用与清理,保存了每个主机的连接池
type connPool struct {
mu sync.Mutex // protects the following fields
m map[string]*httpConnPool
}
type httpConnPool struct {
mu sync.Mutex
maxIdle int // 最大空闲连接数
idleConns []*httpConn // 存储空闲连接的列表
}
type httpConn struct {
conn net.Conn
lastUsed time.Time // 记录上次使用时间
}
connPool:维护了一个哈希表,键为目标主机(通常为主机名或ip地址),值为该主机对应的连接池(httpConnPool)
httpConnPool:每个主机对应的连接池,它维护了一组空闲连接idleConns,以及最大空闲连接数maxIdle
httpConn:每个连接的结构体,包含一个net.Conn和lastUsed字段来记录连接的最后使用时间
连接复用过程
主要在getConn和putConn方法中
getConn,获取连接
func (p *connPool) getConn(network, addr string, ctx context.Context) (conn net.Conn, err error) {
p.mu.Lock()
defer p.mu.Unlock()
// 获取连接池
pool, exists := p.m[addr]
if !exists {
pool = &httpConnPool{maxIdle: defaultMaxIdleConns}
p.m[addr] = pool
}
// 从连接池中取出一个空闲连接
if len(pool.idleConns) > 0 {
conn = pool.idleConns[len(pool.idleConns)-1]
pool.idleConns = pool.idleConns[:len(pool.idleConns)-1]
return conn, nil
}
// 如果没有空闲连接,创建一个新的连接
conn, err = p.dial(ctx, network, addr)
if err != nil {
return nil, err
}
return conn, nil
}
- 首先检查是否有该主机的连接池,如果没有就新建一个
- 再去看连接池里面有没有空闲连接,有就取出来,没有就新建
putConn,归还连接
func (p *connPool) putConn(addr string, conn net.Conn) {
p.mu.Lock()
defer p.mu.Unlock()
// 获取目标主机的连接池
pool, exists := p.m[addr]
if !exists {
pool = &httpConnPool{maxIdle: defaultMaxIdleConns}
p.m[addr] = pool
}
// 将连接放入连接池中
if len(pool.idleConns) < pool.maxIdle {
pool.idleConns = append(pool.idleConns, &httpConn{conn: conn, lastUsed: time.Now()})
} else {
conn.Close() // 如果连接池已满,则关闭该连接
}
}
将连接归还到目标主机的连接池里面,如果达到最大空闲连接数就关闭连接,没有就塞进去