[Go 入门] 第十七章 创建 HTTP 服务器

Go 入门系列参考于互联网资料与 人民邮电出版社 《Go 语言入门经典》 与 《Effective Go》,编写目的在于学习交流,如有侵权,请联系删除

Web 服务器可提供网页、Web 服务和文件, Go 语言为创建 Web 服务器提供了强大支持, 本文内容:

创建一个 Web 服务器

标准库中的 net/http 包提供了多种创建 HTTP 服务器的方法,它还提供了一个基本路由器。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "net/http"

func helloWorld(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello world!"))
}

func main() {
http.HandleFunc("/", helloWorld)
http.ListenAndServe(":8000", nil)
}

解读如下:

  • 导入 net/http 包
  • 在 main 函数中,使用方法 HandleFunc 创建了路由 /。这个方法接收一个模式和一个函数,其中前者描述了路径,后者指定了如何对发送到该路径的请求做出响应
  • 函数 helloWorld 接收一个 http.ResponseWriter 和一个指向请求的指针。可以使用 Write 方法来生成响应。这个方法生成的 HTTP 响应包含状态、报头和响应体。[]byte 声明一个字节切片并将字符串值转换为字节
  • 使用方法 ListenAndServe 来启动一个服务器,这个服务器监听 localhost 和端口 8000

查看请求和响应

查看发送请求和相应,我们将使用工具 curl

使用 curl 发送请求

先将之前编写的 Web 服务运行,然后使用终端执行,Windows 上可以先安装 git ,然后使用 git bash 来执行

script
1
curl -is http://localhost:8000

如果这个命令成功了,将会看到:

1
2
3
4
5
6
HTTP/1.1 200 OK
Date: Sun, 02 Feb 2020 16:30:28 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8

Hello world!

解读如下:

  • 这个响应使用的协议为 HTTP1.1 状态码为 200
  • 报头 Date 详细地描述了响应的发送时间
  • 报头 Content-Length 详细地指出了响应的长度,这里是12字节
  • 报头 Content-Type 指出了内容的类型以及使用的编码,在这里,响应的内容类型为 text/plain,是使用 utf-8 进行编码的
  • 最后输出的是响应体,这里是 Hello world!

详谈路由

HandleFunc 用于注册对 URL 地址映射进行响应的函数。简单地说,HandleFunc 创建了一个路由表,让 HTTP 服务器能够正确的做出响应

例如 http.HandleFunc("/", helloWorld)

则当访问 / 时,都将调用函数 helloWorld

有关路由器的行为,有以下几点需要注意:

  • 路由器默认将没有指定处理程序的请求定向到 /
  • 路由必须完全匹配
  • 路由不关心请求的类型,而只管将与路由匹配的请求传递给相应的处理程序

使用处理函数

Go 语言中,路由器负责将路由映射到函数,但如何处理请求以及如何向客户端返回响应,是由处理程序函数定义的。处理程序函数负责完成如下常见任务:

  • 读写报头
  • 查看请求的类型
  • 从数据库中取回数据
  • 分析请求数据
  • 验证身份

处理程序函数能够访问请求和响应,因此一种常见的模式是,先完成对请求的所有处理,再将响应返回客户端。响应生成后,就不能再对其做进一步的处理了。

1
2
3
4
func helloWorld(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-My-Header", "test header")
w.Write([]byte("Hello world!"))
}

注意, Write 是发送响应,因此需要放置到设置表头等后面,否则将不会有报头

处理 404 错误

默认路由的行为是将所有没有指定处理程序的请求都定向到 /。回到第一个示例,如果用户请求的页面不存在,将调用 / 指定的处理程序,从而返回相应 Hello world! 和状态码 200

script
1
2
3
4
5
6
7
$ curl -is http://localhost:8000/test
HTTP/1.1 200 OK
Date: Sun, 02 Feb 2020 16:44:21 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8

Hello world!

然而,鉴于请求的路由不存在,原本应该返回 404 错误。为此可在处理默认路由的函数中检查路径,如果路径不为 / ,就返回 404 错误

