標籤:plain urlencode bytes 常用 sprintf length pen 操作 資訊
請求的結構
HTTP 的互動以請求和響應的接聽模式。Go 的請求我們早就見過了,handler 函數的第二個參數 http.Requests。其結構為:
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 這些結構。
URL
HTTP 的 url 請求格式為 scheme://[[email protected]]host/path[?query][#fragment], Go 的提供了一個 URL 結構,用來映射 HTTP 的請求 URL。
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 讀取 query
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
header 也是 HTTP 中重要的組成部分。Request 結構中就有 Header 結構,Header 本質上是一個 map(map[string][]string)。將 http 協議的 header的 key-value 進行映射成一個圖:
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。
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 數。
Body
http 中資料通訊,主要通過 body 傳輸。Go 把 body 封裝成 Request 的 Body,它是一個 ReadCloser 介面。介面方法 Reader 也是一個介面,後者有一個Read(p []byte) (n int, err error)方法,因此 body 可以通過讀取 byte 數組擷取請求的資料。
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 如下:
# 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 --------------------------d07972c7800e4c23Content-Disposition: form-data; name="name"vanyar--------------------------d07972c7800e4c23Content-Disposition: form-data; name="age"27--------------------------d07972c7800e4c23--
表單
解析 body 可以讀取用戶端請求的資料。而這個資料是無論是索引值對還是 form-data 資料,都比較原始。直接讀取解析還是挺麻煩的。這些 body 資料通常也是表單提供。因此 Go 提供處理這些表單資料的方法。
Form
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 中的內容。
MutilpartFrom
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 操作了。
JSON
現在流行前後端分離,用戶端興起了一些架構,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.Personmain.Person{Name:"vanyar", Age:27}
更多關於 json 的細節,以後再做討論。訪問官網文檔擷取更多的資訊。
Response
請求和響應是 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 OKDate: Wed, 07 Dec 2016 09:13:04 GMTContent-Length: 95Content-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 ImplementedDate: Wed, 07 Dec 2016 09:14:58 GMTContent-Length: 31Content-Type: text/plain; charset=utf-8No 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 FoundLocation: https://google.comDate: Wed, 07 Dec 2016 09:20:19 GMTContent-Length: 31Content-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,同樣響應的資料也可以是 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 OKContent-Type: application/jsonDate: Thu, 08 Dec 2016 06:45:17 GMTContent-Length: 54{"User":"vanyar","Threads":["first","second","third"]}
當然,更多的 json 處理細節稍後再做介紹。
總結
對於 web 應用程式,處理請求,返迴響應是基本的內容。Golang 很好的封裝了 Request 和 ReponseWriter 給開發人員。無論是請求還是響應,都是針對 url,header 和 body 相關資料的處理。也是 http 協議的基本內容。
除了 body 的資料處理,有時候也需要處理 header 中的資料,一個常見的例子就是處理 cookie。這將會在 cookie 的話題中討論。
Go Http包 使用簡介