如何在 Go 中发出 HTTP 请求

作者选择了 多样性在技术基金作为 写给捐款计划的一部分接受捐款。

介绍

当一个程序需要与另一个程序进行通信时,许多开发人员会使用 HTTP。Go 的优势之一是其标准库的宽度,而 HTTP 也不例外。Go net/http包不仅支持 创建 HTTP 服务器,而且还可以作为客户端进行 HTTP 请求。

在本教程中,您将创建一个程序,该程序将向 HTTP 服务器发送多种类型的 HTTP 请求。 首先,您将使用默认的 Go HTTP 客户端发送一个GET请求。 然后,您将改进您的程序,以便使用一个身体发送一个POST请求。

前提条件

要遵循本教程,您将需要:

做一个GET请求

Go net/http包有几种不同的方法来使用它作为客户端. 您可以使用一个常见的全球 HTTP 客户端,具有诸如 http.Get等功能,快速创建一个 HTTP GET 请求,只有一个 URL 和一个身体,或者您可以创建一个 http.Request来开始定制个别请求的某些方面。

使用http.Get来提出请求

在您的程序的第一次迭代中,您将使用http.Get函数向您在您的程序中运行的HTTP服务器发送请求. http.Get函数是有用的,因为您不需要在您的程序中进行任何额外的设置来发送请求。

要开始创建你的程序,你需要一个目录,以保持该程序的目录. 在本教程中,你将使用名为项目的目录。

首先,创建项目目录并导航到它:

1mkdir projects
2cd projects

接下来,为您的项目创建目录并导航,在这种情况下,使用目录 httpclient:

1mkdir httpclient
2cd httpclient

httpclient目录中,使用nano或您最喜欢的编辑器打开main.go文件:

1nano main.go

main.go文件中,开始添加这些行:

 1[label main.go]
 2package main
 3
 4import (
 5    "errors"
 6    "fmt"
 7    "net/http"
 8    "os"
 9    "time"
10)
11
12const serverPort = 3333

您添加了的名称main,以便您的程序被编译成一个可以运行的程序,然后添加一个导入声明与您将在该程序中使用的各种包. 之后,您创建了一个名为serverPortconst值为3333,您将使用它作为您的HTTP服务器正在收听的端口和您的HTTP客户端将连接的端口。

接下来,在 main.go 文件中创建一个主要函数,并设置一个 goroutine 来启动 HTTP 服务器:

 1[label main.go]
 2...
 3func main() {
 4    go func() {
 5    	mux := http.NewServeMux()
 6    	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 7    		fmt.Printf("server: %s /\n", r.Method)
 8    	})
 9    	server := http.Server{
10    		Addr:    fmt.Sprintf(":%d", serverPort),
11    		Handler: mux,
12    	}
13    	if err := server.ListenAndServe(); err != nil {
14    		if !errors.Is(err, http.ErrServerClosed) {
15    			fmt.Printf("error running http server: %s\n", err)
16    		}
17    	}
18    }()
19
20    time.Sleep(100 * time.Millisecond)

