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消失的过程,第二个网址第一次访问时能看到消息,当刷新后一个网址时,就不能看到消息了