Request参数解析

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编程[郑兆雄]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值