如何用 Go 创建 HTTP 服务器

作者选择了 多样性在技术基金作为 写给捐款计划的一部分接受捐款。

介绍

许多开发人员花了至少一部分时间来创建服务器,在互联网上分发内容。 Hypertext Transfer Protocol(HTTP)服务了大部分此类内容,无论是请求猫图像还是请求加载您正在阅读的教程。

在本教程中,您将使用 Go 的标准库创建 HTTP 服务器,然后扩展您的服务器,从请求的查询字符串、体和表单数据中读取数据。

前提条件

要遵循本教程,您将需要:

创建项目

在 Go 中,大部分 HTTP 功能由标准库中的 net/http包提供,而其余的网络通信则由 net包提供。

在本节中,您将创建一个使用 http.ListenAndServe功能的程序来启动响应请求路径的 HTTP 服务器,然后将该程序扩展到在同一个程序中运行多个 HTTP 服务器。

但是,在你写任何代码之前,你需要创建你的程序目录. 许多开发人员将他们的项目保存在目录中,以保持它们的组织。

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

1mkdir projects
2cd projects

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

1mkdir httpserver
2cd httpserver

现在你已经创建了你的程序目录,你在httpserver目录中,你可以开始实施你的HTTP服务器。

聆听请求并提供响应

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

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

1nano main.go

main.go文件中,您将创建两个函数,即getRootgetHello,以作为您的处理函数。然后,您将创建一个main函数,并使用它来设置您的请求处理器使用http.HandleFunc函数,通过getRoot处理器函数的/路径和getHello处理器函数的/hello路径。一旦您设置了处理器,请拨打http.ListenAndServe函数启动服务器并听取请求。

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

 1[label main.go]
 2package main
 3
 4import (
 5    "errors"
 6    "fmt"
 7    "io"
 8    "net/http"
 9    "os"
10)
11
12func getRoot(w http.ResponseWriter, r *http.Request) {
13    fmt.Printf("got / request\n")
14    io.WriteString(w, "This is my website!\n")
15}
16func getHello(w http.ResponseWriter, r *http.Request) {
17    fmt.Printf("got /hello request\n")
18    io.WriteString(w, "Hello, HTTP!\n")
19}

在这个第一块代码中,你为你的Go程序设置了,为你的程序导入所需的包,并创建了两个函数:getRoot 函数和getHello 函数. 这些函数都有相同的函数签名,它们接受相同的参数:一个http.ResponseWriter值和一个*http.Request值。 这个函数签名用于 HTTP 处理函数,并被定义为 http.HandlerFunc

http.HandlerFunc中,使用http.ResponseWriter(LINK0)值(您的处理器中命名为w)来控制返回请求的客户端的响应信息,例如响应的体或状态代码。

目前,在您的 HTTP 处理器中,您使用「fmt.Printf」来打印处理器函数的请求时,然后使用「http.ResponseWriter」向响应器发送一些文本。

现在,继续创建你的程序,开始你的主要函数:

