目录
问题" >问题
排查" >排查
超时原因" >超时原因
正确姿势" >正确姿势
源码分析" >源码分析
总结" >总结
首页 后端开发 Golang i/o timeout , 希望你不要踩到这个net/http包的坑

i/o timeout , 希望你不要踩到这个net/http包的坑

Jul 25, 2023 pm 03:56 PM
timeout net/http

问题

我们来看一段日常代码。

 1package main
 2
 3import (
 4    "bytes"
 5    "encoding/json"
 6    "fmt"
 7    "io/ioutil"
 8    "net"
 9    "net/http"
10    "time"
11)
12
13var tr *http.Transport
14
15func init() {
16    tr = &http.Transport{
17        MaxIdleConns: 100,
18        Dial: func(netw, addr string) (net.Conn, error) {
19            conn, err := net.DialTimeout(netw, addr, time.Second*2) //设置建立连接超时
20            if err != nil {
21                return nil, err
22            }
23            err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //设置发送接受数据超时
24            if err != nil {
25                return nil, err
26            }
27            return conn, nil
28        },
29    }
30}
31
32func main() {
33    for {
34        _, err := Get("http://www.baidu.com/")
35        if err != nil {
36            fmt.Println(err)
37            break
38        }
39    }
40}
41
42
43func Get(url string) ([]byte, error) {
44    m := make(map[string]interface{})
45    data, err := json.Marshal(m)
46    if err != nil {
47        return nil, err
48    }
49    body := bytes.NewReader(data)
50    req, _ := http.NewRequest("Get", url, body)
51    req.Header.Add("content-type", "application/json")
52
53    client := &http.Client{
54        Transport: tr,
55    }
56    res, err := client.Do(req)
57    if res != nil {
58        defer res.Body.Close()
59    }
60    if err != nil {
61        return nil, err
62    }
63    resBody, err := ioutil.ReadAll(res.Body)
64    if err != nil {
65        return nil, err
66    }
67    return resBody, nil
68}
登录后复制

做的事情,比较简单,就是循环去请求 http://www.baidu.com/ , 然后等待响应。

看上去貌似没啥问题吧。

代码跑起来,也确实能正常收发消息

但是这段代码跑一段时间,就会出现 i/o timeout 的报错。


这其实是最近排查了的一个问题,发现这个坑可能比较容易踩上,我这边对代码做了简化。

实际生产中发生的现象是,golang服务在发起http调用时,虽然http.Transport设置了3s超时,会偶发出现i/o timeout的报错。

但是查看下游服务的时候,发现下游服务其实 100ms 就已经返回了。


排查

i/o timeout , 希望你不要踩到这个net/http包的坑
五层网络协议对应的消息体变化分析

就很奇怪了,明明服务端显示处理耗时才100ms,且客户端超时设的是3s, 怎么就出现超时报错 i/o timeout 呢?


这里推测有两个可能。

  • 因为服务端打印的日志其实只是服务端应用层打印的日志。但客户端应用层发出数据后,中间还经过客户端的传输层,网络层,数据链路层和物理层,再经过服务端的物理层,数据链路层,网络层,传输层到服务端的应用层。服务端应用层处耗时100ms,再原路返回。那剩下的3s-100ms可能是耗在了整个流程里的各个层上。比如网络不好的情况下,传输层TCP使劲丢包重传之类的原因。

  • 网络没问题,客户端到服务端链路整个收发流程大概耗时就是100ms左右。客户端处理逻辑问题导致超时。


一般遇到问题,大部分情况下都不会是底层网络的问题,大胆怀疑是自己的问题就对了,不死心就抓个包看下。

i/o timeout , 希望你不要踩到这个net/http包的坑

抓包结果

分析下,从刚开始三次握手(画了红框的地方)。

到最后出现超时报错 i/o timeout画了蓝框的地方)。

time那一列从710,确实间隔3s。而且看右下角的蓝框,是51169端口发到80端口的一次Reset连接。

80端口是服务端的端口。换句话说就是客户端3s超时主动断开链接的。

但是再仔细看下第一行三次握手到最后客户端超时主动断开连接的中间,其实有非常多次HTTP请求

