介绍

表单是我们平常编写Web应用常用的工具,通过表单我们可以方便的让客户端和服务器进 行数据的交互。对于以前开发过Web的用户来说表单都非常熟悉。
表单是一个包含表单元素的区域。表单元素是允许用户在表单中(比如:文本域、下拉列表、 单选框、复选框等等)输入信息的元素。表单使用表单标签(<form>)定义。

1、处理表单的输入

先看一个例子吧。

注意:所有文件要放在src文件夹下
在之前文章的代码基础之下,创建一个view文件夹,用来存放界面的代码文件,在view文件中创建一个login.ctpl文件,用来写前端代码,后缀名可根据自己喜好自定义。下面看下这个文件内的代码:

<html>
<head>
<title>登录</title>
</head>
<body>
<form action="http://localhost:9090/login" method="post">
用户名:<input type="text" name="username">
密码:<input type="password" name="password">
<input type="submit" value="登陆">
</form>
</body>
</html>

从代码中可知,我们的界面非常简单,就是个简单的用户名,密码的表单,且通过Post方法提交到http://localhost:9090/login

然后我们再看一下这个后端对于login的处理,同样在之前的代码基础上,在myserver.go文件中添加一个login的函数。
代码如下:

package webserver

import (
"fmt"
"net/http"
"text/template"
) func login(w http.ResponseWriter, r *http.Request) {
r.ParseForm() //解析form
fmt.Println("method: ", r.Method)
//判断传递方式
if r.Method == "GET" {
//加载界面模板
t, _ := template.ParseFiles("./view/login.ctpl")
//将解析好的模板应用到data上,这里data为nil
t.Execute(w, nil)
} else if r.Method == "POST" {
fmt.Println("username: ", r.Form["username"])
fmt.Println("password: ", r.Form["password"])
}
}

当代码执行到login函数的时候,首先我们要判断是通过什么方式传递过来的,是GET,还是POST。这个我们可以在r.Method 中取得,然后根据不同的方法分别处理。

在get方法中,我们调用了html/template包中的2个函数,下面看下这两个函数的定义吧:

  • ParseFiles
func (t *Template) ParseFiles(filenames ...string) (*Template, error)

ParseFiles方法解析filenames指定的文件里的模板定义并将解析结果与t关联。如果发生错误,会停止解析并返回nil,否则返回(t, nil)。至少要提供一个文件。

  • Execute
func (t *Template) Execute(wr io.Writer, data interface{}) error

Execute方法将解析好的模板应用到data上,并将输出写入wr。如果执行时出现错误,会停止执行,但有可能已经写入wr部分数据。模板可以安全的并发执行。

在Post方法中,这里只是做了简单的控制台打印,检测是否跟我们输入的一致。

好,我们的login函数写好了,那么我们怎么调用呢?当然是路由喽,在之前的自定义的路由中加入一个新的路由,补充myserver.go中的ServeHTTP代码如下:

type MyMux struct {
}
func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/login" {
login(w, r)
return
}
http.NotFound(w, r)
return
}

然后运行,跑起来。浏览器地址栏中输入:http://localhost:9090/login,结果如下:

在页面中输入:用户名:chain 密码:123456 并点击登录按钮。控制台结果如下:

如图所示,我们打印出来了我们的输入。

request.Form是一个url.Values类型,里面存储的是对应的类似key=value的信息,下面 展示了可以对form数据进行的一些操作:

v := url.Values{}
v.Set("name", "Ava")
v.Add("friend", "Jess")
v.Add("friend", "Sarah") v.Add("friend", "Zoe")
// v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe"
fmt.Println(v.Get("name"))
fmt.Println(v.Get("friend"))
fmt.Println(v["friend"])

Tips: Request本身也提供了FormValue()函数来获取用户提交的参数。如 r.Form[“username”]也可写成r.FormValue(“username”)。调用r.FormValue时会自动调用 r.ParseForm,所以不必提前调用。r.FormValue只会返回同名参数中的第一个,若参数不存 在则返回空字符串。

