跳转到主要内容

介绍

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

在本教程中,您将创建一个向HTTP服务器发出多种类型HTTP请求的程序。首先,您将使用默认的GoHTTP客户端发出GET请求。然后,您将增强您的程序,以使用body发出POST请求。最后,您将自定义POST请求以包含HTTP头,并添加一个超时,如果您的请求耗时过长,则会触发该超时。

先决条件

要遵循本教程,您需要:

  • 安装1.16或更高版本。要设置此设置,请遵循操作系统的“如何安装Go”教程。
  • 在Go中创建HTTP服务器的经验,可以在教程“如何在Go上创建HTTP服务器”中找到。
  • 熟悉goroutines和阅读频道。有关更多信息,请参阅教程“如何在Go中并发运行多个函数”。
  • 建议了解HTTP请求的组成和发送方式。

提出GET请求

Go-net/http包有几种不同的方式将其用作客户端。您可以使用具有HTTP等功能的通用全局HTTP客户端。Get只需一个URL和一个正文就可以快速发出HTTP Get请求,或者您可以创建一个HTTP。请求开始定制个别请求的某些方面。在本节中,您将使用http创建一个初始程序。获取一个HTTP请求,然后将其更新为使用HTTP。使用默认HTTP客户端请求。

使用 http.Get 提出请求

在程序的第一次迭代中,您将使用http。Get函数向程序中运行的HTTP服务器发出请求。http:。Get函数很有用,因为您不需要在程序中进行任何其他设置来发出请求。如果您需要快速请求,请使用http。获取可能是最好的选择。

要开始创建程序,您需要一个目录来保存程序的目录。在本教程中,您将使用名为projects的目录。

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

  1. mkdir projects
  2. cd projects

接下来,为项目创建目录并导航到它。在这种情况下,请使用目录httpclient:

  1. mkdir httpclient
  2. cd httpclient

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

在main.go文件,首先添加以下行:

package main

import (
	"errors"
	"fmt"
	"net/http"
	"os"
	"time"
)

const serverPort = 3333

 

您可以添加包名称main,以便将程序编译为可以运行的程序,然后在该程序中将使用的各种包中包含一个import语句。之后,创建一个名为serverPort的常量,其值为3333,将用作HTTP服务器正在侦听的端口和HTTP客户端将连接的端口。

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

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,您的程序就会占用时间。短时间睡眠。这个睡眠时间允许HTTP服务器有足够的时间启动并开始响应下一个请求。

现在,同样在主函数中,使用fmt设置请求URL。Sprintf将http://localhost主机名和服务器正在侦听的serverPort值。然后,使用http。获取对该URL的请求,如下所示:

...
	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服务器已设置为使用fmt。Printf可在请求根/路径时打印有关传入请求的信息。它还设置为侦听serverPort。最后,一旦启动服务器goroutine,您的程序就会占用时间。短时间睡眠。这个睡眠时间允许HTTP服务器有足够的时间启动并开始响应下一个请求。

现在,同样在主函数中,使用fmt设置请求URL。Sprintf将http://localhost主机名和服务器正在侦听的serverPort值。然后,使用http。获取对该URL的请求,如下所示:

  1. go run main.go

 

您将看到以下输出:


 

Output

server: GET /

client: got response!

client: status code: 200

 

在输出的第一行,服务器打印它从客户机收到了一个GET请求,请求/路径。然后,下面两行表示客户机从服务器返回了一个响应,并且该响应的状态代码是200。

http:。Get函数对于快速HTTP请求很有用,如您在本节中所做的请求。然而,http。请求提供了更广泛的选项来定制您的请求。

使用http.Request提出请求

与http相反。获取,http。请求函数为您提供了对请求的更大控制,而不仅仅是HTTP方法和被请求的URL。您还不会使用其他功能,而是使用http。现在请求,您将能够在本教程稍后添加这些自定义设置。

在您的代码中,第一个更新是更改HTTP服务器处理程序,以使用fmt.Fprintf返回假JSON数据响应。如果这是一个完整的HTTP服务器,那么该数据将使用Go的encoding/json包生成。如果您想了解有关在围棋中使用JSON的更多信息,我们的“如何在围棋里使用JSON”教程提供了。此外,您还需要将io/ioutil作为导入包含进来,以便在本次更新中稍后使用。