回去看代码设置超时的方式。

 1    tr = &http.Transport{
 2        MaxIdleConns: 100,
 3        Dial: func(netw, addr string) (net.Conn, error) {
 4            conn, err := net.DialTimeout(netw, addr, time.Second*2) //设置建立连接超时
 5            if err != nil {
 6                return nil, err
 7            }
 8            err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //设置发送接受数据超时
 9            if err != nil {
10                return nil, err
11            }
12            return conn, nil
13        },
14    }
登录后复制

也就是说,这里的3s超时,其实是在建立连接之后开始算的,而不是单次调用开始算的超时。

看注释里写的是

SetDeadline sets the read and write deadlines associated with the connection.



超时原因

大家知道HTTP是应用层协议,传输层用的是TCP协议。

HTTP协议从1.0以前,默认用的是短连接,每次发起请求都会建立TCP连接。收发数据。然后断开连接。

TCP连接每次都是三次握手。每次断开都要四次挥手。

其实没必要每次都建立新连接,建立的连接不断开就好了,每次发送数据都复用就好了。

于是乎,HTTP协议从1.1之后就默认使用长连接。具体相关信息可以看之前的 这篇文章。

那么golang标准库里也兼容这种实现。

通过建立一个连接池,针对每个域名建立一个TCP长连接,比如http://baidu.comhttp://golang.com 就是两个不同的域名。

第一次访问http://baidu.com 域名的时候会建立一个连接,用完之后放到空闲连接池里,下次再要访问http://baidu.com 的时候会重新从连接池里把这个连接捞出来复用

i/o timeout , 希望你不要踩到这个net/http包的坑
复用长连接


插个题外话:这也解释了之前这篇文章里最后的疑问,为什么要强调是同一个域名:一个域名会建立一个连接,一个连接对应一个读goroutine和一个写goroutine。正因为是同一个域名,所以最后才会泄漏3个goroutine,如果不同域名的话,那就会泄漏 1+2*N 个协程,N就是域名数。


假设第一次请求要100ms,每次请求完http://baidu.com 后都放入连接池中,下次继续复用,重复29次,耗时2900ms

30次请求的时候,连接从建立开始到服务返回前就已经用了3000ms,刚好到设置的3s超时阈值,那么此时客户端就会报超时 i/o timeout

虽然这时候服务端其实才花了100ms,但耐不住前面29次加起来的耗时已经很长。

也就是说只要通过 http.Transport 设置了 err = conn.SetDeadline(time.Now().Add(time.Second * 3)),并且你用了长连接,哪怕服务端处理再快,客户端设置的超时再长,总有一刻,你的程序会报超时错误。

正确姿势

原本预期是给每次调用设置一个超时,而不是给整个连接设置超时。

另外,上面出现问题的原因是给长连接设置了超时,且长连接会复用。

基于这两点,改一下代码。

 1package main
 2
 3import (
 4    "bytes"
 5    "encoding/json"
 6    "fmt"
 7    "io/ioutil"
 8    "net/http"
 9    "time"
10)
11
12var tr *http.Transport
13
14func init() {
15    tr = &http.Transport{
16        MaxIdleConns: 100,
17        // 下面的代码被干掉了
18        //Dial: func(netw, addr string) (net.Conn, error) {
19        //  conn, err := net.DialTimeout(netw, addr, time.Second*2) //设置建立连接超时
20        //  if err != nil {
21        //      return nil, err
22        //  }
23        //  err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //设置发送接受数据超时
24        //  if err != nil {
25        //      return nil, err
26        //  }
27        //  return conn, nil
28        //},
29    }
30}
31
32
33func Get(url string) ([]byte, error) {
34    m := make(map[string]interface{})
35    data, err := json.Marshal(m)
36    if err != nil {
37        return nil, err
38    }
39    body := bytes.NewReader(data)
40    req, _ := http.NewRequest("Get", url, body)
41    req.Header.Add("content-type", "application/json")
42
43    client := &http.Client{
44        Transport: tr,
45        Timeout: 3*time.Second,  // 超时加在这里,是每次调用的超时
46    }
47    res, err := client.Do(req) 
48    if res != nil {
49        defer res.Body.Close()
50    }
51    if err != nil {
52        return nil, err
53    }
54    resBody, err := ioutil.ReadAll(res.Body)
55    if err != nil {
56        return nil, err
57    }
58    return resBody, nil
59}
60
61func main() {
62    for {
63        _, err := Get("http://www.baidu.com/")
64        if err != nil {
65            fmt.Println(err)
66            break
67        }
68    }
69}
登录后复制

