跳转到主要内容

介绍

许多开发人员至少花了一些时间来创建服务器,以便在互联网上分发内容。超文本传输协议(HTTP)提供了大部分内容,无论是对猫图像的请求还是加载您正在阅读的教程的请求。Go标准库提供了内置支持,用于创建一个HTTP服务器来服务您的web内容或向这些服务器发出HTTP请求。

在本教程中,您将使用Go的标准库创建一个HTTP服务器,然后扩展服务器以从请求的查询字符串、正文和表单数据中读取数据。您还将更新程序,以使用自己的HTTP头和状态代码响应请求。

先决条件

要遵循本教程,您需要:

  • 安装1.16或更高版本。要设置此设置,请遵循操作系统的“如何安装Go”教程。
  • 能够使用curl进行web请求。要阅读curl,请查看How To Download Files with curl。
  • 熟悉在Golang中使用JSON,这可以在《如何在Golang教程中使用JSON》中找到。
  • 体验Golang的上下文包,可以在教程“如何在Golang中使用上下文”中获得。
  • 体验运行goroutines和阅读频道,这可以从教程“如何在Go中并发运行多个函数”中获得。
  • 熟悉HTTP请求的组成和发送方式(推荐)。

设置项目

在Go中,大多数HTTP功能由标准库中的net/HTTP包提供,而其余的网络通信由net包提供。net/http包不仅包括发出http请求的能力,还提供了一个可以用来处理这些请求的http服务器。

在本节中,您将创建一个使用http的程序。ListenAndServe函数启动HTTP服务器,响应请求路径/和/hello。然后,您将扩展该程序以在同一程序中运行多个HTTP服务器。

不过,在编写任何代码之前,您需要创建程序的目录。许多开发人员将他们的项目保存在一个目录中,以保持项目的有序性。在本教程中,您将使用名为projects的目录。

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

mkdir projects
cd projects

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

mkdir httpserver
cd httpserver

现在您已经创建了程序的目录,并且您位于httpserver目录中,您可以开始实现HTTP服务器了。

监听请求和服务响应

GoHTTP服务器包括两个主要组件:侦听来自HTTP客户端的请求的服务器和一个或多个将响应这些请求的请求处理程序。在本节中,您将从使用函数http开始。HandleFunc告诉服务器调用哪个函数来处理对服务器的请求。然后,您将使用http。ListenAndServe函数来启动服务器并告诉它侦听新的HTTP请求,然后使用您设置的处理程序函数为它们提供服务。

现在,在您创建的httpserver目录中,使用nano或您最喜欢的编辑器打开main。go文件:

nano main.go

总的来说。go文件中,您将创建两个函数getRoot和getHello,作为处理程序函数。然后,您将创建一个主函数,并使用它来设置带有http的请求处理程序。HandleFunc函数,方法是为其传递getRoot处理程序函数的/path和getHello处理程序的/hello路径。设置好处理程序后,调用http。ListenAndServe函数用于启动服务器并侦听请求。

将以下代码添加到文件中以启动程序并设置处理程序:

package main

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

func getRoot(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("got / request\n")
    io.WriteString(w, "This is my website!\n")
}
func getHello(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("got /hello request\n")
    io.WriteString(w, "Hello, HTTP!\n")
}

在这第一段代码中,您为Go程序设置了包,导入程序所需的包,并创建两个函数:getRoot函数和getHello函数。这两个函数都有相同的函数签名,它们接受相同的参数:http.ResponseWriter值和*http.Request值。此函数签名用于HTTP处理程序函数,并定义为HTTP.HandlerFunc。当向服务器发出请求时,它将使用有关所发出请求的信息设置这两个值,然后使用这些值调用处理程序函数。

在http.HandlerFunc中,http.ResponseWriter值(在处理程序中命名为w)用于控制写回发出请求的客户端的响应信息,例如响应正文或状态代码。然后,*http.Request值(在处理程序中命名为r)用于获取有关进入服务器的请求的信息,例如POST请求中发送的主体或发出请求的客户端的信息。

现在,在两个HTTP处理程序中,当处理程序函数收到请求时,使用fmt.Printf打印,然后使用HTTP.ResponseWriter向响应正文发送一些文本。http.ResponseWriter是一个io.Writer,这意味着您可以使用任何能够写入该接口的东西来写入响应主体。在本例中,您使用io.WriteString函数将响应写入正文。

