请求的结构

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

scheme://[userinfo@]host/path[?query][#fragment]
type URL struct {
  Scheme   string
  Opaque   string
  User     *Userinfo
  Host     string
  Path     string
  RawQuery string
  Fragment string
}
&key1=value1&key2=value2
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

map[string][]string
    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"},
    }
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
PrintPrintlnPrintfPrintPrintlnPrintfSprintSprinlnSprintfFprintFprintfFprinlnio.Writerhttp.ReponseWriter

Body

Read(p []byte) (n int, err error)

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
 --------------------------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.Formr.PostFormParseFormr.FormValuer.PostFormValue("lang")ParseForm
POST
☁  ~  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的数据优先,两者的数据都有,并不会覆盖。

PostFormPostFormValue
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
Postr.MultipartForm.Value

文件上传

r.MultipartForm.Valuer.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)
}
r.FormFile
    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

ResponseWriter
type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(int)
}
HeaderWriteHeaderWrite

我们已经使用了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>%                                                                                  ☁  ~
text/htmlContent-Type
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
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

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的话题中讨论。