您的 HTTP 服务器已设置为使用 fmt.Printf’ 来打印每当需要 root /路径的请求信息。 它也设置为在serverPort上聆听。 最后,一旦您启动服务器 goroutine,您的程序会使用time.Sleep` 短时间。

现在,在函数中,使用fmt.Sprintf设置请求URL,将http://localhost主机名与服务器正在收听的serverPort值相结合。

 1[label main.go]
 2...
 3    requestURL := fmt.Sprintf("http://localhost:%d", serverPort)
 4    res, err := http.Get(requestURL)
 5    if err != nil {
 6    	fmt.Printf("error making http request: %s\n", err)
 7    	os.Exit(1)
 8    }
 9
10    fmt.Printf("client: got response!\n")
11    fmt.Printf("client: status code: %d\n", res.StatusCode)
12}

http.Get函数被调用时,Go 将使用默认的 HTTP 客户端向所提供的 URL 发送 HTTP 请求,然后返回 http.Response或如果请求失败,则返回错误值。

保存并关闭文件,当你完成。

要运行您的程序,请使用go run命令,并为其提供main.go文件:

1go run main.go

您将看到以下结果:

1[secondary_label Output]
2server: GET /
3client: got response!
4client: status code: 200

在第一个输出行上,服务器打印了从您的客户端收到一个GET请求,用于/路径,然后,下面的两行表示客户端从服务器获得了回复,而响应的状态代码是200

http.Get函数对快速的HTTP请求有用,就像你在本节中提出的请求一样。

使用http.Request来提出请求

http.Get不同的是,http.Request功能为您提供了对请求的更大的控制,而不仅仅是请求的HTTP方法和URL。

在您的代码中,第一个更新是更改 HTTP 服务器处理器以使用 fmt.Fprintf 返回假 JSON 数据响应。如果这是一个完整的 HTTP 服务器,则该数据将使用 Go 的 encoding/json)包生成。如果您想了解更多关于在 Go 中使用 JSON 的信息,我们的 如何在 Go 中使用 JSON教程可用。

现在,重新打开你的main.go文件,并更新你的程序,以便开始使用http.Request,如下所示:

 1[label main.go]
 2package main
 3
 4import (
 5    ...
 6    "io/ioutil"
 7    ...
 8)
 9
10...
11
12func main() {
13    ...
14    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
15    	fmt.Printf("server: %s /\n", r.Method)
16    	fmt.Fprintf(w, `{"message": "hello!"}`)
17    })
18    ...

现在,更新您的 HTTP 请求代码,以便您不使用http.Get向服务器发送请求,而是使用http.NewRequesthttp.DefaultClientDo方法:

 1[label main.go]
 2...
 3    requestURL := fmt.Sprintf("http://localhost:%d", serverPort)
 4    req, err := http.NewRequest(http.MethodGet, requestURL, nil)
 5    if err != nil {
 6    	fmt.Printf("client: could not create request: %s\n", err)
 7    	os.Exit(1)
 8    }
 9
10    res, err := http.DefaultClient.Do(req)
11    if err != nil {
12    	fmt.Printf("client: error making http request: %s\n", err)
13    	os.Exit(1)
14    }
15
16    fmt.Printf("client: got response!\n")
17    fmt.Printf("client: status code: %d\n", res.StatusCode)
18
19    resBody, err := ioutil.ReadAll(res.Body)
20    if err != nil {
21    	fmt.Printf("client: could not read response body: %s\n", err)
22    	os.Exit(1)
23    }
24    fmt.Printf("client: response body: %s\n", resBody)
25}

在此更新中,您使用「http.NewRequest」函数生成「http.Request」值,或处理错误,如果该值无法创建,但与「http.Get」函数不同,但「http.NewRequest」函数不会立即向服务器发送HTTP请求,因为它不会立即发送请求,因此您可以在发送请求之前对请求做出任何更改。

一旦http.Request被创建并配置,你会使用http.DefaultClientDo方法将请求发送到服务器上。http.DefaultClient值是Go的默认HTTP客户端,与http.Get相同,但这一次,你会直接使用它来告诉它发送你的http.Request。HTTP客户端的Do方法会返回你从http.Get函数中收到的相同值,这样你就可以以相同的方式处理答案。

在打印请求结果后,您将使用 ioutil.ReadAll函数读取 HTTP 响应的 BodyBody 是一个 io.ReadCloser值,是 io.Readerio.Closer的组合,这意味着您可以使用任何可以从 io.Reader值读取的物体数据来读取。

要运行您的更新程序,保存您的更改,并使用运行命令:

1go run main.go

这一次,你的输出应该看起来非常相似,但有一个补充:

1[secondary_label Output]
2server: GET /
3client: got response!
4client: status code: 200
5client: response body: {"message": "hello!"}

在第一行中,你可以看到服务器仍在接收一个GET请求到/路径上。客户端也收到服务器的200响应,但它也正在读取和打印服务器的身体响应。

在本节中,您创建了一个HTTP服务器的程序,您以各种方式提出HTTP请求。 首先,您使用http.Get函数向服务器提出GET请求,仅使用服务器的URL。 然后,您更新了您的程序以使用http.NewRequest创建一个http.Request值。 一旦创建,您使用了Go的默认HTTP客户端http.DefaultClientDo方法来提出请求并将http.Response``Body打印到输出中。

虽然HTTP协议使用的不仅仅是GET请求在程序之间进行通信,而GET请求是当你想从另一个程序接收信息时有用的,但另一种HTTP方法,即POST方法,可以用于当你想从你的程序发送信息到服务器时。

发送邮件请求

REST API中,一个GET请求仅用于从服务器中获取信息,因此,为了让您的程序完全参与REST API,您的程序还需要支持发送POST请求。

在本节中,您将更新您的程序以将您的请求发送为POST请求,而不是GET请求。

要开始创建这些更新,打开你的main.go文件,并添加一些新的包,你将使用你的导入陈述:

 1[label main.go]
 2...
 3
 4import (
 5    "bytes"
 6    "errors"
 7    "fmt"
 8    "io/ioutil"
 9    "net/http"
10    "os"
11    "strings"
12    "time"
13)
14
15...

然后,更新服务器处理器函数,以打印有关请求的各种信息,例如查询字符串值、标题值和请求体:

 1[label main.go]
 2...
 3  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 4      fmt.Printf("server: %s /\n", r.Method)
 5      fmt.Printf("server: query id: %s\n", r.URL.Query().Get("id"))
 6      fmt.Printf("server: content-type: %s\n", r.Header.Get("content-type"))
 7      fmt.Printf("server: headers:\n")
 8      for headerName, headerValue := range r.Header {
 9    	  fmt.Printf("\t%s = %s\n", headerName, strings.Join(headerValue, ", "))
10      }
11
12      reqBody, err := ioutil.ReadAll(r.Body)
13      if err != nil {
14    		 fmt.Printf("server: could not read request body: %s\n", err)
15      }
16      fmt.Printf("server: request body: %s\n", reqBody)
17
18      fmt.Fprintf(w, `{"message": "hello!"}`)
19  })
20...

在此更新中,服务器的 HTTP 请求处理器中,您添加了一些更有用的 fmt.Printf 语句来查看有关请求的信息. 您使用 r.URL.Query().Get' 来获取名为 id’ 的查询字符串值,以及 r.Header.Get' 来获取名为 content-type’ 的标题值. 您还使用 for 循环与 `r.Header' 来打印服务器接收的每个 HTTP 标题的名称和值。

