hello,大家好呀,我是既写 Java 又写 Go 的小楼,在写 Go 的过程中经常对比这两种语言的特性,踩了不少坑,也发现了不少有意思的地方,今天就来聊聊 Go 自带的 HttpClient 的超时机制。
Java HttpClient 超时底层原理
在介绍 Go 的 HttpClient 超时机制之前,我们先看看 Java 是如何实现超时的。
写一个 Java 原生的 HttpClient,设置连接超时、读取超时时间分别对应到底层的方法分别是:
再追溯到 JVM 源码,发现是对系统调用的封装,其实不光是 Java,大部分的编程语言都借助了操作系统提供的超时能力。
然而 Go 的 HttpClient 却提供了另一种超时机制,挺有意思,我们来盘一盘。但在开始之前,我们先了解一下 Go 的 Context。
Go Context 简介
Context 是什么?
根据 Go 源码的注释:
// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
// Context's methods may be called by multiple goroutines simultaneously.
Context 简单来说是一个可以携带超时时间、取消信号和其他数据的接口,Context 的方法会被多个协程同时调用。
Context 有点类似 Java 的ThreadLocal,可以在线程中传递数据,但又不完全相同,它是显示传递,ThreadLocal 是隐式传递,除了传递数据之外,Context 还能携带超时时间、取消信号。
Context 只是定义了接口,具体的实现在 Go 中提供了几个:
- Background :空的实现,啥也没做
- TODO:还不知道用什么 Context,先用 TODO 代替,也是啥也没做的空 Context
- cancelCtx:可以取消的 Context
- timerCtx:主动超时的 Context
针对 Context 的三个特性,可以通过 Go 提供的 Context 实现以及源码中的例子来进一步了解下。
Context 三个特性例子
这部分的例子来源于 Go 的源码,位于 src/context/example_test.go
携带数据
使用 context.WithValue
来携带,使用 Value
来取值,源码中的例子如下:
// 来自 src/context/example_test.go
func ExampleWithValue() {
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
// Output:
// found value: Go
// key not found: color
}
取消
先起一个协程执行一个死循环,不停地往 channel 中写数据,同时监听 ctx.Done()
的事件
// 来自 src/context/example_test.go
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // returning not to leak the goroutine
case dst <- n:
n++
}
}
}()
return dst
}
然后通过 context.WithCancel
生成一个可取消的 Context,传入 gen
方法,直到 gen
返回 5 时,调用 cancel
取消 gen
方法的执行。
// 来自 src/context/example_test.go
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
// Output:
// 1
// 2
// 3
// 4
// 5
这么看起来,可以简单理解为在一个协程的循环中埋入结束标志,另一个协程去设置这个结束标志。
超时
有了 cancel 的铺垫,超时就好理解了,cancel 是手动取消,超时是自动取消,只要起一个定时的协程,到时间后执行 cancel 即可。
设置超时时间有2种方式:context.WithTimeout
与 context.WithDeadline
,WithTimeout 是设置一段时间后,WithDeadline 是设置一个截止时间点,WithTimeout 最终也会转换为 WithDeadline。
// 来自 src/context/example_test.go
func ExampleWithTimeout() {
// Pass a context with a timeout to tell a blocking function that it
// should abandon its work after the timeout elapses.
ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
// Output:
// context deadline exceeded
}
Go HttpClient 的另一种超时机制
基于 Context 可以设置任意代码段执行的超时机制,就可以设计一种脱离操作系统能力的请求超时能力。
超时机制简介
看一下 Go 的 HttpClient 超时配置说明:
client := http.Client{
Timeout: 10 * time.Second,
}
// 来自 src/net/http/client.go
type Client struct {
// ... 省略其他字段
// Timeout