2、验证表单的输入

开发Web的一个原则就是,不能信任用户输入的任何信息,所以验证和过滤用户的输入信 息就变得非常重要,我们经常会在微博、新闻中听到某某网站被入侵了,存在什么漏洞,这 些大多是是因为网站对于用户输入的信息没有做严格的验证引起的,所以为了编写出安全 可靠的Web程序,验证表单输入的意义重大。

我们平常编写Web应用主要有两方面的数据验证,一个是在页面端的js验证(目前在这方 面有很多的插件库,比如ValidationJS插件),一个是在服务器端的验证,我们这小节讲解 的是如何在服务器端验证。

必填字段

通过len函数获取表单字段长度来判断字段是否为空:

if len(r.Form["username"][0])==0{
//为空的处理
}

r.Form对不同类型的表单元素的留空有不同的处理, 对于空文本框、空文本区域以及文件 上传,元素的值为空值,而如果是未选中的复选框和单选按钮,则根本不会在r.Form中产生 相应条目,如果我们用上面例子中的方式去获取数据时程序就会报错。所以我们需要通过 r.Form.Get()来获取值,因为如果字段不存在,通过该方式获取的是空值。但是通过 r.Form.Get()只能获取单个的值,如果是map的值,必须通过上面的方式来获取。

数字

如果我们是判断正整数,那么我们先转化成int类型,然后进行处理

age, err := strconv.Atoi(r.Form.Get("age"))
if err != nil{
fmt.Fprintf(w, "The format of the input is not correct")
}
if age < 18{
fmt.Fprintf(w, "Minors are not registered")
}

下拉框,复选框,单选框

这种的校验可以预先在服务端创建与客户端选项一致的切片,当客户端提交的时候再与服务端的相比较,决策出输入是否合理。

正则表达式

对于一些复杂的字符串可能需要用正则表达式来匹配

if m, _ := regexp.MatchString(`表达式字符串`,
r.Form.Get("key")); !m {
//错误处理
}

下面提供几种正则匹配规则:

  • 中文匹配
^[\\x{4e00}-\\x{9fa5}]+$
  • 英文
^[a-zA-Z]+$
  • 邮箱
`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`
  • 手机号码
`^(1[3|4|5|8][0-9]\d{4,8})$`
  • 身份证号码
//验证15位身份证,15位的是全部数字
`^(\d{15})$`
//验证18位身份证,18位前17位为数字,最后一位是校验位,可能为数字或字符X。
`^(\d{17})([0-9]|X)$`

例子

修改下之前的login.ctpl和login函数的代码,测试下我们的表单验证。
login.ctpl:

<html>
<head>
<title></title>
</head>
<body>
<form action="http://localhost:9090/login" method="post">
用户名:<input type="text" name="username">
年龄:<input type="number" name="age">
邮箱:<input type="text" name="email">
<input type="submit" value="按钮">
</form>
</body>
</html>

login函数

func login(w http.ResponseWriter, r *http.Request){
r.ParseForm() //解析form
fmt.Println("method: ", r.Method)
if r.Method == "GET"{
t, _ := template.ParseFiles("./view/login.ctpl")
t.Execute(w, nil)
}else if r.Method == "POST"{
if len(r.Form["username"][])=={
fmt.Fprintf(w, "username: null or empty \n")
}
age, err := strconv.Atoi(r.Form.Get("age"))
if err != nil{
fmt.Fprintf(w, "age: The format of the input is not correct \n")
}
if age < {
fmt.Fprintf(w, "age: Minors are not registered \n")
} if m, _ := regexp.MatchString(`^([\w\.\_]{,})@(\w{,}).([a-z]{,})$`,
r.Form.Get("email")); !m {
fmt.Fprintf(w, "email: The format of the input is not correct \n")
}
}
}

运行结果:

3、表单文件上传

要使表单能够上传文件,首先第一步就是要添加form的enctype属性,enctype属性有如下三种情况:

