代码版本:go1.20.2
咱们晓得在用go的http client时不须要咱们被动敞开Request Body,上面你Client.Do的源码应用阐明:

//src/net/http/client.go//...// 这里说了底层的Transport会被动敞开request Body// The request Body, if non-nil, will be closed by the underlying// Transport, even on errors.//// ...func (c *Client) Do(req *Request) (*Response, error) {    return c.do(req)}

咱们在我的项目中须要用到这个个性,就是须要client被动帮忙咱们敞开request.Body,然而咱们发现协程泄露,最初定位到可能是因为request.Body没有被被动敞开导致.难懂是官网的形容有问题吗?最初咱们在github issue中看到了有人提出request.Body在特定状况下不会被关掉的场景, 最初官网也进行了修复.

咱们先来看下这个issue(https://github.com/golang/go/issues/49621):

他还写了演示示例(https://play.golang.com/p/lku8lEgiPu6)

重点次要是这上图中说的在writeLoop()里,可能pc.writech和pc.closech都有内容,然而执行到了<-pc.closech导致Request.Body没有被close
咱们先来看下writeLoop()源码,重点看下中文正文:

//src/net/http/transport.gofunc (pc *persistConn) writeLoop() {    defer close(pc.writeLoopDone)    for {        select {        case wr := <-pc.writech:            startBytesWritten := pc.nwrite            // 这外面会去敞开Request.Body,具体细节就不去看了            err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))            if bre, ok := err.(requestBodyReadError); ok {                //...            }            if err == nil {                err = pc.bw.Flush()            }            if err != nil {                if pc.nwrite == startBytesWritten {                    err = nothingWrittenError{err}                }            }            pc.writeErrCh <- err // to the body reader, which might recycle us            wr.ch <- err         // to the roundTrip function            if err != nil {                pc.close(err)                return            }        case <-pc.closech: //间接退出            return        }    }}

咱们能够看到如果失常申请下须要进入到case wr := <-pc.writech才会对request进行操作,才会在外面close request.Body.如果case wr := <-pc.writechcase <-pc.closech都满足,然而进入到了case <-pc.closech就会导致request.Body不会被敞开。那么这种状况在什么时候会产生了呢?

//src/net/http/transport.gofunc (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {    // ...    // Write the request concurrently with waiting for a response,    // in case the server decides to reply before reading our full    // request body.    startBytesWritten := pc.nwrite    writeErrCh := make(chan error, 1)    // 这里写入pc.writech    pc.writech <- writeRequest{req, writeErrCh, continueCh}    //...}

下面的roundTrip()写入了pc.writech,然而pc.closech是在其余协程写入的

//src/net/http/transport.go// close closes the underlying TCP connection and closes// the pc.closech channel.//// The provided err is only for testing and debugging; in normal// circumstances it should never be seen by users.func (pc *persistConn) close(err error) {    pc.mu.Lock()    defer pc.mu.Unlock()    pc.closeLocked(err)}func (pc *persistConn) closeLocked(err error) {    if err == nil {        panic("nil error")    }    pc.broken = true    if pc.closed == nil {        pc.closed = err        pc.t.decConnsPerHost(pc.cacheKey)        // Close HTTP/1 (pc.alt == nil) connection.        // HTTP/2 closes its connection itself.        if pc.alt == nil {            if err != errCallerOwnsConn {                pc.conn.Close()            }            close(pc.closech) // 这里唤醒pc.closech        }    }    pc.mutateHeaderFunc = nil}

咱们能够看到pc.closech次要是在persistConn close()的时候唤醒.所以大抵逻辑就是申请到了一条连贯persistConn而后在Read/Write的时候疾速失败,因为这两个在不同的协程导致pc.writechpc.closech同时满足条件。go官网修复了这个bug(https://go-review.googlesource.com/c/go/+/461675),咱们来看下怎么修复的:
https://go-review.googlesource.com/c/go/+/461675/4/src/net/ht...

看下面批改局部就是在(t *Transport) roundTrip(req *Request)外面再去尝试敞开request.Body.咱们再看下这次pr的测试用例,很清晰:
https://go-review.googlesource.com/c/go/+/461675/4/src/net/ht...

上面把重要局部解释下:

// https://go.dev/issue/49621func TestConnClosedBeforeRequestIsWritten(t *testing.T) {    run(t, testConnClosedBeforeRequestIsWritten, testNotParallel, []testMode{http1Mode})}func testConnClosedBeforeRequestIsWritten(t *testing.T, mode testMode) {    ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {}),        func(tr *Transport) {            tr.DialContext = func(_ context.Context, network, addr string) (net.Conn, error) {                // Connection会疾速返回谬误                return &funcConn{                    // 这里本人定义一个conn,不论是Read还是Write都会即刻返回谬误                    read: func([]byte) (int, error) {                        return 0, errors.New("error")                    },                    write: func([]byte) (int, error) {                        return 0, errors.New("error")                    },                }, nil            }        },    ).ts    // 这里设置了一个hook就是在进入RoundTrip前劳动一下给足够的工夫让closech被close    SetEnterRoundTripHook(func() {        time.Sleep(1 * time.Millisecond)    })    defer SetEnterRoundTripHook(nil)    var closes int    _, err := ts.Client().Post(ts.URL, "text/plain", countCloseReader{&closes, strings.NewReader("hello")})    if err == nil {        t.Fatalf("expected request to fail, but it did not")    }    // 这里的closes应该等于1    if closes != 1 {        t.Errorf("after RoundTrip, request body was closed %v times; want 1", closes)    }}

目前这个bug fix曾经合入了master,然而什么时候公布到正式版本未知

总结

不要太置信官网的操作,官网也是可能有bug的,要大胆猜疑并去摸索。

相干链接

  • bug fix: https://go-review.googlesource.com/c/go/+/461675
  • issue: https://github.com/golang/go/issues/49621