
问题背景
不得不说 Golang 的 DX 做的很舒服,比如在测试有问题的并发代码中会提醒你 Data Race 。这样就不用等你发现数据出问题了,再去找代码的问题。👀 最近开始用的一个很有帮助的工具就是 Goleak。来帮助我们找到泄漏的 goroutine。可以理解成其它语言的线程👀,泄漏意思就是工作做完之后,还有些这个工作开的 goroutine 没有结束。就像申请的内存在使用完之后没有释放一样。
按教程给我的代码测试加个了 goleak 测试。
但是一下测试, goleak 和我说我的代码有 goroutine 泄漏。🤯 我代码有不少并发代码,但是都是用的 sync.WaitGroup 来控制的。没有网上常见的什么 channel 导致的问题,所以我很奇怪为什么会有 goroutine 泄漏的问题。
通过给子函数加单元测试二分查找一下,我很快定位问题的位置,并不是我写的并发代码产生的 goroutine 泄漏。这个泄漏居然产生一个 Http 请求的代码里面。🤯
代码通过简化大概如下 👇
代码:
package httptest
import ( "net/http")
func HttpGet(url string) error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() return nil}
测试:
package httptest_test
import ( "fmt" "runtime" "testing"
"github.com/stretchr/testify/assert" "go.uber.org/goleak"
httptest "test")
func TestFail(t *testing.T) { fmt.Println("开始时goroutine的数量:", runtime.NumGoroutine())
defer goleak.VerifyNone(t)
err := httptest.HttpGet("https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/main/package.json") assert.NoError(t, err) fmt.Println("结束时goroutine的数量:", runtime.NumGoroutine())}
测试结果: 可以看到结果时的 goroutine 数量比开始时多了一个。说明有一个 goroutine 泄漏了。
开始时goroutine的数量: 2结束时goroutine的数量: 3--- FAIL: TestFail (0.52s) /Users/ctrdh/zeabur_reproduce/main_test.go:22: found unexpected goroutines: [Goroutine 36 in state IO wait, with internal/poll.runtime_pollWait on top of the stack: goroutine 36 [IO wait]: internal/poll.runtime_pollWait(0x109273fc0, 0x72) /opt/homebrew/Cellar/go/1.21.0/libexec/src/runtime/netpoll.go:343 +0xa0 internal/poll.(*pollDesc).wait(0x140000de480?, 0x140001c2000?, 0x0) /opt/homebrew/Cellar/go/1.21.0/libexec/src/internal/poll/fd_poll_runtime.go:84 +0x28 internal/poll.(*pollDesc).waitRead(...) /opt/homebrew/Cellar/go/1.21.0/libexec/src/internal/poll/fd_poll_runtime.go:89 internal/poll.(*FD).Read(0x140000de480, {0x140001c2000, 0x1300, 0x1300}) /opt/homebrew/Cellar/go/1.21.0/libexec/src/internal/poll/fd_unix.go:164 +0x200 net.(*netFD).Read(0x140000de480, {0x140001c2000?, 0x1400018e340?, 0x102242674?}) /opt/homebrew/Cellar/go/1.21.0/libexec/src/net/fd_posix.go:55 +0x28
如果看到我该 close 的都 close 的🤔,在好几个群里问来问去,我尝试换 URL 之类的还出过几个乌龙。最后谷歌了一下,在 stack overflow 找到了问题所在,这个原因是由于http.Get(url)
导致的。第二天另一个 Zeabur 的同学也找到了同样的帖子帮我解决了这个问题。🤣这里不得不感谢 Zeabur 了,经常遇到啥技术问题都发在他们工单群里,他们没有踢掉我不说还经常帮我解决问题。
解决方案
直接上解决该问题之后的代码:
func HttpGet(url string) ([]byte, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return []byte{}, err } client := http.Client{} resp, err := client.Do(req) if err != nil { return []byte{}, err } defer resp.Body.Close() defer client.CloseIdleConnections() // <-- 把空闲的连接关闭掉
result, err := io.ReadAll(resp.Body) if err != nil { return []byte{}, err }
return result, nil}
这样就能通过测试了。🎉
原因
上面的 Stack overflow 其实也说了这个问题是因为http.Client
使用了Transport
,而这个会维护一个连接池保持那些kept-alive
的连接。所以我们需要在defer
里面调用CloseIdleConnections
来关闭这些连接。