Golang中http.TimeoutHandler未正确终止对应ServeHTTP协程的goroutine泄漏问题
Golang中http.TimeoutHandler未正确终止对应ServeHTTP协程的goroutine泄漏问题 TimeoutHandler 将 ServeHTTP 执行移至新的 goroutine,但在计时器结束后无法终止该 goroutine。每次请求都会创建两个 goroutine,但 ServeHTTP 的 goroutine 永远不会通过上下文被终止。
package main
import (
"fmt"
"io"
"net/http"
"runtime"
"time"
)
type api struct{}
func (a api) ServeHTTP(w http.ResponseWriter, req *http.Request) {
i := 0
for {
if i == 500 {
break
}
//fmt.Printf("@time: %s\n", time.Now())
fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine())
time.Sleep(1 * time.Second)
i++
}
_, _ = io.WriteString(w, "Hello World!")
}
func main() {
var a api
s := http.NewServeMux()
s.Handle("/", a)
h := http.TimeoutHandler(s, 1*time.Second, `Timeout`)
fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine())
_ = http.ListenAndServe(":8080", h)
}
更多关于Golang中http.TimeoutHandler未正确终止对应ServeHTTP协程的goroutine泄漏问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你的处理程序无法从外部停止。你必须在处理程序中实现取消上下文。TimeoutHandler不会/无法终止协程,它会取消上下文并以503服务不可用错误响应。你需要自行处理。
package main
import (
"fmt"
"io"
"net/http"
"runtime"
"time"
)
type api struct{}
func (a api) ServeHTTP(w http.ResponseWriter, req *http.Request) {
i := 0
for {
if i == 500 {
break
}
//fmt.Printf("@time: %s\n", time.Now())
fmt.Printf("i: %d #goroutines: %d\n", i, runtime.NumGoroutine())
time.Sleep(1 * time.Second)
select {
case <-req.Context().Done():
{
fmt.Println("context canceled")
return
}
default:
}
i++
}
_, _ = io.WriteString(w, "Hello World!")
}
func main() {
var a api
s := http.NewServeMux()
s.Handle("/", a)
h := http.TimeoutHandler(s, 10*time.Second, `Timeout`)
fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine())
_ = http.ListenAndServe(":8081", h)
}
更多关于Golang中http.TimeoutHandler未正确终止对应ServeHTTP协程的goroutine泄漏问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
这是一个典型的 goroutine 泄漏问题。http.TimeoutHandler 确实会在超时后向客户端返回超时响应,但无法强制终止正在执行的 ServeHTTP 协程。让我分析问题并提供解决方案:
问题分析
TimeoutHandler在新的 goroutine 中执行ServeHTTP- 超时后,handler 返回 503 状态码,但原始 goroutine 继续运行
- 每次请求都会泄漏一个 goroutine
解决方案:使用 context 进行协程控制
package main
import (
"context"
"fmt"
"io"
"net/http"
"runtime"
"time"
)
type api struct{}
func (a api) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
i := 0
for {
select {
case <-ctx.Done():
// 上下文被取消,立即返回
fmt.Printf("Request cancelled: %v\n", ctx.Err())
return
default:
if i == 500 {
break
}
fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine())
time.Sleep(1 * time.Second)
i++
}
}
// 检查上下文是否已完成
if ctx.Err() != nil {
fmt.Printf("Context error before write: %v\n", ctx.Err())
return
}
_, _ = io.WriteString(w, "Hello World!")
}
func main() {
var a api
s := http.NewServeMux()
s.Handle("/", a)
h := http.TimeoutHandler(s, 1*time.Second, `Timeout`)
fmt.Printf("Initial #goroutines: %d\n", runtime.NumGoroutine())
_ = http.ListenAndServe(":8080", h)
}
更健壮的解决方案:自定义超时处理
package main
import (
"context"
"fmt"
"io"
"net/http"
"runtime"
"time"
)
type api struct{}
func (a api) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
// 使用带超时的上下文
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
done := make(chan struct{})
go func() {
defer close(done)
i := 0
for i < 500 {
select {
case <-ctx.Done():
return
default:
fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine())
time.Sleep(1 * time.Second)
i++
}
}
// 只有在上下文未取消时才写入响应
if ctx.Err() == nil {
_, _ = io.WriteString(w, "Hello World!")
}
}()
// 等待完成或超时
select {
case <-done:
// 正常完成
case <-ctx.Done():
http.Error(w, "Timeout", http.StatusServiceUnavailable)
}
}
func main() {
var a api
mux := http.NewServeMux()
mux.Handle("/", a)
fmt.Printf("Initial #goroutines: %d\n", runtime.NumGoroutine())
_ = http.ListenAndServe(":8080", mux)
}
使用 http.TimeoutHandler 的正确方式
package main
import (
"context"
"fmt"
"io"
"net/http"
"runtime"
"time"
)
type api struct{}
func (a api) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
i := 0
for i < 10 { // 减少循环次数用于测试
select {
case <-ctx.Done():
fmt.Printf("Goroutine properly terminated: %v\n", ctx.Err())
return
default:
fmt.Printf("#goroutines: %d, i: %d\n", runtime.NumGoroutine(), i)
time.Sleep(100 * time.Millisecond) // 更短的睡眠时间
i++
}
}
if ctx.Err() == nil {
_, _ = io.WriteString(w, "Hello World!")
}
}
func main() {
var a api
mux := http.NewServeMux()
mux.Handle("/", a)
// 使用 TimeoutHandler,但确保业务逻辑响应上下文取消
timeoutHandler := http.TimeoutHandler(mux, 500*time.Millisecond, "Timeout")
fmt.Printf("Initial #goroutines: %d\n", runtime.NumGoroutine())
_ = http.ListenAndServe(":8080", timeoutHandler)
}
关键点在于在 ServeHTTP 方法中定期检查 req.Context().Done() 通道,当超时发生时及时返回,避免 goroutine 泄漏。