现在,打开主管道。再次转到文件并更新您的程序以开始使用http。请求如下:

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。要向服务器发出请求,请使用http。NewRequest和http。DefaultClient的Do方法:

...
	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。请求值,或在无法创建值时处理错误。与http不同。不过,Get函数是http。NewRequest函数不会立即向服务器发送HTTP请求。由于它不会立即发送请求,因此您可以在发送请求之前对其进行任何更改。

一旦http。创建和配置请求时,使用http的Do方法。DefaultClient将请求发送到服务器。http:。DefaultClient值是Go的默认HTTP客户端,与您在HTTP.Get中使用的相同。不过,这一次,您直接使用它来告诉它发送您的http.Request。HTTP客户端的Do方法返回从HTTP接收到的相同值。Get函数,以便您可以以相同的方式处理响应。

打印请求结果后,使用ioutil。ReadAll函数读取HTTP响应的正文。身体是一个io。ReadCloser值,io的组合。阅读器和io。更近一些,这意味着你可以使用任何可以从io读取的东西来读取身体的数据。读取器值。ioutil。ReadAll函数很有用,因为它将从io读取。读取器,直到它到达数据末尾或遇到错误。然后,它将返回数据作为[]字节值,您可以使用fmt打印。Printf或它遇到的错误值。

要运行更新的程序,请保存更改并使用go-run命令:

  1. go run main.go

 

这一次,您的输出应该与之前非常相似,但有一点:


 

Output

server: GET /

client: got response!

client: status code: 200

client: response body: {"message": "hello!"}

在第一行中,您可以看到服务器仍在接收对/path的GET请求。客户端还从服务器接收200响应,但它也在读取和打印服务器响应的正文。在更复杂的程序中,您可以接受{“message”:“hello!”}值,并使用encoding/JSON包将其处理为JSON。

在本节中,您使用HTTP服务器创建了一个程序,并以各种方式向其发出HTTP请求。首先,您使用了http。Get函数仅使用服务器的URL向服务器发出Get请求。然后,您将程序更新为使用http。NewRequest创建http。请求值。创建后,使用Go的默认HTTP客户端HTTP的Do方法。DefaultClient,以发出请求并打印http。输出的响应主体。

不过,HTTP协议不仅仅使用GET请求在程序之间进行通信。当您想从其他程序接收信息时,GET请求很有用,但当您想将信息从程序发送到服务器时,可以使用另一种HTTP方法POST方法。

发送POST请求

在REST API中,GET请求仅用于从服务器检索信息,因此为了让您的程序完全参与REST API,您的程序还需要支持发送POST请求。POST请求几乎与GET请求相反,客户端将数据发送到请求主体中的服务器。

在本节中,您将更新程序以将请求作为POST请求而不是GET请求发送。您的POST请求将包括一个请求主体,您将更新服务器以打印出有关您从客户端发出的请求的更多信息。

要开始进行这些更新,请打开主菜单。转到文件并添加一些您将要使用的新包到导入语句中:

...

import (
    "bytes"
    "errors"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "strings"
    "time"
)

...

然后,更新您的服务器处理程序函数,以打印有关传入请求的各种信息,例如查询字符串值、头值和请求正文:

...
  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请求的正文。

更新服务器处理程序函数后,更新主函数的请求代码,以便它发送带有请求主体的POST请求:

...
 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)
...

在对主函数请求的更新中,您正在定义的一个新值是jsonBody值。在本例中,该值表示为[]字节而不是标准字符串,因为如果您使用encoding/json包对json数据进行编码,它将返回[]字节,而不是字符串。

下一个值bodyReader是字节。包装jsonBody数据的读取器。一个http。请求正文要求值为io。Reader和jsonBody的[]字节值不实现io。阅读器,所以您不能将其单独用作请求主体。字节。存在读取器值以提供该io。读取器接口,因此可以使用jsonBody值作为请求主体。

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

最后,http。NewRequest函数调用被更新为使用带有http的POST方法。通过将最后一个参数从nil主体更新为bodyReader(JSON数据io.Reader)来包含请求主体。

保存更改后,可以使用go run运行程序:

go run main.go

由于您更新服务器以显示其他信息,输出将比以前更长:


 

Output

server: 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请求传递到/path。第二行显示添加到请求URL的id查询字符串值的1234值。第三行显示了客户端发送的Content-Type头的值,该头在该请求中恰好为空。

