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