Request结构
Request结构主要由以下部分组成
URL字段
Header字段
Body字段
Form字段、PostForm字段和MultipartForm字段
type Request struct {
Method string
URL *url.URL
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
Header Header
Body io.ReadCloser
GetBody func() (io.ReadCloser, error)
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
Trailer Header
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
Cancel <-chan struct{}
Response *Response
ctx context.Context
}
请求URL
Request结构中的URL字段用于表示请求行中包含的URL
type URL struct {
Scheme string
Opaque string // encoded opaque data
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)
ForceQuery bool // append a query ('?') even if RawQuery is empty
RawQuery string // encoded query values, without '?'
Fragment string // fragment for references, without '#'
}
URL的一般格式:[scheme:][//[userinfo@]host][/]path[?query][#fragment]
客户端通过URL的查询参数向服务器传递信息,而URL结构的RawQuery字段记录的就是客户端向服务器传递的查询参数字符串。如:http://www.example.com/post?id=1234&name=chen ,则RawQuery字段的值存储的就是id=1234&name=chen ,要想获取键值对格式的查询参数,要对RawQuery值进行语法分析,但是使用Request结构的Form字段,系统提供了解析RawQuery的方法,将解析的键值对信息会存储在Form字段中。下面会主要会对Request结构的Form字段、PostForm字段和MultipartForm字段进行讲解。
请求首部
http.Header类型拥有4中基本方法,这些方法根据给定的键执行添加、删除、获取和设置等操作。
import (
"net/http"
"fmt"
)
func headers(w http.ResponseWriter,r *http.Request) {
r.ParseForm()
h := r.Header
fmt.Println(r.Header.Get("Accept-Encoding")) //gzip, deflate
fmt.Println(r.Header["Accept-Encoding"]) //[gzip, deflate]
fmt.Fprintln(w,h)
}
func main() {
http.HandleFunc("/headers",headers)
http.ListenAndServe(":8080",nil)
}
r.Header.Get("Accept-Encoding")是通过Header类型的Get方法获取的头信息,其返回的就是字符串形式的首部值,而r.Header["Accept-Encoding"]是以切片的形式返回的。
请求主体
请求和响应的主体都是由Request结构的Body字段表示,该字段是一个io.ReadCloser接口。
import (
"net/http"
"fmt"
)
func body(w http.ResponseWriter,r *http.Request) {
bys := make([]byte,r.ContentLength)
r.Body.Read(bys)
w.Write(bys)
}
func main() {
http.HandleFunc("/body",body)
http.ListenAndServe(":8080",nil)
}
通过ContentLength表示的是主体数据的字节长度,根据该长度创建字节切片,在通过Read方法将主体数据读取到字节切片中。因为GET请求不包含报文主体,所以使用的是POST表单请求。
Go语言提供了诸如FormValue和FormFile这种的方法来提取通过POST方法提交的表单,所以用户一般不需要自行读取主体中未经处理的表单,以下讲解FormValue和FormFile等方法。
Go与HTML表单
HTML表单的内容类型(Content-Type)决定了POST请求在发送键值对时将使用何种格式,其中,表单的enctype(也就是Content-Type)属性指定的值可以是application/x-www-form-urlencoded和multipart/form-data等。如果enctype属性的值设置为application/x-www-form-urlencoded,那么表单中的数据编码为一个连续的“长查询字符串”,这种编码和URL编码是一样的,如:id=1234&name=chen。如enctype属性的值设置为multipart/form-data,那么表单中的数据将被转换为一条MIME报文,表单中的每个键值对都构成了这条报文的一部分,并且每个键值对都带有它们各自的内容类型以及内容配置。以下是使用multipart/form-data编码对表单数据进行格式化的例子:
----------------------------780741602912379251409253
Content-Disposition: form-data; name="id"
123
----------------------------780741602912379251409253
Content-Disposition: form-data; name="name"
chen
----------------------------780741602912379251409253--
那么application/x-www-form-urlencoded和multipart/form-data两种编码应该如何选择呢,如果表单传送的简单的文本数据,那么使用URL编码格式更好,因为这种编码更为简单、高效,并且它所需的计算量要比另一种编码少,如果表单需要传送大量数据(如上传文件),那么使用multipart/form-data编码格式会更好一些。
Form字段
前面提到了如果直接获取请求体数据,需要自行进行语法分析,解析出键值对数据,而net/http库已经提供了一套用途相当广泛的函数,这些函数一般都能够满足用户对数据提取方面的需求,所以我们很少需要自行对表单数据进行语法分析。
通过调用Request结构提供的方法,用户可以将URL、主体又或者以上两者记录的数据提取到该结构的Form、PostForm和MultipartForm等字段当中。跟我们平常通过POST请求获取到的数据一样,存储在这些字段里面的数据也是以键值对形式表示的。使用Request结构的方法获取表单数据的一般步骤是:
(1)调用ParseForm()方法或者ParseMultipartForm()方法,对请求进行语法分析。
(2)根据步骤1调用的方法,访问相应的Form字段、PostForm字段或MultipartForm字段。
使用ParseForm()方法对表单进行语法分析的例子:
import (
"net/http"
"fmt"
)
func process(w http.ResponseWriter,r *http.Request) {
fmt.Println(r.Form) //map[]
r.ParseForm()
fmt.Println(r.Form) //map[id:[123] name:[chen hello world] hello:[world]]
fmt.Fprint(w,r.Form)
}
func main() {
http.HandleFunc("/process",process)
http.ListenAndServe(":8080",nil)
}
必须先使用ParseForm()方法对请求进行语法分析,然后在访问Form字段,获取具体的表单,否则Form字段为空。
请求表单可以完成的工作:
1.通过POST方法将表单发送至地址http://localhost:8080/process?name=hello world&hello=world
2.通过设置body编码enctype(Content-Type)属性为application/x-www-form-urlencoded
3.将id=123和name=chen这两个表单参数发送至服务器
需要注意的是,这个表单为相同的键name提供了两个不同的值,其中值hello world是通过URL提供的,而值chen是通过表单(请求体)提供的。执行请求后,返回的数据是map[name:[chen hello world] id:[123] hello:[world]]
这是服务器在对请求进行语法分析之后,使用字符串形式显示出来的未经处理的Form结构,这个结构是一个映射,它的键是字符串,而值是一个由字符串组成的切片。正如所见,这些值都进行了相应的URL解码,比如在hello world之间就能后正常的看到空格,而不是编码之后的%20。
对于id这种只会出现在表单(请求体)或者URL两者其中一个地方的键来说,执行语句r.Form["id"]将返回一个切片,切片里面包含了这个键的表单值或者URL值,如:[123]。而对name这种同时出现在表单(请求体)和URL两个地方的键来说,执行语句r.Form["name"]将返回一个同时包含了键的表单值和URL值的切片,并且表单值在切片中总是排在URL值的前面,如:[chen hello world]
PostForm字段
如果一个键同时拥有表单键值对和URL键值对,但是用户只想要获取表单键值对而不是URL键值对,那么可以访问Request结构的PostForm字段,这个字段只会包含键的表单值(请求体),而不包含任何同名键的URL值。如果我们把前面代码中的r.Form语句改为r.PostForm语句,
func process(w http.ResponseWriter,r *http.Request) {
r.ParseForm()
fmt.Fprint(w,r.PostForm)
}
那么程序将打印以下结果:map[id:[123] name:[chen]]
上面这个输出使用的是application/x-www-form-urlencoded内容类型,如果我们使用enctype(Content-Type):multipart/form-data作为内容类型,并对服务器代码进行调整,让它重新使用r.Form语句而不是r.PostForm语句,
func process(w http.ResponseWriter,r *http.Request) {
r.ParseForm()
fmt.Fprint(w,r.Form)
}
那么程序打印的结果为:map[hello:[world] name:[hello world]]
因为PostForm字段只支持application/x-www-form-urlencoded编码,所以现在的r.Form语句将不再返回任何表单值,而是只返回URL查询值。为了解决这个问题,我们需要通过MultipartForm字段来获取multipart/form-data编码的表单(请求体)数据。
MultipartForm字段
为了取得multipart/form-data编码的表单数据,我们需要用到Request结构的ParseMultipartForm()方法和MultipartForm字段,而不再使用ParseForm()方法和Form字段,不过ParseMultipartForm()方法在需要时也会自行调用ParseForm()方法。现在把程序改为:
func process(w http.ResponseWriter,r *http.Request) {
r.ParseMultipartForm(1024)
fmt.Fprint(w,r.MultipartForm)
}
这里的r.ParseMultipartForm(1024)说明了我们想要从multipart编码的表单里面取出对少字节的数据,这时的服务器执行结果为:&{map[name:[chen] id:[123]] map[]}
因为MultipartForm字段只包含表单键值对而不包含URL键值对,所以这次打印出来的只有表单键值对而没有URL键值对。另外需要注意的是,MultipartForm字段的值也不再是一个映射,而是一个包含了两个映射的结构,其中第一个映射的键为字符串,值为字符串组成的切片,而第二个映射这是空的,这个映射之所以会为空,是因为它是用来记录用户上传的文件的,关于这个映射的具体信息下面在进行讲解。
MultipartForm *multipart.Form
type Form struct {
Value map[string][]string
File map[string][]*FileHeader
}
除了上面提到的几个方法之外,Request结构还提供了另外一些方法,它们可以让用户更容易的获取表单中的键值对,其中,FormValue()方法允许直接访问与给定键相关联的值,就像访问Form字段中的键值对一样,唯一的区别在于,因为FormValue()方法在需要时会自动调用ParseForm()方法或者ParseMultipartForm()方法,所以用户在执行FormValue()方法之前,不需要手动调用上面提到的两个语法分析方法。
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) 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()
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
}
这意味着,如果我们把
func process(w http.ResponseWriter,r *http.Request) {
r.ParseMultipartForm(1024)
fmt.Fprint(w,r.MultipartForm)
}
换成
func process(w http.ResponseWriter,r *http.Request) {
fmt.Fprint(w,r.FormValue("name"))
}
并将客户端的表单的enctype(Content-Type)属性的值设置为application/x-www-form-urlencoded,那么服务器将打印出这样的结果:chen
因为FormValue()方法即使在给定键拥有多个值的情况下,也只会从Form结构中取出给定键的第一个值,所以如果想要获取给定键包含的所有值,那么就需要直接访问Form结构
fmt.Fprint(w,r.FormValue("name"))
fmt.Fprint(w,r.Form)
上面这两条语句输出如下:
chen
map[id:[123] name:[chen hello world] hello:[world]]
PostFormValue()除了访问的是PostForm字段而不是Form字段之外,PostFormValue()方法的作用跟上面介绍的FormValue()方法的作用基本相同。PostFormValue()方法的使用如下:
func process(w http.ResponseWriter,r *http.Request) {
fmt.Fprintln(w,r.PostFormValue("name"))
fmt.Fprintln(w,r.PostForm)
}
运行输出如下:
chen
map[id:[123] name:[chen]]
正如结果所示,PostFormValue()方法只会返回表单(请求体)键值对而不会返回URL键值对。
FormValue()方法和PostFormValue()方法都会在需要时自动去调用ParseMultipartForm()方法,因此用户并不需要手动调用
ParseMultipartForm()方法。
文件
multipart/form-data编码通常用于实现文件上传功能,这种功能需要用到file类型
func process(w http.ResponseWriter,r *http.Request) {
r.ParseMultipartForm(1024)
file,err := r.MultipartForm.File["file"][0].Open()
if err == nil {
data,err := ioutil.ReadAll(file)
if err == nil {
fmt.Fprintln(w,r.MultipartForm)
w.Write(data)
}
}
}
正如之前所说,服务器在处理文件上传时首先要做的就是执行ParseMultipartForm()方法,接着从MultipartForm字段的File字段里面取出文件头*FileHeader,然后通过调用文件头的Open()方法来打开文件。在此之后,服务器会将文件的内容读取到一个字节数组中,并将这个数组的内容打印出来。
跟FormValue()方法和PostFormValue()方法类似,net/http库也提供了一个FormFile()方法,它可以快速的获取被上传的文件,FormFile()方法在被调用时将返回给定键的第一个值,因此它在客户端只上传了一个文件的情况下,使用起来非常方便。如下:
func process(w http.ResponseWriter,r *http.Request) {
file,_,err := r.FormFile("file")
if err == nil {
data,err := ioutil.ReadAll(file)
if err == nil {
fmt.Fprintln(w,r.MultipartForm)
w.Write(data)
}
}
}
正如代码所示,FormFile()方法将同时返回文件和文件头作为结果,在使用FormFile()方法时,将不再需要手动调用ParseMultipartForm()方法,只需要对返回的文件进行处理即可。
我对处理文件最了简单的封装,这样在使用的时候可以更加的方便
controller
import (
"io"
"mime/multipart"
"net/http"
"path"
)
const BASE_IMAGE_ADDRESS = "./img/"
type Controller struct {
Data interface{}
}
type FileInfoTO struct {
//图片id -- 暂时没有用
ID int64
//缩略图路径 -- 暂时没有用
CompressPath string
//原图路径 ,保存数据库的路径
Path string
//原始的文件名
OriginalFileName string
//存储文件名 如:uuidutil
FileName string
//文件大小
FileSize int64
}
//获取上传文件的数量
func (p *Controller) GetFileNum(r *http.Request,keys ...string) int {
r.ParseMultipartForm(32 << 20)
m := r.MultipartForm
if m == nil {
return 0
}
if len(keys) == 0{
var num int
for _,fileHeaders := range m.File {
num += len(fileHeaders)
}
return num
} else {
var num int
for _,value := range keys {
num += len(m.File[value])
}
return num
}
}
//解析Form-data中的文件,如果不传keys,不管上传的文件的字段名(filename)是什么,都会解析,否则只会解析keys指定的文件
func (p *Controller) SaveFiles(r *http.Request,,relativePath string,keys ...string) []*FileInfoTO {
r.ParseMultipartForm(32 << 20)
m := r.MultipartForm
if m == nil {
log.Println("not multipartfrom !")
return nil
}
fileInfos := make([]*FileInfoTO,0)
filePath := BASE_IMAGE_ADDRESS + relativePath
fileutil.MakeDir(filePath)
if len(keys) == 0 {
for _,fileHeaders := range m.File { //遍历所有的所有的字段名(filename)获取FileHeaders
for _,fileHeader := range fileHeaders{
to := p.saveFile(filePath,relativePath,fileHeader)
fileInfos = append(fileInfos,to)
}
}
} else {
for _,value := range keys {
fileHeaders := m.File[value]//根据上传文件时指定的字段名(filename)获取FileHeaders
for _,fileHeader := range fileHeaders{
to := p.saveFile(filePath,relativePath,fileHeader)
fileInfos = append(fileInfos,to)
}
}
}
return fileInfos
}
//保存单个文件
func (p *Controller) saveFile(filePath,relativePath string,fileHeader *multipart.FileHeader)*FileInfoTO {
file,err := fileHeader.Open()
if err != nil {
log.Println(err)
return nil
}
defer file.Close()
name,err := uuidutil.RandomUUID()
if err != nil {
log.Println(err)
return nil
}
fileType := fileutil.Ext(fileHeader.Filename,".jpg")
newName := name + fileType
dst,err := os.Create(filePath + newName)
if err != nil {
log.Println(err)
return nil
}
defer dst.Close()
fileSize,err := io.Copy(dst,file)
if err != nil {
log.Println(err)
return nil
}
return &FileInfoTO{Path:relativePath + newName,OriginalFileName:fileHeader.Filename,FileName:newName,FileSize:fileSize}
}
fileutil
import (
"os"
"path"
)
//创建多级目录
func MkDirAll(path string) bool {
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
return false
}
return true
}
//检测文件夹或文件是否存在
func Exist(file string) bool {
if _,err := os.Stat(file);os.IsNotExist(err){
return false
}
return true
}
//获取文件的类型,如:.jpg
//如果获取不到,返回默认类型defaultExt
func Ext(fileName string,defaultExt string) string {
t := path.Ext(fileName)
if len(t) == 0 {
return defaultExt
}
return t
}
/// 检验文件夹是否存在,不存在 就创建
func MakeDir(filePath string){
if !Exist(filePath) {
MkDirAll(filePath)
}
}
//删除文件
func Remove(name string) bool {
err := os.Remove(name)
if err != nil{
return false
}
return true
}
uuidtuil
import (
"encoding/base64"
"math/rand"
)
func RandomUUID() (string,error) {
b := make([]byte,32)
if _,err := rand.Read(b);err != nil{
return "",err
}
return base64.URLEncoding.EncodeToString(b),nil
}
处理带有JSON主体的POST请求体
func process(w http.ResponseWriter,r *http.Request) {
type Post struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
if r.Header.Get("Content-Type") == "application/json" {
bys := make([]byte,r.ContentLength)
n,err := r.Body.Read(bys)
if err != nil && err != io.EOF{
fmt.Fprintln(w,n)
fmt.Fprintln(w,err.Error())
return
}
post := &Post{}
err = json.Unmarshal(bys,post)
if err != nil {
fmt.Fprintln(w,err.Error())
return
}
fmt.Fprintln(w,post)
}
}
以上是基于go1.10.1版本
参考:Go Web编程[郑兆雄]