现在,通过启动主功能继续创建程序:

...
func main() {
    http.HandleFunc("/", getRoot)
    http.HandleFunc("/hello", getHello)

    err := http.ListenAndServe(":3333", nil)
...

在main函数中,您有两个对http.HandleFunc函数的调用。对该函数的每次调用都为默认服务器多路复用器中的特定请求路径设置一个处理程序函数。服务器多路复用器是一个http.Handler,它能够查看请求路径并调用与该路径关联的给定处理程序函数。因此,在您的程序中,您告诉默认的服务器多路复用器在某人请求/path时调用getRoot函数,在某人请求/hello路径时调用getHello函数。

设置处理程序后,调用http.ListenAndServe函数,该函数告诉全局http服务器使用可选的http.Handler侦听特定端口上的传入请求。在程序中,您告诉服务器侦听“:3333”。通过在冒号前不指定IP地址,服务器将侦听与您的计算机关联的每个IP地址,并将侦听端口3333。网络端口(如这里的3333)是一台计算机同时有多个程序相互通信的一种方式。每个程序都使用自己的端口,因此当客户端连接到特定端口时,计算机就知道要将其发送到哪个端口。如果您只想允许连接到localhost,即IP地址127.0.0.1的主机名,您可以改为127.0.0.1:3333。

http.ListenAndServe函数还为http.Handler参数传递一个nil值。这会告诉ListenAndServe函数您希望使用默认的服务器多路复用器,而不是您设置的服务器多路器。

ListenAndServe是一个阻塞调用,这意味着在ListenAndServe完成运行之前,程序不会继续运行。然而,ListenAndServe在程序运行完毕或HTTP服务器被告知关闭之前不会完成运行。尽管ListenAndServe正在阻塞,并且您的程序不包含关闭服务器的方法,但包含错误处理仍然很重要,因为有几种方法可以调用ListenAndServe失败。因此,在主函数中为ListenAndServe添加错误处理,如下所示:

...

func main() {
    ...
    err := http.ListenAndServe(":3333", nil)
  if errors.Is(err, http.ErrServerClosed) {
        fmt.Printf("server closed\n")
    } else if err != nil {
        fmt.Printf("error starting server: %s\n", err)
        os.Exit(1)
    <^>}
}

您正在检查的第一个错误是http。ErrServerClosed,当服务器被告知关闭或关闭时返回。这通常是一个预期的错误,因为您将自己关闭服务器,但它也可以用于显示服务器在输出中停止的原因。在第二次错误检查中,您将检查是否存在任何其他错误。如果发生这种情况,它会将错误打印到屏幕上,然后使用操作系统退出程序,错误代码为1。退出功能。

运行程序时可能会看到的一个错误是地址已在使用错误。当ListenAndServe无法侦听您提供的地址或端口时,可能会返回此错误,因为其他程序已经在使用它。有时,如果该端口是常用的,并且计算机上的另一个程序正在使用该端口,则会发生这种情况,但如果您多次运行自己的程序的多个副本,也可能发生这种情况。如果在学习本教程时看到此错误,请确保在再次运行程序之前已从上一步停止程序。

注意:如果您看到地址已在使用错误,并且您没有运行程序的另一个副本,则可能意味着其他程序正在使用该地址。如果发生这种情况,无论您在本教程中看到3333,请将其更改为1024以上65535以下的其他数字,如3334,然后重试。如果您仍然看到错误,您可能需要继续尝试查找未使用的端口。一旦找到一个可以工作的端口,就可以将其用于本教程中的所有命令。

现在代码已经准备好了,保存main。go文件并使用go-run运行程序。与您可能编写的其他Golang程序不同,此程序不会立即自行退出。运行程序后,继续执行以下命令:

go run main.go

由于您的程序仍在终端中运行,您需要打开第二个终端与服务器交互。当您看到与下面命令颜色相同的命令或输出时,意味着在第二个终端中运行它。

在第二个终端中,使用curl程序向HTTP服务器发出HTTP请求。curl是一个通常默认安装在许多系统上的实用程序,可以向各种类型的服务器发出请求。在本教程中,您将使用它进行HTTP请求。您的服务器正在侦听计算机端口3333上的连接,因此您需要向同一端口上的本地主机发出请求:

curl http://localhost:3333

输出如下:

Output
This is my website!

在输出中,您将看到“这是我的网站!”!因为您访问了HTTP服务器上的/path。

现在,在同一终端中,向同一主机和端口发出请求,但在curl命令的末尾添加/hello路径:

curl http://localhost:3333/hello

您的输出如下:

Output
Hello, HTTP!

这一次,您将看到Hello,HTTP!getHello函数的响应。

如果您返回到运行HTTP服务器功能的终端,您现在有两行来自服务器的输出。一个用于/request,另一个用于/hello请求:

Output
got / request
got /hello request

由于服务器将继续运行,直到程序运行完毕,您需要自己停止它。为此,按CONTROL+C向程序发送中断信号以停止程序。

在本节中,您创建了一个HTTP服务器程序,但它使用默认的服务器多路复用器和默认的HTTP服务器。使用默认值或全局值可能会导致难以复制的错误,因为程序的多个部分可能会在不同的时间更新它们。如果这导致错误状态,则很难追踪错误,因为它可能只存在于以特定顺序调用某些函数的情况下。因此,为了避免这个问题,您将更新服务器以使用您在下一节中创建的服务器多路复用器。

多路复用请求处理程序

在上一节中启动HTTP服务器时,为ListenAndServe函数传递了一个HTTP的nil值。Handler参数,因为您使用的是默认的服务器多路复用器。因为http。Handler是一个接口,可以创建实现该接口的自己的结构。但有时,您只需要一个基本的http。为特定请求路径调用单个函数的处理程序,如默认服务器多路复用器。在本节中,您将更新程序以使用http。ServeMux,一个服务器多路复用器和http。net/http包提供的处理程序实现,可用于这些情况。

http:。ServeMux结构可以配置为与默认的服务器多路复用器相同,因此您不需要对程序进行太多更新,就可以开始使用自己的而不是全局默认值。要更新程序,请使用http。ServeMux,打开你的总管。再次打开文件并更新您的程序以使用您自己的http.ServeMux:

...

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", getRoot)
    mux.HandleFunc("/hello", getHello)

    err := http.ListenAndServe(":3333", mux)
    
    ...
}

