如何在Go中发出HTTP请求
作为 Write for DOnations 计划的一部分,作者选择了 Diversity in Tech Fund 来接受捐赠。
介绍
当一个程序需要与另一个程序通信时,很多开发者会使用HTTP。 Go 的优势之一是其标准库的广度,HTTP 也不例外。 Go net/http 包不仅支持 创建 HTTP 服务器 ,还可以作为客户端发出 HTTP 请求。
在本教程中,您将创建一个向 HTTP 服务器发出多种类型的 HTTP 请求的程序。 首先,您将使用默认的 Go HTTP 客户端发出 GET
请求。 然后,您将增强您的程序以发出带有主体的 POST
请求。 最后,您将自定义您的 POST
请求以包含一个 HTTP 标头并添加一个超时,如果您的请求花费太长时间就会触发。
先决条件
要遵循本教程,您将需要:
- 安装 1.16 或更高版本。 要进行此设置,请按照适用于您的操作系统的 如何安装 Go 教程进行操作。
- 在 Go 中创建 HTTP 服务器的经验,可以在教程 How To Make an HTTP Server in Go 中找到。
- 熟悉goroutines和阅读通道。 有关更多信息,请参阅教程 如何在 Go 中同时运行多个函数。
- 建议了解 HTTP 请求的组成和发送方式。
发出 GET 请求
Go net/http 包有几种不同的方式将其用作客户端。 您可以使用具有 http.Get 等功能的通用全局 HTTP 客户端快速发出仅包含 URL 和正文的 HTTP GET
请求,也可以创建 http.Request 开始自定义单个请求的某些方面。 在本节中,您将使用 http.Get
创建一个初始程序来发出 HTTP 请求,然后您将更新它以使用带有默认 HTTP 客户端的 http.Request
。
使用 http.Get
发出请求
在程序的第一次迭代中,您将使用 http.Get
函数向您在程序中运行的 HTTP 服务器发出请求。 http.Get
函数很有用,因为您不需要在程序中进行任何额外的设置来发出请求。 如果您需要提出一个快速请求,http.Get
可能是最佳选择。
要开始创建程序,您需要一个目录来保存程序的目录。 在本教程中,您将使用一个名为 projects
的目录。
首先,创建 projects
目录并导航到它:
mkdir projects cd projects
接下来,为您的项目创建目录并导航到它。 在这种情况下,使用目录 httpclient
:
mkdir httpclient cd httpclient
在 httpclient
目录中,使用 nano
或您喜欢的编辑器打开 main.go
文件:
nano main.go
在 main.go
文件中,首先添加以下行:
main.go
package main import ( "errors" "fmt" "net/http" "os" "time" ) const serverPort = 3333
您添加 package
名称 main
以便将您的程序编译为您可以运行的程序,然后在您将使用的各种包中包含一个 import
语句这个程序。 之后,您创建一个名为 serverPort
的 const
,其值为 3333
,您将使用它作为您的 HTTP 服务器正在侦听的端口和您的 HTTP 客户端将使用的端口连接至。
接下来,在 main.go
文件中创建一个 main
函数并设置一个 goroutine 来启动一个 HTTP 服务器:
main.go
... func main() { go func() { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Printf("server: %s /\n", r.Method) }) server := http.Server{ Addr: fmt.Sprintf(":%d", serverPort), Handler: mux, } if err := server.ListenAndServe(); err != nil { if !errors.Is(err, http.ErrServerClosed) { fmt.Printf("error running http server: %s\n", err) } } }() time.Sleep(100 * time.Millisecond)
您的 HTTP 服务器设置为在请求根 /
路径时使用 fmt.Printf
打印有关传入请求的信息。 它还设置为在 serverPort
上收听。 最后,一旦你启动了服务器 goroutine,你的程序会在短时间内使用 time.Sleep
。 此休眠时间允许 HTTP 服务器有时间启动并开始为您接下来将发出的请求提供响应。
现在,同样在 main
函数中,使用 fmt.Sprintf
设置请求 URL,以将 http://localhost
主机名与服务器正在侦听的 serverPort
值结合起来。 然后,使用 http.Get
向该 URL 发出请求,如下所示:
main.go
... requestURL := fmt.Sprintf("http://localhost:%d", serverPort) res, err := http.Get(requestURL) if err != nil { fmt.Printf("error making http request: %s\n", err) os.Exit(1) } fmt.Printf("client: got response!\n") fmt.Printf("client: status code: %d\n", res.StatusCode) }
当调用 http.Get
函数时,Go 将使用默认 HTTP 客户端向提供的 URL 发出 HTTP 请求,然后返回 http.Response 或 error
值如果请求失败。 如果请求失败,它将打印错误,然后使用 os.Exit 退出程序,错误代码为 1
。 如果请求成功,您的程序将打印出它收到了响应以及收到的 HTTP 状态代码。
完成后保存并关闭文件。
要运行您的程序,请使用 go run
命令并向其提供 main.go
文件:
go run main.go
您将看到以下输出:
Outputserver: GET / client: got response! client: status code: 200
在输出的第一行,服务器打印出它收到了来自客户端的 /
路径的 GET
请求。 然后,下面两行表示客户端从服务器收到响应,响应的状态码是 200
。
http.Get
函数对于快速 HTTP 请求很有用,就像您在本节中所做的那样。 但是,http.Request
提供了更广泛的选项来自定义您的请求。
使用 http.Request
发出请求
与 http.Get
相比,http.Request
函数为您提供了对请求的更大控制,而不仅仅是 HTTP 方法和被请求的 URL。 您还不会使用其他功能,但现在通过使用 http.Request
,您将能够在本教程后面添加这些自定义项。
在您的代码中,第一个更新是更改 HTTP 服务器处理程序以使用 fmt.Fprintf
返回虚假 JSON 数据响应。 如果这是一个完整的 HTTP 服务器,则该数据将使用 Go 的 encoding/json 包生成。 如果您想了解有关在 Go 中使用 JSON 的更多信息,请参阅我们的 如何在 Go 中使用 JSON 教程。 此外,您还需要包含 io/ioutil
作为导入,以便稍后在此更新中使用。
现在,再次打开您的 main.go
文件并更新您的程序以开始使用 http.Request
,如下所示:
main.go
package main import ( ... "io/ioutil" ... ) ... func main() { ... mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Printf("server: %s /\n", r.Method) fmt.Fprintf(w, `{"message": "hello!"}`) }) ...
现在,更新您的 HTTP 请求代码,以便使用 http.NewRequest
和 http.DefaultClient
的 Do
方法而不是使用 http.Get
向服务器发出请求:
main.go
... requestURL := fmt.Sprintf("http://localhost:%d", serverPort) req, err := http.NewRequest(http.MethodGet, requestURL, nil) if err != nil { fmt.Printf("client: could not create request: %s\n", err) os.Exit(1) } res, err := http.DefaultClient.Do(req) if err != nil { fmt.Printf("client: error making http request: %s\n", err) os.Exit(1) } fmt.Printf("client: got response!\n") fmt.Printf("client: status code: %d\n", res.StatusCode) resBody, err := ioutil.ReadAll(res.Body) if err != nil { fmt.Printf("client: could not read response body: %s\n", err) os.Exit(1) } fmt.Printf("client: response body: %s\n", resBody) }
在本次更新中,您使用 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 的组合,表示可以读取 body 的使用任何可以从 io.Reader 值读取的数据。 ioutil.ReadAll
函数很有用,因为它会从 io.Reader
读取,直到到达数据末尾或遇到 error
。 然后它将数据返回为您可以使用 fmt.Printf
打印的 []byte
值,或者它遇到的 error
值。
要运行更新后的程序,请保存更改并使用 go run
命令:
go run main.go
这一次,您的输出应该看起来与之前非常相似,但添加了一个:
Outputserver: GET / client: got response! client: status code: 200 client: response body: {"message": "hello!"}
在第一行中,您看到服务器仍在接收到 /
路径的 GET
请求。 客户端也接收到来自服务器的 200
响应,但它也在读取和打印服务器响应的 Body
。 在更复杂的程序中,您可以将您从服务器收到的 {"message": "hello!"}
值作为正文,并使用 encoding/json 包将其作为 JSON 处理。
在本节中,您创建了一个带有 HTTP 服务器的程序,您以各种方式向该服务器发出 HTTP 请求。 首先,您使用 http.Get
函数仅使用服务器的 URL 向服务器发出 GET
请求。 然后,您更新了程序以使用 http.NewRequest
创建一个 http.Request
值。 创建完成后,您使用 Go 的默认 HTTP 客户端 http.DefaultClient
的 Do
方法发出请求并将 http.Response
Body
打印到输出.
但是,HTTP 协议不仅仅使用 GET
请求在程序之间进行通信。 当您想从其他程序接收信息时,GET
请求很有用,但是当您想从您的程序向服务器发送信息时,可以使用另一种 HTTP 方法,即 POST
方法.
发送 POST 请求
在 REST API 中,GET
请求仅用于从服务器检索信息,因此您的程序要完全参与 REST API,您的程序还需要支持发送 [X200X ] 请求。 POST
请求几乎与 GET
请求相反,其中客户端在请求正文中向服务器发送数据。
在本节中,您将更新您的程序以将您的请求作为 POST
请求而不是 GET
请求发送。 您的 POST
请求将包含一个请求正文,您将更新您的服务器以打印出有关您从客户端发出的请求的更多信息。
要开始进行这些更新,请打开您的 main.go
文件并将一些您将要使用的新包添加到您的 import
语句中:
main.go
... import ( "bytes" "errors" "fmt" "io/ioutil" "net/http" "os" "strings" "time" ) ...
然后,更新您的服务器处理函数以打印出有关传入请求的各种信息,例如查询字符串值、标头值和请求正文:
main.go
... mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Printf("server: %s /\n", r.Method) fmt.Printf("server: query id: %s\n", r.URL.Query().Get("id")) fmt.Printf("server: content-type: %s\n", r.Header.Get("content-type")) fmt.Printf("server: headers:\n") for headerName, headerValue := range r.Header { fmt.Printf("\t%s = %s\n", headerName, strings.Join(headerValue, ", ")) } reqBody, err := ioutil.ReadAll(r.Body) if err != nil { fmt.Printf("server: could not read request body: %s\n", err) } fmt.Printf("server: request body: %s\n", reqBody) fmt.Fprintf(w, `{"message": "hello!"}`) }) ...
在对服务器 HTTP 请求处理程序的此更新中,您添加了一些更有用的 fmt.Printf
语句以查看有关传入请求的信息。 您使用 r.URL.Query().Get
获取名为 id
的查询字符串值,并使用 r.Header.Get
获取名为 content-type
的标头的值。 您还可以使用带有 r.Header
的 for
循环来打印服务器接收到的每个 HTTP 标头的名称和值。 如果您的客户端或服务器未按您预期的方式运行,此信息可用于解决问题。 最后,您还使用 ioutil.ReadAll
函数读取 r.Body
中的 HTTP 请求正文。
更新服务器处理函数后,更新 main
函数的请求代码,使其发送带有请求正文的 POST
请求:
main.go
... time.Sleep(100 * time.Millisecond) jsonBody := []byte(`{"client_message": "hello, server!"}`) bodyReader := bytes.NewReader(jsonBody) requestURL := fmt.Sprintf("http://localhost:%d?id=1234", serverPort) req, err := http.NewRequest(http.MethodPost, requestURL, bodyReader) ...
在对 main
函数请求的更新中,您定义的新值之一是 jsonBody
值。 在此示例中,该值表示为 []byte
而不是标准的 string
因为如果您使用 encoding/json
包对 JSON 数据进行编码,它将为您提供 [X172X ] 代替 string
。
下一个值 bodyReader
是一个 bytes.Reader,它包装了 jsonBody
数据。 http.Request
主体要求值是 io.Reader
,而 jsonBody
的 []byte
值不实现 io.Reader
,所以你无法单独将其用作请求主体。 bytes.Reader
值的存在是为了提供 io.Reader
接口,因此您可以使用 jsonBody
值作为请求正文。
requestURL
值也更新为包含 id=1234
查询字符串值,主要是为了显示查询字符串值如何与其他标准 URL 组件一起包含在请求 URL 中。
最后,http.NewRequest
函数调用被更新为使用带有 http.MethodPost
的 POST
方法,并且通过从 nil
更新最后一个参数来包含请求正文正文为 bodyReader
,JSON 数据 io.Reader
。
保存更改后,您可以使用 go run
运行程序:
go run main.go
由于您对服务器进行了更新以显示更多信息,因此输出将比以前更长:
Outputserver: POST / server: query id: 1234 server: content-type: server: headers: Accept-Encoding = gzip User-Agent = Go-http-client/1.1 Content-Length = 36 server: request body: {"client_message": "hello, server!"} client: got response! client: status code: 200 client: response body: {"message": "hello!"}
服务器的第一行显示您的请求现在作为 POST
请求通过 /
路径。 第二行显示您添加到请求 URL 的 id
查询字符串值的 1234
值。 第三行显示了客户端发送的 Content-Type
标头的值,在此请求中恰好为空。
第四行可能与您在上面看到的输出略有不同。 在 Go 中,当您使用 range
对其进行迭代时,无法保证 map
值的顺序,因此 r.Headers
中的标头可能会以不同的顺序打印出来。 根据您使用的 Go 版本,您可能还会看到与上述版本不同的 User-Agent
版本。
最后,输出中的最后一个变化是服务器显示了它从客户端接收到的请求正文。 然后,服务器可以使用 encoding/json
包来解析客户端发送的 JSON 数据并制定响应。
在本节中,您更新了程序以发送 HTTP POST
请求而不是 GET
请求。 您还更新了程序以发送请求正文,其中包含 bytes.Reader
正在读取的 []byte
数据。 最后,您更新了服务器处理函数以打印出有关您的 HTTP 客户端发出的请求的更多信息。
通常在 HTTP 请求中,客户端或服务器会告诉对方它在正文中发送的内容类型。 但是,正如您在最后一个输出中看到的那样,您的 HTTP 请求没有包含 Content-Type
标头来告诉服务器如何解释正文的数据。 在下一节中,您将进行一些更新以自定义 HTTP 请求,包括设置 Content-Type
标头以让服务器知道您发送的数据类型。
自定义 HTTP 请求
随着时间的推移,HTTP 请求和响应已被用于在客户端和服务器之间发送更多种类的数据。 在某一时刻,HTTP 客户端可能会假设他们从 HTTP 服务器接收到的数据是 HTML,并且很有可能是正确的。 但现在,它可以是 HTML、JSON、音乐、视频或任何数量的其他数据类型。 为了提供有关通过 HTTP 发送的数据的更多信息,该协议包括 HTTP 标头,其中重要的标头之一是 Content-Type
标头。 此标头告诉服务器(或客户端,取决于数据的方向)如何解释它接收的数据。
在本节中,您将更新程序以在 HTTP 请求上设置 Content-Type
标头,以便服务器知道它正在接收 JSON 数据。 您还将更新您的程序以使用不同于 Go 的默认 http.DefaultClient
的 HTTP 客户端,以便您可以自定义发送请求的方式。
要进行这些更新,请再次打开 main.go
文件并更新 main
函数,如下所示:
main.go
... req, err := http.NewRequest(http.MethodPost, requestURL, bodyReader) if err != nil { fmt.Printf("client: could not create request: %s\n", err) os.Exit(1) } req.Header.Set("Content-Type", "application/json") client := http.Client{ Timeout: 30 * time.Second, } res, err := client.Do(req) if err != nil { fmt.Printf("client: error making http request: %s\n", err) os.Exit(1) } ...
在此更新中,您使用 req.Header
访问 http.Request
标头,然后将请求上的 Content-Type
标头的值设置为 application/json
。 application/json
媒体类型在 媒体类型 列表中定义为 JSON 的媒体类型。 这样,当服务器收到您的请求时,它就知道将正文解释为 JSON 而不是,例如 XML。
下一个更新是在 client
变量中创建您自己的 http.Client
实例。 在此客户端中,您将 Timeout
值设置为 30 秒。 这很重要,因为它表示客户端发出的任何请求都会在 30 秒后放弃并停止尝试接收响应。 Go 的默认 http.DefaultClient
没有指定超时,因此如果您使用该客户端发出请求,它将一直等到收到响应、被服务器断开连接或您的程序结束。 如果您有许多像这样等待响应的请求,您可能正在使用计算机上的大量资源。 设置 Timeout
值会限制请求在您定义的时间之前等待的时间。
最后,您更新了使用 client
变量的 Do
方法的请求。 您无需在此处进行任何其他更改,因为您一直在对 http.Client
值调用 Do
。 Go 的默认 HTTP 客户端 http.DefaultClient
只是默认创建的 http.Client
。 因此,当您调用 http.Get
时,该函数正在为您调用 Do
方法,而当您更新使用 http.DefaultClient
的请求时,您使用的是 [ X162X] 直接。 现在唯一的区别是您创建了这次使用的 http.Client
值。
现在,保存文件并使用 go run
运行程序:
go run main.go
您的输出应该与之前的输出非常相似,但包含有关内容类型的更多信息:
Outputserver: POST / server: query id: 1234 server: content-type: application/json server: headers: Accept-Encoding = gzip User-Agent = Go-http-client/1.1 Content-Length = 36 Content-Type = application/json server: request body: {"client_message": "hello, server!"} client: got response! client: status code: 200 client: response body: {"message": "hello!"}
您将看到来自服务器的 content-type
值,并且客户端发送了 Content-Type
标头。 这就是您可以让相同的 HTTP 请求路径同时为 JSON 和 XML API 提供服务的方式。 通过指定请求的内容类型,服务器和客户端可以不同地解释数据。
但是,此示例不会触发您配置的客户端超时。 要查看当请求花费太长时间并触发超时时会发生什么,请打开 main.go
文件并将 time.Sleep
函数调用添加到 HTTP 服务器处理函数。 然后,使 time.Sleep
的持续时间超过您指定的超时时间。 在这种情况下,您将其设置为 35 秒:
main.go
... func main() { go func() { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { ... fmt.Fprintf(w, `{"message": "hello!"}`) time.Sleep(35 * time.Second) }) ... }() ... }
现在,保存您的更改并使用 go run
运行您的程序:
go run main.go
这次运行它,退出时间会比之前更长,因为它要等到 HTTP 请求完成后才会退出。 由于您添加了 time.Sleep(35 * time.Second)
,HTTP 请求将在 30 秒超时之前完成:
Outputserver: POST / server: query id: 1234 server: content-type: application/json server: headers: Content-Type = application/json Accept-Encoding = gzip User-Agent = Go-http-client/1.1 Content-Length = 36 server: request body: {"client_message": "hello, server!"} client: error making http request: Post "http://localhost:3333?id=1234": context deadline exceeded (Client.Timeout exceeded while awaiting headers) exit status 1
在此程序输出中,您会看到服务器接收到请求并对其进行处理,但是当它到达您的 time.Sleep
函数调用所在的 HTTP 处理程序函数的末尾时,它开始休眠 35 秒。 同时,您的 HTTP 请求的超时时间正在倒计时,并在 HTTP 请求完成前达到 30 秒的限制。 这会导致 client.Do
方法调用失败并出现 context deadline exceeded
错误,因为请求的 30 秒截止日期已过。 然后,您的程序使用 os.Exit(1)
以 1
的故障状态码退出。
在本节中,您更新了程序以通过向其添加 Content-Type
标头来自定义 HTTP 请求。 您还更新了程序以创建一个具有 30 秒超时的新 http.Client
,然后使用该客户端发出 HTTP 请求。 您还通过将 time.Sleep
添加到 HTTP 请求处理程序来测试 30 秒超时。 最后,您还看到了为什么如果您想避免许多请求可能永远空闲,那么使用您自己的 http.Client
值和超时设置很重要。
结论
在本教程中,您创建了一个带有 HTTP 服务器的新程序,并使用 Go 的 net/http
包向该服务器发出 HTTP 请求。 首先,您使用 http.Get
函数通过 Go 的默认 HTTP 客户端向服务器发出 GET
请求。 然后,您使用 http.NewRequest
和 http.DefaultClient
的 Do
方法发出 GET
请求。 接下来,您更新了请求,使其成为 POST
请求,其正文使用 bytes.NewReader
。 最后,您在 http.Request
的 Header
字段上使用了 Set
方法来设置请求的 Content-Type
标头,并在通过创建您自己的 HTTP 客户端而不是使用 Go 的默认客户端,请求的持续时间。
net/http 包不仅包含您在本教程中使用的功能。 它还包括一个 http.Post 函数,可用于发出 POST
请求,类似于 http.Get
函数。 该软件包还支持保存和检索 cookies 等功能。
本教程也是 DigitalOcean How to Code in Go 系列的一部分。 该系列涵盖了许多 Go 主题,从第一次安装 Go 到如何使用语言本身。