发现了Go并发下载的Bug-《GO开发知识笔记》

admin 2025-11-04 01:23:20 编程 来源:ZONE.CI 全球网 0 阅读模式
  • 01 排查过程
  • 02 curl 和 Go 代码行为异同

    前几天我写了一篇文章:Go项目实战:一步步构建一个并发文件下载器,有小伙伴评论问,请求 [https://studygolang.com/dl/golang/go1.16.5.src.tar.gz](https://studygolang.com/dl/golang/go1.16.5.src.tar.gz) 为什么没有返回 Accept-Ranges。在写那篇文章时,我也试了,确实没有返回,因此我以为它不支持。但有一个小伙伴很认真,他改用 GET 方法请求这个地址,结果却有 Accept-Ranges,于是就很困惑,问我什么原因。经过一顿操作猛如虎,终于知道原因了。记录下排查过程,供大家参考!(小伙伴的留言可以查看那篇文章)

    01 排查过程

    通过 curl 命令,分别用 GET 和 HEAD 方法请求这个地址,结果如下:

    1. $ curl -X GET --head https://studygolang.com/dl/golang/go1.16.5.src.tar.gz
    2. HTTP/1.1 303 See Other
    3. Server: nginx
    4. Date: Wed, 07 Jul 2021 09:09:35 GMT
    5. Content-Length: 0
    6. Connection: keep-alive
    7. Location: https://golang.google.cn/dl/go1.16.5.src.tar.gz
    8. X-Request-Id: 83ee595c-6270-4fb0-a2f1-98fdc4d315be
    9. $ curl --head https://studygolang.com/dl/golang/go1.16.5.src.tar.gz
    10. HTTP/1.1 200 OK
    11. Server: nginx
    12. Date: Wed, 07 Jul 2021 09:09:44 GMT
    13. Connection: keep-alive
    14. X-Request-Id: f2ba473d-5bee-44c3-a591-02c358551235

    虽然都没有 Accept-Ranges,但有一个奇怪现象:一个状态码是 303,一个是 200。很显然,303 是正确的,HEAD 为什么会是 200?我以为是 Nginx 对 HEAD 请求做了特殊处理,于是直接访问 Go 服务的方式(不经过 Nginx 代理),结果一样。于是,我用 Go 实现一个简单的 Web 服务,Handler 里面也重定向。

    1. func main() {
    2. http.HandleFunc("/dl", func(w http.ResponseWriter, r *http.Request) {
    3. http.Redirect(w, r, "/", http.StatusSeeOther)
    4. })
    5. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    6. fmt.Fprintf(w, "Hello World")
    7. })
    8. http.ListenAndServe(":2022", nil)
    9. }

    用 curl 请求 http://localhost:2022/dl,GET 和 HEAD 都返回 303。于是我怀疑是不是 Echo 框架哪里的问题(studygolang 使用 Echo 框架构建的)。所以,我用 Echo 框架写个 Web 服务测试:

    1. func main() {
    2. e := echo.New()
    3. e.GET("/dl", func(ctx echo.Context) error {
    4. return ctx.Redirect(http.StatusSeeOther, "/")
    5. })
    6. e.GET("/", func(ctx echo.Context) error {
    7. return ctx.String(http.StatusOK, "Hello World!")
    8. })
    9. e.Logger.Fatal(e.Start(":2022"))
    10. }

    同样用 curl 请求 http://localhost:2022/dl,GET 返回 303,而 HEAD 报 405 Method Not Allowed,这符合预期。我们的路由设置只允许 GET 请求。但为什么 studygolang 没有返回 405,因为它也限制只能 GET 请求。于是我对随便一个地址发起 HEAD 请求,发现都返回 200,可见 HTTP 错误被“吞掉”了。查找 studygolang 的中间件,发现了这个:

    1. func HTTPError() echo.MiddlewareFunc {
    2. return func(next echo.HandlerFunc) echo.HandlerFunc {
    3. return func(ctx echo.Context) error {
    4. if err := next(ctx); err != nil {
    5. if !ctx.Response().Committed {
    6. if he, ok := err.(*echo.HTTPError); ok {
    7. switch he.Code {
    8. case http.StatusNotFound:
    9. if util.IsAjax(ctx) {
    10. return ctx.String(http.StatusOK, `{"ok":0,"error":"接口不存在"}`)
    11. }
    12. return Render(ctx, "404.html", nil)
    13. case http.StatusForbidden:
    14. if util.IsAjax(ctx) {
    15. return ctx.String(http.StatusOK, `{"ok":0,"error":"没有权限访问"}`)
    16. }
    17. return Render(ctx, "403.html", map[string]interface{}{"msg": he.Message})
    18. case http.StatusInternalServerError:
    19. if util.IsAjax(ctx) {
    20. return ctx.String(http.StatusOK, `{"ok":0,"error":"接口服务器错误"}`)
    21. }
    22. return Render(ctx, "500.html", nil)
    23. }
    24. }
    25. }
    26. return nil
    27. }
    28. }
    29. }

    这里对 404、403、500 错误都做了处理,但其他 HTTP 错误直接忽略了,导致最后返回了 200 OK。只需要在上面 switch 语句加一个 default 分支,同时把 err 原样 return,采用系统默认处理方式:

    1. default:
    2. return err

    这样 405 Method Not Allowed 会正常返回。同时,为了解决 HEAD 能用来判断下载行为,针对下载路由,我加上了允许 HEAD 请求,这样就解决了小伙伴们的困惑。

    02 curl 和 Go 代码行为异同

    不知道大家发现没有,通过 curl 请求 [https://studygolang.com/dl/golang/go1.16.5.src.tar.gz](https://studygolang.com/dl/golang/go1.16.5.src.tar.gz) 和 Go 代码请求,结果是不一样的:

    1. $ curl -X GET --head https://studygolang.com/dl/golang/go1.16.5.src.tar.gz
    2. HTTP/1.1 303 See Other
    3. Server: nginx
    4. Date: Thu, 08 Jul 2021 02:05:10 GMT
    5. Content-Length: 0
    6. Connection: keep-alive
    7. Location: https://golang.google.cn/dl/go1.16.5.src.tar.gz
    8. X-Request-Id: 14d741ca-65c1-4b05-90b8-bef5c8b5a0a3

    返回的是 303 重定向,自然没有 Accept-Ranges 头。但改用如下 Go 代码:

    1. resp, err := http.Get("https://studygolang.com/dl/golang/go1.16.5.src.tar.gz")
    2. if err != nil {
    3. fmt.Println("get err", err)
    4. return
    5. }
    6. fmt.Println(resp)
    7. fmt.Println("ranges", resp.Header.Get("Accept-Ranges"))

    返回的是 200,且有 Accept-Ranges 头。可以猜测,应该是 Go 根据重定向递归请求重定向后的地址。可以查看源码确认下。通过这个可以看到:https://docs.studygolang.com/src/net/http/client.go?s=20406:20458#L574,核心代码如下(比较容易看懂):

    1. // 循环处理所有需要处理的 url(包括重定向后的)
    2. for {
    3. // For all but the first request, create the next
    4. // request hop and replace req.
    5. if len(reqs) > 0 {
    6. // 如果是重定向,请求重定向地址
    7. loc := resp.Header.Get("Location")
    8. if loc == "" {
    9. resp.closeBody()
    10. return nil, uerr(fmt.Errorf("%d response missing Location header", resp.StatusCode))
    11. }
    12. u, err := req.URL.Parse(loc)
    13. if err != nil {
    14. resp.closeBody()
    15. return nil, uerr(fmt.Errorf("failed to parse Location header %q: %v", loc, err))
    16. }
    17. host := ""
    18. if req.Host != "" && req.Host != req.URL.Host {
    19. // If the caller specified a custom Host header and the
    20. // redirect location is relative, preserve the Host header
    21. // through the redirect. See issue #22233.
    22. if u, _ := url.Parse(loc); u != nil && !u.IsAbs() {
    23. host = req.Host
    24. }
    25. }
    26. ireq := reqs[0]
    27. req = &Request{
    28. Method: redirectMethod,
    29. Response: resp,
    30. URL: u,
    31. Header: make(Header),
    32. Host: host,
    33. Cancel: ireq.Cancel,
    34. ctx: ireq.ctx,
    35. }
    36. if includeBody && ireq.GetBody != nil {
    37. req.Body, err = ireq.GetBody()
    38. if err != nil {
    39. resp.closeBody()
    40. return nil, uerr(err)
    41. }
    42. req.ContentLength = ireq.ContentLength
    43. }
    44. // Copy original headers before setting the Referer,
    45. // in case the user set Referer on their first request.
    46. // If they really want to override, they can do it in
    47. // their CheckRedirect func.
    48. copyHeaders(req)
    49. // Add the Referer header from the most recent
    50. // request URL to the new one, if it's not https->http:
    51. if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref != "" {
    52. req.Header.Set("Referer", ref)
    53. }
    54. err = c.checkRedirect(req, reqs)
    55. // Sentinel error to let users select the
    56. // previous response, without closing its
    57. // body. See Issue 10069.
    58. if err == ErrUseLastResponse {
    59. return resp, nil
    60. }
    61. // Close the previous response's body. But
    62. // read at least some of the body so if it's
    63. // small the underlying TCP connection will be
    64. // re-used. No need to check for errors: if it
    65. // fails, the Transport won't reuse it anyway.
    66. const maxBodySlurpSize = 2 << 10
    67. if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
    68. io.CopyN(io.Discard, resp.Body, maxBodySlurpSize)
    69. }
    70. resp.Body.Close()
    71. if err != nil {
    72. // Special case for Go 1 compatibility: return both the response
    73. // and an error if the CheckRedirect function failed.
    74. // See https://golang.org/issue/3795
    75. // The resp.Body has already been closed.
    76. ue := uerr(err)
    77. ue.(*url.Error).URL = loc
    78. return resp, ue
    79. }
    80. }
    81. reqs = append(reqs, req)
    82. var err error
    83. var didTimeout func() bool
    84. if resp, didTimeout, err = c.send(req, deadline); err != nil {
    85. // c.send() always closes req.Body
    86. reqBodyClosed = true
    87. if !deadline.IsZero() && didTimeout() {
    88. err = &httpError{
    89. // TODO: early in cycle: s/Client.Timeout exceeded/timeout or context cancellation/
    90. err: err.Error() + " (Client.Timeout exceeded while awaiting headers)",
    91. timeout: true,
    92. }
    93. }
    94. return nil, uerr(err)
    95. }
    96. // 确认重定向行为
    97. var shouldRedirect bool
    98. redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
    99. if !shouldRedirect {
    100. return resp, nil
    101. }
    102. req.closeBody()
    103. }

    可以进一步看 redirectBehavior 函数 https://docs.studygolang.com/src/net/http/client.go?s=20406:20458#L497:

    1. func redirectBehavior(reqMethod string, resp *Response, ireq *Request) (redirectMethod string, shouldRedirect, includeBody bool) {
    2. switch resp.StatusCode {
    3. case 301, 302, 303:
    4. redirectMethod = reqMethod
    5. shouldRedirect = true
    6. includeBody = false
    7. // RFC 2616 allowed automatic redirection only with GET and
    8. // HEAD requests. RFC 7231 lifts this restriction, but we still
    9. // restrict other methods to GET to maintain compatibility.
    10. // See Issue 18570.
    11. if reqMethod != "GET" && reqMethod != "HEAD" {
    12. redirectMethod = "GET"
    13. }
    14. case 307, 308:
    15. redirectMethod = reqMethod
    16. shouldRedirect = true
    17. includeBody = true
    18. // Treat 307 and 308 specially, since they're new in
    19. // Go 1.8, and they also require re-sending the request body.
    20. if resp.Header.Get("Location") == "" {
    21. // 308s have been observed in the wild being served
    22. // without Location headers. Since Go 1.7 and earlier
    23. // didn't follow these codes, just stop here instead
    24. // of returning an error.
    25. // See Issue 17773.
    26. shouldRedirect = false
    27. break
    28. }
    29. if ireq.GetBody == nil && ireq.outgoingLength() != 0 {
    30. // We had a request body, and 307/308 require
    31. // re-sending it, but GetBody is not defined. So just
    32. // return this response to the user instead of an
    33. // error, like we did in Go 1.7 and earlier.
    34. shouldRedirect = false
    35. }
    36. }
    37. return redirectMethod, shouldRedirect, includeBody
    38. }

    很清晰了吧。

    以太坊cppgolang区别 编程

    以太坊cppgolang区别

    以太坊是一种去中心化的开源平台,它采用智能合约技术,旨在构建和运行不受干扰的分布式应用程序。作为目前最受欢迎的区块链平台之一,以太坊提供了多种编程语言的支持,其
    progolang 编程

    progolang

    Go语言(Golang)是由Google开发的一门静态类型编程语言。作为一名专业的Golang开发者,我深知这门语言的优势和特点。在本文中,我将介绍Golang
    golangn个发送者 编程

    golangn个发送者

    Golang是一种开源的编程语言,由Google团队开发,旨在提高程序的并发性和简化软件开发过程。在Go语言中,有时需要向多个接收者发送信息。本文将介绍如何在G
    golang技能图谱 编程

    golang技能图谱

    从互联网行业的快速发展到人工智能技术的日益成熟,各种编程语言也应运而生。而在这众多的编程语言中,Golang(即Go)作为一门强大且高效的开发语言备受关注。Go
    评论:0   参与:  9