在这次更新中,您创建了一个新的http。使用http的ServeMux。NewServeMux构造函数并将其分配给mux变量。之后,您只需要更新http。HandleFunc调用以使用mux变量,而不是调用http包。最后,您将调用更新为http。ListenAndServe为其提供http。您创建的处理程序(mux)而不是nil值。

现在,您可以使用go run再次运行程序:

go run main.go

您的程序将像上次一样继续运行,因此您需要运行命令与另一个终端中的服务器交互。首先,使用curl再次请求/path:

curl http://localhost:3333

输出如下:

Output
This is my website!

您将看到此输出与之前相同。

接下来,对/hello路径运行前面的相同命令:

curl http://localhost:3333/hello

输出如下:

Output
Hello, HTTP!

此路径的输出也与之前相同。

最后,如果您返回原始终端,您将看到/和/hello请求的输出,如前所述:

Output
got / request
got /hello request

您对程序进行的更新在功能上是相同的,但这次您使用的是自己的http。处理程序而不是默认处理程序。

最后,再次按CONTROL+C退出服务器程序。

一次运行多台服务器

除了使用自己的http之外。Handler,Go-net/http包还允许您使用默认服务器以外的http服务器。有时,您可能希望自定义服务器的运行方式,或者您可能希望在同一个程序中同时运行多个HTTP服务器。例如,您可能有一个公共网站和一个私人管理网站,您希望从同一程序运行。由于您只能有一个默认的HTTP服务器,因此您无法使用默认的服务器来执行此操作。在本节中,您将更新程序以使用两个http。net/http包为此类情况提供的服务器值-当您希望对服务器进行更多控制或同时需要多个服务器时。

在你的主体中。go文件,您将使用HTTP.Server设置多个HTTP服务器。您还将更新处理程序函数以访问上下文。传入*http.Request的上下文。这将允许您在上下文中设置请求来自哪个服务器。上下文变量,因此您可以在处理程序函数的输出中打印服务器。

打开主电源。再次转到文件并更新,如下所示:

package main

import (
    // Note: Also remove the 'os' import.
    "context"
    "errors"
    "fmt"
    "io"
    "net"
    "net/http"
)

