表单 处理表单输入 编写 login.gtpl
文件:
1 2 3 4 5 6 7 8 9 10 11 12 <html > <head > <title > </title > </head > <body > <form action ="/login" method ="post" > 用户名:<input type ="text" name ="username" > 密码:<input type ="password" name ="password" > <input type ="submit" value ="登录" > </form > </body > </html >
处理登录的逻辑,其中 r.ParseForm()
方法用于解析参数 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport ( "fmt" "html/template" "log" "net/http" ) func login (w http.ResponseWriter, r *http.Request) { fmt.Println("method:" , r.Method) if r.Method == http.MethodGet { t, _ := template.ParseFiles("./login.gtpl" ) log.Println(t.Execute(w, nil )) } else { err := r.ParseForm() if err != nil { log.Fatal("ParseForm error:" , err) } fmt.Println("username:" , r.Form["username" ]) fmt.Println("password:" , r.Form["password" ]) } } func main () { http.HandleFunc("/login" , login) err := http.ListenAndServe(":9000" , nil ) if err != nil { log.Fatal("ListenAndServe error:" , err) } }
r.Form 里面包含了所有请求的参数,比如 URL 中 query-string 、POST 的数据 、PUT 的数据 ,所以当你在 URL 中的 query-string 字段和 POST 冲突时,会保存成一个 slice ,里面存储了多个值。
r.From 是一个 url.Values 类型的值,可以对其进行一些操作( Get、Set、Add、Del、Has ):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 type Values map [string ][]string func (v Values) Get (key string ) string { if v == nil { return "" } vs := v[key] if len (vs) == 0 { return "" } return vs[0 ] } func (v Values) Set (key, value string ) { v[key] = []string {value} } func (v Values) Add (key, value string ) { v[key] = append (v[key], value) } func (v Values) Del (key string ) { delete (v, key) } func (v Values) Has (key string ) bool { _, ok := v[key] return ok }
注意:
在处理表单的逻辑事,不要忘了对表单的输入的验证 ,不能信任用户输入的任何信息。
预防跨站脚本 对 XSS 的防护主要在于两方面:
对输入 的验证,检测攻击。
对所有输出 数据进行适当的处理,以防止任何已成功注入的脚本在浏览器端运行。
对于对输出数据的处理,Go 的 html/template 里面带有下面几个函数可以转义 :
func HTMLEscape (w io.Writer, b [] byte)
:把 b 进行转义之后写到 w 中。
func HTMLEscapeString (s string) string
:转义 s 之后返回结果字符串。
func HTMLEscaper (args ...interface {}) string
:支持多个参数一起转义,返回结果字符串
如果需要输出 HTML 标签,可以使用 template.HTML 类型,它用于封装一个已知安全的 HTML 文档片段 。它不应被第三方使用,也不能用于含有未闭合的标签或注释的 HTML 文本。
1 2 3 4 5 6 import "html/template" ... t, err := template.New("foo" ).Parse(`{{define "T"}}Hello, {{.}}!{{end}}` ) err = t.ExecuteTemplate(out, "T" , template.HTML("<script>alert('you have been pwned')</script>" ))
防止多次提交表单 解决方案是在表单中添加一个带有唯一值的隐藏字段 :
1 <input type ="hidden" name ="token" value ="{{.}}" >
在验证表单时,先检查带有该唯一值( 如 MD5(时间戳) )的表单 是否已经递交过了:
如果是,拒绝再次递交。
如果不是,则处理表单进行逻辑处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func login (w http.ResponseWriter, r *http.Request) { fmt.Println("method:" , r.Method) if r.Method == "GET" { crutime := time.Now().Unix() h := md5.New() io.WriteString(h, strconv.FormatInt(crutime, 10 )) token := fmt.Sprintf("%x" , h.Sum(nil )) t, _ := template.ParseFiles("login.gtpl" ) t.Execute(w, token) } else { r.ParseForm() token := r.Form.Get("token" ) if token != "" { } else { } fmt.Println("username length:" , len (r.Form["username" ][0 ])) fmt.Println("username:" , template.HTMLEscapeString(r.Form.Get("username" ))) fmt.Println("password:" , template.HTMLEscapeString(r.Form.Get("password" ))) template.HTMLEscape(w, []byte (r.Form.Get("username" ))) } }
处理文件上传 form 表单的 enctype 属性指明了发送到服务器时时浏览器使用的编码类型,有三种取值:
application/x-www-form-urlencoded
:默认 编码类型,在发送前编码所有的字符。
multipart/form-data
:不对字符进行编码,指定传输的数据为二进制 类型,所以可以用于文件上传。
text/plain
:空格转换为 “+” 加号,但不对特殊字符编码。
文件上传 创建名为 upload.gtpl
的模板文件用于文件上传:
1 2 3 4 5 6 7 8 9 10 11 12 <html > <head > <title > 上传文件</title > </head > <body > <form enctype ="multipart/form-data" action ="/upload" method ="post" > <input type ="file" name ="uploadfile" /> <input type ="hidden" name ="token" value ="{{.}}" /> <input type ="submit" value ="upload" /> </form > </body > </html >
编写服务器文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package mainimport ( "crypto/md5" "fmt" "html/template" "io" "net/http" "os" "strconv" "time" ) func upload (w http.ResponseWriter, r *http.Request) { fmt.Println("method:" , r.Method) if r.Method == http.MethodGet { curTime := time.Now().Unix() h := md5.New() _, err := io.WriteString(h, strconv.FormatInt(curTime, 10 )) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } token := fmt.Sprintf("%x" , h.Sum(nil )) t, _ := template.ParseFiles("upload.gtpl" ) t.Execute(w, token) } else { r.ParseMultipartForm(32 << 20 ) file, handler, err := r.FormFile("uploadfile" ) if err != nil { fmt.Println(err) return } defer file.Close() fmt.Fprintf(w, "%v" , handler.Header) f, err := os.OpenFile("./test/" +handler.Filename, os.O_WRONLY|os.O_CREATE, 0666 ) if err != nil { fmt.Println(err) return } defer f.Close() io.Copy(f, file) } } func main () { http.HandleFunc("/upload" , upload) http.ListenAndServe(":9000" , nil ) }
处理文件上传需要调用 r.ParseMultipartForm( maxMemory )
函数,其中 maxMemroy
表示文件存储在内存中的大小,如果文件大小超过了该值,则剩余的部分将存储在系统的临时文件中。使用 r.FormFile
函数可以获取文件句柄以及文件属性。
其中,文件 handler 的类型为 *multipart.FileHeader ,结构体如下:
1 2 3 4 5 6 7 8 9 type FileHeader struct { Filename string Header textproto.MIMEHeader Size int64 content []byte tmpfile string }
访问数据库 database/sql 接口 Go 定义了了一些标准的接口,开发者可以根据定义的接口来开发相应的数据库驱动。
sql.Register 1 func Register (name string , driver driver.Driver)
Register 注册并命名 一个数据库,可以在 Open 函数中使用该命名启用该驱动。
mymysql、sqlite3的驱动通过 init()
函数注册自己的数据库驱动名称以及相应的 driver 实现:
1 2 3 4 5 6 7 8 9 10 11 12 func init () { sql.Register("sqlite3" , &SQLiteDriver{}) } var d = Driver{proto: "tcp" , raddr: "127.0.0.1:3306" }func init () { Register("SET NAMES utf8" ) sql.Register("mymysql" , &d) }
而在 database/sql 内部,通过一个 map 来存储用户定义的驱动,所以可以通过注册函数 Register 同时注册多个数据库驱动:
1 var drivers = make (map [string ]driver.Driver)
sql.DB DB 中 freeConn 是一个简易的连接池 ,它的实现相当的简单:
当执行 db.prepare
-> db.prepareDC
的时候会 defer dc.releaseConn
。
调用db.putConn
,也就是把这个连接放入连接池。
每次调用 db.conn
的时候会先判断 freeConn 的长度是否大于 0,大于 0 说明有可以复用的 conn,直接拿出来用就是了,如果不大于 0,则创建一个 conn,然后再返回之。
1 2 3 4 5 6 7 type DB struct { driver driver.Driver dsn string mu sync.Mutex freeConn []driver.Conn closed bool }
driver.Driver Driver 是一个数据库驱动接口,它定义了一个方法 Open ,这个方法返回一个数据库 Conn 接口:
1 2 3 4 5 6 7 8 9 type Driver interface { Open(name string ) (Conn, error) }
注意 ,返回的 Conn 连接同一时间只能被一个 goroutine 使用。
driver.Conn Conn 是一个数据库连接的接口定义,他定义了一系列方法,这个 Conn 只能应用在一个 goroutine 里面,不能使用在多个 goroutine 里面:
1 2 3 4 5 6 7 8 9 10 11 12 13 type Conn interface { Prepare(query string ) (Stmt, error) Close() error Begin() (Tx, error) }
Prepare 函数返回与当前连接相关的执行 SQL 语句的准备状态 ,可以进行查询、删除等操作。
Close 函数关闭 当前的连接,执行释放连接拥有的资源等清理工作。因为驱动实现了 database/sql 里面建议的 conn pool,所以你不用再去实现缓存 conn 之类的,这样会容易引起问题。
Begin 函数返回一个代表事务 处理的 Tx,通过它你可以进行查询,更新等操作,或者对事务进行回滚、递交。
driver.Stmt Stmt 是一种准备好的状态,和 Conn 相关联( Stmt会绑定到一个连接 ),而且只能应用于一个 goroutine 中,不能应用于多个 goroutine:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type Stmt interface { Close() error NumInput() int Exec(args []Value) (Result, error) Query(args []Value) (Rows, error) }
Close 函数关闭 当前的链接状态,但是如果当前正在执行 query,query 还是有效返回 rows 数据。
NumInput 函数返回当前预留参数的个数 ,当返回 >=0 时数据库驱动就会智能检查调用者的参数。当数据库驱动包不知道预留参数的时候,返回 -1。
Exec 函数执行 Prepare 准备好的 SQL,传入参数执行 update/insert 等操作,返回 Result 数据。
Query 函数执行 Prepare 准备好的 SQL,传入需要的参数执行 select 操作,返回 Rows 结果集。
driver.Tx 事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以。Tx 代表一次事务 :
1 2 3 4 type Tx interface { Commit() error Rollback() error }
driver.Execer 这是一个 Conn 可选择实现的接口:
1 2 3 type Execer interface { Exec(query string , args []Value) (Result, error) }
如果这个接口没有定义,那么在调用 DB.Exec ,就会首先调用 Prepare 返回 Stmt ,然后执行 Stmt 的 Exec ,然后关闭 Stmt 。
driver.Result 这个是执行 Update/Insert 等操作返回的结果接口定义:
1 2 3 4 5 6 7 type Result interface { LastInsertId() (int64 , error) RowsAffected() (int64 , error) }
driver.Rows Rows 是执行查询得到的结果的迭代器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type Rows interface { Columns() []string Close() error Next(dest []Value) error }
Columns 函数返回查询数据库表的字段信息,这个返回的 slice 和 sql 查询的字段一一对应,而不是返回整个表的所有字段。
Close 函数用来关闭 Rows 迭代器。
Next 函数用来返回下一条数据,把数据赋值给 dest 。dest 里面的元素必须是 driver.Value 的值除了 string,返回的数据里面所有的 string 都必须要转换成 [] byte。如果最后没数据了,Next 函数最后返回 io.EOF。
driver.RowsAffected RowsAffected 其实就是一个 int64 的别名,但是他实现了 Result 接口 ,用于 insert 或 update 操作,这些操作会修改零到多行数据。:
1 2 3 4 5 type RowsAffected int64 func (RowsAffected) LastInsertId () (int64 , error) func (v RowsAffected) RowsAffected () (int64 , error)
driver.Value
Value 是驱动必须能处理的值。它要么是nil,要么是如下类型的实例:
1 2 3 4 5 6 int64 float64 bool []byte string [*] Rows.Next不会返回该类型值time.Time
driver.ValueConverter ValueConverter 接口定义了如何把一个普通的值转化成 driver.Value 的接口:
1 2 3 type ValueConverter interface { ConvertValue(v interface {}) (Value, error) }
在开发的数据库驱动包里面实现这个接口的函数在很多地方会使用到,这个 ValueConverter 有很多好处:
转化 driver.value 到数据库表相应的字段,例如 int64 的数据如何转化成数据库表 uint16 字段。
把数据库查询结果转化成 driver.Value 值。
在 scan 函数里面如何把 driver.Value 值转化成用户定义的值。
driver.Valuer Valuer 接口定义了返回一个 driver.Value 的方式,很多类型都实现了这个 Value 方法,用来自身与 driver.Value 的转化。
1 2 3 type Valuer interface { Value() (Value, error) }
MySQL数据库 github.com/go-sql-driver/mysql 为支持 MySQL 的驱动,它支持 database/sql 标准。
打开驱动 1 2 3 4 5 6 7 dsn := "user:password@tcp(ip:port)/dbname?charset=utf8" db, err := sql.Open("mysql" , dsn) if err != nil { panic (err) } defer db.Close()
插入数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 stmt, err := db.Prepare("INSERT userinfo SET username=?,department=?,created=?" ) if err != nil { log.Fatal(err) } res, err := stmt.Exec("dawn" , "研发部门" , "2022-05-15" ) if err != nil { log.Fatal(err) } id, err := res.LastInsertId() if err != nil { log.Fatal(err) } fmt.Println(id)
更新数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 stmt, err = db.Prepare("update userinfo set username=? where uid=?" ) if err != nil { log.Fatal(err) } res, err = stmt.Exec("zh" , id) if err != nil { log.Fatal(err) } affect, err := res.RowsAffected() if err != nil { log.Fatal(err) } fmt.Println(affect)
查询数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 rows, err := db.Query("SELECT * FROM userinfo" ) if err != nil { log.Fatal(err) } for rows.Next() { var uid int var username string var department string var created string err = rows.Scan(&uid, &username, &department, &created) if err != nil { log.Fatal(err) } }
删除数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 stmt, err = db.Prepare("delete from userinfo where uid=?" ) if err != nil { log.Fatal(err) } res, err = stmt.Exec(id) if err != nil { log.Fatal(err) } affect, err = res.RowsAffected() if err != nil { log.Fatal(err) } fmt.Println(affect)