看注释会发现,改动的点有两个

  • http.Transport里的建立连接时的一些超时设置干掉了。

  • 在发起http请求的时候会场景http.Client,此时加入超时设置,这里的超时就可以理解为单次请求的超时了。同样可以看下注释

Timeout specifies a time limit for requests made by this Client.

到这里,代码就改好了,实际生产中问题也就解决了。

实例代码里,如果拿去跑的话,其实还会下面的错

1Get http://www.baidu.com/: EOF
登录后复制

这个是因为调用得太猛了,http://www.baidu.com 那边主动断开的连接,可以理解为一个限流措施,目的是为了保护服务器,毕竟每个人都像这么搞,服务器是会炸的。。。

解决方案很简单,每次HTTP调用中间加个sleep间隔时间就好。


到这里,其实问题已经解决了,下面会在源码层面分析出现问题的原因。对读源码不感兴趣的朋友们可以不用接着往下看,直接拉到文章底部右下角,做点正能量的事情(点两下)支持一下。(疯狂暗示,拜托拜托,这对我真的很重要!

源码分析

用的go版本是1.12.7

从发起一个网络请求开始跟。

 1res, err := client.Do(req)
 2func (c *Client) Do(req *Request) (*Response, error) {
 3    return c.do(req)
 4}
 5
 6func (c *Client) do(req *Request) {
 7    // ...
 8    if resp, didTimeout, err = c.send(req, deadline); err != nil {
 9    // ...
10  }
11    // ...  
12}  
13func send(ireq *Request, rt RoundTripper, deadline time.Time) {
14    // ...    
15    resp, err = rt.RoundTrip(req)
16     // ...  
17} 
18
19// 从这里进入 RoundTrip 逻辑
20/src/net/http/roundtrip.go: 16
21func (t *Transport) RoundTrip(req *Request) (*Response, error) {
22    return t.roundTrip(req)
23}
24
25func (t *Transport) roundTrip(req *Request) (*Response, error) {
26    // 尝试去获取一个空闲连接,用于发起 http 连接
27  pconn, err := t.getConn(treq, cm)
28  // ...
29}
30
31// 重点关注这个函数,返回是一个长连接
32func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistConn, error) {
33  // 省略了大量逻辑,只关注下面两点
34    // 有空闲连接就返回
35    pc := <-t.getIdleConnCh(cm)
36
37  // 没有创建连接
38  pc, err := t.dialConn(ctx, cm)
39
40}
登录后复制

这里上面很多代码,其实只是为了展示这部分代码是怎么跟踪下来的,方便大家去看源码的时候去跟一下。

最后一个上面的代码里有个 getConn 方法。在发起网络请求的时候,会先取一个网络连接,取连接有两个来源。

  • 如果有空闲连接,就拿空闲连接

    1/src/net/http/tansport.go:810
    2func (t *Transport) getIdleConnCh(cm connectMethod) chan *persistConn {
    3 // 返回放空闲连接的chan
    4 ch, ok := t.idleConnCh[key]
    5   // ...
    6 return ch
    7}
    登录后复制
  • 没有空闲连接,就创建长连接。

1/src/net/http/tansport.go:1357
2func (t *Transport) dialConn() {
3  //...
4  conn, err := t.dial(ctx, "tcp", cm.addr())
5  // ...
6  go pconn.readLoop()
7  go pconn.writeLoop()
8  // ...
9}
登录后复制

第一次发起一个http请求时,这时候肯定没有空闲连接,会建立一个新连接。同时会创建一个读goroutine和一个写goroutine

i/o timeout , 希望你不要踩到这个net/http包的坑
读写协程