const keyServerAddr = "serverAddr"

func getRoot(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    fmt.Printf("%s: got / request\n", ctx.Value(keyServerAddr))
    io.WriteString(w, "This is my website!\n")
}
func getHello(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))
    io.WriteString(w, "Hello, HTTP!\n")
}

在上面的代码更新中,您更新了import语句以包含更新所需的包。然后,您创建了一个名为keyServerAddr的常量字符串值,作为HTTP中HTTP服务器地址值的键。请求上下文。最后,您更新了getRoot和getHello函数以访问http。请求的上下文。上下文值。一旦有了该值,就可以在fmt中包含HTTP服务器的地址。Printf输出,以便您可以看到两个服务器中的哪一个处理了HTTP请求。

现在,通过添加两个http中的第一个开始更新主函数。服务器值:

...
func main() {
    ...
    mux.HandleFunc("/hello", getHello)

    ctx, cancelCtx := context.WithCancel(context.Background())
    serverOne := &http.Server{
        Addr:    ":3333",
        Handler: mux,
        BaseContext: func(l net.Listener) context.Context {
            ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
            return ctx
        },
    }

在更新的代码中,您所做的第一件事是创建一个新的上下文。具有可用函数cancelCtx的上下文值,以取消上下文。然后,定义serverOne http。服务器值。该值与您已经使用的HTTP服务器非常相似,但不是将地址和处理程序传递给HTTP。ListenAndServe函数,将它们设置为http.Server的Addr和Handler值。

另一个变化是添加了BaseContext函数。BaseContext是一种更改部分上下文的方法。处理程序函数在调用*http.Request的Context方法时接收的上下文。在您的程序中,您要添加服务器正在侦听的地址(l.Addr())。String())添加到带有键serverAddr的上下文中,然后将其打印到处理程序函数的输出中。

接下来,定义第二台服务器serverTwo:

...

