Go语言学习笔记(十八)
一、创建HTTP服务器
Web服务器可提供网页、Web服务和文件,而Go语言为创建Web服务器提供了强大的支持。本篇将介绍如何创建Web服务器,使其能够响应不同的路由、不同类型的请求和不同类型的内容。使用Go语言编写Web服务器时,可使用Go语言提供的并发的方法。
1 通过Hello World Web服务器宣告您的存在
标准库中的net/http包提供了多种创建HTTP服务器的方法,他还提供了一个基本路由器。传统上通过创建一个Hello World程序来宣告您的存在。如下面所示程序是编写的最基本的HTTP服务器。
//example01.go
package main
import (
"net/http"
)
func helloWorld(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World\n"))
}
func main() {
http.HandleFunc("/", helloWorld)
http.ListenAndServe(":8000", nil)
}
go run以上程序后,在浏览器中访问http://localhost:8000
,我们将在服务器中看到Hello World
- 导入net/http包
- 在main函数中,使用方法
HandleFunc
创建了路由/。这个方法接受一个模式和一个函数,其中前者描述了路径,后者指定如何对发送到该路径的请求做出响应。- 函数helloWorld接受一个
http.ResponseWriter
和一个指向请求的指针。这意味着在这个函数中,可查看或操作请求,再将响应返回给客户端。在这里使用了方法Write来生成响应。这个方法生成的HTTP响应包含状态、报头和响应体。[]byte
声明了一个字节切片并将字符串值转换为字节。这意味着方法Write可以使用[]byte,因为这个方法将一个字节切片作为参数。- 为相应客户端,使用了方法ListenAndServe来启动一个服务器,这个服务器监听localhost和端口8000。
2 查看请求和响应
为了查看发送请求和收到的响应,我们将使用工具curl
curl是一款用于发起HTTP请求的命令行工具,他几乎可在任何平台上使用。macOS系统预先安装了curl;linux系统通常也安装了curl。Windows系统没有安装curl;我们可以通过下载GIT for Windows来安装curl(GIT 自带curl)
核实工具安装
1a. 在macOS或Linux系统中,打开终端
1b. 在Windows系统中,通过"开始"菜单打开Git Bash,或者直接打开cmd。
2.输入curl并按回车
3.如果成功显示如下:
2.1 使用curl发出请求
安装curl后,我们就可以在开发和调试Web服务器时使用它。我们可不需要使用Web浏览器,而使用curl来向服务器发送各种请求以及查看响应,为前面的Hello World Web服务器发出请求,过程如下:
go run example01.go
- 打开
git bash
,执行如下命令,-is
指定打印报头,并忽略一些不感兴趣的内容。
curl -is http://localhost:8000
效果如下:
a.这个响应使用的协议为HTTP 1.1,状态码为200
b.报头Date详细地描述了响应的发送时间
c.报头Content-Length详细地指出了响应的长度,这里是12字节
d.报头Content-Type指出了内容的类型以及使用的编码。在这里,响应的内容类型为text/plain,是使用utf-8进行编码的
e.最后输出的是响应体,这里是Hello World
2.2 详谈路由
HandleFunc用于注册对URL地址映射进行相应的而函数。简单地说,HandleFunc创建一个路由表,让HTTP服务器能够正确地做出响应。
http.HandleFunc("/",helloWorld)
http.HandleFunc("/users/",usersHandler)
http.HandLeFunc("/projects/",projectsHandler)
在这个实例中,每当用户向/发出请求时,都将调用函数helloWorld,每当用户向/users/发出请求时,都将调用函数usersHandler,以此类推。
有关路由器的行为,有以下几点需要注意。
a.路由器默认将没有指定处理程序的请求定向到
/
。
b.路由必须完全匹配。例如,对于向/users
发出的请求,将重定向到/
,因为这里末尾少了斜杠。
c. 路由器不关心请求的类型,而直管将与路由匹配的请求传给相应的处理程序。
3 使用处理程序函数
在Go语言中,路由器负责将路由映射到函数,但如何处理请求以及如何向客户端返回响应,是处理程序函数定义的。很多编程语言和Web框架都采用这样的模式,即先由函数来处理请求和响应,再返回响应。在这方面,Go语言也如此。处理程序函数负责完成如下常见任务。
1.读写报头。
2.查看请求的类型。
3.从数据库中取回数据。
4.分析请求数据。
5.验证身份
处理程序函数能够访问请求和响应,因此一种常见的模式是,先完成对请求的所有处理,在将响应返回给客户端。
响应生成后,就不能在对其做进一步的处理了。在下面的实例中,使用了方法Write来发送请求,接下来的一行设置了响应的一个报头。鉴于响应已经发送,这行代码不会有任何作用,但能够通过编译。这里要说的是发送响应是最后一步。
func helloWorld(w http.ResponseWriter,r *http.Request){
w.Write([]byte("Hello World\n"))
// This has no effect, as the response is already written
w.Header().Set("X-My-Header","I am setting a header!")
}
4 处理404错误
默认路由器的行为是将所有没有指定处理程序的请求都定向到/
。回到第一个示例,如果用户请求的页面不存在,将调用给/
指定的处理程序,从而返回响应Hello World
和状态码200
。
然而,鉴于请求的路由不存在,原本应返回404错误(页面未找到)。为此,可在处理默认路由的函数中检查路径,如果路径不为/
,就返回404错误,如下所示:
//example02.go
package main
import (
"net/http"
)
func helloWorld(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Write([]byte("Hello World\n"))
}
func main() {
http.HandleFunc("/", helloWorld)
http.ListenAndServe(":8000", nil)
}
运行结果如下:
相比于原来的Hello World Web 服务器,这里所做的修改如下:
- 在处理程序函数hello World中,检查路径是否是
/
。- 如果不是,就调用http包中的方法NotFound,并将响应和请求传递给它。这将向客户端返回一个404响应
- 如果路径与/匹配,则if语句将被忽略,进而发送响应Hello World
5 设置报头
创建HTTP服务器时,经常需要设置响应的报头。在创建、读取、更新和删除报头方面,Go语言提供了强大的支持。在下面的示例中,假设服务器将发送一些JSON数据。通过设置Content-Type报头,服务器可告诉客户端,发送的是JSON数据。处理程序函数可使用ResponseWriter来添加报头,如下:
w.Header().Set("Content-Type","application/json;charaset=utf-8")
只要这行代码是在响应被发送给客户端之前执行的,这个报头就会被添加到响应中。如下例(注意此时的数据虽然是一个字符串但json数据往往是从别的地方读取的,并编码为json格式):
//example03.go
package main
import (
"net/http"
)
func helloWorld(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write([]byte(`{"hello": "world"}`))
}
func main() {
http.HandleFunc("/", helloWorld)
http.ListenAndServe(":8000", nil)
}
效果如下:
6 响应以不同类型的内容
响应客户端时,HTTP服务器通常提供多种类型的内容。一些常用的内容类型包括text/plain、text/html、application/json和application/xml
。如果服务器支持多种类型的内容,客户端可使用Accept报头请求指定类型的内容。这意味着同一个URL可能向浏览器提供HTML,而向API提供JSON。只需对本章的示例稍作修改,就可以让它查看客户端发送的Accept报头,如下:
//example04.go
package main
import (
"net/http"
)
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; charset=utf-8")
w.Write([]byte(`{"message": "Hello World"}`))
case "application/xml":
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.Write([]byte(`<?xml version="1.0" encoding="utf-8"?><Message>Hello World</Message>`))
default:
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte("Hello World\n"))
}
}
func main() {
http.HandleFunc("/", helloWorld)
http.ListenAndServe(":8000", nil)
}
执行效果如下:
在这里使用
-H
参数,设置请求的报头
7 响应不同类型的请求
除响应以不同类型的内容外,HTTP服务器通常也需要能够响应不同类型的请求。客户端可发送的请求类型是HTTP规范中定义的,包括GET、POST、PUT和DELETE。要使用Go语言创建出响应不同请求类型的HTTP服务器,可采用类似于提供多种类型内容的方法,如下:在这里我们使用switch来检查请求类型:
//example05.go
package main
import (
"net/http"
)
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("Received a GET request\n"))
case "POST":
w.Write([]byte("Received a POST request\n"))
default:
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte(http.StatusText(http.StatusNotImplemented) + "\n"))
}
}
func main() {
http.HandleFunc("/", helloWorld)
http.ListenAndServe(":8000", nil)
}
这里对Web服务器所做的拓展如下。
1.服务器鄙视根据内容类型来生成响应,而是根据请求方法来生成响应。
2.switch语句根据请求类型发送不同的响应
3.在这个示例中,发送一个plain/text响应来指出请求的类型
4.如果请求方法不是GET或者POST,就执行default子句,发送501(Not Implemented HTTP)响应。代码501意味着服务器不明白或不支持客户机使用的HTTP请求方法
运行效果如下(要修改请求类型可使用
-X
参数):
8 获取GET和POST请求中的数据
HTTP客户端可在HTTP请求中向HTTP服务器发送数据,这样的典型示例包括以下几点。
- 提交表单
- 设置有关要返回的数据的选项
- 通过API管理数据
在Go语言中,获取客户端请求中的数据很简单,但获取方式遂请求类型而异。对于GET请求,其中的数通常是通过查询字符串设置的。一个通过GET请求发送数据的例子是,在Google网站上搜索。在这个示例中,URL包含以查询字符串的方式指定的搜索关键字。
https://www.google.com/?q=golang
Web服务器可能读取查询字符串中的数据,并使用它来做些事情,如从数据库获取相应的数据,再将其返回给客户端。在Go语言中,以字符串映射的方式提供了请求中的查询字符串参数,我们可使用range子句来遍历它们
func queryParams(w http.ResponseWriter, r *http.Request){
for k,v := range r.URL.Query(){
fmt.Printf("%s: %s\n",k,v)
}
}
在POST请求中,数据通常是在请求体重发送的。要读取并使用这些数据,可像下面这样做。
func queryParams(w http.ResponseWriter,r *http.Request){
reqBody,err:=ioutil.ReadAll(r.Body)
if err!=nil{
log.Fatal(err)
}
fmt.Printf("%s",reqBody)
}
下面是一个完整的代码示例,演示如何处理不同请求的数据:
//example06.go
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
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)
}
w.Write([]byte("Received a GET request\n"))
case "POST":
reqBody, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", reqBody)
w.Write([]byte("Received a POST request\n"))
default:
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte(http.StatusText(http.StatusNotImplemented)))
}
}
func main() {
http.HandleFunc("/", helloWorld)
http.ListenAndServe(":8000", nil)
}
运行结果如下:
示例一
示例二
使用-X
和-d
修改请求的类型和对应的内容
9 相关问题:
9.1 在路由模式中,可使用变量么?如/proucts/:id这样的模式,其中:id是一个变量。
http
包默认使用ServeMux
来处理路由,因此不支持变量也不支持正则表达式。社区创建的一些流行的路由器支持变量以及请求和内容类型等特性。一班而言,他们能够与http
包集成起来
9.2 Go语言有创建服务器的框架库么?
Go语言同样存在Go语言框架库。在大多数情况下,http包提供了我们需要的所有功能
9.3 如何创建HTTPS服务器?
http包提供了方法
ListenAndServeTLS
,可用来创建HTTPS(TLS)
服务器。这个方法的工作原理和ListenAndServe
相同,但必须向它传递证书和密钥文件。
9.4 HTTP GET请求和POST请求有何不同?
GET请求向指定资源请求数据,而POST请求向指定资源提交数据。GET请求可能通过查询字符串参数发送数据,而POST请求通过消息题向服务器发送数据。
9.5 在处理程序函数中,w.Write在处理程序响应方面做了什么(其中一个w是ResponseWriter)?
对ResponseWriter调用Write导致响应被发送给客户端,这包括报头和响应体的内容。响应一旦发送,就无法对其进行修改。
9.6 该信任用户提交给HTTP服务器的数据么?
不应该,绝对不要信任客户端提交给服务器的数据,应确定没问题后在使用。
参考书籍
[1]: 【Go语言入门经典】[英] 乔治·奥尔波 著 张海燕 译