前面咱们学习了处理器和处理器函数,如何编写和注册处理器。本文咱们将学习如何从请求中获取信息。html
经过前面的学习,咱们知道处理器函数须要符合下面的签名:git
func (w http.ResponseWriter, r *http.Request)
其中,http.Request
就是请求的类型。客户端传递的数据均可以经过这个结构来获取。结构Request
定义在包 net/http 中:github
// src/net/http/request.go type Request struct { Method string URL *url.URL Proto string ProtoMajor int ProtoMinor int Header Header Body io.ReadCloser ContentLength int // 省略一些字段... }
咱们来看一下几个重要的字段。golang
Method
请求中的Method
字段表示客户端想要调用服务器的哪一个方法。在第一篇文章中,咱们提到过 HTTP 协议方法。其取值有GET/POST/PUT/DELETE
等。服务器根据请求方法的不一样会进行不一样的处理,例如GET
方法只是获取信息(用户基本信息,商品信息等),POST
方法建立新的资源(注册新用户,上架新商品等)。web
URL
Tim Berners-Lee 在建立万维网的同时,也引入了使用字符串来表示互联网资源的概念。他称该字符串为统一资源标识符(URI,Uniform Resource Identifier)。URI 由两部分组成。一部分表示资源的名称,即统一资源名称(URN,Uniform Resource Name)。另外一部分表示资源的位置,即统一资源定位符(URL,Uniform Resource Location)。ajax
在 HTTP 请求中,使用 URL 来对要操做的资源位置进行描述。URL 的通常格式为:编程
[scheme:][//[userinfo@]host][/]path[?query][#fragment]
scheme
:协议名,常见的有httphttpsftp
;userInfo
:如有,则表示用户信息,如用户名和密码可写做dj:password
;host
:表示主机域名或地址,和一个可选的端口信息。若端口未指定,则默认为 80。例如www.example.com
,www.example.com:8080
,127.0.0.1:8080
;path
:资源在主机上的路径,以/
分隔,如/posts
;query
:可选的查询字符串,客户端传输过来的键值对参数,键值直接用=
,多个键值对之间用&
链接,如page=1&count=10
;fragment
:片断,又叫锚点。表示一个页面中的位置信息。由浏览器发起的请求 URL 中,一般没有这部分信息。可是能够经过ajax
等代码的方式发送这个数据;咱们来看一个完整的 URL:json
http://dj:password@www.example.com/posts?page=1&count=10#fmt
Go 中的 URL 结构定义在net/url
包中:浏览器
// net/url/url.go type URL struct { Scheme string Opaque string User *Userinfo Host string Path string RawPath string RawQuery string Fragment string }
能够经过请求对象中的URL
字段获取这些信息。接下来,咱们编写一个程序来具体看看(使用上一篇文章讲的 Web 程序基本结构,只须要增长处理器函数和注册便可):安全
func urlHandler(w http.ResponseWriter, r *http.Request) { URL := r.URL fmt.Fprintf(w, "Scheme: %s\n", URL.Scheme) fmt.Fprintf(w, "Host: %s\n", URL.Host) fmt.Fprintf(w, "Path: %s\n", URL.Path) fmt.Fprintf(w, "RawPath: %s\n", URL.RawPath) fmt.Fprintf(w, "RawQuery: %s\n", URL.RawQuery) fmt.Fprintf(w, "Fragment: %s\n", URL.Fragment) } // 注册 mux.HandleFunc("/url", urlHandler)
运行服务器,经过浏览器访问localhost:8080/url/posts?page=1&count=10#main
:
Scheme: Host: Path: /url/posts RawPath: RawQuery: page=1&count=10 Fragment:
为何会出现空字段?注意到源码Request
结构中URL
字段上有一段注释:
// URL specifies either the URI being requested (for server // requests) or the URL to access (for client requests). // // For server requests, the URL is parsed from the URI // supplied on the Request-Line as stored in RequestURI. For // most requests, fields other than Path and RawQuery will be // empty. (See RFC 7230, Section 5.3) // // For client requests, the URL's Host specifies the server to // connect to, while the Request's Host field optionally // specifies the Host header value to send in the HTTP // request.
大意是做为服务器收到的请求时,URL
中除了Path
和RawQuery
,其它字段大多为空。对于这个问题,Go 的 Github 仓库上Issue 28940有过讨论。
咱们还能够经过URL
结构获得一个 URL 字符串:
URL := &net.URL { Scheme: "http", Host: "example.com", Path: "/posts", RawQuery: "page=1&count=10", Fragment: "main", } fmt.Println(URL.String())
上面程序运行输出字符串:
http://example.com/posts?page=1&count=10#main
Proto/ProtoMajor/ProtoMinor
Proto
表示 HTTP 协议版本,如HTTP/1.1
,ProtoMajor
表示大版本,ProtoMinor
表示小版本。
func protoFunc(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Proto: %s\n", r.Proto) fmt.Fprintf(w, "ProtoMajor: %d\n", r.ProtoMajor) fmt.Fprintf(w, "ProtoMinor: %d\n", r.ProtoMinor) } mux.HandleFunc("/proto", protoFunc)
启动服务器,浏览器请求localhost:8080
返回:
Proto: HTTP/1.1 ProtoMajor: 1 ProtoMinor: 1
当前 HTTP/1.1 是主流的版本。
Header
Header
中存放的客户端发送过来的首部信息,键-值对的形式。Header
类型底层实际上是map[string][]string
:
// src/net/http/header.go type Header map[string][]string
每一个首部的键和值都是字符串,能够设置多个相同的键。注意到Header
值为[]string
类型,存放相同的键的多个值。浏览器发起 HTTP 请求的时候,会自动添加一些首部。咱们编写一个程序来看看:
func headerHandler(w http.ResponseWriter, r *http.Request) { for key, value := range r.Header { fmt.Fprintf(w, "%s: %v\n", key, value) } } mux.HandleFunc("/header", headerHandler)
启动服务器,浏览器请求localhost:8080/header
返回:
Accept-Encoding: [gzip, deflate, br] Sec-Fetch-Site: [none] Sec-Fetch-Mode: [navigate] Connection: [keep-alive] Upgrade-Insecure-Requests: [1] User-Agent: [Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36] Sec-Fetch-User: [?1] Accept: [text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3] Accept-Language: [zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7]
我使用的是 Chrome 浏览器,不一样的浏览器添加的首部不彻底相同。
常见的首部有:
Accept
:客户端想要服务器发送的内容类型;Accept-Charset
:表示客户端能接受的字符编码;Content-Length
:请求主体的字节长度,通常在 POST/PUT 请求中较多;Content-Type
:当包含请求主体的时候,这个首部用于记录主体内容的类型。在发送 POST 或 PUT 请求时,内容的类型默认为x-www-form-urlecoded
。可是在上传文件时,应该设置类型为multipart/form-data
。User-Agent
:用于描述发起请求的客户端信息,如什么浏览器。Content-Length/Body
Content-Length
表示请求体的字节长度,请求体的内容能够从Body
字段中读取。细心的朋友可能发现了Body
字段是一个io.ReadCloser
接口。在读取以后要关闭它,不然会有资源泄露。可使用defer
简化代码编写。
func bodyHandler(w http.ResponseWriter, r *http.Request) { data := make([]byte, r.ContentLength) r.Body.Read(data) // 忽略错误处理 defer r.Body.Close() fmt.Fprintln(w, string(data)) } mux.HandleFunc("/body", bodyHandler)
上面代码将客户端传来的请求体内容回传给客户端。还可使用io/ioutil
包简化读取操做:
data, _ := ioutil.ReadAll(r.Body)
直接在浏览器中输入 URL 发起的是GET
请求,没法携带请求体。有不少种方式能够发起带请求体的请求,下面介绍两种:
经过 HTML 的表单咱们能够向服务器发送 POST 请求,将表单中的内容做为请求体发送。
func indexHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, ` <html> <head> <title>Go Web 编程之 request</title> </head> <body> <form method="post" action="/body"> <label for="username">用户名:</label> <input type="text" id="username" name="username"> <label for="email">邮箱:</label> <input type="text" id="email" name="email"> <button type="submit">提交</button> </form> </body> </html> `) } mux.HandleFunc("/", indexHandler)
在 HTML 中使用form
来显示一个表单。点击提交按钮后,浏览器会发送一个 POST 请求到路径/body
上,将用户名和邮箱做为请求包体。
启动服务器,进入主页localhost:8080/
,显示表单。填写信息,点击提交:
浏览器向服务器发送 POST 请求,URL 为/body
,bodyHandler
处理完成后将包体回传给客户端。最后客户端显示:
上面的数据使用了x-www-form-urlencoded
编码,这是表单的默认编码。后文还有详述。
Postman 是一款功能很是强大的 API 测试工具。
GET/POST/PUT/DELETE
)。json/xml/http
等各类格式的内容;接下来咱们看看如何使用 PostMan 测试咱们的bodyHandler
。
上面咱们分析了 Go 中 HTTP 请求的常见字段。在实际开发中,客户端一般须要在请求中传递一些参数。参数传递的方式通常有两种方式:
下面依次来介绍。
前文中介绍 URL 的通常格式时提到过,URL 的后面能够跟一个可选的查询字符串,以?
与路径分隔,形如key1=value1&key2=value2
。
URL 结构中有一个RawQuery
字段。这个字段就是查询字符串。
func queryHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, r.URL.RawQuery) } mux.HandleFunc("/query", queryHandler)
若是咱们以localhost:8080/query?name=dj&age=20
请求,查询字符串name=dj&age=20
会原样传回客户端。可是RawQuery
是字符串类型的,使用字符串方法解析也能用,可是太麻烦了!!!
表单狭义上说是经过表单发送请求,广义上说能够将数据放在请求体中发送到服务器。接下来咱们简单编写一个 HTML 页面,经过页面表单发送 HTTP 请求:
<html> <head> <title>Go Web 编程之 request</title> </head> <body> <form action="/form?lang=cpp&name=dj" method="post" enctype="application/x-www-form-urlencoded"> <label>Form:</label> <input type="text" name="lang" /> <input type="text" name="age" /> <button type="submit">提交</button> </form> </body> </html>
action
表示提交表单时请求的 URL,method
表示请求的方法。若是使用GET
请求,因为GET
方法没有请求体,参数将会拼接到 URL 尾部;enctype
指定请求体的编码方式,默认为application/x-www-form-urlencoded
。若是须要发送文件,必须指定为multipart/form-data
;咱们介绍一下什么是urlencoded
编码。RFC 3986 中定义了 URL 中的保留字以及非保留字,全部保留字符都须要进行 URL 编码。URL 编码会把字符转换成它在 ASCII 编码中对应的字节值,接着把这个字节值表示为一个两位长的十六进制数字,最后在这个数字前面加上一个百分号(%)。例如空格的 ASCII 编码为 32,十六进制为 20,故 URL 编码为%20
。
Form
字段使用x-www-form-urlencoded
编码的请求体,在处理时首先调用请求的ParseForm
方法解析,而后从Form
字段中取数据:
func formHandler(w http.ResponseWriter, r *http.Request) { r.ParseForm() fmt.Fprintln(w, r.Form) } mux.HandleFunc("/form", formHandler)
运行程序,验证结果:
Form
字段的类型url.Values
底层其实是map[string][]string
。调用ParseForm
方法以后,可使用url.Values
的方法操做数据。
使用ParseForm
还能解析查询字符串,将上面的表单改成:
<html> <head> <title>Go Web 编程之 request</title> </head> <body> <form action="/form?lang=cpp&name=dj" method="post" enctype="application/x-www-form-urlencoded"> <label>Form:</label> <input type="text" name="lang" /> <input type="text" name="age" /> <button type="submit">提交</button> </form> </body> </html>
请求结果:
能够看出,查询字符串中的键值对和表单中解析处理的合并到一块儿了。同一个键下,表单值老是排在前面,如[golang cpp]
。
PostForm
字段若是一个请求,同时有 URL 键值对和表单数据,而用户只想获取表单数据,可使用PostForm
字段。
使用PostForm
只会返回表单数据,不包括 URL 键值。若是把上面的程序中,r.Form
改成r.PostForm
,那么程序将显示如下结果:
MultipartForm
字段若是要处理上传的文件,那么就必须使用multipart/form-data
编码。与以前的Form/PostForm
相似,处理multipart/form-data
编码的请求时,也须要先解析后使用。只不过使用的方法不一样,解析使用ParseMultipartForm
,以后从MultipartForm
字段取值。
<form action="/multipartform?lang=cpp&name=dj" method="post" enctype="multipart/form-data"> <label>MultipartForm:</label> <input type="text" name="lang" /> <input type="text" name="age" /> <input type="file" name="uploaded" /> <button type="submit">提交</button> </form>
func multipartFormHandler(w http.ResponseWriter, r *http.Request) { r.ParseMultipartForm(1024) fmt.Fprintln(w, r.MultipartForm) fileHeader := r.MultipartForm.File["uploaded"][0] file, err := fileHeader.Open() if err != nil { fmt.Println("Open failed: ", err) return } data, err := ioutil.ReadAll(file) if err == nil { fmt.Fprintln(w, string(data)) } } mux.HandleFunc("/multipartform", multipartFormHandler)
运行程序:
MultipartForm
包含两个map
类型的字段,一个表示表单键值对,另外一个为上传的文件信息。
使用表单中文件控件名获取MultipartForm.File
获得经过该控件上传的文件,能够是多个。获得的是multipart.FileHeader
类型,经过该类型能够获取文件的各个属性。
须要注意的是,这种方式用来处理文件。为了安全,ParseMultipartForm
方法须要传一个参数,表示最大使用内存,避免上传的文件占用空间过大。
FormValue/PostFormValue
为了方便地获取值,net/http
包提供了FormValue/PostFormValue
方法。它们在须要时会自动调用ParseForm/ParseMultipartForm
方法。
FormValue
方法返回请求的Form
字段中指定键的值。若是同一个键对应多个值,那么返回第一个。若是须要获取所有值,直接使用Form
字段。下面代码将返回hello
对应的第一个值:
fmt.Fprintln(w, r.FormValue("hello"))
PostFormValue
方法返回请求的PostForm
字段中指定键的值。若是同一个键对应多个值,那么返回第一个。若是须要获取所有值,直接使用PostForm
字段
注意:
当编码被指定为multipart/form-data
时,FormValue/PostFormValue
将不会返回任何值,它们读取的是Form/PostForm
字段,而ParseMultipartForm
将数据写入MultipartForm
字段。
经过 AJAX 之类的技术能够发送其它格式的数据,例如application/json
等。这种状况下:
Content-Type
来获知具体是什么格式;r.Body
读取字节流;本文介绍了net/http
包中请求的各方面内容。从Request
结构到如何传递参数,最后介绍各类编码的请求如何处理。