func main() {
    ...
    serverOne := &http.Server {
        ...
    }

    serverTwo := &http.Server{
        Addr:    ":4444",
        Handler: mux,
        BaseContext: func(l net.Listener) context.Context {
            ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
            return ctx
        },
    }

此服务器的定义方式与第一台服务器相同,只是您将Addr字段设置为:4444,而不是:3333。这样,一台服务器将侦听端口3333上的连接,第二台服务器将监听端口4444上的连接。

现在,更新程序以启动goroutine中的第一台服务器serverOne:

...

func main() {
    ...
    serverTwo := &http.Server {
        ...
    }

    go func() {
        err := serverOne.ListenAndServe()
        if errors.Is(err, http.ErrServerClosed) {
            fmt.Printf("server one closed\n")
        } else if err != nil {
            fmt.Printf("error listening for server one: %s\n", err)
        }
        cancelCtx()
    }()

在goroutine中,使用ListenAndServe启动服务器,与之前相同,但这次不需要像使用http那样为函数提供参数。ListenAndServe,因为http。已配置服务器值。然后,执行与之前相同的错误处理。在函数的末尾,调用cancelCtx以取消提供给HTTP处理程序和服务器BaseContext函数的上下文。这样,如果服务器出于某种原因结束,上下文也将结束。

最后,更新程序以启动goroutine中的第二台服务器:

...

func main() {
    ...
    go func() {
        ...
    }()
    go func() {
        err := serverTwo.ListenAndServe()
        if errors.Is(err, http.ErrServerClosed) {
            fmt.Printf("server two closed\n")
        } else if err != nil {
            fmt.Printf("error listening for server two: %s\n", err)
        }
        cancelCtx()
    }()

    <-ctx.Done()
}

这个goroutine在功能上与第一个相同,它只是启动serverTwo而不是serverOne。此更新还包括主函数的结尾,在从主函数返回之前,您可以从ctx.Done通道读取。这可以确保程序在服务器goroutines结束并调用cancelCtx之前一直运行。上下文结束后,程序将退出。

完成后保存并关闭文件。

使用go-Run命令运行服务器:

go run main.go

您的程序将再次继续运行,因此在您的第二个终端中运行curl命令,从监听3333的服务器请求/path和/hello路径,与之前的请求相同:

curl http://localhost:3333
curl http://localhost:3333/hello

输出如下:

Output
This is my website!
Hello, HTTP!

在输出中,您将看到与之前相同的响应。

现在,再次运行这些相同的命令,但这次使用端口4444,该端口对应于程序中的serverTwo:

curl http://localhost:4444
curl http://localhost:4444/hello

输出如下:

Output 

This is my website! 

Hello, HTTP!

 

您将看到这些请求的输出与serverOne服务的端口3333上的请求的输出相同。

最后,回顾服务器运行的原始终端:

Output
[::]:3333: got / request
[::]:3333: got /hello request
[::]:4444: got / request
[::]:4444: got /hello request

输出与您之前看到的类似,但这次它显示了响应请求的服务器。前两个请求显示它们来自监听端口3333(serverOne)的服务器,后两个请求来自监听端口4444(serverTwo)的服务器。这些是从BaseContext的serverAddr值检索的值。

根据您的计算机是否设置为使用IPv6,您的输出也可能与上面的输出略有不同。如果是,您将看到与上面相同的输出。如果不是,您将看到0.0.0.0而不是[::]。原因是,如果配置了,您的计算机将通过IPv6与自己通信,并且[::]是0.0.0.0的IPv6符号。

完成后,再次使用CONTROL+C停止服务器。

在本节中,您使用HTTP.HandleFunc和HTTP.ListenAndServe创建了一个新的HTTP服务器程序来运行和配置默认服务器。然后,您将其更新为使用http.ServeMux作为http.Handler,而不是默认的服务器多路复用器。最后,您更新了程序,使用http.Server在同一程序中运行多个http服务器。

虽然现在有一个HTTP服务器在运行,但它不是很交互式。您可以添加它响应的新路径,但用户无法通过该路径与它进行交互。HTTP协议包括用户可以在路径之外与HTTP服务器交互的多种方式。在下一节中,您将更新程序以支持其中的第一个:查询字符串值。

检查请求的查询字符串

用户能够影响从HTTP服务器返回的HTTP响应的方法之一是使用查询字符串。查询字符串是添加到URL末尾的一组值。它以一个开头?字符,使用&作为分隔符添加附加值。查询字符串值通常用作过滤或自定义HTTP服务器作为响应发送的结果的方法。例如,一个服务器可以使用results值来允许用户指定类似results=10的值,以表示他们希望在结果列表中看到10个项目。

在本节中,您将更新getRoot处理程序函数以使用其*http。请求值以访问查询字符串值并将其打印到输出。

首先,打开主管道。go文件并更新getRoot函数以访问带有r.URL的查询字符串。查询方法。然后,更新main方法以删除serverTwo及其所有相关代码,因为您不再需要它:

...

func getRoot(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    hasFirst := r.URL.Query().Has("first")
    first := r.URL.Query().Get("first")
    hasSecond := r.URL.Query().Has("second")
    second := r.URL.Query().Get("second")

    fmt.Printf("%s: got / request. first(%t)=%s, second(%t)=%s\n",
        ctx.Value(keyServerAddr),
        hasFirst, first,
        hasSecond, second)
    io.WriteString(w, "This is my website!\n")
}
...

在getRoot函数中,您使用getRoot的*http的r.URL字段。请求访问有关所请求URL的属性。然后使用r.URL字段的Query方法访问请求的查询字符串值。访问查询字符串值后,可以使用两种方法与数据交互。Has方法返回一个bool值,指定查询字符串是否具有带有提供的键的值,例如first。然后,Get方法返回一个字符串,其中包含提供的键的值。

理论上,您可以始终使用Get方法来检索查询字符串值,因为它将始终返回给定键的实际值,或者在键不存在的情况下返回空字符串。对于许多用途,这已经足够好了——但在某些情况下,您可能想知道用户提供空值与根本不提供值之间的区别。根据您的用例,您可能想知道用户是否提供了一个什么都没有的过滤器值,或者他们是否根本没有提供过滤器。如果他们提供了一个什么都没有的过滤值,你可能会想将其视为“什么都不显示”,而不提供过滤值则意味着“什么都显示”。使用Has和Get可以区分这两种情况。

在getRoot函数中,您还更新了输出,以显示第一个和第二个查询字符串值的Has和Get值。

现在,更新您的主功能以再次使用一台服务器:

...

func main() {
    ...
    mux.HandleFunc("/hello", getHello)
    
    ctx := context.Background()
    server := &http.Server{
        Addr:    ":3333",
        Handler: mux,
        BaseContext: func(l net.Listener) context.Context {
            ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
            return ctx
        },
    }

    err := server.ListenAndServe()
    if errors.Is(err, http.ErrServerClosed) {
        fmt.Printf("server closed\n")
    } else if err != nil {
        fmt.Printf("error listening for server: %s\n", err)
    }
}

在main函数中,您删除了对serverTwo的引用,并将运行服务器(以前的serverOne)从goroutine移到了main函数,类似于您运行http的方式。早点收听和服务。您也可以将其更改回http。ListenAndServe而不是使用http。服务器值,因为您只有一台服务器再次运行,但使用http。服务器,如果您希望在将来对服务器进行任何其他自定义,则需要更新的内容将更少。

现在,保存更改后,使用go run再次运行程序:

go run main.go

您的服务器将再次开始运行,因此切换回第二个终端,运行带有查询字符串的curl命令。在这个命令中,您需要用单引号(')将URL括起来,否则终端的shell可能会将查询字符串中的&符号解释为许多shell都包含的“在后台运行此命令”功能。在URL中,第一个值为first=1,第二个值为second=1:

curl 'http://localhost:3333?first=1&second='

输出如下:

Output
This is my website!

您将看到curl命令的输出与以前的请求没有变化。

但是,如果切换回服务器程序的输出,您将看到新的输出包括查询字符串值:

Output
[::]:3333: got / request. first(true)=1, second(true)=

第一个查询字符串值的输出显示Has方法返回true,因为first有一个值,Get也返回了值1。第二个查询字符串的输出显示,Has返回true是因为包含了第二个,但是Get方法除了空字符串之外没有返回任何其他值。您还可以尝试通过添加和删除第一个和第二个或设置不同的值来发出不同的请求,以查看它如何更改这些函数的输出。

完成后,按CONTROL+C停止服务器。

在本节中,您将程序更新为只使用一个http。服务器,但您还添加了对从getRoot处理程序函数的查询字符串中读取第一个和第二个值的支持。

不过,使用查询字符串并不是用户向HTTP服务器提供输入的唯一方法。向服务器发送数据的另一种常见方式是在请求正文中包含数据。在下一节中,您将更新程序以从*http读取请求的正文。请求数据。

读取请求正文

在创建基于HTTP的API(如REST API)时,用户可能需要发送的数据超过URL长度限制所能包含的数据,或者您的页面可能需要接收与数据解释方式无关的数据,如过滤器或结果限制。在这些情况下,通常在请求的主体中包含数据,并通过POST或PUT HTTP请求发送数据。

在Go http中。HandlerFunc,*http。Request值用于访问关于传入请求的信息,它还包括一种使用body字段访问请求正文的方法。在本节中,您将更新getRoot处理程序函数以读取请求的正文。

要更新getRoot方法,请打开main。转到文件并将其更新为使用ioutil。ReadAll读取r.Body请求字段:

 

package main

import (
    ...
    "io/ioutil"
    ...
)

...

func getRoot(w http.ResponseWriter, r *http.Request) {
    ...
    second := r.URL.Query().Get("second")

    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        fmt.Printf("could not read body: %s\n", err)
    }

    fmt.Printf("%s: got / request. first(%t)=%s, second(%t)=%s, body:\n%s\n",
        ctx.Value(keyServerAddr),
        hasFirst, first,
        hasSecond, second,
        body)
    io.WriteString(w, "This is my website!\n")
}

...

在本次更新中,您将使用ioutil。ReadAll函数读取*http的r.Body属性。请求访问请求的正文。ioutil。ReadAll函数是一个实用函数,将从io读取数据。读取器,直到遇到错误或数据结束。因为r.身体是一个io。读者,你可以用它来阅读身体。读完正文后,还更新了fmt。Printf将其打印到输出。

保存更新后,使用go-run命令运行服务器:

go run main.go

由于服务器将继续运行,直到您停止它,所以请转到您的另一个终端,使用带有-X POST选项的curl和带有-d选项的body发出POST请求。您还可以使用前面的第一个和第二个查询字符串值:

curl -X POST -d 'This is the body' 'http://localhost:3333?first=1&second='

您的输出如下所示:

Output
This is my website!

处理程序函数的输出是相同的,但您会看到服务器日志再次更新:

Output
[::]:3333: got / request. first(true)=1, second(true)=, body:
This is the body

在服务器日志中,您将看到前面的查询字符串值,但现在您还将看到这是curl命令发送的正文数据。

现在,按CONTROL+C停止服务器。

在本节中,您更新了程序,将请求的正文读入打印到输出的变量中。通过将这种方式读取主体与其他功能(例如编码/json以将json主体解组为Go数据)相结合,您将能够创建用户可以通过其他API熟悉的方式进行交互的API。

不过,并非所有用户发送的数据都是API形式的。许多网站都有要求用户填写的表单,因此在下一节中,您将更新您的程序,以读取除请求正文和查询字符串之外的表单数据。

检索表单数据

长期以来,使用表单发送数据是用户向HTTP服务器发送数据并与网站交互的标准方式。表单现在不像过去那么流行了,但它们仍然有很多用途,可以作为用户向网站提交数据的方式。*http。http中的请求值。HandlerFunc还提供了一种访问该数据的方法,类似于它提供对查询字符串和请求正文的访问。在本节中,您将更新getHello程序,以从表单中接收用户名,并用用户名回复他们。

打开主电源。转到并更新getHello函数以使用*http.Request的PostFormValue方法:

...

func getHello(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))

    myName := r.PostFormValue("myName")
    if myName == "" {
        myName = "HTTP"
    }
    io.WriteString(w, fmt.Sprintf("Hello, %s!\n", myName))
}

