如何在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 语句这个程序。 之后,您创建一个名为 serverPortconst,其值为 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.Responseerror 值如果请求失败。 如果请求失败,它将打印错误,然后使用 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.NewRequesthttp.DefaultClientDo 方法而不是使用 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.DefaultClientDo 方法将请求发送到服务器。 http.DefaultClient 值是 Go 的默认 HTTP 客户端,与 http.Get 使用的相同。 不过,这一次,您直接使用它来告诉它发送您的 http.Request。 HTTP 客户端的 Do 方法返回您从 http.Get 函数接收到的相同值,以便您可以以相同的方式处理响应。

打印请求结果后,使用 ioutil.ReadAll 函数读取 HTTP 响应的 BodyBody 是一个 io.ReadCloser 值,是 io.Readerio.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.DefaultClientDo 方法发出请求并将 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.Headerfor 循环来打印服务器接收到的每个 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.MethodPostPOST 方法,并且通过从 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/jsonapplication/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.NewRequesthttp.DefaultClientDo 方法发出 GET 请求。 接下来,您更新了请求,使其成为 POST 请求,其正文使用 bytes.NewReader。 最后,您在 http.RequestHeader 字段上使用了 Set 方法来设置请求的 Content-Type 标头,并在通过创建您自己的 HTTP 客户端而不是使用 Go 的默认客户端,请求的持续时间。

net/http 包不仅包含您在本教程中使用的功能。 它还包括一个 http.Post 函数,可用于发出 POST 请求,类似于 http.Get 函数。 该软件包还支持保存和检索 cookies 等功能。

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