上一篇文章中,咱們介紹了請求的結構與處理。本文將詳細介紹如何響應客戶端的請求。其實在前面幾篇文章中,咱們已經使用過響應的功能——經過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
的方式。
PATH
目錄中便可在Cmd
或Powershell
界面中使用;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上,很是建議你們本身編寫運行一遍以便加深印象。