更新服务器处理函数后,更新主要函数的请求代码,以便它发送一个POST请求与请求体:

 1[label main.go]
 2...
 3 time.Sleep(100 * time.Millisecond)
 4    
 5 jsonBody := []byte(`{"client_message": "hello, server!"}`)
 6 bodyReader := bytes.NewReader(jsonBody)
 7
 8 requestURL := fmt.Sprintf("http://localhost:%d?id=1234", serverPort)
 9 req, err := http.NewRequest(http.MethodPost, requestURL, bodyReader)
10...

在更新到函数的请求中,您定义的新值之一是jsonBody值. 在本示例中,该值被表示为[]byte而不是标准的string,因为如果您使用encoding/json包来编码 JSON 数据,它会给您一个[]byte返回而不是一个string

下一个值是bodyReader,它包含了jsonBody数据,一个http.Request体需要该值为io.Reader,而jsonBody[]byte值不会实现io.Reader,因此您无法将其作为单独的请求体使用。

requestURL 值也被更新为包含一个id=1234查询字符串值,主要是为了显示查询字符串值如何可以与其他标准 URL 组件一起纳入请求 URL。

最后,http.NewRequest函数调用被更新为使用POST方法与http.MethodPost,并通过将最后一个参数更新到bodyReader,即JSON数据io.Reader