注意上面代码里的t.dial(ctx, "tcp", cm.addr()),如果像文章开头那样设置了 http.Transport

 1Dial: func(netw, addr string) (net.Conn, error) {
 2   conn, err := net.DialTimeout(netw, addr, time.Second*2) //设置建立连接超时
 3   if err != nil {
 4      return nil, err
 5   }
 6   err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //设置发送接受数据超时
 7   if err != nil {
 8      return nil, err
 9   }
10   return conn, nil
11},
登录后复制

那么这里就会在下面的dial里被执行到

1func (t *Transport) dial(ctx context.Context, network, addr string) (net.Conn, error) {
2   // ...
3  c, err := t.Dial(network, addr)
4  // ...
5}
登录后复制

这里面调用的设置超时,会执行到

 1/src/net/net.go
 2func (c *conn) SetDeadline(t time.Time) error {
 3    //...
 4    c.fd.SetDeadline(t)
 5    //...
 6}
 7
 8//...
 9
10func setDeadlineImpl(fd *FD, t time.Time, mode int) error {
11    // ...
12    runtime_pollSetDeadline(fd.pd.runtimeCtx, d, mode)
13    return nil
14}
15
16
17//go:linkname poll_runtime_pollSetDeadline internal/poll.runtime_pollSetDeadline
18func poll_runtime_pollSetDeadline(pd *pollDesc, d int64, mode int) {
19    // ...
20  // 设置一个定时器事件
21  rtf = netpollDeadline
22    // 并将事件注册到定时器里
23  modtimer(&pd.rt, pd.rd, 0, rtf, pd, pd.rseq)
24}
登录后复制

上面的源码,简单来说就是,当第一次调用请求的,会建立个连接,这时候还会注册一个定时器事件,假设时间设了3s,那么这个事件会在3s后发生,然后执行注册事件的逻辑。而这个注册事件就是netpollDeadline注意这个netpollDeadline,待会会提到。

i/o timeout , 希望你不要踩到这个net/http包的坑
读写协程定时器事件

设置了超时事件,且超时事件是3s后之后,发生。再次期间正常收发数据。一切如常。

直到3s过后,这时候看读goroutine,会等待网络数据返回。