1[label main.go]
2...
3func main() {
4    http.HandleFunc("/", getRoot)
5    http.HandleFunc("/hello", getHello)
6
7    err := http.ListenAndServe(":3333", nil)
8...

函数中,你有两个调用到http.HandleFunc函数。每个调用到函数中设置了一个处理函数,用于默认服务器多路由器中的特定请求路径。服务器多路由器是一个 http.Handler 可以查看请求路径,并调用与该路径相关的特定处理函数。所以,在你的程序中,你正在告诉默认服务器多路由器在某人要求/路径时调用get Root函数,并在某人要求/hello路径时调用getHello函数。

一旦设置了处理器,你会拨打http.ListenAndServe函数,该函数告诉全球HTTP服务器在一个特定的端口上收听所收到的请求,使用可选的http.Handler。在你的程序中,你会告诉服务器在33333上收听。在不指定IP地址之前,服务器会收听与您的计算机相关的每个IP地址,并在3333端口上收听。 一个网络端口(如33333)是一个计算机可以同时与多个程序进行通信的方式。 每个程序都使用自己的端口,因此客户端会连接到特定端口,计算机知道该程序将其发送到哪个端口。 如果你只想允许连接到localhost,IP地址的主机名是`127.0

您的http.ListenAndServe函数也将为http.Handler参数传递一个值,这会告诉ListenAndServe函数您想要使用默认服务器 multiplexer,而不是您已设置的 multiplexer。

ListenAndServe是一个阻止呼叫,这意味着你的程序不会继续运行,直到ListenAndServe完成运行后。然而,ListenAndServe不会结束运行,直到你的程序完成运行或HTTP服务器被告知关闭。即使ListenAndServe正在阻止,你的程序没有包括关闭服务器的方法,但仍然重要的是包括错误处理,因为有几种方式呼叫ListenAndServe可能会失败。

 1[label main.go]
 2...
 3
 4func main() {
 5    ...
 6    err := http.ListenAndServe(":3333", nil)
 7  if errors.Is(err, http.ErrServerClosed) {
 8    	fmt.Printf("server closed\n")
 9    } else if err != nil {
10    	fmt.Printf("error starting server: %s\n", err)
11    	os.Exit(1)
12    }
13}

您正在检查的第一个错误, http.ErrServerClosed,当服务器被要求关闭或关闭时返回. 这是通常是一个预期的错误,因为您将关闭服务器自己,但也可以用它来显示服务器为什么停止输出。

您在运行程序时可能会看到的一个错误是已使用的地址错误。当ListenAndServe无法听取您提供的地址或端口时,可能会返回此错误,因为另一个程序已经使用它。

<$>[注] 注: 如果您看到已使用的地址错误,并且您没有运行程序的另一个副本,这可能意味着其他程序正在使用它。如果发生这种情况,无论您看到本教程中提到的3333,请将其更改为1024以上和65535以下的另一个数字,例如3334,然后再试一次。如果您仍然看到错误,您可能需要继续尝试寻找未使用的端口。

现在你的代码已经准备好了,保存你的 main.go 文件,并使用 go run 运行你的程序. 与你写过的其他 Go 程序不同,这个程序不会自行退出。

1[environment local]
2go run main.go

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

在这个第二个终端中,使用 curl程序向您的 HTTP 服务器发送 HTTP 请求。 curl 是一个通常在许多系统上默认安装的实用程序,可以向不同类型的服务器发送请求。

1[environment second]
2curl http://localhost:3333

结果将是这样的:

1[environment second]
2[secondary_label Output]
3This is my website!

在输出中,你会看到从getRoot函数的This is my website!响应,因为你访问了HTTP服务器上的/路径。

现在,在相同的终端中,向相同的主机和端口发出请求,但将‘hello’路径添加到你的‘curl’命令的末尾:

1[environment second]
2curl http://localhost:3333/hello

你的输出将看起来像这样:

1[environment second]
2[secondary_label Output]
3Hello, HTTP!

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

如果您重定向到您正在运行的 HTTP 服务器函数的终端,您现在有两个来自服务器的输出线,一个用于 / 请求,另一个用于 /hello 请求:

1[environment local]
2[secondary_label Output]
3got / request
4got /hello request

由于服务器将继续运行,直到程序完成运行,您需要自己停止它. 要做到这一点,请按CONTROL+C来发送您的程序中断信号来停止它。

在本节中,您创建了一个 HTTP 服务器程序,但它使用了默认服务器 multiplexer 和默认 HTTP 服务器。使用默认值或全球值可能会导致很难重复的错误,因为您的程序的多个部分可能会在不同的时间更新它们。

Multiplexing 请求处理器

当您在上一节开始 HTTP 服务器时,您通过了ListenAndServe函数为http.Handler参数的值,因为您正在使用默认服务器多元化,因为http.Handler是一个 接口,所以您可以创建自己的结构来实现接口,但有时您只需要一个基本的http.Handler来调用特定请求路径的单个函数,例如默认服务器多元化。

http.ServeMux结构可以与默认服务器多重组件相同配置,所以你不需要对你的程序进行许多更新,以便开始使用自己的程序而不是全球默认。

 1[label main.go]
 2...
 3
 4func main() {
 5    mux := http.NewServeMux()
 6    mux.HandleFunc("/", getRoot)
 7    mux.HandleFunc("/hello", getHello)
 8
 9    err := http.ListenAndServe(":3333", mux)
10    
11    ...
12}

在此更新中,您使用http.NewServeMux构建器创建了一种新的http.ServeMux,并将其分配给mux变量。之后,您只需要更新http.HandleFunc调用以使用mux变量,而不是调用http包。最后,您更新了调用到http.ListenAndServe以提供您创建的http.Handler(mux)而不是nil值。

现在你可以使用Go Run重新运行你的程序:

1[environment local]
2go run main.go

您的程序将继续像上次一样运行,因此您需要运行命令以与其他终端的服务器进行交互。

1[environment second]
2curl http://localhost:3333

结果将看起来如下:

1[environment second]
2[secondary_label Output]
3This is my website!

你会看到这个输出与以前一样。

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

1[environment second]
2curl http://localhost:3333/hello

结果将是这样的:

1[environment second]
2[secondary_label Output]
3Hello, HTTP!

这个路径的输出也和以前一样。

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

1[environment local]
2[secondary_label Output]
3got / request
4got /hello request

您对该程序的更新功能相同,但这次您使用自己的http.Handler而不是默认的更新。

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

同时运行多个服务器

除了使用自己的http.Handler,Gonet/http包还允许您使用默认服务器以外的HTTP服务器。有时您可能想要自定义服务器的运行方式,或者您可能希望在同一程序中同时运行多个HTTP服务器。例如,您可能有一个公共网站和一个私人管理网站,您希望从同一个程序中运行。由于您只能有一个默认的HTTP服务器,您将无法使用默认的服务器这样做。

在您的「main.go」文件中,您将使用「http.Server」设置多个HTTP服务器,您还将更新您的处理函数,以访问入口的「http.Request」的「context.Context」(https://pkg.go.dev/context#Context)。

再次打开你的 main.go 文件,并如下所示更新它:

 1[label main.go]
 2package main
 3
 4import (
 5    // Note: Also remove the 'os' import.
 6    "context"
 7    "errors"
 8    "fmt"
 9    "io"
10    "net"
11    "net/http"
12)
13
14const keyServerAddr = "serverAddr"
15
16func getRoot(w http.ResponseWriter, r *http.Request) {
17    ctx := r.Context()
18
19    fmt.Printf("%s: got / request\n", ctx.Value(keyServerAddr))
20    io.WriteString(w, "This is my website!\n")
21}
22func getHello(w http.ResponseWriter, r *http.Request) {
23    ctx := r.Context()
24
25    fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))
26    io.WriteString(w, "Hello, HTTP!\n")
27}

在上面的代码更新中,您更新了导入声明,以包括更新所需的包。然后,您创建了一个名为keyServerAddrconst字符串值,以在http.Request背景下作为 HTTP 服务器的地址值的密钥。最后,您更新了您的getRootgetHello函数,以访问http.Requestcontext.Context值。一旦您拥有该值,您将 HTTP 服务器的地址包含在fmt.Printf输出中,这样您就可以看到哪两个服务器处理了 HTTP 请求。

现在,开始更新你的主要函数,添加你的两个http.Server值中的第一个值:

 1[label main.go]
 2...
 3func main() {
 4    ...
 5    mux.HandleFunc("/hello", getHello)
 6
 7    ctx, cancelCtx := context.WithCancel(context.Background())
 8    serverOne := &http.Server{
 9    	Addr:    ":3333",
10    	Handler: mux,
11    	BaseContext: func(l net.Listener) context.Context {
12    		ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
13    		return ctx
14    	},
15    }

在更新的代码中,您首先创建一个新的context.Context值,使用可用的函数cancelCtx,以取消文本. 然后,您定义您的serverOne``http.Server值. 这个值与您已经使用的HTTP服务器非常相似,但而不是将地址和处理器传输到http.ListenAndServe函数,您将它们设置为AddrHandler值。

另一个变化是添加一个BaseContext函数。BaseContext是改变处理函数在调用http.RequestContext方法时接收的context.Context部分的一种方式。在您的程序中,您正在将服务器正在收听的地址(‘l.Addr().String()’)添加到框架中,并使用键serverAddr打印到处理函数的输出中。

接下来,定义您的第二个服务器,‘serverTwo’:

 1[label main.go]
 2...
 3
 4func main() {
 5    ...
 6    serverOne := &http.Server {
 7    	...
 8    }
 9
10    serverTwo := &http.Server{
11    	Addr:    ":4444",
12    	Handler: mux,
13    	BaseContext: func(l net.Listener) context.Context {
14    		ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
15    		return ctx
16    	},
17    }

此服务器的定义与第一个服务器相同,除非Addr字段的:3333而不是4444

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

 1[label main.go]
 2...
 3
 4func main() {
 5    ...
 6    serverTwo := &http.Server {
 7    	...
 8    }
 9
10    go func() {
11    	err := serverOne.ListenAndServe()
12    	if errors.Is(err, http.ErrServerClosed) {
13    		fmt.Printf("server one closed\n")
14    	} else if err != nil {
15    		fmt.Printf("error listening for server one: %s\n", err)
16    	}
17    	cancelCtx()
18    }()

在goroutine中,你用ListenAndServe启动服务器,就像你以前一样,但这次你不需要提供函数的参数,就像你用http.ListenAndServe一样,因为http.Server值已经配置了。然后,你会像以前一样处理错误。在函数的结束时,你会打电话给cancelCtx,以取消向HTTP处理器和服务器BaseContext的两个函数提供的语境。

最后,更新您的程序,以便在goroutine中启动第二个服务器:

 1[label main.go]
 2...
 3
 4func main() {
 5    ...
 6    go func() {
 7    	...
 8    }()
 9    go func() {
10    	err := serverTwo.ListenAndServe()
11    	if errors.Is(err, http.ErrServerClosed) {
12    		fmt.Printf("server two closed\n")
13    	} else if err != nil {
14    		fmt.Printf("error listening for server two: %s\n", err)
15    	}
16    	cancelCtx()
17    }()
18
19    <-ctx.Done()
20}

这个goroutine在功能上与第一个相同,它只会启动serverTwo而不是serverOne。这个更新还包括主要函数的结束,在您从ctx.Done频道中读取,然后从主要函数返回。

保存并关闭文件,当你完成。

使用go run命令运行您的服务器:

1[environment local]
2go run main.go

您的程序将继续运行,所以在您的第二个终端中运行弯曲命令,请求从听到3333的服务器请求/路径和/hello路径,与之前的请求相同:

1[environment second]
2curl http://localhost:3333
3curl http://localhost:3333/hello

结果将是这样的:

1[environment second]
2[secondary_label Output]
3This is my website!
4Hello, HTTP!

在输出中,你会看到你以前看到的相同的答案。

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

1[environment second]
2curl http://localhost:4444
3curl http://localhost:4444/hello

结果将看起来如下:

1[environment second]
2[secondary_label Output]
3This is my website!
4Hello, HTTP!

对于这些请求,您将看到相同的输出,就像您对serverOne服务的端口3333上的请求一样。

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

1[environment local]
2[secondary_label Output]
3[::]:3333: got / request
4[::]:3333: got /hello request
5[::]:4444: got / request
6[::]:4444: got /hello request

输出看起来与以前所看到的类似,但这次它显示了响应请求的服务器. 第一两个请求显示他们来自在端口3333(serverOne)上听服务器,第二两个请求来自在端口4444(serverTwo)上听服务器。

您的输出也可能与上面的输出略有不同,取决于您的计算机是否设置为使用 IPv6。如果是,则您将看到与上面的输出相同。

一旦完成,请再次使用CONTROL+C来停止服务器。

在本节中,您使用http.HandleFunchttp.ListenAndServe创建了一个新的 HTTP 服务器程序来运行和配置默认服务器,然后,您更新它以使用http.ServeMux用于http.Handler而不是默认服务器 multiplexer。最后,您更新了您的程序以使用http.Server在同一程序中运行多个 HTTP 服务器。

虽然你现在有一个运行 HTTP 服务器,但它并不非常互动,你可以添加新的路径,它响应,但实际上没有办法让用户在过去与它进行交互。

检查请求的查询字符串

一个用户能够影响他们从 HTTP 服务器获得的 HTTP 响应的方法之一是使用 查询字符串。 查询字符串是添加到 URL的末尾的一组值。 它从一个 ? 字符开始,使用 & 作为分界器添加额外的值。 查询字符串值通常被用来过滤或定制 HTTP 服务器作为响应发送的结果。

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

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

 1[label main.go]
 2...
 3
 4func getRoot(w http.ResponseWriter, r *http.Request) {
 5    ctx := r.Context()
 6
 7    hasFirst := r.URL.Query().Has("first")
 8    first := r.URL.Query().Get("first")
 9    hasSecond := r.URL.Query().Has("second")
10    second := r.URL.Query().Get("second")
11
12    fmt.Printf("%s: got / request. first(%t)=%s, second(%t)=%s\n",
13    	ctx.Value(keyServerAddr),
14    	hasFirst, first,
15    	hasSecond, second)
16    io.WriteString(w, "This is my website!\n")
17}
18...

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

在理论上,你总是可以使用Get方法来检索查询字符串值,因为它总是会返回给定的密钥的实际值或空字符串,如果密钥不存在,对于许多用途来说,这足够好,但在某些情况下,你可能想知道用户提供空值或不提供值之间的差异。

getRoot函数中,您还更新了输出,以显示HasGet值,用于第一第二查询字符串值。

现在,更新您的函数以再次使用一个服务器:

 1[label main.go]
 2...
 3
 4func main() {
 5    ...
 6    mux.HandleFunc("/hello", getHello)
 7    
 8    ctx := context.Background()
 9    server := &http.Server{
10    	Addr:    ":3333",
11    	Handler: mux,
12    	BaseContext: func(l net.Listener) context.Context {
13    		ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
14    		return ctx
15    	},
16    }
17
18    err := server.ListenAndServe()
19    if errors.Is(err, http.ErrServerClosed) {
20    	fmt.Printf("server closed\n")
21    } else if err != nil {
22    	fmt.Printf("error listening for server: %s\n", err)
23    }
24}

函数中,你删除了对serverTwo的引用,并将运行服务器(以前是serverOne)从一个goroutine移到函数中,类似于你以前运行http.ListenAndServe的方式。

现在,一旦您保存了更改,请使用运行来重新运行您的程序:

1[environment local]
2go run main.go

您的服务器将再次启动,所以您可以返回您的第二个终端,以运行一个弯曲命令和一个查询字符串. 在此命令中,您需要用单个引文(```)围绕您的 URL,否则您的终端的壳可能会将查询字符串中的&符号解释为在背景中运行这个命令功能,许多壳都包含。