...

现在,在getHello函数中,您正在读取发布到处理程序函数的表单值,并查找名为myName的值。如果找不到值或找到的值是空字符串,则将myName变量设置为HTTP的默认值,这样页面就不会显示空名称。然后,您更新了用户的输出,以显示他们发送的名称,如果他们没有发送名称,则显示HTTP。

要使用这些更新运行服务器,请保存更改并使用go-run运行:

go run main.go

现在,在您的第二个终端中,将curl与-X POST选项一起用于/hello URL,但这次不是使用-d来提供数据体,而是使用-F“myName=Sammy”选项来提供带有值Sammy的myName字段的表单数据:

curl -X POST -F 'myName=Sammy' 'http://localhost:3333/hello'

输出如下:




Output

Hello, Sammy!

在上面的输出中,您将看到预期的Hello,Sammy!问候,因为你给curl发的表格上写着我的名字是Sammy。

您在getHello函数中用于检索myName表单值的r.PostFormValue方法是一种特殊的方法,它只包括从请求正文中的表单发布的值。但是,也可以使用r.FormValue方法,该方法包括表单正文和查询字符串中的任何值。因此,如果您使用r.FormValue(“myName”),还可以删除-F选项,并在查询字符串中包含myName=Sammy,以查看Sammy的返回情况。但是,如果在不更改r.FormValue的情况下执行此操作,则会看到名称的默认HTTP响应。小心从哪里检索这些值可以避免名称中的潜在冲突或难以追踪的错误。更严格地使用r.PostFormValue是有用的,除非您希望在查询字符串中也有灵活性。

