在前一篇中,我们直接在 index 这个 handler func 中解析了模板,定义了数据,然后执行模板显示“拼合”了数据的网页。这是一个客户被动看的页面。实际的应用显然需要能够处理用户的请求。对于浏览器客户端的请求,我们先要来了解和请求有关的东西,如 URL、请求头部、请求主体、表单、文件上传、JSON主体、cookie等。
继续前面web学习随便记1中的代码:添加一个 handler func headers 如下
func headers(w http.ResponseWriter, r *http.Request) {
url := r.URL
// fmt.Fprintln(w, url.Scheme)
// fmt.Fprintln(w, url.Opaque)
// fmt.Fprintln(w, url.User.Username())
// fmt.Fprintln(w, url.Host)
fmt.Fprintln(w, url.Path)
fmt.Fprintln(w, url.RawQuery)
// fmt.Fprintln(w, url.Fragment)
h := r.Header
fmt.Fprintln(w, h)
fmt.Fprintln(w, h["User-Agent"])
}
主函数中添加路由 /headers 的处理
// ...............
mux.HandleFunc("/", index)
mux.HandleFunc("/headers", headers)
server := &http.Server{
Addr: "0.0.0.0:8088",
Handler: mux,
}
// .................
运行后,在浏览器地址栏输入 http://localhost:8088/headers?k1=v1&k2=bbb 结果类似如下:
看来,读取 URL 和 请求头部中的信息还是比较容易的。
类似地,我们添加 handler func body 如下:获取请求主体的长度,定义一个字节数组,将主体读入该数组,转成字符串显示
func body(w http.ResponseWriter, r *http.Request) {
len := r.ContentLength
body := make([]byte, len)
r.Body.Read(body)
fmt.Fprintln(w, string(body))
}
路由添加 mux.HandleFunc("/body", body),运行。对于显示主体,因为我们没有表单,所以,用 curl 命令来查看结果
sjg@sjg-PC:~/go/src$ curl -id "family_name=Shen&name=Beta" 127.0.0.1:8088/body
HTTP/1.1 200 OK
Date: Fri, 05 May 2023 09:22:55 GMT
Content-Length: 27
Content-Type: text/plain; charset=utf-8
family_name=Shen&name=Beta
我们来进入表单的世界。在用来存放静态文件的目录 chitchat/public 下创建 client.html:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Golang表单数据获取</title>
</head>
<body>
<form action="/process?key1=张三&key2=123" method="post" enctype="application/x-www-form-urlencoded">
<input type="text" name="key1" value="李四" />
<input type="text" name="key3" value="456" />
<input type="submit" />
</form>
</body>
</html>
表单最后会提交到 /process 这个路由路径上,所以,我们在主函数main中添加 mux.HandleFunc("/process", process),同时创建 handler func process 如下:http.Request对象的方法ParseForm()会对请求进行语法分析,而该对象Form字段可以获取表单字段信息
func process(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fmt.Fprintln(w, r.Form)
}
现在打开浏览器,地址栏输入 http://localhost:8088/static/client.html,将显示如下表单
点击提交后,显示如下
上述显示结果表明,使用 r.Form字段获取的信息为一个map,对于表单和URL两者同名的键,对应的值都会保存在该字段。简单来说,r.Form字段信息中某个key对应的值为一个切片,当key在表单和URL中是唯一的时候,用r.Form获取该key对应值没有问题;而key不唯一时,将无法区分两个值(虽然一般表单值会排在前面,但通常不该依赖顺序)。那么,要只获取表单Post提交的数据怎么办呢?答案是使用 r.PostForm 字段。
修改 handler func process:
func process(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fmt.Fprintln(w, r.PostForm)
}
重复前面的过程,显示如下:
上图只显示表单中的字段和值,这个道理和 PHP 中超级全局变量$_REQUEST可以同时获取GET和POST提交的信息,而超级全局变量$_POST只获取POST提交的信息是类似的。但下面的情况,两者就不类似了。
我们把表单enctype修改一下:
<form action="/process?key1=张三&key2=123" method="post" enctype="multipart/form-data">
重复前面的过程,显示将是
这表明,r.PostForm 字段无法获取 multipart/form-data 格式编码的表单信息。而如果使用r.Form字段,显示将变成
只能获取URL中的字段信息,还是没有表单信息。此时,我们需要通过 r.MultipartForm 字段来获取 multipart/form-data 格式编码的表单数据(注意:multipart/form-data格式和url编码格式不同,语法解析的方法也不同,要用 r.ParseMultipartForm(指定长度),整个请求体解析后,指定长度的数据会放入内存,其余部分放在临时文件中)。
func process(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024)
fmt.Fprintln(w, r.MultipartForm)
}
显示结果为
我们可以发现,r.MultipartForm字段的值不是单个map,而是两个map组成的结构体。上图中第一个map包含了表单的值,第二个map是空的,因为它是用于记录用户上传的文件的(第二个map相当于PHP中的超级全局变量$_FILES,而第一个对应$_POST)。
有时候,我们只需要一两个表单字段的值,获取包含全体字段的map,再根据键取值有点麻烦。此时,我们可以使用 r.FormValue(键名) 和 r.PostFormValue(键名) 这两个方法(使用这两个方法时,不需要在其前面使用ParseForm或者ParseMultipartForm方法)
修改 handler func process 如下,表单使用url编码:
func process(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, r.FormValue("key1"))
fmt.Fprintln(w, r.FormValue("key2"))
fmt.Fprintln(w, r.FormValue("key3"))
}
显示结果为:
注意:key1对应的值,只显示表单中的值,没有显示URL中的值。查询 r.FormValue 文档可以知道,它返回查询到的第一个值,而 POST和PUT传递的值会优先于URL查询串中的值。
将上述代码中的 FormValue 全部替换为 PostFormValue,显示结果如下:
此时 key2因为没有Post方法对应值,所以没有输出。也许是因为我的Golang版本1.17,书上说的(Golang1.4)表单编码使用multipart/form-data编码时,FormValue或PostFormValue无法获取值的情况不存在。
事实上,不是为了文件上传,我们不会使用multipart/form-data来编码表单,因为url编码方式更高效。下面来看看文件上传问题。我们给表单添加一个file类型的字段:
................
<input type="text" name="key1" value="李四" />
<input type="text" name="key3" value="456" />
<input type="file" name="upload" />
<input type="submit" />
................
修改 handler func process 如下 (解析表单,获得上传的第1个文件的文件头,打开该文件头对应的文件对象,读取文件数据并在浏览器输出)
func process(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024)
fileHeader := r.MultipartForm.File["upload"][0]
file, err := fileHeader.Open()
if err == nil {
data, err := ioutil.ReadAll(file)
if err == nil {
fmt.Fprintln(w, string(data))
}
}
}
试验时,注意选择文本文件,并且最好只有英文字符(因为中文涉及编码,有可能看到的是“乱码”)。我们要直接获取上传的文件的信息也是可能的,从而不用先parse,再获取头部,最后打开这样的复制步骤。答案是使用 r.FormFile()方法返回3值,parse、获取头部,打开一气呵成。
func process(w http.ResponseWriter, r *http.Request) {
// r.ParseMultipartForm(1024)
// fileHeader := r.MultipartForm.File["upload"][0]
// file, err := fileHeader.Open()
file, _, err := r.FormFile("upload")
if err == nil {