1[environment second]
2curl 'http://localhost:3333?first=1&second='

结果将是这样的:

1[environment second]
2[secondary_label Output]
3This is my website!

您将看到从弯曲命令的输出没有从以前的请求中改变。

但是,如果您重新切换到服务器程序的输出,则会看到新输出包含查询字符串值:

1[environment local]
2[secondary_label Output]
3[::]:3333: got / request. first(true)=1, second(true)=

查询字符串值的输出显示Has方法返回true,因为first有值,而且Get还返回1值。Second的输出显示Has返回true,因为包含了Second,但Get方法没有返回除了空字符串之外的任何东西。

一旦完成,请按CONTROL+C来停止您的服务器。

在本节中,您更新了您的程序,再一次只使用一个http.Server,但您还增加了从getRoot处理函数的查询字符串中读取第一第二值的支持。

然而,使用查询字符串并非用户向 HTTP 服务器提供输入的唯一方法. 另一种常见的方式是将数据发送到服务器,就是将数据列入请求的体内。

阅读一个请求身体

创建基于 HTTP 的 API 时,例如 REST API,用户可能需要发送更多的数据,而不是 URL 长度限制,或者您的页面可能需要接收数据,而不是如何解释数据,例如过滤器或结果限制。

在 Go http.HandlerFunc 中,使用 *http.Request 值来访问有关接收请求的信息,它还包括使用 Body 字段访问请求的身体的方式。