如果您回顾服务器日志,您会看到/hello请求的日志记录与以前的请求类似:

Output
[::]:3333: got /hello request

要停止服务器,请按CONTROL+C。

在本节中,您更新了getHello处理程序函数,从发布到页面的表单数据中读取名称,然后将该名称返回给用户。

此时,在您的程序中,在处理请求时可能会出现一些问题,您的用户不会收到通知。在下一节中,您将更新处理程序函数以返回HTTP状态代码和标头。

使用标题和状态代码进行响应

HTTP协议使用了一些用户通常看不到的功能来发送数据以帮助浏览器或服务器通信。其中一个特性称为状态代码,服务器使用它来让HTTP客户端更好地了解服务器是否认为请求成功,或者服务器端或客户端发送的请求是否出错。

HTTP服务器和客户端通信的另一种方式是使用头字段。头字段是一个键和值,客户端或服务器将发送给另一方,让他们了解自己。HTTP协议预定义了许多标头,例如Accept,客户端使用它来告诉服务器它可以接受和理解的数据类型。也可以通过在它们前面加上x,然后再加上名称的其余部分来定义自己的名称。

在本节中,您将更新程序,使getHello的myName表单字段成为必填字段。如果没有为myName字段发送值,服务器将向客户端发送一个“错误请求”状态代码,并添加一个x-missing-field头,让客户端知道缺少了哪个字段。

