HTTP 的交互以請求和響應的應答模式。Go 的請求咱們早就見過了,handler 函數的第二個參數 http.Requests。其結構爲:html
type Request struct { Method string URL *url.URL Proto string // "HTTP/1.0" ProtoMajor int // 1 ProtoMinor int // 0 Header Header Body io.ReadCloser ContentLength int64 TransferEncoding []string Close bool Host string Form url.Values PostForm url.Values MultipartForm *multipart.Form .... ctx context.Context }
從 request 結構能夠看到,http 請求的基本信息都囊括了。對於請求而言,主要關注一下請求的 URL,Method,Header,Body 這些結構。vue
HTTP 的 url 請求格式爲 scheme://[userinfo@]host/path[?query][#fragment], Go 的提供了一個 URL 結構,用來映射 HTTP 的請求 URL。react
type URL struct { Scheme string Opaque string User *Userinfo Host string Path string RawQuery string Fragment string }
URL 的格式比較明確,其實更好的名詞應該是 URI,統一資源定位。url 中比較重要的是查詢字符串 query。一般做爲 get 請求的參數。query 是一些使用 & 符號分割的 key1=value1&key2=value2 鍵值對,因爲 url 編碼是 ASSIC 碼,所以 query 須要進行 urlencode。Go 能夠經過 request.URI.RawQuery 讀取 querygolang
func indexHandler(w http.ResponseWriter, r *http.Request) { info := fmt.Sprintln("URL", r.URL, "HOST", r.Host, "Method", r.Method, "RequestURL", r.RequestURI, "RawQuery", r.URL.RawQuery) fmt.Fprintln(w, info) }
$ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27' "http://127.0.0.1:8000?lang=zh&version=1.1.0" URL /?lang=zh&version=1.1.0 HOST 127.0.0.1:8000 Method POST RequestURL /?lang=zh&version=1.1.0 RawQuery lang=zh&version=1.1.0
header 也是 HTTP 中重要的組成部分。Request 結構中就有 Header 結構,Header 本質上是一個 map(map[string][]string)。將 http 協議的 header的 key-value 進行映射成一個圖:web
Host: example.com accept-encoding: gzip, deflate Accept-Language: en-us fOO: Bar foo: two Header = map[string][]string{ "Accept-Encoding": {"gzip, deflate"}, "Accept-Language": {"en-us"}, "Foo": {"Bar", "two"}, }
header 中的字段包含了不少通訊的設置,不少時候請求都須要指定 Content-Type。json
func indexHandler(w http.ResponseWriter, r *http.Request) { info := fmt.Sprintln(r.Header.Get("Content-Type")) fmt.Fprintln(w, info) }
$ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27' "http://127.0.0.1:8000?lang=zh&version=1.1.0" application/x-www-form-urlencoded
Golng 提供了很多打印函數,基本上分爲三類三種。即 Print Println 和 Printf。
Print 比較簡單,打印輸出到標準輸出流,Println 則也同樣不一樣在於多打印一個換行符。至於 Printf 則是打印格式化字符串,三個方法都返回打印的 bytes 數。Sprint,Sprinln 和 Sprintf 則返回打印的字符串,不會輸出到標準流中。Fprint,Fprintf 和 Fprinln 則把輸出的結果打印輸出到 io.Writer 接口中,http 中則是 http.ReponseWriter 這個對象中,返回打印的 bytes 數。後端
http 中數據通訊,主要經過 body 傳輸。Go 把 body 封裝成 Request 的 Body,它是一個 ReadCloser 接口。接口方法 Reader 也是一個接口,後者有一個Read(p []byte) (n int, err error)方法,所以 body 能夠經過讀取 byte 數組獲取請求的數據。api
func indexHandler(w http.ResponseWriter, r *http.Request) { info := fmt.Sprintln(r.Header.Get("Content-Type")) len := r.ContentLength body := make([]byte, len) r.Body.Read(body) fmt.Fprintln(w, info, string(body)) }
$ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27' "http://127.0.0.1:8000?lang=zh&version=1.1.0" application/x-www-form-urlencoded name=vanyar&age=27
可見,當請求的 content-type 爲 application/x-www-form-urlencoded, body 也是和 query 同樣的格式,key-value 的鍵值對。換成 json 的請求方式則以下:數組
$ curl -X POST -H "Content-Type: application/json" -d '{name: "vanyar", age: 27}' "http://127.0.0.1:8000?lang=zh&version=1.1.0" application/json {name: "vanyar", age: 27}
multipart/form-data 的格式用來上傳圖片,請求的 body 以下:bash
# curl -X POST -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" -F "name=vanyar" -F "age=27" "http://127.0.0.1:8000?lang=zh&version=1.1.0" multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW; boundary=------------------------d07972c7800e4c23 --------------------------d07972c7800e4c23 Content-Disposition: form-data; name="name" vanyar --------------------------d07972c7800e4c23 Content-Disposition: form-data; name="age" 27 --------------------------d07972c7800e4c23--
解析 body 能夠讀取客戶端請求的數據。而這個數據是不管是鍵值對仍是 form-data 數據,都比較原始。直接讀取解析仍是挺麻煩的。這些 body 數據一般也是表單提供。所以 Go 提供處理這些表單數據的方法。
Go 提供了 ParseForm 方法用來解析表單提供的數據,即 content-type 爲 x-www-form-urlencode 的數據。
func indexHandler(w http.ResponseWriter, r *http.Request) { contentType := fmt.Sprintln(r.Header.Get("Content-Type")) r.ParseForm() fromData := fmt.Sprintf("%#v", r.Form) fmt.Fprintf(w, contentType, fromData) }
$ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27' "http://127.0.0.1:8000?lang=zh&version=1.1.0" application/x-www-form-urlencoded %!(EXTRA string=url.Values{"name":[]string{"vanyar"}, "age":[]string{"27"}, "lang":[]string{"zh"}, "version":[]string{"1.1.0"}})%
用來讀取數據的結構和方法大體有下面幾個:
fmt.Println(r.Form["lang"]) fmt.Println(r.PostForm["lang"]) fmt.Println(r.FormValue("lang")) fmt.Println(r.PostFormValue("lang"))
其中 r.Form 和 r.PostForm 必須在調用 ParseForm 以後,纔會有數據,不然則是空數組。而 r.FormValue 和 r.PostFormValue("lang") 無需 ParseForm 的調用就能讀取數據。
此外 r.Form 和 r.PostForm 都是數組結構,對於 body 和 url 都存在的同名參數,r.Form 會有兩個值,即 ["en", "zh"],而帶 POST 前綴的數組和方法,都只能讀取 body 的數據。
$ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27&lang=en' "http://127.0.0.1:8000?lang=zh&version=1.1.0" application/x-www-form-urlencoded %!(EXTRA string=url.Values{"version":[]string{"1.1.0"}, "name":[]string{"vanyar"}, "age":[]string{"27"}, "lang":[]string{"en", "zh"}})%
此時能夠看到,lang 參數不只 url 的 query 提供了,post 的 body 也提供了,Go 默認以 body 的數據優先,二者的數據都有,並不會覆蓋。
若是不想讀取 url 的參數,調用 PostForm 或 PostFormValue 讀取字段的值便可。
r.PostForm["lang"][0] r.PostFormValue["lang"]
對於 form-data 的格式的數據,ParseForm 的方法只會解析 url 中的參數,並不會解析 body 中的參數。
$ curl -X POST -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" -F "name=vanyar" -F "age=27" -F "lang=en" "http://127.0.0.1:8000?lang=zh&version=1.1.0" multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW; boundary=------------------------5f87d5bfa764488d %!(EXTRA string=url.Values{"lang":[]string{"zh"}, "version":[]string{"1.1.0"}} )%
所以當請求的 content-type 爲 form-data 的時候,ParseFrom 則須要改爲 MutilpartFrom,不然 r.From 是讀取不到 body 的內容,只能讀取到 query string 中的內容。
ParseMutilpartFrom 方法須要提供一個讀取數據長度的參數,而後使用一樣的方法讀取表單數據,MutilpartFrom 只會讀取 body 的數據,不會讀取 url 的 query 數據。
func indexHandler(w http.ResponseWriter, r *http.Request) { r.ParseMultipartForm(1024) fmt.Println(r.Form["lang"]) fmt.Println(r.PostForm["lang"]) fmt.Println(r.FormValue("lang")) fmt.Println(r.PostFormValue("lang")) fmt.Println(r.MultipartForm.Value["lang"]) fmt.Fprintln(w, r.MultipartForm.Value) }
能夠看到請求以後返回 map[name:[vanyar] age:[27] lang:[en]]。即 r.MultipartForm.Value 並無 url 中的參數。
總結一下,讀取 urlencode 的編碼方式,只須要 ParseForm 便可,讀取 form-data 編碼須要使用 ParseMultipartForm 方法。若是參數中既有 url,又有 body,From 和 FromValue 方法都能讀取。而帶Post 前綴的方法,只能讀取 body 的數據內容。其中 MultipartForm 的數據經過 r.MultipartForm.Value 訪問獲得。
form-data 格式用得最多方式就是在圖片上傳的時候。r.MultipartForm.Value 是 post 的 body 字段數據,r.MultipartForm.File 則包含了圖片數據:
func indexHandler(w http.ResponseWriter, r *http.Request) { r.ParseMultipartForm(1024) fileHeader := r.MultipartForm.File["file"][0] fmt.Println(fileHeader) file, err := fileHeader.Open() if err == nil{ data, err := ioutil.ReadAll(file) if err == nil{ fmt.Println(len(data)) fmt.Fprintln(w, string(data)) } } fmt.Println(err) }
發出請求以後,能夠看見返回了圖片。固然,Go 提供了更好的工具函數 r.FormFile,直接讀取上傳文件數據。而不須要再使用 ParseMultipartForm 方法。
file, _, err := r.FormFile("file") if err == nil{ data, err := ioutil.ReadAll(file) if err == nil{ fmt.Println(len(data)) fmt.Fprintln(w, string(data)) } } fmt.Println(err)
這種狀況只適用於出了文件字段沒有其餘字段的時候,若是仍然須要讀取 lang 參數,仍是須要加上 ParseMultipartForm 調用的。讀取到了上傳文件,接下來就是很普通的寫文件的 io 操做了。
如今流行先後端分離,客戶端興起了一些框架,angular,vue,react 等提交的數據,一般習慣爲 json 的格式。對於 json 格式,body 就是原生的 json 字串。也就是 Go 解密 json 爲 Go 的數據結構。
type Person struct { Name string Age int } func indexHandler(w http.ResponseWriter, r *http.Request) { decode := json.NewDecoder(r.Body) var p Person err := decode.Decode(&p) if err != nil{ log.Fatalln(err) } info := fmt.Sprintf("%T\n%#v\n", p, p) fmt.Fprintln(w, info) }
$ curl -X POST -H "Content-Type: application/json" -d '{"name": "vanyar", "age": 27 }' "http://127.0.0.1:8000?lang=zh&version=1.1.0" main.Person main.Person{Name:"vanyar", Age:27}
更多關於 json 的細節,之後再作討論。訪問官網文檔獲取更多的信息。
請求和響應是 http 的孿生兄弟,不只它們的報文格式相似,相關的處理和構造也相似。Go 構造響應的結構是 ResponseWriter 接口。
type ResponseWriter interface { Header() Header Write([]byte) (int, error) WriteHeader(int) }
裏面的方法也很簡單,Header 方法返回一個 header 的 map 結構。WriteHeader 則會返回響應的狀態碼。Write 返回給客戶端的數據。
咱們已經使用了 fmt.Fprintln 方法,直接向 w 寫入響應的數據。也能夠調用 Write 方法返回的字符。
func indexHandler(w http.ResponseWriter, r *http.Request) { str := `<html> <head><title>Go Web Programming</title></head> <body><h1>Hello World</h1></body> </html>` w.Write([]byte(str)) }
$ curl -i http://127.0.0.1:8000/ HTTP/1.1 200 OK Date: Wed, 07 Dec 2016 09:13:04 GMT Content-Length: 95 Content-Type: text/html; charset=utf-8 <html> <head><title>Go Web Programming</title></head> <body><h1>Hello World</h1></body> </html>%
Go 根據返回的字符,自動修改爲了 text/html 的 Content-Type 格式。返回數據自定義一般須要修改 header 相關信息。
func indexHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(501) fmt.Fprintln(w, "No such service, try next door") }
$ curl -i http://127.0.0.1:8000/ HTTP/1.1 501 Not Implemented Date: Wed, 07 Dec 2016 09:14:58 GMT Content-Length: 31 Content-Type: text/plain; charset=utf-8 No such service, try next door
重定向的功能能夠更加設置 header 的 location 和 http 狀態碼實現。
func indexHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", "https://google.com") w.WriteHeader(302) }
$ curl -i http://127.0.0.1:8000/ HTTP/1.1 302 Found Location: https://google.com Date: Wed, 07 Dec 2016 09:20:19 GMT Content-Length: 31 Content-Type: text/plain; charset=utf-8
重定向是經常使用的功能,所以 Go 也提供了工具方法,http.Redirect(w, r, "https://google.com", http.StatusFound)。
與請求的 Header 結構同樣,w.Header 也有幾個方法用來設置 headers
func (h Header) Add(key, value string) { textproto.MIMEHeader(h).Add(key, value) } func (h Header) Set(key, value string) { textproto.MIMEHeader(h).Set(key, value) } func (h MIMEHeader) Add(key, value string) { key = CanonicalMIMEHeaderKey(key) h[key] = append(h[key], value) } func (h MIMEHeader) Set(key, value string) { h[CanonicalMIMEHeaderKey(key)] = []string{value} }
Set和Add方法均可以設置 headers,對於已經存在的 key,Add 會追加一個值 value 的數組中,,set 則是直接替換 value 的值。即 append 和賦值的差異。
請求發送的數據能夠是 JSON,一樣響應的數據也能夠是 json。restful 風格的 api 也是返回 json 格式的數據。對於請求是解碼 json 字串,響應則是編碼 json 字串,Go 提供了標準庫 encoding/json
type Post struct { User string Threads []string } func indexHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") post := &Post{ User: "vanyar", Threads: []string{"first", "second", "third"}, } json, _ := json.Marshal(post) w.Write(json) }
$ curl -i http://127.0.0.1:8000/ HTTP/1.1 200 OK Content-Type: application/json Date: Thu, 08 Dec 2016 06:45:17 GMT Content-Length: 54 {"User":"vanyar","Threads":["first","second","third"]}
固然,更多的 json 處理細節稍後再作介紹。
對於 web 應用程式,處理請求,返回響應是基本的內容。Golang 很好的封裝了 Request 和 ReponseWriter 給開發者。不管是請求仍是響應,都是針對 url,header 和 body 相關數據的處理。也是 http 協議的基本內容。
除了 body 的數據處理,有時候也須要處理 header 中的數據,一個常見的例子就是處理 cookie。這將會在 cookie 的話題中討論。