作者选择了 多样性在技术基金作为 写给捐款计划的一部分接受捐款。
介绍
当一个程序需要与另一个程序进行通信时,许多开发人员会使用 HTTP。Go 的优势之一是其标准库的宽度,而 HTTP 也不例外。Go net/http
包不仅支持 创建 HTTP 服务器,而且还可以作为客户端进行 HTTP 请求。
在本教程中,您将创建一个程序,该程序将向 HTTP 服务器发送多种类型的 HTTP 请求。 首先,您将使用默认的 Go HTTP 客户端发送一个GET
请求。 然后,您将改进您的程序,以便使用一个身体发送一个POST
请求。
前提条件
要遵循本教程,您将需要:
- Go 版本 1.16 或更高版本已安装. 要设置此功能,请遵循操作系统的 How To Install Go 教程。
- 在 Go 中创建 HTTP 服务器的经验,可以在教程中找到, How To Make a HTTP Server in Go。
- 熟悉 goroutines 和阅读渠道。 有关更多信息,请参阅教程, How To Run Multiple Functions Concurrently in Go。
- 了解如何编写和发送 HTTP 请求的建议。
做一个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
,以便您的程序被编译成一个可以运行的程序,然后添加一个导入
声明与您将在该程序中使用的各种包. 之后,您创建了一个名为serverPort
的const
值为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.NewRequest
和http.DefaultClient
的Do
方法:
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.DefaultClient
的Do
方法将请求发送到服务器上。http.DefaultClient
值是Go的默认HTTP客户端,与http.Get
相同,但这一次,你会直接使用它来告诉它发送你的http.Request
。HTTP客户端的Do
方法会返回你从http.Get
函数中收到的相同值,这样你就可以以相同的方式处理答案。
在打印请求结果后,您将使用 ioutil.ReadAll
函数读取 HTTP 响应的 Body
。 Body
是一个 io.ReadCloser
值,是 io.Reader
和 io.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.DefaultClient
的Do
方法来提出请求并将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/json
。application/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.NewRequest
与 http.DefaultClient
的 Do
方法发送一个 GET
请求。 接下来,您更新了您的请求,以使用 byte.NewReader
进行POST
请求。 最后,您使用 Set
方法在 http.Request
的 Header
字段中设置请求的 Content-Type
头,并通过创
该 net/http
包包含不仅仅是您在本教程中使用的功能。 它还包含一个 http.Post
功能,可以用来发送一个POST
请求,类似于http.Get
功能。 该包还支持存储和检索 cookies,其他功能。
本教程也是 DigitalOcean How to Code in Go系列的一部分,该系列涵盖了许多 Go 主题,从首次安装 Go 到如何使用语言本身。