要更新你的「getRoot」方法,打开你的「main.go」文件,并更新它以使用「ioutil.ReadAll」读取「r.Body」请求字段:

 1[label main.go]
 2package main
 3
 4import (
 5    ...
 6    "io/ioutil"
 7    ...
 8)
 9
10...
11
12func getRoot(w http.ResponseWriter, r *http.Request) {
13    ...
14    second := r.URL.Query().Get("second")
15
16    body, err := ioutil.ReadAll(r.Body)
17    if err != nil {
18    	fmt.Printf("could not read body: %s\n", err)
19    }
20
21    fmt.Printf("%s: got / request. first(%t)=%s, second(%t)=%s, body:\n%s\n",
22    	ctx.Value(keyServerAddr),
23    	hasFirst, first,
24    	hasSecond, second,
25    	body)
26    io.WriteString(w, "This is my website!\n")
27}
28
29...

在此更新中,您使用ioutil.ReadAll函数读取*http.Requestr.Body属性,以访问请求的体。ioutil.ReadAll函数是一个实用函数,将读取从 io.Reader的数据,直到它遇到错误或数据的结束。

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

1[environment local]
2go run main.go

由于服务器将继续运行,直到你停止它,转到你的另一个终端,使用-X POST选项和使用-d选项的POST请求,你也可以使用以前的第一第二查询字符串值:

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

