Go语言httpClient协程未关闭引发内存泄漏,求修复方案
Let’s walk through what’s going wrong with your code and fix those memory leaks and unclosed goroutines step by step:
1. Your sync.WaitGroup logic is broken
The second loop in your main code adds unnecessary wg.Add(1) calls and calls wg.Done() on the receiving side—this completely skews the WaitGroup’s counter. The WaitGroup should only track the worker goroutines you spawn to check proxies, not the result-receiving logic. This mismatch is likely why you’re seeing goroutines not terminate properly.
2. Unclosed http.Transport is causing lingering goroutines
Even with DisableKeepAlives: true, the http.Transport spins up background goroutines to manage connections and cleanup. If you don’t explicitly shut down the transport after use, these goroutines will stick around, leading to memory leaks and unclosed goroutines.
Fixed Code
Updated checkProxySOCKS Function
func checkProxySOCKS(prox string, c chan QR, wg *sync.WaitGroup) error { defer wg.Done() // Handle SOCKS dialer errors properly instead of ignoring them dialer, err := proxy.SOCKS5("tcp", prox, nil, proxy.Direct) if err != nil { c <- QR{Addr: prox, Res: false} return err } timeout := 2 * time.Second // Create a dedicated transport for each proxy check transport := &http.Transport{ DisableKeepAlives: true, Dial: dialer.Dial, ResponseHeaderTimeout: timeout, // Add DialContext for better context-aware dialing (matches modern Go practices) DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return dialer.Dial(network, addr) }, } // Critical: Close idle connections to shut down background transport goroutines defer transport.CloseIdleConnections() httpClient := &http.Client{ Timeout: timeout, Transport: transport, } res, err := httpClient.Get("https://telegram.org/") if err != nil { c <- QR{Addr: prox, Res: false} return err } defer res.Body.Close() // Always read the full body to avoid connection leaks (even with DisableKeepAlives) _, err = io.Copy(ioutil.Discard, res.Body) if err != nil { c <- QR{Addr: prox, Res: false} return err } c <- QR{Addr: prox, Res: true} return nil }
Updated Main Goroutine Logic
// Use a buffered channel to prevent worker goroutines from blocking on send respChan := make(chan QR, len(splitedProxies)) var wg sync.WaitGroup // Spawn all proxy-checking workers for _, proxy := range splitedProxies { wg.Add(1) go checkProxySOCKS(proxy, respChan, &wg) } // Wait for all workers to finish, then close the channel to signal the receiver loop go func() { wg.Wait() close(respChan) }() // Collect all results without messing with WaitGroup for r := range respChan { if r.Res { checkedProxiesArray = append(checkedProxiesArray, r.Addr) } }
Key Fixes Explained
- Correct WaitGroup Usage: We only track the worker goroutines. Once all workers are done, we close the response channel, letting the receiver loop exit gracefully without extra WaitGroup calls.
- Explicit Transport Cleanup:
transport.CloseIdleConnections()ensures that all background goroutines managed by the HTTP transport are shut down, eliminating memory leaks and lingering goroutines. - Buffered Channel: Using a buffered channel with the length of your proxy list prevents worker goroutines from blocking if the receiver isn’t ready to read immediately.
- Better Error Handling: We no longer ignore errors from the SOCKS dialer, and ensure that any failure during body reading still sends a proper result to the channel.
内容的提问来源于stack exchange,提问作者anon

