http.ResponseWriter接口
前一篇中的例子已经“庞大”,我们这篇将编写新的例子来学习服务器的响应和cookie。在看例子代码之前,我们先来看handler func 的函数签名
func process(w http.ResponseWriter, r *http.Request) {
...................
}
代表“请求”的 r 是一个 http.Request 类型的指针,而代表响应的 w 却是一个值类型 http.ResponseWriter ?在标准库http中,存在 Response 这一结构体,但创建响应时,并非直接生成一个 Response 对象,填充数据并返回。 理解这一点的要点在于,http/server.go文件中定义的 ResponseWriter 是一个 interface 而非具体化的结构体,该接口约定了3个方法:Write、WriteHeader、Header,而 Response 类型在实现接口方法时,接收者使用的是指针式接收者,从而在调用接口方法时,可以用“值”形式的语法,而作为其他函数的参数或者赋值时的右值时,必须使用地址。(参考 golang学习随便记8-接口_sjg20010414的博客-CSDN博客 中 方法接收者T和*T的差别)。不过,在 handler func 中,并未使用 *http.Response 作为类型,而是使用了“父类型”接口类型,是变相引用传递(即本质上方法接收者是*http.Response类型)。
因为w和r本质上都是引用,故一旦 handler func 中对请求或响应有所修改,那么main函数中的server是可以感知这种变化的。
接口方法Write(..)
http.ResponseWriter的Write接口方法接受一个字节数组作为入参,这个字节数组会写入HTTP响应的body主体中。如果调用Write写入body前,没有设定内容相应的类型,那么使用的默认头部将通过检测字节数组前512字节来决定内容类型。
编写下面的代码 server.go 并运行:
package main
import "net/http"
func writeTest1(w http.ResponseWriter, r *http.Request) {
str := `<html>
<head><title>Go Web 响应测试</title></head>
<body><h1>Hello, Go Response 1</h1></body>
</html>`
w.Write([]byte(str))
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8088",
}
http.HandleFunc("/response1", writeTest1)
server.ListenAndServe()
}
用 curl 测试如下(观察头部信息):(也可以用浏览器,但需要打开开发者工具,观察“网络” - response1 - “标头”部分)
sjg@sjg-PC:~$ curl -i 127.0.0.1:8088/response1
HTTP/1.1 200 OK
Date: Mon, 08 May 2023 02:42:37 GMT
Content-Length: 105
Content-Type: text/html; charset=utf-8
<html>
<head><title>Go Web 响应测试</title></head>
<body><h1>Hello, Go Response 1</h1></body>
</html>
我们路由中添加2条,handler func 相应添加2个:
// ...........................
func writeTest2(w http.ResponseWriter, r *http.Request) {
str := `<?xml version="1.0" encoding="UTF-8"?><html>
<head><title>Go Web 响应测试</title></head>
<body><h1>Hello, Go Response 2</h1></body>
</html>`
w.Write([]byte(str))
}
func writeTest3(w http.ResponseWriter, r *http.Request) {
str := `<root>
<head><title>Go Web 响应测试</title></head>
<body><h1>Hello, Go Response 3</h1></body>
</root>`
w.Write([]byte(str))
}
func writeTest4(w http.ResponseWriter, r *http.Request) {
str := `{
head: {title:"Go Web 响应测试"},
body: "Hello, Go Response 3"
}`
w.Write([]byte(str))
}
// .........................
http.HandleFunc("/response2", writeTest2)
http.HandleFunc("/response3", writeTest3)
http.HandleFunc("/response4", writeTest4)
// .........................
可以发现,访问 http://127.0.0.1:8088/response2 响应的内容类型为 text/xml,而访问 http://127.0.0.1:8088/response3 和 /response4响应的内容类型为 text/plain,也就是自动识别内容时,xml必须在头部有xml文档声明,而html必须符合html格式,否则会识别为普通文本(上述JSON字符串也被识别为text/plain)。
接口方法 Header()、WriteHeader(..)
接口函数Header()和WriteHeader()都和头部有关,正确的顺序是:Header()返回关于头部的map,如果需要修改默认头部,就是修改这个map有关键值对(对于需要取消的头部,应该将值设成nil)。WriteHeader(statusCode)接受一个状态码作为入参,调用该函数将发送指定的状态码和HTTP响应头部(也就是前述map中信息)。如果不显式调用WriteHeader()而直接调用Write()接口函数输出主体body,那么WriteHeader()将隐式调用,状态码默认为 200 OK —— 换句话说,要响应非 200 OK的状态码时,才必须调用 WriteHeader()
下面两个例子,分别表示服务尚未实现(状态501)和重定向(状态302):
// ........................................................
func headerTest1(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(501)
fmt.Fprintln(w, "本店无此服务,请出门右转")
}
func headerTest2(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "http://10.5.10.200")
w.WriteHeader(302)
}
// ...............................................
http.HandleFunc("/header1", headerTest1)
http.HandleFunc("/redirect", headerTest2)
// ...............................................
测试结果:
sjg@sjg-PC:~$ curl -i 127.0.0.1:8088/header1
HTTP/1.1 501 Not Implemented
Date: Mon, 08 May 2023 04:33:40 GMT
Content-Length: 37
Content-Type: text/plain; charset=utf-8
本店无此服务,请出门右转
sjg@sjg-PC:~$ curl -i 127.0.0.1:8088/redirect
HTTP/1.1 302 Found
Location: http://10.5.10.200
Date: Mon, 08 May 2023 04:33:44 GMT
Content-Length: 0
设置响应的Header头部
下面是在头部指定类型为JSON,然后响应json数据的例子:
// ..............................................................
type Post struct {
User string
Threads []string
}
func jsonTest(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
post := &Post{
User: "张三",
Threads: []string{"娱乐事件1", "八卦事件2", "国家大事1"},
}
json, _ := json.Marshal(post)
w.Write(json)
}
// .......................................
http.HandleFunc("/json", jsonTest)
// .......................................
上面的代码中,post是指针,去掉&改成值传递,结果不变。测试:
sjg@sjg-PC:~$ curl -i 127.0.0.1:8088/json
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 08 May 2023 04:46:51 GMT
Content-Length: 77
{"User":"张三","Threads":["娱乐事件1","八卦事件2","国家大事1"]}
使用Cookie
Cookie也是一种服务器响应的信息,只是它生成时更像是服务器给浏览器客户端下命令,而不是回答客户请求。之后,cookie会成为浏览器和服务器沟通的“桥梁”,它常见的用途包括作为会话跟踪的识别信息。
Cookie是 http/cookie.go 中定义的 struct,必需的字段是 Name、Value。一般,没有设置 Expires 字段信息的cookie是临时cookie(或叫会话cookie),浏览器关闭后它就消失了(被删除)。设置了 Expires 字段的称为持久 cookie,在指定的过期时间来临或者被手动删除前,这类cookie一直存在。另外,cookie是支持US ASCII(同时排除了一些特殊字符)的,对于中文之类的值只能编码或者转义,例如,用url.QueryEscape(..) 转义编码,url.QueryUnescape(..) 解码,或者base64.URLEncoding.EncodeToString(..)编码,base64.URLEncoding.DecodeString(..)解码。
发送cookie给客户
下面的代码,演示服务器将cookie发送给浏览器(可以理解为命令浏览器记录cookie),分别使用直接发送头部的方式和调用http.SetCookie函数的方式(后者更方便,不过前者控制自由度更高,且一些时候可以跳过不必要的判断)
// .............................................................
func cookieTest1(w http.ResponseWriter, r *http.Request) {
c1 := http.Cookie{
Name: "cookie_name1",
Value: "Go Web Programming",
HttpOnly: true,
}
c2 := http.Cookie{
Name: "cookie_name2",
Value: url.QueryEscape("Go Web 编程"), // 只能US-ASCII https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
}
w.Header().Set("Set-Cookie", c1.String())
w.Header().Add("Set-Cookie", c2.String())
}
func cookieTest2(w http.ResponseWriter, r *http.Request) {
c1 := http.Cookie{
Name: "cookie_name1",
Value: "Go Web Programming",
HttpOnly: true,
}
c2 := http.Cookie{
Name: "cookie_name2",
Value: url.QueryEscape("Go Web 编程"), // 只能US-ASCII https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
}
http.SetCookie(w, &c1)
http.SetCookie(w, &c2)
}
// ...........................................
http.HandleFunc("/cookie1", cookieTest1)
http.HandleFunc("/cookie2", cookieTest2)
// ...........................................
运行之后,浏览器访问 http://localhost:8088/cookie1和 http://localhost:8088/cookie2,在开发者工具:应用程序 - 存储 - Cookie 里面观察浏览器记录的cookie。
读取客户发送的cookie
接下来,我们来看服务器如何读取浏览器发送过来的cookie信息:浏览器向服务器发送请求时,会把它记录的还在生命周期内的对应domain的cookie都发送过去(作为请求头的一部分)
// ..........................................................
func cookieTest3(w http.ResponseWriter, r *http.Request) {
cs := r.Header["Cookie"]
fmt.Fprintln(w, cs)
c1, err := r.Cookie("cookie_name1")
if err != nil {
fmt.Fprintln(w, "未能获取 cookie_name1 的值")
} else {
fmt.Fprintln(w, c1)
}
c3, err := r.Cookie("cookie_name3")
if err != nil {
fmt.Fprintln(w, "未能获得 cookie_name3 的值")
} else {
fmt.Fprintln(w, c3)
}
}
// ...........................................
http.HandleFunc("/cookie3", cookieTest3)
// ...........................................
运行并浏览器访问结果如下:
r.Header这个map包含了请求包含的所有头部信息,键Cookie对应的值就是浏览器发送的cookie,这是一个字符串切片,所有cookie混在一起。我们当然可以自己解析这个切片,不过,使用r.Cookie(..) 函数更容易获取指定字段的cookie信息(r.Cookie(..)函数解析时可能因为对应cookie不存在而失败,所以,需要判断)。
下面我们看一下cookie在flash message闪现消息中的使用:向浏览器发送cookie,让浏览器记住消息;浏览器发送回来消息,取出该消息,向浏览器发送cookie命令该cookie失效,显示已经取出的消息
// .........................................................
func setMessage(w http.ResponseWriter, r *http.Request) {
msg := []byte("这是一条 Flash Message 消息")
c := http.Cookie{
Name: "flash-message",
Value: base64.URLEncoding.EncodeToString(msg),
}
http.SetCookie(w, &c)
}
func showMessage(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("flash-message")
if err != nil {
if err == http.ErrNoCookie {
fmt.Fprintln(w, "未找到消息")
}
} else {
rc := http.Cookie{
Name: "flash-message",
MaxAge: -1,
Expires: time.Unix(1, 0),
}
http.SetCookie(w, &rc) // 清除cookie就是让它失效
v, _ := base64.URLEncoding.DecodeString(c.Value)
fmt.Fprintln(w, string(v))
}
}
// .................................................
http.HandleFunc("/set-message", setMessage)
http.HandleFunc("/show-message", showMessage)
// .................................................
运行,测试时(可以同时打开开发者工具观察cookie的产生和消失),浏览器分别访问 http://localhost:8088/set-message和http://localhost:8088/show-message,应该能看到消息被记录到浏览器cookie和从cookie消失的过程,第二个网址第一次访问时能看到消息,当刷新后一个网址时,就不能看到消息了