application/x-www-form-urlencoded   表示在发送前编码所有字符(默认)
multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
text/plain 空格转换为 "+" 加号,但不对特殊字符编码。

下面我们基于之前的代码基础上,在view文件夹中创建upload.ctpl文件,用来练习文件上传功能。其代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>上传文件</title>
</head>
<body>
<form enctype="multipart/form-data" action="http://127.0.0.1:9090/upload" method="post">
<input type="file" name="uploadfile" />
<input type="hidden" name="token" value="{{.}}"/>
<input type="submit" value="上传" />
</form>
</body>
</html>

从代码中可以看到,我们的form中新增了enctype属性,其值为multipart/form-data

然后,我们需要在myserver.go文件中增加对uplaod的处理方法:

func upload(w http.ResponseWriter, r *http.Request){
r.ParseForm()
if r.Method == "GET"{
time := time.Now().Unix()
h := md5.New()
h.Write([]byte(strconv.FormatInt(time,)))
token := hex.EncodeToString(h.Sum(nil))
t, _ := template.ParseFiles("./view/upload.ctpl")
t.Execute(w, token)
}else if r.Method == "POST"{
//把上传的文件存储在内存和临时文件中
r.ParseMultipartForm( << )
//获取文件句柄,然后对文件进行存储等处理
file, handler, err := r.FormFile("uploadfile")
if err != nil{
fmt.Println("form file err: ", err)
return
}
defer file.Close()
fmt.Fprintf(w, "%v", handler.Header)
//创建上传的目的文件
f, err := os.OpenFile("./files/" + handler.Filename, os.O_WRONLY | os.O_CREATE, )
if err != nil{
fmt.Println("open file err: ", err)
return
}
defer f.Close()
//拷贝文件
io.Copy(f, file)
}
}

记得建个files文件夹

增加一条路由:

func (p *MyMux)ServeHTTP(w http.ResponseWriter, r *http.Request){
if r.URL.Path == "/"{
sayHelloName(w, r)
return
}
if r.URL.Path == "/about"{
about(w, r)
return
}
if r.URL.Path == "/login"{
login(w,r)
return
}
if r.URL.Path == "/upload"{
upload(w,r)
return
}
http.NotFound(w,r)
return
}

下面我们就运行看看结果吧,浏览器地址栏中输入地址:http://localhost:9090/upload,展现出以下界面:

选择文件,并点击upload按钮:

点击upload之后,结果如下:

然后我们看到我们的目录中就多出了上传的文件:

这就说明我们的上传是成功的啊。

通过以上的代码及结果可以看到,处理文件上传我们需要调用r.ParseMultipartForm,里面的参数表示maxMemory,调用ParseMultipartForm之后,上传的文件存储在maxMemory大小的内存里面,如果文件大小超过了maxMemory,那么剩下的部分将存储在系统的临时文件中。我们可以通过r.FormFile获取上面的文件句柄,然后实例中使用了io.Copy来存储文件。

通过上面的实例我们可以看到我们上传文件主要三步处理:

  1. 表单中增加enctype=“multipart/form-data”
  2. 服务端调用r.ParseMultipartForm,把上传的文件存储在内存和临时文件中
  3. 使用r.FormFile获取文件句柄,然后对文件进行存储等处理。

客户端上传

我们上面的例子演示了如何通过表单上传文件,然后在服务器端处理文件,其实Go支持模拟客户端表单功能支持文件上传。
下面我直接在main.go文件中增加个uploadfile方法,来测试客户端上传的功能:

