上一篇文章中,咱们介绍了请求的结构与处理。本文将详细介绍如何响应客户端的请求。其实在前面几篇文章中,咱们已经使用过响应的功能——经过http.ResponseWriter
发送字符串给客户端。 可是这种方式仅限于发送字符串。本文咱们将介绍如何定制响应的参数。html
ResponseWriter
接口若是你看了我前面几篇文章,应该对处理器和处理器函数都很是熟悉了。处理器函数即拥有如下签名的函数:git
func (w http.ResponseWriter, r *http.Request) 复制代码
这里的ResponseWriter
实际上是定义在net/http
包中的一个接口:程序员
// src/net/http/
type ReponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
复制代码
咱们响应客户端请求都是经过该接口的 3 个方法进行的。例如以前fmt.Fprintln(w, "Hello World")
其实底层调用了Write
方法。github
收到请求后,多路复用器会自动建立一个http.response
对象,它实现了http.ResponseWriter
接口,而后将该对象和请求对象做为参数传给处理器。那为何请求对象使用的时结构指针*http.Request
,而响应要使用接口呢?golang
实际上,请求对象使用指针是为了能在处理逻辑中方便地获取请求信息。而响应使用接口来操做,一方面底层也是对象指针,能够保存修改。另外一方面,我认为是为了扩展性。能够很方便地用新的实现替换而不用修改应用层代码,即处理器接口不用修改。例如,Go 标准库提供了一个测试 HTTP 请求的工具包net/http/httptest
。它定义了一个ResponseRecorder
结构,该结构实现了接口http.ResponseWriter
。这个结构不将写入的数据发送给客户端,而是将数据记录下来,方便测试断言。web
接口ResponseWriter
有 3 个方法,下面依次来介绍如何使用:chrome
Write
;WriteHeader
;Header
。Write
方法因为接口ResponseWriter
拥有方法Write([]byte) (int, error)
,因此实现了ResponseWriter
接口的结构也实现了io.Writer
接口:shell
// src/io/io.go
type Writer interface {
Write(p []byte) (n int, err error)
}
复制代码
这也是为何http.ResponseWriter
类型的变量w
能在下面代码中使用的缘由(fmt.Fprintln
的第一个参数接收一个io.Writer
接口):编程
fmt.Fprintln(w, "Hello World")
复制代码
咱们也能够直接调用Write
方法来向响应中写入数据:json
func writeHandler(w http.ResponseWriter, r *http.Request) {
str := `<html> <head><title>Go Web 编程之 响应</title></head> <body><h1>直接使用 Write 方法<h1></body> </html>`
w.Write([]byte(str))
}
mux.HandleFunc("/write", writeHandler)
复制代码
下面,咱们介绍一个工具curl
来测试咱们的 Web 应用。因为浏览器只会展现响应中主体的内容,其它元信息须要进行一些操做才能查看,不够直观。curl
是一个 Linux 命令行程序,可用来发起 HTTP 请求,功能很是强大,如设置首部/请求体,展现响应首部等。
一般 Linux 系统会自带curl
命令。简单介绍几种 Windows 上安装curl
的方式。
直接在curl官网下载可执行程序,下载完成后放在PATH
目录中便可在Cmd
或Powershell
界面中使用;
Windows 提供了一个软件包管理工具chocolatey,能够安装/更新/删除 Windows 软件。安装chocolatey
后,直接在Cmd
或Powershell
界面执行如下命令便可安装curl
,也比较方便:
choco install curl
复制代码
git
。安装git for windows后,就能够直接在Git Bash
中使用curl
命令。实际上,git for windows
使用了mingw
来在 Windows 上模拟 Linux 环境。它提供了不少 Linux 命令的 Windows 版本,很是推荐使用。启动服务器,使用下面命令测试Write
方法:
curl -i localhost:8080/write
复制代码
选项-i
的做用是显示响应首部。该命令返回:
HTTP/1.1 200 OK
Date: Thu, 19 Dec 2019 13:36:32 GMT
Content-Length: 113
Content-Type: text/html; charset=utf-8
<html>
<head><title>Go Web 编程之 响应</title></head>
<body><h1>直接使用 Write 方法<h1></body>
</html>
复制代码
能够看出很清晰地看出响应的各个部分。也能够继续使用浏览器来测试:
可是若是要查看首部,状态码等信息就必须使用浏览器的开发者工具了。Chrome 的开发者工具能够经过 F12 唤出,而后切换到Network
标签,点击刚刚发送的请求:
咱们看到上面红色的两个部分为响应的元信息,下面的绿色部分为请求的基本信息。
注意到,若是咱们没有设置响应码,则响应码默认为200。 并且咱们也没有设置内容类型,可是返回的首部中有Content-Type: text/html; charset=utf-8
,说明net/http
会自动推断。net/http
包是经过读取响应体中前面的若干个字节来推断的,并非百分百准确的。
如何设置状态码和响应内容的类型呢?这就是WriteHeader
和Header()
两个方法的做用。
WriteHeader
方法WriteHeader
方法的名字带有一点误导性,它并不能用于设置响应首部。WriteHeader
接收一个整数,并将这个整数做为 HTTP 响应的状态码返回。调用这个返回以后,能够继续对ResponseWriter
进行写入,可是不能对响应的首部进行任何修改操做。若是用户在调用Write
方法以前没有执行过WriteHeader
方法,那么程序默认会使用 200 做为响应的状态码。
若是,咱们定义了一个 API,还未定义其实现。那么请求这个 API 时,能够返回一个 501 Not Implemented 做为状态码。
func writeHeaderHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(501)
fmt.Fprintln(w, "This API not implemented!!!")
}
mux.HandleFunc("/writeheader", writeHeaderHandler)
复制代码
使用curl
来测试刚刚编写的处理器:
curl -i localhost:8080/writeheader
复制代码
返回:
HTTP/1.1 501 Not Implemented
Date: Thu, 19 Dec 2019 14:15:16 GMT
Content-Length: 28
Content-Type: text/plain; charset=utf-8
This API not implemented!!!
复制代码
Header
方法Header
方法其实返回的是一个http.Header
类型,该类型的底层类型为map[string][]string
:
// src/net/http/header.go
type Header map[string][]string
复制代码
类型Header
定义了 CRUD 方法,能够经过这些方法操做首部。
func headerHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "http://baidu.com")
w.WriteHeader(302)
}
复制代码
经过第一篇文章咱们知道 302 表示重定向,浏览器收到该状态码时会再发起一个请求到首部中Location
指向的地址。使用curl
测试:
curl -i localhost:8080/header
复制代码
返回:
HTTP/1.1 302 Found
Location: http://baidu.com
Date: Thu, 19 Dec 2019 14:17:49 GMT
Content-Length: 0
复制代码
如何在浏览器中打开localhost:8080/header
,网页会重定向到百度首页。
接下来,咱们看看如何设置自定义的内容类型。经过Header.Set
方法设置响应的首部Contet-Type
便可。咱们编写一个返回 JSON 数据的处理器:
type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Age int `json:"age"`
Hobbies []string `json:"hobbies"`
}
func jsonHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
u := &User {
FirstName: "lee",
LastName: "darjun",
Age: 18,
Hobbies: []string{"coding", "math"},
}
data, _ := json.Marshal(u)
w.Write(data)
}
mux.HandleFunc("/json", jsonHandler)
复制代码
经过curl
发送请求:
curl -i localhost:8080/json
复制代码
返回:
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 19 Dec 2019 14:31:03 GMT
Content-Length: 78
{"first_name":"lee","last_name":"darjun","age":18,"hobbies":["coding","math"]}
复制代码
能够看到响应首部中类型Content-Type
被设置成了application/json
。相似的格式还有 xml(application/xml
)/pdf(application/pdf
)/png(image/png
)等等。
什么是 cookie?
cookie 的出现是为了解决 HTTP 协议的无状态性的。客户端经过 HTTP 协议与服务器通讯,屡次请求之间没法记录状态。服务器能够在响应中设置 cookie,客户端保存这些 cookie。而后每次请求时都带上这些 cookie,服务器就能够经过这些 cookie 记录状态,辨别用户身份等。
整个计算机行业的收入都创建在 cookie 机制之上,广告领域更是如此。
上面的说法虽然有些夸张,可是可见 cookie 的重要性。
咱们知道广告是互联网最多见的盈利方式。其中有一个很厉害的广告模式,叫作联盟广告。你有没有这样一种经历,刚刚在百度上搜索了某个关键字,而后打开淘宝或京东后发现相关的商品已经被推荐到首页或边栏了。这是因为这些网站组成了广告联盟,只要加入它们,就能够共享用户浏览器的 cookie 数据。
Go 中 cookie 使用http.Cookie
结构表示,在net/http
包中定义:
// src/net/http/cookie.go
type Cookie struct {
Name string
Value string
Path string
Domain string
Expires time.Time
RawExpires string
MaxAge int
Secure bool
HttpOnly bool
SameSite SameSite
Raw string
Unparsed []string
}
复制代码
Name/Value
:cookie 的键值对,都是字符串类型;Expires
字段的 cookie 被称为会话 cookie 或临时 cookie,这种 cookie 在浏览器关闭时就会自动删除。设置了Expires
字段的 cookie 称为持久 cookie,这种 cookie 会一直存在,直到指定的时间来临或手动删除;HttpOnly
字段设置为true
时,该 cookie 只能经过 HTTP 访问,不能使用其它方式操做,如 JavaScript。提升安全性;注意:
Expires
和MaxAge
均可以用于设置 cookie 的过时时间。Expires
字段设置的是 cookie 在什么时间点过时,而MaxAge
字段表示 cookie 自建立以后可以存活多少秒。虽然 HTTP 1.1 中废弃了Expires
,推荐使用MaxAge
代替。可是几乎全部的浏览器都仍然支持Expires
;并且,微软的 IE6/IE7/IE8 都不支持 MaxAge
。因此为了更好的可移植性,能够只使用Expires
或同时使用这两个字段。
cookie 须要经过响应的首部发送给客户端。浏览器收到Set-Cookie
首部时,会将其中的值解析成 cookie 格式保存在浏览器中。下面咱们来具体看看如何设置 cookie:
func setCookie(w http.ResponseWriter, r *http.Request) {
c1 := &http.Cookie {
Name: "name",
Value: "darjun",
HttpOnly: true,
}
c2 := &http.Cookie {
Name: "age",
Value: 18,
HttpOnly: true,
}
w.Header().Set("Set-Cookie", c1.String())
w.Header().Add("Set-Cookie", c2.String())
}
mux.HandleFunc("/set_cookie", setCookie)
复制代码
运行程序,打开浏览器输入localhost:8080/set_cookie
,浏览器中什么都没有显示,咱们须要经过开发者工具查看 cookie。在 chrome 浏览器(其它浏览器相似)按下 F12,切换到 Application(应用)标签,在左侧 Cookies 下点击测试的 URL,右侧便可显示咱们刚刚设置的 cookie:
固然,咱们也可使用curl
测试。可是curl
返回的结果就只是响应中的Set-Cookie
首部:
curl -i localhost:8080/set_cookie
复制代码
HTTP/1.1 200 OK
Set-Cookie: name=darjun; HttpOnly
Set-Cookie: age=18; HttpOnly
Date: Fri, 20 Dec 2019 14:08:01 GMT
Content-Length: 0
复制代码
上面构造 cookie 的代码中,有几点须要注意:
Set-Cookie
;Cookie
类型的String
方法将其转为字符串再设置;Header
类型的Set
方法,添加第二个 cookie 时调用Add
方法。Set
会将同名的键覆盖掉。若是第二个也调用Set
方法,那么第一个 cookie 将会被覆盖。为了使用的便捷,net/http
包还提供了SetCookie
方法。用法以下:
func setCookie2(w http.ResponseWriter, r *http.Request) {
c1 := &http.Cookie {
Name: "name",
Value: "darjun",
HttpOnly: true,
}
c2 := &http.Cookie {
Name: "age",
Value: "18",
HttpOnly: true,
}
http.SetCookie(w, c1)
http.SetCookie(w, c2)
}
mux.HandleFunc("/set_cookie2", setCookie2)
复制代码
若是收到的响应中有 cookie 信息,浏览器会将这些 cookie 保存下来。只有没有过时,在向同一个主机发送请求时都会带上这些 cookie。在服务端,咱们能够从请求的Header
字段读取Cookie
属性来得到 cookie:
func getCookie(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Host:", r.Host)
fmt.Fprintln(w, "Cookies:", r.Header["Cookie"])
}
mux.HandleFunc("/get_cookie", getCookie)
复制代码
第一次启动服务器,请求localhost:8080/get_cookie
时,结果以下,没有 cookie 信息:
先请求一次localhost:8080/set_cookie
,而后再次请求localhost:8080/get_cookie
,结果以下,浏览器将 cookie 传过来了:
r.Header["Cookie"]
返回一个切片,这个切片又包含了一个字符串,而这个字符串又包含了客户端发送的任意多个 cookie。若是想要取得单个键值对格式的 cookie,就须要解析这个字符串。 为此,net/http
包在http.Request
上提供了一些方法使咱们更容易地获取 cookie:
func getCookie2(w http.ResponseWriter, r *http.Request) {
name, err := r.Cookie("name")
if err != nil {
fmt.Fprintln(w, "cannot get cookie of name")
}
cookies := r.Cookies()
fmt.Fprintln(w, c1)
fmt.Fprintln(w, cookies)
}
mux.HandleFunc("/get_cookies", getCookies2)
复制代码
Cookie
方法返回以传入参数为键的 cookie,若是该 cookie 不存在,则返回一个错误;Cookies
方法返回客户端传过来的全部 cookie。测试新的 URL get_cookie2
:
有一点须要注意,cookie 是与主机名绑定的,不考虑端口。咱们上面查看 cookie 的图中有一列Domain
表示的就是主机名。能够这样来验证一下,建立两个服务器,一个绑定在 8080 端口,一个绑定在 8081 端口,先请求localhost:8080/set_cookie
设置 cookie,而后请求localhost:8081/get_cookie
:
func main() {
mux1 := http.NewServeMux()
mux1.HandleFunc("/set_cookie", setCookie)
mux1.HandleFunc("/get_cookie", getCookie)
server1 := &http.Server{
Addr: ":8080",
Handler: mux1,
}
mux2 := http.NewServeMux()
mux2.HandleFunc("/get_cookie", getCookie)
server2 := &http.Server {
Addr: ":8081",
Handler: mux2,
}
wg := sync.WaitGroup{}
wg.Add(2)
go func () {
defer wg.Done()
if err := server1.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
go func() {
defer wg.Done()
if err := server2.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
wg.Wait()
}
复制代码
发送给端口 8081 的请求一样能够获取 cookie:

上面代码中,不能直接在主 goroutine 中依次ListenAndServe
两个服务器。由于ListenAndServe
只有在出错或关闭时才会返回。在此以前,第二个服务器永远得不到机会运行。因此,我建立两个 goroutine 各自运行一个服务器,而且使用sync.WaitGroup
来同步。不然,主 goroutine 运行结束以后,整个程序就退出了。
本文介绍了如何响应客户端的请求和 cookie 的相关知识。相关代码在Github上,很是建议你们本身编写运行一遍以便加深印象。