第四行可能与上面看到的输出略有不同。在Go中,当您使用range迭代映射值时,无法保证映射值的顺序,因此您的r.headers头可能会以不同的顺序打印出来。根据您使用的Go版本,您可能还会看到与上面不同的User Agent版本。

最后,输出中的最后一个变化是服务器显示了从客户端接收到的请求主体。然后,服务器可以使用encoding/json包解析客户端发送的json数据并制定响应。

在本节中,您更新了程序以发送HTTP POST请求而不是GET请求。您还更新了程序,以发送一个请求正文,其中[]字节数据由bytes.Reader读取。最后,您更新了服务器处理程序函数,以打印出有关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之外的HTTP客户端。DefaultClient,以便您可以自定义发送请求的方式。

要进行这些更新,请打开主菜单。再次打开文件并更新主函数,如下所示:

...

  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)
  }

...

在此更新中,您可以访问http。使用req请求标头。Header,然后将请求的Content-Type头的值设置为application/json。application/json媒体类型在媒体类型列表中定义为json的媒体类型。这样,当服务器接收到您的请求时,它知道将主体解释为JSON,而不是XML。

下一个更新是创建自己的http。客户端变量中的客户端实例。在此客户端中,您将超时值设置为30秒。这一点很重要,因为它表示,向客户端发出的任何请求都将在30秒后放弃并停止尝试接收响应。Go的默认http。DefaultClient没有指定超时,因此如果您使用该客户端发出请求,它将等待直到收到响应、服务器断开连接或程序结束。如果您有许多请求像这样等待响应,那么您可能正在使用计算机上的大量资源。设置超时值可限制请求在您定义的时间之前等待的时间。

最后,您更新了请求,以使用客户端变量的Do方法。您不需要在此进行任何其他更改,因为您一直在http上调用Do。客户重视整个时间。Go的默认HTTP客户端HTTP。DefaultClient只是一个http。默认情况下创建的客户端。所以,当你调用http时。Get,函数正在为您调用Do方法,当您更新请求以使用http时。DefaultClient,您使用的是http。客户直接。现在唯一的区别是您创建了http。你这次使用的客户价值。

现在,保存文件并使用go-run运行程序:

go run main.go

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

Output
server: 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!"}

您将看到来自服务器的内容类型值,以及客户端正在发送的内容类型标头。这就是为什么可以同时为JSON和XML API提供相同的HTTP请求路径。通过指定请求的内容类型,服务器和客户端可以对数据进行不同的解释。

不过,这个示例不会触发您配置的客户端超时。要查看当请求花费太长时间并且触发超时时会发生什么,请打开主窗口。去文件并添加时间。对HTTP服务器处理程序函数的睡眠函数调用。然后,腾出时间。睡眠持续时间超过指定的超时时间。在这种情况下,将其设置为35秒:

...

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秒超时之前不会完成:

Output
server: 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

在这个程序输出中,您可以看到服务器收到了请求并进行了处理,但当它到达HTTP处理程序函数的末尾时,您的时间就到了。睡眠功能调用是,它开始睡眠35秒。同时,HTTP请求的超时正在倒计时,并在HTTP请求完成之前达到30秒的限制。这将导致客户端。Do方法调用失败,上下文截止日期超过错误,因为请求的30秒截止日期已过。然后,您的程序使用os.Exit(1)退出,失败状态代码为1。

在本节中,您更新了程序,通过向HTTP请求添加Content-Type头来定制HTTP请求。您还更新了程序以创建新的http。客户端超时30秒,然后使用该客户端发出HTTP请求。您还通过添加时间测试了30秒超时。睡眠到HTTP请求处理程序。最后,您还了解了为什么使用自己的http很重要。如果您想避免许多请求可能永远处于空闲状态,请设置超时的客户端值。

结论

在本教程中,您使用HTTP服务器创建了一个新程序,并使用Go的net/HTTP包向该服务器发出HTTP请求。首先,您使用了http。Get函数使用Go的默认HTTP客户端向服务器发出Get请求。然后,您使用了http。带有http的NewRequest。DefaultClient的Do方法发出GET请求。接下来,您使用bytes.NewReader更新了请求,使其成为带有正文的POST请求。最后,您在http上使用了Set方法。Request's Header字段用于设置请求的Content-Type头,并通过创建自己的HTTP客户端而不是使用Go的默认客户端来设置请求持续时间的30秒超时。

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

文章链接