fasthttp
Go语言的快速HTTP实现。
fasthttp可能并不适合你!
fasthttp是为某些高性能边缘场景设计的。除非你的服务器/客户端需要每秒处理数千个小型到中型请求,并且需要保持一致的低毫秒级响应时间,否则fasthttp可能并不适合你。对于大多数情况,net/http
要好得多,因为它更易于使用且能处理更多场景。在大多数情况下,你甚至不会注意到性能差异。
基本信息和链接
目前,VertaMedia在生产环境中成功使用fasthttp,每台物理服务器可处理超过150万个并发保持连接,提供高达每秒20万次的请求处理能力。
与net/http的HTTP服务器性能比较
简而言之,fasthttp服务器的速度比net/http快至多10倍。 以下是基准测试结果。
GOMAXPROCS=1
net/http服务器:
$ GOMAXPROCS=1 go test -bench=NetHTTPServerGet -benchmem -benchtime=10s
BenchmarkNetHTTPServerGet1ReqPerConn 1000000 12052 ns/op 2297 B/op 29 allocs/op
BenchmarkNetHTTPServerGet2ReqPerConn 1000000 12278 ns/op 2327 B/op 24 allocs/op
BenchmarkNetHTTPServerGet10ReqPerConn 2000000 8903 ns/op 2112 B/op 19 allocs/op
BenchmarkNetHTTPServerGet10KReqPerConn 2000000 8451 ns/op 2058 B/op 18 allocs/op
BenchmarkNetHTTPServerGet1ReqPerConn10KClients 500000 26733 ns/op 3229 B/op 29 allocs/op
BenchmarkNetHTTPServerGet2ReqPerConn10KClients 1000000 23351 ns/op 3211 B/op 24 allocs/op
BenchmarkNetHTTPServerGet10ReqPerConn10KClients 1000000 13390 ns/op 2483 B/op 19 allocs/op
BenchmarkNetHTTPServerGet100ReqPerConn10KClients 1000000 13484 ns/op 2171 B/op 18 allocs/op
fasthttp服务器:
$ GOMAXPROCS=1 go test -bench=kServerGet -benchmem -benchtime=10s
BenchmarkServerGet1ReqPerConn 10000000 1559 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet2ReqPerConn 10000000 1248 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10ReqPerConn 20000000 797 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10KReqPerConn 20000000 716 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet1ReqPerConn10KClients 10000000 1974 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet2ReqPerConn10KClients 10000000 1352 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10ReqPerConn10KClients 20000000 789 ns/op 2 B/op 0 allocs/op
BenchmarkServerGet100ReqPerConn10KClients 20000000 604 ns/op 0 B/op 0 allocs/op
GOMAXPROCS=4
net/http服务器:
$ GOMAXPROCS=4 go test -bench=NetHTTPServerGet -benchmem -benchtime=10s
BenchmarkNetHTTPServerGet1ReqPerConn-4 3000000 4529 ns/op 2389 B/op 29 allocs/op
BenchmarkNetHTTPServerGet2ReqPerConn-4 5000000 3896 ns/op 2418 B/op 24 allocs/op
BenchmarkNetHTTPServerGet10ReqPerConn-4 5000000 3145 ns/op 2160 B/op 19 allocs/op
BenchmarkNetHTTPServerGet10KReqPerConn-4 5000000 3054 ns/op 2065 B/op 18 allocs/op
BenchmarkNetHTTPServerGet1ReqPerConn10KClients-4 1000000 10321 ns/op 3710 B/op 30 allocs/op
BenchmarkNetHTTPServerGet2ReqPerConn10KClients-4 2000000 7556 ns/op 3296 B/op 24 allocs/op
BenchmarkNetHTTPServerGet10ReqPerConn10KClients-4 5000000 3905 ns/op 2349 B/op 19 allocs/op
BenchmarkNetHTTPServerGet100ReqPerConn10KClients-4 5000000 3435 ns/op 2130 B/op 18 allocs/op
fasthttp服务器:
$ GOMAXPROCS=4 go test -bench=kServerGet -benchmem -benchtime=10s
BenchmarkServerGet1ReqPerConn-4 10000000 1141 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet2ReqPerConn-4 20000000 707 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10ReqPerConn-4 30000000 341 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10KReqPerConn-4 50000000 310 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet1ReqPerConn10KClients-4 10000000 1119 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet2ReqPerConn10KClients-4 20000000 644 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet10ReqPerConn10KClients-4 30000000 346 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet100ReqPerConn10KClients-4 50000000 282 ns/op 0 B/op 0 allocs/op
与net/http的HTTP客户端比较
简而言之,fasthttp客户端的速度比net/http快至多10倍。 以下是基准测试结果。
GOMAXPROCS=1 net/http 客户端:
$ GOMAXPROCS=1 go test -bench='HTTPClient(Do|GetEndToEnd)' -benchmem -benchtime=10s
BenchmarkNetHTTPClientDoFastServer 1000000 12567 ns/op 2616 B/op 35 allocs/op
BenchmarkNetHTTPClientGetEndToEnd1TCP 200000 67030 ns/op 5028 B/op 56 allocs/op
BenchmarkNetHTTPClientGetEndToEnd10TCP 300000 51098 ns/op 5031 B/op 56 allocs/op
BenchmarkNetHTTPClientGetEndToEnd100TCP 300000 45096 ns/op 5026 B/op 55 allocs/op
BenchmarkNetHTTPClientGetEndToEnd1Inmemory 500000 24779 ns/op 5035 B/op 57 allocs/op
BenchmarkNetHTTPClientGetEndToEnd10Inmemory 1000000 26425 ns/op 5035 B/op 57 allocs/op
BenchmarkNetHTTPClientGetEndToEnd100Inmemory 500000 28515 ns/op 5045 B/op 57 allocs/op
BenchmarkNetHTTPClientGetEndToEnd1000Inmemory 500000 39511 ns/op 5096 B/op 56 allocs/op
fasthttp 客户端:
$ GOMAXPROCS=1 go test -bench='kClient(Do|GetEndToEnd)' -benchmem -benchtime=10s
BenchmarkClientDoFastServer 20000000 865 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd1TCP 1000000 18711 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd10TCP 1000000 14664 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd100TCP 1000000 14043 ns/op 1 B/op 0 allocs/op
BenchmarkClientGetEndToEnd1Inmemory 5000000 3965 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd10Inmemory 3000000 4060 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd100Inmemory 5000000 3396 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd1000Inmemory 5000000 3306 ns/op 2 B/op 0 allocs/op
GOMAXPROCS=4
net/http 客户端:
$ GOMAXPROCS=4 go test -bench='HTTPClient(Do|GetEndToEnd)' -benchmem -benchtime=10s
BenchmarkNetHTTPClientDoFastServer-4 2000000 8774 ns/op 2619 B/op 35 allocs/op
BenchmarkNetHTTPClientGetEndToEnd1TCP-4 500000 22951 ns/op 5047 B/op 56 allocs/op
BenchmarkNetHTTPClientGetEndToEnd10TCP-4 1000000 19182 ns/op 5037 B/op 55 allocs/op
BenchmarkNetHTTPClientGetEndToEnd100TCP-4 1000000 16535 ns/op 5031 B/op 55 allocs/op
BenchmarkNetHTTPClientGetEndToEnd1Inmemory-4 1000000 14495 ns/op 5038 B/op 56 allocs/op
BenchmarkNetHTTPClientGetEndToEnd10Inmemory-4 1000000 10237 ns/op 5034 B/op 56 allocs/op
BenchmarkNetHTTPClientGetEndToEnd100Inmemory-4 1000000 10125 ns/op 5045 B/op 56 allocs/op
BenchmarkNetHTTPClientGetEndToEnd1000Inmemory-4 1000000 11132 ns/op 5136 B/op 56 allocs/op
fasthttp 客户端:
$ GOMAXPROCS=4 go test -bench='kClient(Do|GetEndToEnd)' -benchmem -benchtime=10s
BenchmarkClientDoFastServer-4 50000000 397 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd1TCP-4 2000000 7388 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd10TCP-4 2000000 6689 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd100TCP-4 3000000 4927 ns/op 1 B/op 0 allocs/op
BenchmarkClientGetEndToEnd1Inmemory-4 10000000 1604 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd10Inmemory-4 10000000 1458 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd100Inmemory-4 10000000 1329 ns/op 0 B/op 0 allocs/op
BenchmarkClientGetEndToEnd1000Inmemory-4 10000000 1316 ns/op 5 B/op 0 allocs/op
安装
go get -u github.com/valyala/fasthttp
从 net/http 切换到 fasthttp
遗憾的是,fasthttp 并未提供与 net/http 完全相同的 API。 详情请参阅 FAQ。 虽然有 net/http -> fasthttp 处理器转换器, 但为了充分利用 fasthttp 的所有优势(尤其是高性能),最好手动编写 fasthttp 请求处理器。
重要事项:
-
Fasthttp 使用 RequestHandler 函数 而不是实现 Handler 接口 的对象。 幸运的是,可以轻松地将绑定的结构体方法传递给 fasthttp:
type MyHandler struct { foobar string } // net/http 风格的请求处理器,即绑定到 MyHandler 结构体的方法。 func (h *MyHandler) HandleFastHTTP(ctx *fasthttp.RequestCtx) { // 注意,我们可以在这里访问 MyHandler 的属性 - 见 h.foobar。 fmt.Fprintf(ctx, "Hello, world! Requested path is %q. Foobar is %q", ctx.Path(), h.foobar) } // fasthttp 风格的请求处理器,即普通函数。 func fastHTTPHandler(ctx *fasthttp.RequestCtx) { fmt.Fprintf(ctx, "Hi there! RequestURI is %q", ctx.RequestURI()) } // 将绑定的结构体方法传递给 fasthttp myHandler := &MyHandler{ foobar: "foobar", } fasthttp.ListenAndServe(":8080", myHandler.HandleFastHTTP) // 将普通函数传递给 fasthttp fasthttp.ListenAndServe(":8081", fastHTTPHandler)
-
RequestHandler 只接受一个参数 - RequestCtx。 它包含了处理 HTTP 请求和写入响应所需的所有功能。 下面是一个简单的请求处理器从 net/http 转换到 fasthttp 的例子。
// net/http 请求处理器 requestHandler := func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/foo": fooHandler(w, r) case "/bar": barHandler(w, r) default: http.Error(w, "Unsupported path", http.StatusNotFound) } }
// 对应的 fasthttp 请求处理程序
requestHandler := func(ctx *fasthttp.RequestCtx) {
switch string(ctx.Path()) {
case "/foo":
fooHandler(ctx)
case "/bar":
barHandler(ctx)
default:
ctx.Error("不支持的路径", fasthttp.StatusNotFound)
}
}
- Fasthttp 允许以任意顺序设置响应头和写入响应体。不像 net/http 那样有"先头部,后主体"的限制。以下代码在 fasthttp 中是有效的:
requestHandler := func(ctx *fasthttp.RequestCtx) {
// 首先设置一些头部和状态码
ctx.SetContentType("foo/bar")
ctx.SetStatusCode(fasthttp.StatusOK)
// 然后写入第一部分主体
fmt.Fprintf(ctx, "这是主体的第一部分\n")
// 再设置更多头部
ctx.Response.Header.Set("Foo-Bar", "baz")
// 继续写入更多主体
fmt.Fprintf(ctx, "这是主体的第二部分\n")
// 然后覆盖已写入的主体
ctx.SetBody([]byte("这是完全新的主体内容"))
// 再更新状态码
ctx.SetStatusCode(fasthttp.StatusNotFound)
// 基本上,在 RequestHandler 返回之前,
// 任何内容都可以多次更新。
//
// 与 net/http 不同,fasthttp 直到
// RequestHandler 返回后才将响应发送到网络。
}
-
Fasthttp 没有提供 ServeMux, 但有更强大的第三方路由器和支持 fasthttp 的 Web 框架:
使用简单 ServeMux 的 net/http 代码可以轻松转换为 fasthttp 代码:
// net/http 代码
m := &http.ServeMux{}
m.HandleFunc("/foo", fooHandlerFunc)
m.HandleFunc("/bar", barHandlerFunc)
m.Handle("/baz", bazHandler)
http.ListenAndServe(":80", m)
// 对应的 fasthttp 代码
m := func(ctx *fasthttp.RequestCtx) {
switch string(ctx.Path()) {
case "/foo":
fooHandlerFunc(ctx)
case "/bar":
barHandlerFunc(ctx)
case "/baz":
bazHandler.HandlerFunc(ctx)
default:
ctx.Error("未找到", fasthttp.StatusNotFound)
}
}
fasthttp.ListenAndServe(":80", m)
- 因为为每个请求创建一个新的通道代价太高,所以 RequestCtx.Done() 返回的通道只在服务器关闭时才会关闭。
func main() {
fasthttp.ListenAndServe(":8080", fasthttp.TimeoutHandler(func(ctx *fasthttp.RequestCtx) {
select {
case <-ctx.Done():
// ctx.Done() 只在服务器关闭时才会关闭。
log.Println("上下文已取消")
return
case <-time.After(10 * time.Second):
log.Println("处理成功完成")
}
}, time.Second*2, "超时"))
}
-
net/http -> fasthttp 转换对照表:
-
以下所有伪代码假设 w、r 和 ctx 具有以下类型:
var ( w http.ResponseWriter r *http.Request ctx *fasthttp.RequestCtx )
- r.Body -> ctx.PostBody()
- r.URL.Path -> ctx.Path()
- r.URL -> ctx.URI()
- r.Method -> ctx.Method()
- r.Header -> ctx.Request.Header
- r.Header.Get() -> ctx.Request.Header.Peek()
- r.Host -> ctx.Host()
- r.Form -> ctx.QueryArgs() + ctx.PostArgs()
- r.PostForm -> ctx.PostArgs()
- r.FormValue() -> ctx.FormValue()
- r.FormFile() -> ctx.FormFile()
- r.MultipartForm -> ctx.MultipartForm()
- r.RemoteAddr -> ctx.RemoteAddr()
- r.RequestURI -> ctx.RequestURI()
- r.TLS -> ctx.IsTLS()
- r.Cookie() -> ctx.Request.Header.Cookie()
- r.Referer() -> ctx.Referer()
- r.UserAgent() -> ctx.UserAgent()
- w.Header() -> ctx.Response.Header
- w.Header().Set() -> ctx.Response.Header.Set()
- w.Header().Set("Content-Type") -> ctx.SetContentType()
- w.Header().Set("Set-Cookie") -> ctx.Response.Header.SetCookie()
- w.Write() -> ctx.Write(), ctx.SetBody(), ctx.SetBodyStream(), ctx.SetBodyStreamWriter()
- w.WriteHeader() -> ctx.SetStatusCode()
- w.(http.Hijacker).Hijack() -> ctx.Hijack()
- http.Error() -> ctx.Error()
- http.FileServer() -> fasthttp.FSHandler(), fasthttp.FS
- http.ServeFile() -> fasthttp.ServeFile()
- http.Redirect() -> ctx.Redirect()
- http.NotFound() -> ctx.NotFound()
- http.StripPrefix() -> fasthttp.PathRewriteFunc
-
非常重要! Fasthttp 禁止在 RequestHandler 返回后保留对 RequestCtx 或其成员的引用。 否则,数据竞争是不可避免的。 仔细检查所有转换为 fasthttp 的 net/http 请求处理程序,确保它们在返回后不会保留对 RequestCtx 或其成员的引用。 RequestCtx 为这种情况提供了以下"应急措施":
- 使用 TimeoutHandler 包装 RequestHandler。
- 如果存在对 RequestCtx 或其成员的引用,在 RequestHandler 返回之前调用 TimeoutError。 有关更多详细信息,请参阅示例。
使用这个出色的工具 - race detector - 来检测和消除程序中的数据竞争。如果在程序中检测到与 fasthttp 相关的数据竞争,那么很可能是因为您忘记在 RequestHandler 返回之前调用 TimeoutError。
-
盲目从 net/http 切换到 fasthttp 不会带来性能提升。 虽然 fasthttp 经过了速度优化,但其性能很容易被缓慢的 RequestHandler 所饱和。 因此,在切换到 fasthttp 后,请分析并优化你的代码。例如,使用 quicktemplate 代替 html/template。
-
另请参阅 fasthttputil、 fasthttpadaptor 和 expvarhandler。
多核系统性能优化技巧
- 使用 reuseport 监听器。
- 为每个 CPU 核心运行一个单独的服务器实例,并设置 GOMAXPROCS=1。
- 使用 taskset 将每个服务器实例固定到单独的 CPU 核心。
- 确保多队列网卡的中断在 CPU 核心之间均匀分布。 详情请参阅这篇文章。
- 使用最新版本的 Go,因为每个版本都包含性能改进。
Fasthttp 最佳实践
- 尽可能地重复使用对象和
[]byte
缓冲区,而不是重新分配。Fasthttp的API设计鼓励这种做法。 - sync.Pool是你最好的朋友。
- 在生产环境中分析你的程序。
go tool pprof --alloc_objects your-program mem.pprof
通常比go tool pprof your-program cpu.pprof
能提供更好的优化机会洞察。 - 为热点路径编写测试和基准。
- 避免在
[]byte
和string
之间进行转换,因为这可能导致内存分配和复制。Fasthttp API为[]byte
和string
都提供了函数 - 使用这些函数而不是手动在[]byte
和string
之间转换。有一些例外情况 - 更多细节请参见这个wiki页面。 - 定期在竞态检测器下验证你的测试和生产代码。
- 在你的Web服务器中优先使用quicktemplate而不是html/template。
[]byte
缓冲区的技巧
以下技巧被fasthttp使用。在你的代码中也可以使用它们。
- 标准Go函数接受nil缓冲区
var (
// 两个缓冲区都未初始化
dst []byte
src []byte
)
dst = append(dst, src...) // 如果dst为nil和/或src为nil也是合法的
copy(dst, src) // 如果dst为nil和/或src为nil也是合法的
(string(src) == "") // 如果src为nil则为true
(len(src) == 0) // 如果src为nil则为true
src = src[:0] // 对nil的src也能完美工作
// 即使src为nil,这个for循环也不会panic
for i, ch := range src {
doSomething(i, ch)
}
所以从你的代码中删除对[]byte
缓冲区的nil检查。例如,
srcLen := 0
if src != nil {
srcLen = len(src)
}
变成
srcLen := len(src)
- 字符串可以用
append
追加到[]byte
缓冲区
dst = append(dst, "foobar"...)
[]byte
缓冲区可以扩展到其容量
buf := make([]byte, 100)
a := buf[:10] // len(a) == 10, cap(a) == 100.
b := a[:100] // 有效,因为cap(a) == 100.
- 所有fasthttp函数都接受nil
[]byte
缓冲区
statusCode, body, err := fasthttp.Get(nil, "http://google.com/")
uintBuf := fasthttp.AppendUint(nil, 1234)
- 字符串和
[]byte
缓冲区可以无内存分配地转换
func b2s(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
func s2b(s string) (b []byte) {
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh.Data = sh.Data
bh.Cap = sh.Len
bh.Len = sh.Len
return b
}
警告:
这是一种不安全的方法,结果字符串和[]byte
缓冲区共享相同的字节。
请确保在字符串仍然存在时不要修改[]byte
缓冲区中的字节!
相关项目
- fasthttp - 基于fasthttp的项目的各种有用辅助工具。
- fasthttp-routing - 用于fasthttp服务器的快速强大的路由包。
- http2 - fasthttp的HTTP/2实现。
- router - 一个高性能的fasthttp请求路由器,可以很好地扩展。
- fastws - 为fasthttp设计的轻量级WebSocket包,用于并发处理读/写操作。
- gramework - 由fasthttp维护者之一制作的Web框架。
- lu - 一个基于fasthttp的高性能Go中间件Web框架。
- websocket - 基于Gorilla的fasthttp WebSocket实现。
- websocket - 基于事件的高性能WebSocket库,用于零分配的WebSocket服务器和客户端。
- fasthttpsession - 用于fasthttp服务器的快速强大的会话包。
- atreugo - 高性能且可扩展的微型Web框架,在热路径上实现零内存分配。
- kratgo - 简单、轻量级和超快速的HTTP缓存,用于加速你的网站。
- kit-plugins - fasthttp的go-kit传输实现。
- Fiber - 受Expressjs启发的运行在Fasthttp上的Web框架。
- Gearbox - :gear: gearbox是一个用Go编写的Web框架,专注于高性能和内存优化。
- http2curl - 一个将fasthttp请求转换为curl命令行的工具。
常见问题
-
为什么要创建另一个http包而不是优化net/http?
因为net/http API限制了许多优化机会。 例如:
- net/http Request对象的生命周期不限于请求处理器执行时间。所以服务器必须为每个请求创建一个新的请求对象,而不能像fasthttp那样重用现有对象。
- net/http头部存储在
map[string][]string
中。所以服务器必须解析所有头部,将它们从[]byte
转换为string
并放入map中,然后才能调用用户提供的请求处理器。这些都需要不必要的内存分配,而fasthttp避免了这些。 - net/http客户端API要求为每个请求创建一个新的响应对象。
-
为什么fasthttp API与net/http不兼容?
因为net/http API限制了许多优化机会。更多细节请参见上面的回答。此外,某些net/http API部分在使用上并不理想:
-
为什么fasthttp不支持HTTP/2.0和WebSockets?
HTTP/2.0支持正在进行中。WebSockets已经完成。 第三方也可以使用RequestCtx.Hijack来实现这些功能。
-
与fasthttp相比,net/http有哪些已知的优势? 是的:
-
net/http 从 go1.6 开始支持 HTTP/2.0。
-
net/http 的 API 稳定,而 fasthttp 的 API 不断演进。
-
net/http 能处理更多 HTTP 边缘情况。
-
net/http 可以同时流式处理请求和响应体。
-
net/http 可以处理更大的消息体,因为它不会将整个消息体读入内存。
-
net/http 应该包含更少的 bug,因为它被更广泛的用户使用和测试。
-
fasthttp API 为什么倾向于返回
[]byte
而不是string
?因为
[]byte
转换为string
并非免费操作 - 它需要内存分配和复制。如果你更喜欢使用字符串而不是字节切片,可以随意将返回的[]byte
结果包装在string()
中。但请注意,这会带来一定的开销。 -
fasthttp 支持哪些 GO 版本?
Go 1.18.x。不会支持更早的版本。
-
请提供真实的基准测试数据和服务器信息
请参阅这个 issue。
-
是否计划为 fasthttp 添加请求路由功能?
没有计划在 fasthttp 中添加请求路由功能。 可以使用支持 fasthttp 的第三方路由器和 Web 框架:
更多信息请参阅这个 issue。
-
我在 fasthttp 中发现了数据竞争!
很好!提交一个 bug。但在此之前,请检查你的代码中以下几点:
- 确保在从 RequestHandler 返回后没有对 RequestCtx 或其成员的引用。
- 如果有对 RequestCtx 或其成员的引用可能被其他 goroutine 访问,请确保在从 RequestHandler 返回之前调用 TimeoutError。
-
我在这里没有找到我问题的答案
尝试浏览这些问题。