Go Web (Part 2): Request

在了解了使用net/http构建go web服务之后,这篇文章深入了解一下在Go中如何处理一个请求。

1. 请求包含什么

HTTP Message有Request和Response两种。这里详细看看Request Message,一个请求消息的格式如下:

method request-URL version
headers

entity-body

Go中的net/http包提供了一个用于表示HTTP请求消息的结构ReqeustRequest将上面请求消息的内容经过分析后存储在不同的字段中,还包括一系列有用的方法。主要的字段有:

  • URL
  • Header
  • Body
  • Form, PostForm, MultipartForm

除了这些字段,还可以使用Request的一些方法处理请求中的cookie等。

2. URL

Request中的URL字段可以存储请求中URL的信息,在Reqeuest中的URL字段是一个指向net/url包中URL结构的指针,URL结构主要字段如下:

type URL struct {
    Scheme     string
    User       *Userinfo // username and password information
    Host       string    // host or host:port
    Path       string    // path (relative paths may omit leading slash)
    RawPath    string    // encoded path hint (see EscapedPath method)
    RawQuery   string    // encoded query values, without '?'
    Fragment   string    // fragment for references, without '#'
}

URL的一般格式是这样的:

scheme://[userinfo@]host/path[?query][#fragment]

其中[]中的是可选的信息。

net/url包中有解析URL的函数Parse()ParseRequestURI(),两者都是将传进来的URL解析成一个URL结构并返回这个URL的指针。

比如这个URL:https://book.douban.com/people/valineliu/collect?start=30&sort=time&rating=all&filter=all&mode=list#here

解析之后的结果如下:

Field Value
Scheme https
Host book.douban.com
Path /people/valineliu/collect
RawQuery start=0&sort=time&rating=all&filter=all&mode=list
Fragment here

Parse()ParseRequestURI()不解析RawPath字段,需要调用URL.EscapedPath()函数来获得这个字段值。

RawQuery字段包含了没有解析的查询参数,可以通过对这个参数的解析获得URL中的查询参数,不过可以直接使用Request结构中的Form字段来获取,后面再详细介绍。

还有一个需要注意的问题就是,如果请求是通过浏览器发送的话,程序就得不到Fragment字段,因为浏览器会截断Fragment,与net/http库无关。

3. Header

HTTP请求和响应消息都有一个Header结构,在go中,net/http包中有一个Header结构来存储Header结构。

Header本质上就是一个map,定义如下:

type Header map[string][]string

不过值是一个[]string,也就是说,对一个key可以有多个值。

Header有四个基本方法,可以方便地对内容进行操作,分别是Add, Del, Set, Get。其中Set方法会对已有的值进行覆盖。

除了Get方法外,还可以直接访问Header中某个key的值:

fmt.Println(r.Header["Accept-Encoding"])

就像访问一个map一样。这种方式和Get的区别在于,直接访问得到的是一个字符串切片,而使用Get方法得到的是所有的值用逗号分割的字符串:

fmt.Println(r.Header["Accept-Encoding"])
fmt.Println(r.Header.Get("Accept-Encoding"))

结果:

[gzip, deflate, br]
gzip, deflate, br

4. Body

Request中的Body不仅是请求的Body,也是响应的Body,这里不展开描述。

Body是一个io.ReaderCloser接口,意味着可以调用ReadClose两个方法。下面的例子描述了如何读取请求中的Body:

length := r.ContentLength
body := make([]byte, length)
r.Body.Read(body)
fmt.Println(string(body))

Request中有一个ContentLength字段,这个字段记录了Body的长度,可以通过这个字段指定长度来读取Body中的内容。

GET请求没有Body,所以需要一个POST请求,可以使用curl:

curl -id "start=30&sort=time&rating=all&filter=all&mode=list" localhost:8080/world

就可以得到Body了:

start=30&sort=time&rating=all&filter=all&mode=list

5. Form, PostForm & MultipartForm

前面几个字段在请求达到服务器的时候就通过一定的流程解析出来了,而Request中的这几个关于Form的字段不是自动解析的,需要手动调用各自对应的函数来解析。

5.1 概述

这几个字段的定义如下:

Form url.Values
PostForm url.Values
MultipartForm *multipart.Form

其中FormPostForm类型一样:

type Values map[string][]string

也就是一个map,key是字符串,而值是一个字符串切片。即,对于一个key来说,可以有多个值。

MultipartForm的类型不一样:

type Form struct {
    Value map[string][]string
    File  map[string][]*FileHeader
}

多了一个File字段,这是因为MultipartForm还需要存储文件。

请求中的参数可以在URL中,也就是这种格式:/people/valineliu/collect?start=30&sort=time,也可以在Body中,比如:curl -id "start=40" localhost:8080/world。同时请求消息中Body中的数据可以通过不同的格式编码发送给服务器。其中常用的有application/x-www-form-urlencodedmultipart/form-data。前一种格式可以将Body中的参数按照URL中参数的格式编码,而后者会将数据编码成一条MIME报文。

这就导致,三种Form字段在不同的编码方式下能够获得的数据也是不一样的。

5.2 Form & PostForm

为了获得FormPostForm字段,需要调用ParseForm方法:

func (r *Request) ParseForm() error {
    var err error
    if r.PostForm == nil {
        if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
            r.PostForm, err = parsePostForm(r)
        }
        if r.PostForm == nil {
            r.PostForm = make(url.Values)
        }
    }
    if r.Form == nil {
        if len(r.PostForm) > 0 {
            r.Form = make(url.Values)
            copyValues(r.Form, r.PostForm)
        }
        var newValues url.Values
        if r.URL != nil {
            var e error
            newValues, e = url.ParseQuery(r.URL.RawQuery)
            if err == nil {
                err = e
            }
        }
        if newValues == nil {
            newValues = make(url.Values)
        }
        if r.Form == nil {
            r.Form = newValues
        } else {
            copyValues(r.Form, newValues)
        }
    }
    return err
}

这个函数是幂等的,也就是多次调用效果一样。从这个解析过程可以发现:

  • 函数首先解析PostForm字段,然后解析Form字段;
  • 而在parsePostForm()函数中,只对Body中application/x-www-form-urlencoded编码的参数解析,其余编码格式下PostForm字段为空;
  • Form中包含PostForm中的信息,而且PostForm中的信息优先级高;
  • URL中的参数只会到被解析到Form字段中。

下面的例子演示了上面的分析:

r.ParseForm()
fmt.Fprintln(w, r.Form)
fmt.Fprintln(w, r.PostForm)

使用curl模拟一个请求:

curl -id "start=30&mode=list" "localhost:8080/world?start=40&sort=time"

结果:

map[mode:[list] sort:[time] start:[30 40]]
map[mode:[list] start:[30]]

第一个mapForm,第二个是PostForm,从结果可以看到两者的区别。

5.3 MultipartForm

对于MultipartForm,需要调用ParseMultipartForm方法,这个方法会在需要的时候(也就是FormPostFormnil的时候)调用ParseForm方法先来解析FormPostForm两个字段:

func (r *Request) ParseMultipartForm(maxMemory int64) error {
    if r.MultipartForm == multipartByReader {
        return errors.New("http: multipart handled by MultipartReader")
    }
    if r.Form == nil {
        err := r.ParseForm()
        if err != nil {
            return err
        }
    }
    if r.MultipartForm != nil {
        return nil
    }

    mr, err := r.multipartReader(false)
    if err != nil {
        return err
    }

    f, err := mr.ReadForm(maxMemory)
    if err != nil {
        return err
    }

    if r.PostForm == nil {
        r.PostForm = make(url.Values)
    }
    for k, v := range f.Value {
        r.Form[k] = append(r.Form[k], v...)
        // r.PostForm should also be populated. See Issue 9305.
        r.PostForm[k] = append(r.PostForm[k], v...)
    }

    r.MultipartForm = f

    return nil
}

同时需要一个最大读取字节数的参数。

从这个解析过程可以看到:

  • 如果FormPostFormnil的话,这个函数就先调用ParseForm来进行解析;
  • 不过如果编码格式是multipart/form-data的话,Form中就只会有URL中的参数,而PostForm中啥也没有(暂时);
  • ParseMultipartForm方法会解析Body中multipart/form-data格式的数据到MultipartForm字段中;
  • 最后还会将Body中的参数添加到FormPostForm中。

例子:

r.ParseMultipartForm(1024)
fmt.Fprintln(w, r.Form)
fmt.Fprintln(w, r.PostForm)
fmt.Fprintln(w, r.MultipartForm)

使用curl模拟一个请求:

curl -F "start=30" -F "mode=list" -F upload=@hello "localhost:8080/world?start=40&sort=time"

其中前两个-F生成两个multipart/form-data格式的参数,而后一个-F上传一个文件。结果:

map[mode:[list] sort:[time] start:[40 30]]
map[mode:[list] start:[30]]
&{map[mode:[list] start:[30]] map[upload:[0xc0000fa140]]}

前两个map还是和之前的一样,最后一个就是MultipartForm,这是两个map,一个是Body中的参数,一个是上传的文件。

关于Form和之前有一个不同,就是start值的顺序。之前使用ParseForm解析的时候30在前而现在40在前。从两个函数的代码中我们可以看到这个结果的原因。

5.4 Another Way

除了上面调用ParseFormParseMultipartForm函数外,还可以通过其它的方法获取值:FormValue()PostFormValue()。两个方法的定义如下:

func (r *Request) FormValue(key string) string {
    if r.Form == nil {
        r.ParseMultipartForm(defaultMaxMemory)
    }
    if vs := r.Form[key]; len(vs) > 0 {
        return vs[0]
    }
    return ""
}

func (r *Request) PostFormValue(key string) string {
    if r.PostForm == nil {
        r.ParseMultipartForm(defaultMaxMemory)
    }
    if vs := r.PostForm[key]; len(vs) > 0 {
        return vs[0]
    }
    return ""
}

这两个函数都会在必要的时候调用ParseMultipartForm函数来进行解析。和前面的方法不同在于,这两个方法返回的是对应key的第一个value值,而不是一个字符串切片。

例子:

fmt.Fprintln(w, r.FormValue("start"))
fmt.Fprintln(w, r.PostFormValue("start"))

结果:

40
30

这个结果很奇怪是吧,所以对于既在URL中也在Body中的参数,在获取的时候需要小心一些。最好的办法就是参数只出现在一个地方,要么URL中,要么Body中。

其实常用的编码还有application/json,也就是读取的Body数据可以解析成一个Json结构,这也很常见,不过不在这里细说了。

To Be Continued…


  You Will See...