一旦您保存了更改,您可以使用去运行来运行您的程序:

1go run main.go

输出将比以前更长,因为您的服务器更新显示额外的信息:

 1[secondary_label Output]
 2server: POST /
 3server: query id: 1234
 4server: content-type: 
 5server: headers:
 6        Accept-Encoding = gzip
 7        User-Agent = Go-http-client/1.1
 8        Content-Length = 36
 9server: request body: {"client_message": "hello, server!"}
10client: got response!
11client: status code: 200
12client: response body: {"message": "hello!"}

来自服务器的第一个行显示您的请求现在作为POST请求进入/路径,第二行显示您添加到请求的URL的id查询字符串值的1234值,第三行显示了客户端发送的内容类型标题的值,该值在这个请求中是空的。

第四行可能与您上面看到的输出略有不同. 在 Go 中,当您使用范围重复它们时,不会保证地图值的顺序,因此您从r.Headers的标题可能会以不同的顺序打印。

最后,输出的最后一个变化是,服务器正在显示从客户端接收的请求体,然后服务器可以使用编码/json包来分析客户端发送的 JSON 数据并制定响应。

在本节中,您更新了您的程序以发送HTTPPOST请求而不是GET请求,您还更新了您的程序以发送一个请求体的[]byte数据被读取的bytes.Reader

通常情况下,在 HTTP 请求中,客户端或服务器会告诉对方它在体内发送的内容类型. 但是,正如您在上次输出中所看到的那样,您的 HTTP 请求没有包含一个内容类型标题,以告诉服务器如何解释体内的数据。

定制 HTTP 请求

随着时间的推移,HTTP请求和响应已经被用来在客户端和服务器之间发送更多的数据。在某个时候,HTTP客户端可以假设他们从HTTP服务器接收的数据是HTML,并且有很好的可能性是正确的。现在,它可以是HTML,JSON,音乐,视频或任何其他类型的数据。 为了提供有关通过HTTP发送的数据的更多信息,协议包括HTTP标题,其中一个重要标题是内容类型标题。

在本节中,您将更新您的程序以在您的 HTTP 请求中设置内容类型标题,以便服务器知道它正在接收 JSON 数据。

要进行这些更新,请重新打开您的 main.go 文件并更新您的 main 函数:

 1[label main.go]
 2...
 3
 4  req, err := http.NewRequest(http.MethodPost, requestURL, bodyReader)
 5  if err != nil {
 6    	 fmt.Printf("client: could not create request: %s\n", err)
 7    	 os.Exit(1)
 8  }
 9  req.Header.Set("Content-Type", "application/json")
10
11  client := http.Client{
12     Timeout: 30 * time.Second,
13  }
14
15  res, err := client.Do(req)
16  if err != nil {
17      fmt.Printf("client: error making http request: %s\n", err)
18      os.Exit(1)
19  }
20
21...