你的输出将看起来如下:

1[environment second]
2[secondary_label Output]
3This is my website!

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

1[environment local]
2[secondary_label Output]
3[::]:3333: got / request. first(true)=1, second(true)=, body:
4This is the body

在服务器日志中,你会看到之前的查询字符串值,但现在你还会看到发送的弯曲命令的这是身体数据。

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

在本节中,您更新了您的程序,以便将请求的体读成您在输出中打印的变量. 通过将阅读体与其他功能相结合,例如 coding/json以将 JSON 体解析成 Go 数据,您将能够创建用户可以以其他 API 熟悉的方式进行交互的 API。

许多网站都有表单,他们要求用户填写,所以在下一节中,您将更新您的程序以读取表单数据,除了您已经拥有的请求体和查询字符串。

恢复形式数据

很长一段时间以来,使用表单发送数据是用户向 HTTP 服务器发送数据并与网站交互的标准方式。表单现在并不像过去那样受欢迎,但它们仍然有许多用途,以便用户向网站提交数据。在 http.HandlerFunc 中*http.Request值也提供了访问这些数据的方法,类似于它如何提供访问查询字符串和请求体。

打开main.go并更新getHello函数以使用PostFormValue方法的*http.Request:

 1[label main.go]
 2...
 3
 4func getHello(w http.ResponseWriter, r *http.Request) {
 5    ctx := r.Context()
 6
 7    fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))
 8
 9    myName := r.PostFormValue("myName")
