[Go] Http包 使用簡介

請求的結構

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

URL

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

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 數。後端

Body

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 提供處理這些表單數據的方法。

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.Person
main.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 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,一樣響應的數據也能夠是 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 的話題中討論。

相關文章
相關標籤/搜索