要将此功能添加到程序中,请打开主菜单。最后一次转到文件并将验证检查添加到getHello处理程序函数:

...

func getHello(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))

    myName := r.PostFormValue("myName")
    if myName == "" {
        w.Header().Set("x-missing-field", "myName")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    io.WriteString(w, fmt.Sprintf("Hello, %s!\n", myName))
}

...

在本次更新中,当myName为空字符串时,您将向客户端发送错误消息,而不是设置HTTP的默认名称。首先,使用w.Header()。Set方法在响应HTTP头中设置值为myName的x-missing-field头。然后,使用w.WriteHeader方法将任何响应头以及“错误请求”状态代码写入客户端。最后,它将从处理程序函数中返回。你想确保你做到这一点,这样你就不会不小心写了一个Hello!除了错误信息之外,还响应于客户端。

同样重要的是,确保您设置了标题并以正确的顺序发送了状态代码。在HTTP请求或响应中,必须在正文发送到客户端之前发送所有标头,因此任何更新w.Header()的请求都必须在调用w.WriteHeader之前完成。一旦调用w.WriteHeader,页面的状态将与所有标题一起发送,并且只有正文可以在其后写入。

保存更新后,可以使用go-run命令再次运行程序:

go run main.go

现在,使用第二个终端向/hello URL发出另一个curl-X POST请求,但不要使用-F来发送表单数据。您还需要包含-v选项来告诉curl显示详细输出,以便您可以看到请求的所有标头和输出:

curl -v -X POST 'http://localhost:3333/hello'

这一次,在输出中,由于详细的输出,您将在处理请求时看到更多信息:

Output
*   Trying ::1:3333...
* Connected to localhost (::1) port 3333 (#0)
> POST /hello HTTP/1.1
> Host: localhost:3333
> User-Agent: curl/7.77.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< X-Missing-Field: myName
< Date: Wed, 02 Mar 2022 03:51:54 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

输出中的前两行显示curl正在尝试连接到本地主机端口3333。

然后,以>开头的行显示curl向服务器发出的请求。它说curl正在使用HTTP1.1协议以及其他一些头向/helloURL发出POST请求。然后,它发送一个空正文,如空>行所示。

curl发送请求后,您可以看到它从带有<前缀的服务器收到的响应。第一行表示服务器响应了一个坏请求,这也被称为400状态码。然后,您可以看到您设置的X-Missing-Field头包含一个值myName。在发送了几个额外的头之后,请求结束,而不发送任何正文,这可以通过长度为0的Content-Length(或正文)看出。

如果您再次查看服务器输出,您将看到服务器在输出中处理的/hello请求:

Output
[::]:3333: got /hello request

最后一次,按CONTROL+C停止服务器。

在本节中,您更新了HTTP服务器,以向/hello表单输入添加验证。如果名称未作为表单的一部分发送,则使用w.Header()。设置以设置要发送回客户端的标头。设置标头后,您使用w.WriteHeader将标头写入客户端,并使用状态代码向客户端指示这是一个错误的请求。

结论

在本教程中,您使用Go标准库中的net/HTTP包创建了一个新的Go HTTP服务器。然后,您更新了程序以使用特定的服务器多路复用器和多个http。服务器实例。您还更新了服务器,以便通过查询字符串值、请求正文和表单数据读取用户输入。最后,您更新了服务器,使用自定义HTTP头和“错误请求”状态代码将表单验证信息返回给客户端。

Go HTTP生态系统的一个好处是,许多框架都被设计成与Go的net/HTTP包巧妙地集成,而不是重新创建大量已经存在的代码。github。com/go-chi/chi项目就是一个很好的例子。Go中内置的服务器复用器是开始使用HTTP服务器的好方法,但它缺少大型web服务器可能需要的许多高级功能。chi等项目能够实现http。Go标准库中的Handler接口适合标准http。服务器,而不需要重写代码的服务器部分。这使他们能够专注于创建中间件和其他工具,以增强可用性,而不是专注于基本功能。

除了像chi这样的项目,Go-net/http包还包括许多本教程中未涉及的功能。要了解有关使用cookies或服务HTTPS流量的更多信息,net/http包是一个很好的起点。

文章链接