10    if myName == "" {
11    	myName = "HTTP"
12    }
13    io.WriteString(w, fmt.Sprintf("Hello, %s!\n", myName))
14}
15
16...

现在在getHello函数中,您正在阅读向处理函数发布的表单值,并寻找名为myName的值。如果没有找到该值或发现的值是一个空串,则将myName变量设置为HTTP的默认值,以便页面不会显示空名。

若要使用這些更新執行您的伺服器,請儲存您的變更並使用「進行」執行:

1[environment local]
2go run main.go

现在,在您的第二个终端中,使用弯曲-X POST选项到/helloURL,但这一次不是使用-d提供数据体,而是使用-F``myName=Sammy选项提供表单数据与值SammymyName字段:

1[environment second]
2curl -X POST -F 'myName=Sammy' 'http://localhost:3333/hello'

结果将是这样的:

1[environment second]
2[secondary_label Output]
3Hello, Sammy!

在上面的输出中,你会看到预期的Hello, Sammy!问候,因为你用curl发送的表格说myNameSammy

您在getHello函数中使用的r.PostFormValue方法来检索myName表单值是一种特殊方法,它仅包含在请求体内发布的表单中的值。但是,还可使用r.FormValue方法,包括表单体和查询字符串中的任何值。因此,如果您使用r.FormValue(myName),您也可以删除-F选项,并将myName=Sammy列入查询字符串中,以便看到Sammy也返回。如果您没有更改r.FormValue,但您会看到该名称的默认HTTP`响应。要注意从哪个字符串中检索这些值,可以避免难以追踪的名称或错误的

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

1[environment local]
2[secondary_label Output]
3[::]:3333: got /hello request

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

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

在你的程序中,在处理请求时,有些事情可能会出错,而你的用户不会被通知,在下一部分,你会更新你的处理器功能以返回HTTP状态代码和标题。

用标题和状态代码响应

HTTP 协议使用一些用户通常看不到的功能来发送数据,以帮助浏览器或服务器进行通信,其中一个功能被称为 状态代码,并被服务器用来给 HTTP 客户端一个更好的想法,以确定服务器是否认为请求成功,或者是否在服务器侧面或客户端发送的请求上发生了错误。

HTTP 服务器和客户端的另一种沟通方式是使用 头字段。 头字段是一个键和值,一个客户端或服务器将发送给另一个客户端,让他们知道自己。 许多头字段是由 HTTP 协议预定义的,例如接受,客户端使用它来告诉服务器它可以接受和理解的数据类型。

在本节中,您将更新您的程序,以使myName表单字段成为getHello,这是一个必需的字段。如果没有发送myName字段的值,您的服务器会将Bad Request状态代码发送回客户端,并添加一个x-missing-field标题,让客户端知道哪个字段缺少。

要将此功能添加到您的程序中,请最后一次打开main.go文件,并将验证检查添加到getHello处理函数:

 1[label main.go]
 2...
 3
 4func getHello(w http.ResponseWriter, r *http.Request) {
 5    ctx := r.Context()
 6
 7    fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))
 8
 9    myName := r.PostFormValue("myName")
10    if myName == "" {
11    	w.Header().Set("x-missing-field", "myName")
12    	w.WriteHeader(http.StatusBadRequest)
13    	return
14    }
15    io.WriteString(w, fmt.Sprintf("Hello, %s!\n", myName))
16}
17
18...

在此更新中,当「myName」是一个空串时,而不是设置默认名称为「HTTP」时,您会向客户端发送错误消息。 首先,您使用「w.Header().Set」方法将对响应 HTTP 标题中的「x-missing-field」标题设置为「myName」值。 然后,您会使用「w.WriteHeader」方法将任何响应标题以及坏请求状态代码写给客户端。 最后,它将返回处理器功能。

在 HTTP 请求或响应中,所有标题必须在发送到客户端之前发送,所以在呼叫w.WriteHeader之前必须发送任何更新请求,一旦呼叫w.WriteHeader,页面状态就会发送与所有标题一起,然后只能写到后面的标题。

一旦您保存了更新,您可以使用go run命令重新运行您的程序:

1[environment local]
2go run main.go

现在,使用您的第二个终端将另一个curl -X POST请求发送到/hello URL,但不要包含-F来发送表单数据。

1[environment second]
2curl -v -X POST 'http://localhost:3333/hello'

这次在输出中,你会看到更多的信息,因为请求被处理,因为语音输出:

 1[environment second]
 2[secondary_label Output]
 3*   Trying ::1:3333...
 4* Connected to localhost (::1) port 3333 (#0)
 5> POST /hello HTTP/1.1
 6> Host: localhost:3333
 7> User-Agent: curl/7.77.0
 8> Accept: */*
 9> 
10* Mark bundle as not supporting multiuse
11< HTTP/1.1 400 Bad Request
12< X-Missing-Field: myName
13< Date: Wed, 02 Mar 2022 03:51:54 GMT
14< Content-Length: 0
15< 
16* Connection #0 to host localhost left intact

输出中的第一对行显示弯曲试图连接到本地最强端口3333

然后,从>开始的行表示curl正在向服务器发送的请求,它表示curl正在使用HTTP 1.1协议向/helloURL发送POST请求,以及其他几个标题。

一旦‘curl’发送请求,你可以看到它从服务器接收的响应,前缀为‘<’。 第一行说服务器响应了‘Bad Request’,这也被称为400状态代码。 然后,你可以看到你设置的‘X-Missing-Field’标题包含了‘myName’的值。 发送一些额外的标题后,请求结束了没有发送任何的身体,可以通过‘Content-Length’(或身体)的长度‘0’来看到。

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

1[environment local]
2[secondary_label Output]
3[::]:3333: got /hello request

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

在本节中,您更新了您的 HTTP 服务器以添加验证到/hello表单输入。如果一个名称没有作为表单的一部分发送,您使用了w.Header().Set来设置一个标题,以便将其发送回客户端。

结论

在本教程中,您使用 Go 的标准库中的 net/http 包创建了一个新的 Go HTTP 服务器,然后更新您的程序以使用特定的服务器倍配器和多个 http.Server 实例。您还更新了您的服务器以通过查询字符串值、请求体和表单数据读取用户输入。

关于Go HTTP生态系统的一件好事是,许多框架被设计成可以整合到Go的net/http包中,而不是重新发明很多已经存在的代码。 github.com/go-chi/chi]项目就是一个很好的例子。内置在Go的服务器多元化是一个很好的方式来开始使用HTTP服务器,但它缺乏很多更大的Web服务器可能需要的先进功能。 项目如chi能够在Go标准库中实现http.Handler界面,以便直接融入标准的http.Server,而无需重写代码的服务器部分。 这使他们能够专注于创建中间软件(https://en.wikipedia.org/wiki/Middleware)和其他工具来增强可用的功能,而不是在基本功能上工作。

除了像「chi」这样的项目外,Go net/http包还包含了本教程中未涵盖的许多功能。 要了解更多有关使用cookies或服务HTTPS流量的信息,Net/http包是开始的好地方。

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

Published At
Categories with 技术
Tagged with
comments powered by Disqus