1/src/net/http/tansport.go:1642
2func (pc *persistConn) readLoop() {
3    //...
4    for alive {
5        _, err := pc.br.Peek(1)  // 阻塞读取服务端返回的数据
6    //...
7}
登录后复制

然后就是一直跟代码。

 1src/bufio/bufio.go: 129
 2func (b *Reader) Peek(n int) ([]byte, error) {
 3   // ...
 4   b.fill() 
 5   // ...   
 6}
 7
 8func (b *Reader) fill() {
 9    // ...
10    n, err := b.rd.Read(b.buf[b.w:])
11    // ...
12}
13
14/src/net/http/transport.go: 1517
15func (pc *persistConn) Read(p []byte) (n int, err error) {
16    // ...
17    n, err = pc.conn.Read(p)
18    // ...
19}
20
21// /src/net/net.go: 173
22func (c *conn) Read(b []byte) (int, error) {
23    // ...
24    n, err := c.fd.Read(b)
25    // ...
26}
27
28func (fd *netFD) Read(p []byte) (n int, err error) {
29    n, err = fd.pfd.Read(p)
30    // ...
31}
32
33/src/internal/poll/fd_unix.go: 
34func (fd *FD) Read(p []byte) (int, error) {
35    //...
36  if err = fd.pd.waitRead(fd.isFile); err == nil {
37    continue
38  }
39    // ...
40}
41
42func (pd *pollDesc) waitRead(isFile bool) error {
43    return pd.wait(&#39;r&#39;, isFile)
44}
45
46func (pd *pollDesc) wait(mode int, isFile bool) error {
47    // ...
48  res := runtime_pollWait(pd.runtimeCtx, mode)
49    return convertErr(res, isFile)
50}
登录后复制

直到跟到 runtime_pollWait,这个可以简单认为是等待服务端数据返回

 1//go:linkname poll_runtime_pollWait internal/poll.runtime_pollWait
 2func poll_runtime_pollWait(pd *pollDesc, mode int) int {
 3
 4    // 1.如果网络正常返回数据就跳出
 5  for !netpollblock(pd, int32(mode), false) {
 6    // 2.如果有出错情况也跳出
 7        err = netpollcheckerr(pd, int32(mode))
 8        if err != 0 {
 9            return err
10        }
11    }
12    return 0
13}
登录后复制

整条链路跟下来,就是会一直等待数据,等待的结果只有两个

  • 有可以读的数据

  • 出现报错

这里面的报错,又有那么两种

  • 连接关闭

  • 超时

1func netpollcheckerr(pd *pollDesc, mode int32) int {
2    if pd.closing {
3        return 1 // errClosing
4    }
5    if (mode == &#39;r&#39; && pd.rd < 0) || (mode == &#39;w&#39; && pd.wd < 0) {
6        return 2 // errTimeout
7    }
8    return 0
9}
登录后复制

其中提到的超时,就是指这里面返回的数字2,会通过下面的函数,转化为 ErrTimeout, 而 ErrTimeout.Error() 其实就是 i/o timeout

 1func convertErr(res int, isFile bool) error {
 2    switch res {
 3    case 0:
 4        return nil
 5    case 1:
 6        return errClosing(isFile)
 7    case 2:
 8        return ErrTimeout // ErrTimeout.Error() 就是 "i/o timeout"
 9    }
10    println("unreachable: ", res)
11    panic("unreachable")
12}
登录后复制

那么问题来了。上面返回的超时错误,也就是返回2的时候的条件是怎么满足的

1    if (mode == &#39;r&#39; && pd.rd < 0) || (mode == &#39;w&#39; && pd.wd < 0) {
2        return 2 // errTimeout
3    }
登录后复制

还记得刚刚提到的 netpollDeadline吗?

这里面放了定时器3s到点时执行的逻辑。

 1func timerproc(tb *timersBucket) {
 2    // 计时器到设定时间点了,触发之前注册函数
 3    f(arg, seq) // 之前注册的是 netpollDeadline
 4}
 5
 6func netpollDeadline(arg interface{}, seq uintptr) {
 7    netpolldeadlineimpl(arg.(*pollDesc), seq, true, true)
 8}
 9
10/src/runtime/netpoll.go: 428
11func netpolldeadlineimpl(pd *pollDesc, seq uintptr, read, write bool) {
12    //...
13    if read {
14        pd.rd = -1
15        rg = netpollunblock(pd, &#39;r&#39;, false)
16    }
17    //...
18}
登录后复制

这里会设置pd.rd=-1,是指 poller descriptor.read deadline ,含义网络轮询器文件描述符读超时时间, 我们知道在linux里万物皆文件,这里的文件其实是指这次网络通讯中使用到的socket

这时候再回去看发生超时的条件就是if (mode == 'r' && pd.rd < 0)

至此。我们的代码里就收到了 io timeout 的报错。

总结

  • 不要在 http.Transport中设置超时,那是连接的超时,不是请求的超时。否则可能会出现莫名 io timeout报错。

  • 请求的超时在创建client里设置。

以上是i/o timeout , 希望你不要踩到这个net/http包的坑的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它们
4 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

Nginx timeout超时如何配置 Nginx timeout超时如何配置 May 12, 2023 pm 10:07 PM

keepalive_timeouthttp有一个keepalive模式,它告诉webserver在处理完一个请求后保持这个tcp连接的打开状态。若接收到来自客户端的其它请求,服务端会利用这个未被关闭的连接,而不需要再建立一个连接。httpkeep-alive,網頁的每一個請求都是http(圖片,css等),而打開http請求是要先建立tcp連接,而如果一個頁面每個請求都要打開及關閉一個tcp連接就會做成資源的浪費.keepalive_timeout就是當一個http請求完成,其tcp連接會存留下

在Vue应用中使用vue-resource时出现“Error: timeout of xxxms exceeded”怎么办? 在Vue应用中使用vue-resource时出现“Error: timeout of xxxms exceeded”怎么办? Jun 24, 2023 pm 02:21 PM

在Vue应用开发中,使用vue-resource进行HTTP请求是常见的操作。尽管vue-resource提供了很多方便的功能,但有时我们会遇到“Error:timeoutofxxxmsexceeded”这样的错误提示。这种错误通常是因为请求超时而导致的。那么,在这种情况下,我们应该怎样解决这个问题呢?1.增加请求超时时间首先,我们可以通过增加请

win11 clock watchdog timeout蓝屏怎么解决? win11 clock watchdog timeout蓝屏怎么解决? Feb 14, 2024 pm 04:00 PM

不少的用户在升级完win11系统后会出现蓝屏的现象,例如:clockwatchdogtimeout蓝屏,那么这要怎么解决?用户们可以看看更新驱动程序或者是检查过热问题等等来进行操作,下面就让本站来为用户们来仔细的介绍一下clockwatchdogtimeout蓝屏win11解决方法吧。clockwatchdogtimeout蓝屏win11解决方法1、更新驱动程序:更新CPU和主板驱动程序可能会解决问题。可以通过访问制造商的网站下载最新的驱动程序。2、检查过热问题:过热也可能是导致此错误的原因之一

在Vue应用中使用axios时出现“Error: timeout of xxxms exceeded”怎么办? 在Vue应用中使用axios时出现“Error: timeout of xxxms exceeded”怎么办? Jun 24, 2023 pm 03:27 PM

在Vue应用中使用axios时出现“Error:timeoutofxxxmsexceeded”怎么办?随着互联网的快速发展,前端技术也在不断地更新迭代,Vue作为一种优秀的前端框架,近年来受到大家的欢迎。在Vue应用中,我们常常需要使用axios来进行网络请求,但是有时候会出现“Error:timeoutofxxxmsexceeded”的错误

如何使用golang中的net/http/httputil.DumpResponse函数打印HTTP响应信息 如何使用golang中的net/http/httputil.DumpResponse函数打印HTTP响应信息 Nov 18, 2023 am 09:35 AM

如何使用golang中的net/http/httputil.DumpResponse函数打印HTTP响应信息在golang中,我们可以使用net/http包来发送HTTP请求并接收HTTP响应。有时候,我们需要查看HTTP响应的详细信息,例如响应头、响应体等。为了实现这个目的,net/http/httputil包提供了一个有用的DumpResponse函数。

504 gateway timeout怎么解决 504 gateway timeout怎么解决 Nov 27, 2023 am 10:55 AM

504 gateway timeout的解决办法:1、检查服务器负载;2、优化查询和代码;3、增加超时限制;4、检查代理服务器;5、检查网络连接;6、使用负载均衡;7、监控和日志;8、故障排除;9、增加缓存;10、分析请求。解决该错误通常需要综合考虑多个因素,包括服务器性能、网络连接、代理服务器配置和应用程序优化等。

掌握Go语言文档中的net/http.PostForm函数发送表单数据 掌握Go语言文档中的net/http.PostForm函数发送表单数据 Nov 04, 2023 am 09:02 AM

如今,互联网已经成为珍贵无比的资源,任何时候都需要发送数据。为了在这个快节奏的时代中保持竞争力,我们需要掌握各种技能。Go语言已经成为目前非常流行的语言。在Go语言中,发送数据已经变得更加容易和高效。本文介绍了Go语言文档中的net/http.PostForm函数发送表单数据,向读者提供了一个简便的方法,让程序员能够快速轻松地运行程序。HTTPPOS

如何使用golang中的net/http/httputil.DumpRequest函数打印HTTP请求信息 如何使用golang中的net/http/httputil.DumpRequest函数打印HTTP请求信息 Nov 18, 2023 am 09:11 AM

如何使用golang中的net/http/httputil.DumpRequest函数打印HTTP请求信息概述:在Golang中,可以使用net/http包提供的httputil.DumpRequest函数来打印HTTP请求信息。这个函数可以帮助我们方便地查看请求的头部、请求行以及请求体的内容。本文将详细介绍如何使用这个函数,并提供具体的代码示例。步骤一:导

See all articles