func uploadfile(filename string, url string){
time.Sleep( * time.Second)
bodyBuf := &bytes.Buffer{}
bodyWriter :=multipart.NewWriter(bodyBuf)
//模拟创建form表单字段
strs := strings.Split(filename, "/")
destname := strs[len(strs) - ]
filewriter, err := bodyWriter.CreateFormFile("uploadfile", destname)
if err != nil{
fmt.Println("error writing to buffer")
return
}
//打开文件句柄操作
fh, err := os.Open(filename)
if err != nil{
fmt.Println("error open file")
return
}
defer fh.Close() //拷贝文件
_, err = io.Copy(filewriter, fh)
if err != nil{
fmt.Println("error copy file")
return
}
contentType := bodyWriter.FormDataContentType()
bodyWriter.Close() resp, err := http.Post(url, contentType, bodyBuf)
if err != nil{
fmt.Println("error post buffer")
return
}
defer resp.Body.Close()
resp_body, err := ioutil.ReadAll(resp.Body)
if err != nil{
fmt.Println("error read all")
return
}
fmt.Println(resp.Status)
fmt.Println(string(resp_body))
}

在main函数中增加调用:

func main(){
go uploadfile("d:/photo/cut.png", "http://localhost:9090/upload")
webser.Start()
}

由于服务启动比较耗时,我这里为了偷懒,直接让uploadfile函数睡眠30秒再执行。这样在服务启动之后就可以自动用客户端上传文件了。运行结果如下:

然后再回去看下上传目录一样会多出一个文件:

上面的例子详细展示了客户端如何向服务器上传一个文件的例子,客户端通过multipart.Write把文件的文本流写入一个缓存中,然后调用http的Post方法把缓存传到服务器。

4、防止多次递交表单

大家看了前面的代码可能会有个疑问,那就是表单里为什么会有个token的隐藏字段呢,这个是干嘛用的呢?

不知道你是否曾经看到过一个论坛或者博客,在一个帖子或者文章后面出现多条重复的记录,这些大多数是因为用户重复递交了留言的表单引起的。由于种种原因,用户经常会重复递交表单。通常这只是鼠标的误操作,如双击了递交按钮,也可能是为了编辑或者再次核对填写过的信息,点击了浏览器的后退按钮,然后又再次点击了递交按钮而不是浏览器的前进按钮。当然,也可能是故意的——比如,在某项在线调查或者博彩活动中重复投票。那我们如何有效的防止用户多次递交相同的表单呢?

解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该惟一值的表单是否已经递交过了。如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。另外,如果是采用了Ajax模式递交表单的话,当表单递交后,通过javascript来禁用表单的递交按钮。

源码

github 源码地址

转载 Golang Web学习(14)—— 表单处理

