作者选择了 多样性在技术基金作为 写给捐款计划的一部分接受捐款。
介绍
许多开发人员花了至少一部分时间来创建服务器,在互联网上分发内容。 Hypertext Transfer Protocol(HTTP)服务了大部分此类内容,无论是请求猫图像还是请求加载您正在阅读的教程。
在本教程中,您将使用 Go 的标准库创建 HTTP 服务器,然后扩展您的服务器,从请求的查询字符串、体和表单数据中读取数据。
前提条件
要遵循本教程,您将需要:
- Go 版本 1.16 或更高版本已安装. 要设置此设置,请遵循操作系统的 How To Install Go教程。
- 可用 How To Use JSON in Go教程中找到的 How To Use JSON in Go教程。
- Go 的
context
包体验,可以在教程中找到, How To Download Files with cURL。 使用 JSON 在 Go 中的熟悉性。 使用 goroutines 和阅读渠道的经验,可以从教程中获得, [How To Run Multiple Functions Concurrently in Go]教程中发送的
创建项目
在 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
文件中,您将创建两个函数,即getRoot
和getHello
,以作为您的处理函数。然后,您将创建一个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}
在上面的代码更新中,您更新了导入
声明,以包括更新所需的包。然后,您创建了一个名为keyServerAddr
的const
字符串值,以在http.Request
背景下作为 HTTP 服务器的地址值的密钥。最后,您更新了您的getRoot
和getHello
函数,以访问http.Request
的context.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
函数,您将它们设置为Addr
和Handler
值。
另一个变化是添加一个BaseContext
函数。BaseContext
是改变处理函数在调用http.Request
的Context
方法时接收的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.HandleFunc
和http.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.Request
的r.URL
字段访问有关所请求的 URL 的属性,然后您使用r.URL
字段的Query
方法访问请求的查询字符串值。一旦您访问了查询字符串值,您可以使用两种方法与数据进行交互。Has
方法返回一个bool
值,指定查询字符串是否具有与提供的密钥的值,例如first
。
在理论上,你总是可以使用Get
方法来检索查询字符串值,因为它总是会返回给定的密钥的实际值或空字符串,如果密钥不存在,对于许多用途来说,这足够好,但在某些情况下,你可能想知道用户提供空值或不提供值之间的差异。
在getRoot
函数中,您还更新了输出,以显示Has
和Get
值,用于第一
和第二
查询字符串值。
现在,更新您的主
函数以再次使用一个服务器:
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.Request
的r.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
选项到/hello
URL,但这一次不是使用-d
提供数据体,而是使用-F``myName=Sammy
选项提供表单数据与值Sammy
的myName
字段:
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
发送的表格说myName
是Sammy
。
您在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协议向/hello
URL发送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 到如何使用语言本身。