在此更新中,您使用req.Header访问http.Request标题,然后将请求中的Content-Type标题值设置为application/jsonapplication/json媒体类型在媒体类型列表中定义为JSON的媒体类型(https://www.iana.org/assignments/media-types/media-types.xhtml)。

下一个更新是在客户端变量中创建自己的http.Client实例。在这个客户端中,你将Timeout值设置为30秒。这很重要,因为它说与客户端做出的任何请求都会放弃,并且在30秒后停止尝试收到回复。Go的默认http.DefaultClient不指定时间,所以如果你使用该客户端提出请求,它会等到收到回复,被服务器关闭,或者你的程序结束。如果你有许多请求像这样等待响应,你可能会在计算机上使用大量资源。设置一个Timeout值会限制请求等待你定义的时间多久。

最后,您更新了您的请求,使用您的客户端变量的方法。您不需要在这里做出任何其他更改,因为您一直在呼叫http.Client值。 Go 的默认 HTTP 客户端http.DefaultClient只是默认创建的http.Client。所以,当您呼叫http.Get时,该函数为您呼叫方法,当您更新您的请求使用http.DefaultClient时,您正在直接使用http.Client

现在,保存您的文件并使用去运行来运行您的程序:

1go run main.go

您的输出应该与之前的输出非常相似,但包含有关内容类型的更多信息:

 1[secondary_label Output]
 2server: POST /
 3server: query id: 1234
 4server: content-type: application/json
 5server: headers:
 6        Accept-Encoding = gzip
 7        User-Agent = Go-http-client/1.1
 8        Content-Length = 36
 9        Content-Type = application/json
10server: request body: {"client_message": "hello, server!"}
11client: got response!
12client: status code: 200
13client: response body: {"message": "hello!"}

您将看到服务器的内容类型值,并且客户端发送的内容类型标题,这样您就可以使用相同的 HTTP 请求路径同时为 JSON 和 XML API 提供服务。

但是,这个例子不会触发您配置的客户端时间。 若要查看请求需要太长时间并触发时间时间,请打开main.go文件并将time.Sleep函数调用到您的 HTTP 服务器处理函数中,然后使time.Sleep的时间比您指定的时间更长。

 1[label main.go]
 2...
 3
 4func main() {
 5    go func() {
 6    	mux := http.NewServeMux()
 7    	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 8    		...	
 9    		fmt.Fprintf(w, `{"message": "hello!"}`)
10    		time.Sleep(35 * time.Second)
11    	})
12    	...
13    }()
14    ...
15}

现在,保存您的更改并使用去运行来运行您的程序:

1go run main.go

当您这次运行时,退出将比以前更长时间,因为它不会退出,直到 HTTP 请求完成后。

 1[secondary_label Output]
 2server: POST /
 3server: query id: 1234
 4server: content-type: application/json
 5server: headers:
 6        Content-Type = application/json
 7        Accept-Encoding = gzip
 8        User-Agent = Go-http-client/1.1
 9        Content-Length = 36
10server: request body: {"client_message": "hello, server!"}
11client: error making http request: Post "http://localhost:3333?id=1234": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
12exit status 1

在这个程序输出中,你看到服务器接收了请求并处理了它,但当它到达了HTTP处理函数的尽头,在你的time.Sleep函数调用时,它开始睡觉35秒。同时,您的HTTP请求的时长正在计算下来,在HTTP请求结束前达到30秒的限度。

在本节中,您还更新了您的程序,以创建一个新的http.Client,然后使用该客户端创建HTTP请求,您还测试了30秒的时间通过将一个time.Sleep添加到您的HTTP请求处理器中。

结论

在本教程中,您创建了一个新的程序,使用 HTTP 服务器,并使用 Go 的 net/http 套件向该服务器发送 HTTP 请求。 首先,您使用 http.Get 函数向 Go 的默认 HTTP 客户端发送一个 GET 请求。 然后,您使用 http.NewRequesthttp.DefaultClientDo 方法发送一个 GET 请求。 接下来,您更新了您的请求,以使用 byte.NewReader 进行POST 请求。 最后,您使用 Set 方法在 http.RequestHeader 字段中设置请求的 Content-Type 头,并通过创

net/http包包含不仅仅是您在本教程中使用的功能。 它还包含一个 http.Post功能,可以用来发送一个POST请求,类似于http.Get功能。 该包还支持存储和检索 cookies,其他功能。

本教程也是 DigitalOcean How to Code in Go系列的一部分,该系列涵盖了许多 Go 主题,从首次安装 Go 到如何使用语言本身。

Published At
Categories with 技术
Tagged with
comments powered by Disqus