换个语言学一下 Golang (13)——Web表单处理的更多相关文章

  1. HTML_创建易用的Web表单

    首先创建一个表单域集合fieldset fieldset元素允许Web开发者将主题相关的表单组合在一起 <fieldset></fieldset> 要说明的是本例子中每个表单都 ...

  2. 33个与众不同的Web表单设计

    表单在web设计中很重要,因为它具有直接的用户交互.创新?有趣?富有色彩?设计一个交互,需要设计师关注登陆/注册表单的设计元素. 这里有33个与众不同的web表单设计,希望能使你获得设计灵感. 1. ...

  3. ASP。NET Web表单模型,部分呈现和事件

    下载EventExample.zip - 41.33 KB 下载EventandAjaxExample.zip - 41.94 KB 介绍 通过参考ASP获得Web应用程序环境及其约束的概述.NET ...

  4. “此网页上的某个 Web 部件或 Web 表单控件无法显示或导入。找不到该类型,或该类型未注册为安全类型。”

    自从vs装了Resharper,看见提示总是手贱的想去改掉它.于是乎手一抖,把一个 可视web部件的命名空间给改了. 喏,从LibrarySharePoint.WebPart.LibraryAddEd ...

  5. 跟服务器交互的Web表单(form)

    使用HTML来构建可以跟服务器交互的Web表单(form),通过给你的form元素添加一个action属性来达到此目的. action属性的值指定了表单提交到服务器的地址. 例如: <form ...

  6. 基于Extjs的web表单设计器 第七节——取数公式设计之取数公式的使用

    基于Extjs的web表单设计器 基于Extjs的web表单设计器 第一节 基于Extjs的web表单设计器 第二节——表单控件设计 基于Extjs的web表单设计器 第三节——控件拖放 基于Extj ...

  7. 基于Extjs的web表单设计器 第六节——界面框架设计

    基于Extjs的web表单设计器 基于Extjs的web表单设计器 第一节 基于Extjs的web表单设计器 第二节——表单控件设计 基于Extjs的web表单设计器 第三节——控件拖放 基于Extj ...

  8. 基于Extjs的web表单设计器 第五节——数据库设计

    这里列出表单设计器系列的内容,6.7.8节的内容应该在春节后才有时间出了.因为这周末就请假回老家了,准备我的结婚大事.在此提前祝大家春节快乐! 基于Extjs的web表单设计器 基于Extjs的web ...

  9. Flask Web Development —— Web表单(上)

    Flask-WTF扩展使得处理web表单能获得更愉快的体验.该扩展是一个封装了与框架无关的WTForms包的Flask集成. Flask-WTF和它的依赖集可以通过pip来安装: (venv) $ p ...

  10. 使用Visual Studio 2013 从头构建Web表单

    在这篇文章中,我将采取VS 2013中特定的模板,也就是没有身份验证的Web表单模板,并说明如何构建这个项目从头开始.在本教程的最后,你会最终有一个模板,内容几乎是一样的使用Web表单模板没有认证(文 ...

随机推荐

  1. 免费公开课,讲解强大的文档集成组件Aspose,现在可报名

    课程①:Aspose.Total公开课内容:讲解全能型文档管理工具Aspose.Total主要功能及应用领域时间:2016-11-24 14:30 (暂定)报名地址:http://training.e ...

  2. JavaScript权威设计--跨域,XMLHttpRequest(简要学习笔记十九)

    1.跨域指的是什么? URL 说明 是否允许通信 http://www.a.com/a.jshttp://www.a.com/b.js 同一域名下 允许 http://www.a.com/lab/a. ...

  3. Hibernate criteria 增加排序项

  4. xhEditor用法

    xhEditor是一个基于jQuery开发的简单迷你并且高效的在线可视化HTML编辑器,而且兼容很多浏览器,所以就选它了,具体使用如下: 1 .下载xhEditor 最新版本 下载地址:http:// ...

  5. jquery的ajax和原始的ajax这两种方式的使用方法

    jquery的ajax是对原始的ajax进行的封装,方便用户的使用.下面用代码分别举例各自的使用方式. jquery的ajax发送和接收xml数据格式. $.ajax({ type: "PU ...

  6. 永久修改python默认的字符编码为utf-8

    这个修改说来简单,其实不同的系统,修改起来还真不一样.下面来罗列下3中情况 首先所有修改的动作都是要创建一个叫 sitecustomize.py的文件,为什么要创建这个文件呢,是因为python在启动 ...

  7. 转:ef获取某个表中的部分字段值

    我有个新闻表 id,title,body,createtime,author,click 使用ef4.1 仅仅读取 id,title,createtime 并显示在页面上. public static ...

  8. 关于git的简单操作

    首先这篇随笔我是不太想写的,因为网上有很多教程,我也是看廖雪峰大神的git教程自学的.还是一个小学生,就当一个学习笔记了,如果你想看大神的原版,请点击这里.我们原来都是用svn的,但是越来越觉得svn ...

  9. dpkg: 处理软件包 qjackctl (--configure)时出错解决方法

    第一步:备份 $ sudo mv /var/lib/dpkg/info /var/lib/dpkg/info.bk 第二步:新建 $ sudo mkdir /var/lib/dpkg/info 第三步 ...

  10. BZOJ4993 [Usaco2017 Feb]Why Did the Cow Cross the Road II 动态规划 树状数组

    欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目传送门 - BZOJ4993 题意概括 有上下两行长度为 n 的数字序列 A 和序列 B,都是 1 到 n 的排列,若 a ...