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

2 回复

你的处理程序无法从外部停止。你必须在处理程序中实现取消上下文。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 协程。让我分析问题并提供解决方案:

问题分析

  1. TimeoutHandler 在新的 goroutine 中执行 ServeHTTP
  2. 超时后,handler 返回 503 状态码,但原始 goroutine 继续运行
  3. 每次请求都会泄漏一个 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 泄漏。

回到顶部