1
2
3
4
5
6
7
8
func helloWorld(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("X-My-Header", "test header")
w.Write([]byte("Hello world!"))
}

这样再次执行

script
1
2
3
4
5
6
7
8
9
$ curl -is http://localhost:8000/test
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Sun, 02 Feb 2020 16:48:15 GMT
Content-Length: 19

404 page not found

则会返回 404 错误

设置报头

前面有过演示设置报头的方法,使用 ResponseWriter 来添加报头,如

w.Header().Set("X-My-Header", "test header")

需要注意的是,这需要在发送响应(w.Write)前完成

响应以不同类型的内容

响应客户端时,HTTP 服务器通常提供多种类型的内容。一些常用的内容类型包括 text/plaintext/htmlapplication/json。如果服务器支持多种类型的内容,客户端可使用 Accept 报头请求特定的类型内容。例如向浏览器提供 html,向API提供json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func helloWorld(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
switch r.Header.Get("Accept") {
case "application/json":
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"msg": "ok"}`))
default:
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte("Hello world!"))
}
}

当客户端报头 Accept 为 application/json:

script
1
2
3
4
5
6
7
$ curl -si -H 'Accept: application/json' http://localhost:8000
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sun, 02 Feb 2020 16:56:56 GMT
Content-Length: 13

{"msg": "ok"}

当为其他时,则执行 default 分支:

script
1
2
3
4
5
6
7
$ curl -si -H 'Accept: application/xml' http://localhost:8000
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Sun, 02 Feb 2020 16:58:07 GMT
Content-Length: 12

Hello world!

响应不同类型的请求

HTTP 服务器通常也需要响应不同类型的请求,如 GET、POST、PUT、DELETE 等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func helloWorld(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
switch r.Method {
case "GET":
w.Write([]byte("GET Request"))
case "POST":
w.Write([]byte("POST Request"))
default:
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte(http.StatusText(http.StatusNotImplemented)))
}
}

这里 defalut 分支,将会返回 501 (Not Implemented)响应,意味着服务器不明白或不支持客户端使用的HTTP请求方法

后面将会有 HTTP Code 的介绍

要使用curl来修改请求类型,请使用选项 -X

GET:

script
1
2
3
4
5
6
7
8
$ curl -si -X GET http://localhost:8000
HTTP/1.1 200 OK
Date: Sun, 02 Feb 2020 17:04:28 GMT
Content-Length: 11
Content-Type: text/plain; charset=utf-8

GET Request

POST:

script
1
2
3
4
5
6
7
8
$ curl -si -X POST http://localhost:8000
HTTP/1.1 200 OK
Date: Sun, 02 Feb 2020 17:04:49 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8

POST Request

PUT:

script
1
2
3
4
5
6
7
$ curl -si -X PUT http://localhost:8000
HTTP/1.1 501 Not Implemented
Date: Sun, 02 Feb 2020 17:05:14 GMT
Content-Length: 15
Content-Type: text/plain; charset=utf-8

Not Implemented

获取 GET 和 POST 请求中的数据

HTTP 客户端可在 HTTP 请求中向 HTTP 服务器发送数据,这样的典型示例包括以下几点:

  • 提交表单
  • 设置有关要返回的数据的选项
  • 通过 API 管理数据

在 Go 语言中,获取客户端请求中的数据很简单,但获取方式随请求类型而异,对于 GET 请求,其中的数据通常是通过查询字符串设置的,而 POST 请求中,数据通常是在请求体中发送的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func helloWorld(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
switch r.Method {
case "GET":
for k, v := range r.URL.Query() {
fmt.Printf("%s: %s\n", k, v)
}
case "POST":
reqBody, err := ioutil.ReadAll(r.Body)
if err != nil{
log.Fatal(err)
}
fmt.Printf("%s\n", reqBody)
default:
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte(http.StatusText(http.StatusNotImplemented)))
}
}

使用 curl 进行测试:

GET:

script
1
curl -si "http://localhost:8000/?type=test&name=dirname"

运行服务器的终端将会输出:

1
2
type: [test]
name: [dirname]

POST:

script
1
curl -si -X POST -d "some data" http://localhost:8000

运行服务器的终端将会输出:

1
some data

HTTP Code

1xx - 信息提示
这些状态代码表示临时的响应。客户端在收到常规响应之前,应准备接收一个或多个 1xx 响应。

状态码 状态 说明
100 Continue 初始的请求已经接受,客户应当继续发送请求的其余部分(HTTP 1.1新)
101 Switching Protocols 服务器将遵从客户的请求转换到另外一种协议(HTTP 1.1新)

2xx开头 (请求成功)表示成功处理了请求的状态代码

状态码 状态 说明
200 (成功) 服务器已成功处理了请求 通常,这表示服务器提供了请求的网页
201 (已创建) 请求成功并且服务器创建了新的资源
202 (已接受) 服务器已接受请求,但尚未处理
203 (非授权信息) 服务器已成功处理了请求,但返回的信息可能来自另一来源
204 (无内容) 服务器成功处理了请求,但没有返回任何内容
205 (重置内容) 服务器成功处理了请求,但没有返回任何内容
206 (部分内容) 服务器成功处理了部分 GET 请求

3xx 开头 (请求被重定向)表示要完成请求,需要进一步操作。通常,这些状态代码用来重定向

状态码 状态 说明
300 (多种选择) 针对请求,服务器可执行多种操作 服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择
301 (永久移动) 请求的网页已永久移动到新位置 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置
302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
303 (查看其他位置) 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码
304 (未修改) 自从上次请求后,请求的网页未修改过 服务器返回此响应时,不会返回网页内容
305 (使用代理) 请求者只能使用代理访问请求的网页 如果服务器返回此响应,还表示请求者应使用代理
307 (临时重定向) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求

4xx开头 (请求错误)这些状态代码表示请求可能出错,妨碍了服务器的处理

状态码 状态 说明
400 (错误请求) 服务器不理解请求的语法
401 (未授权) 请求要求身份验证 对于需要登录的网页,服务器可能返回此响应
403 (禁止) 服务器拒绝请求
404 (未找到) 服务器找不到请求的网页
405 (方法禁用) 禁用请求中指定的方法
406 (不接受) 无法使用请求的内容特性响应请求的网页
407 (需要代理授权) 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理
408 (请求超时) 服务器等候请求时发生超时
409 (冲突) 服务器在完成请求时发生冲突 服务器必须在响应中包含有关冲突的信息
410 (已删除) 如果请求的资源已永久删除,服务器就会返回此响应
411 (需要有效长度) 服务器不接受不含有效内容长度标头字段的请求
412 (未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件
413 (请求实体过大) 服务器无法处理请求,因为请求实体过大,超出服务器的处理能力
414 (请求的 URI 过长) 请求的 URI(通常为网址)过长,服务器无法处理
415 (不支持的媒体类型) 请求的格式不受请求页面的支持
416 (请求范围不符合要求) 如果页面无法提供请求的范围,则服务器会返回此状态代码
417 (未满足期望值) 服务器未满足”期望”请求标头字段的要求

5xx开头(服务器错误)这些状态代码表示服务器在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求出错:

状态码 状态 说明
500 (服务器内部错误) 服务器遇到错误,无法完成请求
501 (尚未实施) 服务器不具备完成请求的功能 例如,服务器无法识别请求方法时可能会返回此代码
502 (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应
503 (服务不可用) 服务器目前无法使用(由于超载或停机维护) 通常,这只是暂时状态
504 (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求
505 (HTTP 版本不受支持) 服务器不支持请求中所用的 HTTP 协议版本

问题列表

  • 在路由模式中,可以使用变量吗?如类型于 /products/:id 这样的模式,其中 :id 是一个变量

    http 包默认使用 ServerMux 来处理路由,因此是不支持变量也不支持正则表达式的。但可以使用一些其他的 Web 框架或社区流行的路由器来支持

  • 如何创建 HTTPS 服务器

    使用方法 ListenAndServeTLS,这个方法工作原理与 ListenAndServe 相同,但必须向其传递证书和密匙文件