1.
1).GOPATH设置
先设置自己的GOPATH,可以在本机中运行$PATH进行查看:
userdeMacBook-Pro:~ user$ $GOPATH -bash: /Users/user/go: is a directory
在这可见我的GOPATH是/Users/user/go,并在该目录下生成如下作用的三个子目录:
2).应用目录结构
然后之后如果想要自己新建应用或者一个代码包都是在src目录下新建一个文件夹,文件夹一般是代码包名称,比如$GOPATH/src/mymath/sqrt.go,在这里,包名就是mymath,然后其代码中的包名写成package mymath,比如:
package mymath func Sqrt(x float64) float64{ z := 0.0 for i := 0; i <1000; i++ { z -= ( z * z - x ) / ( 2 * x ) } return z }
当然也允许多级目录,例如在src下面新建了目录$GOPATH/src/github.com/astaxie/beedb,在这里包路径就是github.com/astaxie/beedb,包名称为最后一个目录beedb
3).编译应用
假设上面我们建好了自己的mymath应用包,之后的编译安装方法有两种:
编译安装好后,我们就能够到$GOPATH/pkg/${GOOS}_${GOARCH}目录下看见mymath.a这个应用包
${GOOS}_${GOARCH}是平台名,如mac系统是darwin_amd64,linux是linux_amd64
userdeMacBook-Pro:src user$ cd mymath/ userdeMacBook-Pro:mymath user$ ls sqrt.go userdeMacBook-Pro:mymath user$ go install userdeMacBook-Pro:mymath user$ cd .. userdeMacBook-Pro:src user$ cd .. userdeMacBook-Pro:go user$ cd pkg userdeMacBook-Pro:pkg user$ cd darwin_amd64/ userdeMacBook-Pro:darwin_amd64 user$ ls golang.org mymath.a userdeMacBook-Pro:darwin_amd64 user$
4).调用应用
然后就是对该应用进行调用
比如我们再新建一个应用包mathapp,创建一个main.go源码:
package main import( "mymath" "fmt" ) func main() { fmt.Printf("Hello, world. Sqrt(2) = %v \n", mymath.Sqrt(2)) }
然后进入该应用目录,运行go build来编译程序:
userdeMacBook-Pro:src user$ cd mathapp/ userdeMacBook-Pro:mathapp user$ ls main.go userdeMacBook-Pro:mathapp user$ go build userdeMacBook-Pro:mathapp user$ ls main.go mathapp userdeMacBook-Pro:mathapp user$
然后运行该可执行文件,./mathapp,得到返回结果为:
userdeMacBook-Pro:mathapp user$ ./mathapp Hello, world. Sqrt(2) = 1.414213562373095
⚠️
package
package main : 说明该文件是一个可独立执行的文件,它在编译后会产生可执行文件
除了main包外,其他包都会生成*.a文件(也就是包文件),并放在$GOPATH/pkg/${GOOS}_${GOARCH}目录下
每一个可独立执行的go程序中,必定都包含一个package main,在这个main包中必定包含一个入口函数main(),该函数即没有参数,也没有返回值
5).获取远程包
如果你想要获取的是一个远程包,可以使用go get获取,其支持多数的开源社区(如github、googlecode、bitbucket、Launchpad),运行语句为:
go get github.com/astaxie/beedb
go get -u参数可以自动更新包,并且在使用go get时会自动获取该包依赖的其他第三方包
userdeMBP:~ user$ go get github.com/astaxie/beedb userdeMBP:~ user$ cd go/src userdeMBP:src user$ ls mymath golang.org mathapp github.com userdeMBP:src user$ cd github.com/ userdeMBP:github.com user$ ls WeMeetAgain astaxie btcsuite conformal userdeMBP:github.com user$ cd astaxie/ userdeMBP:astaxie user$ ls beedb userdeMBP:astaxie user$ cd ../../.. userdeMBP:go user$ cd pkg userdeMBP:pkg user$ ls darwin_amd64 userdeMBP:pkg user$ cd darwin_amd64/ userdeMBP:darwin_amd64 user$ ls github.com golang.org mymath.a userdeMBP:darwin_amd64 user$ cd github.com/astaxie/ userdeMBP:astaxie user$ ls beedb.a
通过这个命令可以获取相应的源码,对应的开源平台采用不同的源码控制工具,如github采用git,googlecode采用hg。因此想要使用哪个平台的代码就要对应安装相应的源码控制工具
上面的代码在本地的代码结构为:
go get 本质上可以分成两步:
使用方法就是:
import github.com/astaxie/beedb
2.相关http内容可见go标准库的学习-net/http
3.表单学习——form
1)如何处理表单的输入
举例:
package main import( "fmt" "net/http" "log" ) func index(w http.ResponseWriter, r *http.Request){ r.ParseForm() //解析URL传递的参数,对于POST则解析响应包的主体(request body),如果不调用它则无法获取表单的数据 fmt.Println(r.Form) fmt.Println(r.PostForm) fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) //如果使用的是方法FormValue()方法(它只会返回同名参数slice中的第一个,不存在则返回空字符串),则可以不用调用上面的ParseForm()方法 for k, v := range r.Form{ fmt.Println("key :", k) fmt.Println("value :", v) } html := ` "http://localhost:9090/login" method="post"> username: "text" name="username"> password: "text" name="password"> "submit" value="login"> ` fmt.Fprintf(w, html) //将html写到w中,w中的内容将会输出到客户端中 } func login(w http.ResponseWriter, r *http.Request){ fmt.Println("method", r.Method) //获得请求的方法 r.ParseForm() fmt.Println(r.Form) fmt.Println(r.PostForm) fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) if r.Method == "POST"{ fmt.Println("username : ", r.Form["username"]) fmt.Println("password : ", r.Form["password"]) } } func main() { http.HandleFunc("/", index) //设置访问的路由 http.HandleFunc("/login", login) //设置访问的路由 err := http.ListenAndServe(":9090", nil) //设置监听的端口 if err != nil{ log.Fatal("ListenAndServe : ", err) } }
调用http://localhost:9090/后,浏览器返回:
终端返回:
userdeMBP:go-learning user$ go run test.go map[] map[] path / scheme []
浏览器访问http://localhost:9090/login后终端变成:
userdeMBP:go-learning user$ go run test.go map[] map[] path / scheme [] method POST map[username:[hello] password:[world]] map[username:[hello] password:[world]] path /login scheme [] username : [hello] password : [world]
r.Form里面包含所有请求的参数,比如URL中query-string、POST的数据、PUT的数据
当你URL的query-string字段和POST的字段冲突时,该值会被保存成一个slice存储在一起
比如把index函数中html值中的action改成http://localhost:9090/login?username=allen,如下:
"http://localhost:9090/login?username=allen" method="post">
此时的终端为:
method POST map[password:[world] username:[hello allen]] map[password:[world] username:[hello]] path /login scheme [] username : [hello allen] password : [world]
可见r.PostForm中不会存放URL中query-string的数据
2)对表单的输入进行验证
因为不能够信任任何用户的输入,因此我们需要对用户的输入进行有效性验证
主要有两方面的数据验证:
1》必填字段
确保从表单元素中能够得到一个值,如上面例子中的username字段,使用len()获取字符串长度:
if len(r.Form["username"][0]) == 0{ //如果为0则说明该表单元素中没有值,即为空时要做出什么处理 }
2》数字
确保从表单获取的是数字,比如年龄
getInt, err := strconv.Atoi(r.Form.Get("age")) if err != nil { //这就说明数字转化出错了,即输入的可能不是数字,这里进行错误的操作 } //如果确定是数字则继续进行下面的操作 if getInt > 100{ //判断年龄的大小范围的问题 }
还有另一种方法就是使用正则表达式:
if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m{ //如果没有匹配项,则!m为true,说明输入的不是数字 return false }
3》中文
保证从表单中获取的是中文,使用正则表达式
if m, _ := regexp.MatchString("^[\\x{4e00}-\\x{9fa5}]+$", r.Form.Get("realname")); !m{ //如果没有匹配项,则!m为true,说明输入的不是中文 return false }
4》英文
保证从表单中获取的是英文,使用正则表达式
if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("englishname")); !m{ //如果没有匹配项,则!m为true,说明输入的不是英文 return false }
5》电子邮件
查看用户输入的电子邮件是否正确
if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m{ //如果没有匹配项,则!m为true,说明输入邮箱格式不对 return false }
6》手机号码
if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m{ //如果没有匹配项,则!m为true,说明输入电话号码格式不对 return false }
7》下拉菜单
判断表单的
<select name"fruit"> select>
验证方法为:
slice := []string{"apple", "pear", "banana"} for _, v := range slice{ if v == r.Form.Get("fruit"){ return true } } return false
8》单选按钮
单选按钮
"radio" name="gender" value="1">男 "radio" name="gender" value="2">女
验证方法:
slice := []int {1,2} for _, v := range slice{ if v == r.Form.Get("gender"){ return true } } return false
9》复选框
选定用户选中的都是你提供的值,不同之处在于接受到的数据是一个slice
"checkbox" name="interest" value="football">足球 "checkbox" name="interest" value="basketball">篮球 "checkbox" name="interest" value="tennis">网球
验证:
slice := []string{"football", "basketball", "tennis"} a := Slice_diff(r.Form["interest"], slice) if a == nil{//说明接收到的数据中的值都来自slice return true } return false
10》时间和日期
使用time处理包
11》身份证号
//验证15位身份证,15位都是数字 if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m{ //如果没有匹配项,则!m为true,说明输入身份证格式不对 return false } //验证18位身份证,前17位都是数字,最后一位是校验码,可能是数字和X if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m{ //如果没有匹配项,则!m为true,说明输入身份证格式不对 return false }
3)预防跨站脚本
因为现在的网站含有大量动态内容以提高用户体验,动态站点会受到名为“跨站脚本攻击”(即XSS)的威胁,静态站点则不受影响
攻击者会在有漏洞的程序中插入攻击的JavaScript、Vbscript、ActiveX或Flash来欺骗用户在这上面进行操作来盗取用户的账户信息、修改用户设置、盗取/污染COOKIE和植入恶意广告等。
两种防护方法:
该适当的处理使用的是html/template中的函数进行转义:
func HTMLEscape(w io.Writer, b []byte)
函数向w中写入b的HTML转义等价表示。
func HTMLEscapeString(s string) string
返回s的HTML转义等价表示字符串。
func HTMLEscaper(args ...interface{}) string
函数返回其所有参数文本表示的HTML转义等价表示字符串。
Template类型是text/template包的Template类型的特化版本,用于生成安全的HTML文本片段。
func New(name string) *Template
创建一个名为name的模板。
func (t *Template) Parse(src string) (*Template, error)
Parse方法将字符串text解析为模板。嵌套定义的模板会关联到最顶层的t。Parse可以多次调用,但只有第一次调用可以包含空格、注释和模板定义之外的文本。如果后面的调用在解析后仍剩余文本会引发错误、返回nil且丢弃剩余文本;如果解析得到的模板已有相关联的同名模板,会覆盖掉原模板。
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
ExecuteTemplate方法类似Execute,但是使用名为name的t关联的模板产生输出。
因为HTTP是一种无状态的协议,那么要如何判别是否为同一个用户。一般是使用COOKIE(COOKIE是存储在客户端的信息,能够每次通过header和服务器进行交互)
更详细的内容可见go标准库的学习-text/template
举例:
package main import( "fmt" "net/http" "log" "html/template" ) func index(w http.ResponseWriter, r *http.Request){ r.ParseForm() //解析URL传递的参数,对于POST则解析响应包的主体(request body),如果不调用它则无法获取表单的数据 fmt.Println(r.Form) fmt.Println(r.PostForm) fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) //如果使用的是方法FormValue()方法(它只会返回同名参数slice中的第一个,不存在则返回空字符串),则可以不用调用上面的ParseForm()方法 for k, v := range r.Form{ fmt.Println("key :", k) fmt.Println("value :", v) } fmt.Fprintf(w, "hello world") //将html写到w中,w中的内容将会输出到客户端中 } func login(w http.ResponseWriter, r *http.Request){ fmt.Println("method", r.Method) //获得请求的方法 r.ParseForm() if r.Method == "GET"{ // html := ` "http://localhost:9090/login" method="post"> username: "text" name="username"> password: "text" name="password"> "submit" value="login"> ` t := template.Must(template.New("test").Parse(html)) t.Execute(w, nil) }else{ fmt.Println("username : ", template.HTMLEscapeString(r.Form.Get("username")))//在终端即客户端输出 fmt.Println("password : ", template.HTMLEscapeString(r.Form.Get("password")))//把r.Form.Get("password")转义之后返回字符串 template.HTMLEscape(w, []byte(r.Form.Get("username"))) //在客户端输出,把r.Form.Get("username")转义后写到w } } func main() { http.HandleFunc("/", index) //设置访问的路由 http.HandleFunc("/login", login) //设置访问的路由 err := http.ListenAndServe(":9090", nil) //设置监听的端口 if err != nil{ log.Fatal("ListenAndServe : ", err) } }
访问http://localhost:9090/
访问http://localhost:9090/login
如果仅传入字符串:
服务端返回:
method POST username : hello password : allen map[] map[] path /favicon.ico scheme []
客户端:
当时如果username输入的是
客户端返回:
可见html/template包默认帮你过滤了html标签
如果你想要内容不被转义,方法有:
1》使用text/template
import ( "text/template" "os" ) ... t, err := template.New("test").Parse(`{{define "T"}} Hello, {{.}}!{{end}}`) err := template.ExecuteTemplate(os.Stdout, "T", "")
2》使用html/template,和template.HTML
import ( "html/template" "os" ) ... t, err := template.New("test").Parse(`{{define "T"}} Hello, {{.}}!{{end}}`) err := template.ExecuteTemplate(os.Stdout, "T", template.HTML(""))
4)防止多次递交表单
解决办法是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该唯一值的表单是否已经提交过,如果是,则拒绝再次提交;如果不是,则处理表单进行逻辑处理。
如果使用的是Ajax模式递交表单的话,当表单递交后,通过Javascript来禁用表单的递交按钮
比如我们能够使用MD5(时间戳)来获取唯一值,如time.Now().Unix()
举例:
package main import( "fmt" "net/http" "log" "text/template" "crypto/md5" "time" "io" "strconv" ) func index(w http.ResponseWriter, r *http.Request){ r.ParseForm() //解析URL传递的参数,对于POST则解析响应包的主体(request body),如果不调用它则无法获取表单的数据 fmt.Println(r.Form) fmt.Println(r.PostForm) fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) //如果使用的是方法FormValue()方法(它只会返回同名参数slice中的第一个,不存在则返回空字符串),则可以不用调用上面的ParseForm()方法 for k, v := range r.Form{ fmt.Println("key :", k) fmt.Println("value :", v) } fmt.Fprintf(w, "hello world") //将html写到w中,w中的内容将会输出到客户端中 } func login(w http.ResponseWriter, r *http.Request){ fmt.Println("method", r.Method) //获得请求的方法 if r.Method == "GET"{ // html := ` "http://localhost:9090/login" method="post"> username: "text" name="username"> password: "text" name="password"> "hidden" name="token" value="{{.}}"> "submit" value="login"> ` crutime := time.Now().Unix() h := md5.New() io.WriteString(h, strconv.FormatInt(crutime, 10)) token := fmt.Sprintf("%x", h.Sum(nil)) t := template.Must(template.New("test").Parse(html)) t.Execute(w, token) }else{ r.ParseForm() token := r.Form.Get("token") if token != ""{ //验证token的合法性 }else{ //如果不存在token,则报错 log.Fatal("not token") } 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"))) //在客户端输出 } } func main() { http.HandleFunc("/", index) //设置访问的路由 http.HandleFunc("/login", login) //设置访问的路由 err := http.ListenAndServe(":9090", nil) //设置监听的端口 if err != nil{ log.Fatal("ListenAndServe : ", err) } }
浏览器中访问http://localhost:9090/login
可见得到的token时间戳为:"7cf962884609e3810259654d1e766754"
该方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措。但是它不能够排除所有的欺骗性的动机,对此类情况还需要更加复杂的工作
5)处理文件上传——大文件
要使得表单能够上传文件,首先就是要添加form的encrype属性,该属性有三种情况:
举例:
通过表单上传文件,在服务器端处理文件
package main import( "fmt" "net/http" "log" "text/template" "crypto/md5" "time" "io" "strconv" "os" ) func index(w http.ResponseWriter, r *http.Request){ r.ParseForm() //解析URL传递的参数,对于POST则解析响应包的主体(request body),如果不调用它则无法获取表单的数据 fmt.Println(r.Form) fmt.Println(r.PostForm) fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) //如果使用的是方法FormValue()方法(它只会返回同名参数slice中的第一个,不存在则返回空字符串),则可以不用调用上面的ParseForm()方法 for k, v := range r.Form{ fmt.Println("key :", k) fmt.Println("value :", v) } fmt.Fprintf(w, "hello world") //将html写到w中,w中的内容将会输出到客户端中 } func upload(w http.ResponseWriter, r *http.Request){ fmt.Println("method", r.Method) //获得请求的方法 if r.Method == "GET"{ // html := ` "multipart/form-data" action="http://localhost:9090/upload" method="post"> "file" name="uploadfile" /> "hidden" name="token" value="{{.}}" /> "submit" value="upload" /> ` crutime := time.Now().Unix() h := md5.New() io.WriteString(h, strconv.FormatInt(crutime, 10)) token := fmt.Sprintf("%x", h.Sum(nil)) t := template.Must(template.New("test").Parse(html)) t.Execute(w, token) }else{ r.ParseMultipartForm(32 <<20) //表示maxMemory,调用ParseMultipart后,上传的文件存储在maxMemory大小的内存中,如果大小超过maxMemory,剩下部分存储在系统的临时文件中 file, handler, err := r.FormFile("uploadfile") //根据input中的name="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("/", index) //设置访问的路由 http.HandleFunc("/upload", upload) //设置访问的路由 err := http.ListenAndServe(":9090", nil) //设置监听的端口 if err != nil{ log.Fatal("ListenAndServe : ", err) } }
获取其他非文件字段信息的时候就不需要调用r.ParseForm,因为在需要的时候Go自动会去调用。而且ParseMultipartForm调用一次之后,后面再调用不会再有效果
浏览器中返回handler.Header:
test文件夹中也生成了该传入test.txt的副本:
⚠️如果上面的表单form没有设置enctype="multipart/form-data"就会报错:
Content-Type isn't multipart/form-data
上传文件主要三步处理:
客户端上传文件
举例:
package main import( "fmt" "net/http" "io/ioutil" "bytes" "mime/multipart" "os" "io" ) func postFile(filename string, targetUrl string) error { bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf)//把文件的文本流写入一个缓存中,然后调用http.Post方法把缓存传入服务器 //关键操作 fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename) //使用给出的属性名和文件名创建一个新的form-data头。 fmt.Println(fileWriter) //&{0xc00008acc0 false} if err != nil{ fmt.Println("error writing to buffer") return err } //打开文件句柄操作 fh, err := os.Open(filename) if err != nil{ fmt.Println("error open file") return err } //复制 _, err = io.Copy(fileWriter, fh) if err != nil{ return err } contentType := bodyWriter.FormDataContentType()//返回bodyWriter对应的HTTP multipart请求的Content-Type的值,多以multipart/form-data起始。 fmt.Println(contentType) //multipart/form-data; boundary=b7c3357b23c6a6697af5810d1c0dc0184912ae24c5f5074db8aae0fe5198 bodyWriter.Close() resp, err := http.Post(targetUrl, contentType, bodyBuf) if err != nil { return err } defer resp.Body.Close() resp_body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } fmt.Println(resp.Status) //200 OK fmt.Println(string(resp_body)) //map[Content-Disposition:[form-data; name="uploadfile"; filename="./testFmt.txt"] Content-Type:[application/octet-stream]] return nil } func main() { targetUrl := "http://localhost:9090/upload" filename := "./testFmt.txt" postFile(filename, targetUrl) }
运行之前服务端的同时调用该客户端,返回如上数据,然后可见相应的test文件夹中生成了testFmt.txt:
4.访问数据库
1)database/sql接口
更详细的内容可看go标准库的学习-database/sql/driver和go标准库的学习-database/sql
go和PHP不同的地方是Go没有官方提供数据库驱动,而是为开发者开发数据库驱动定义了一些标准接口,开发者可以根据定义的接口来开发相应的数据库驱动。
这样的好处是只要按照标准接口开发的代码,以后需要迁移数据库时,不需要任何更改。
1》sql.Register - 在database/sql中
该函数用来注册数据库驱动。当第三方开发者开发数据库驱动时,都会实现init函数,在init里面调用这个Register(name string, driver driver.Driver)完成本驱动的注册,比如
1>sqlite3的驱动:
//http://github.com/mattn/go-sqlite3驱动 func init(){ sql.Register("sqlite3", &SQLiteDriver{}) }
2>mysql的驱动
//http://github.com/mikespook/mymysql驱动 var d = Driver{proto : "tcp", raddr : "127.0.0.1:3306"} func init(){ Register("SET NAMES utf8") sql.Register("mymysql", &d) }
由上可见第三方数据库驱动都是通过这个函数来注册自己的数据库驱动名称及相应的driver实现。
上面的例子实现的都是注册一个驱动,该函数还能够实现同时注册多个数据库驱动,只要这些驱动不重复,通过一个map来存储用户定义的相应驱动
var drivers = make(map[string]driver.Driver) drivers[name] = driver
在使用database/sql接口和第三方库时经常看见如下:
import( "database/sql" _ "github.com/mattn/go-sqlite3" //上面定义的sqlite3驱动包 )
里面的_的作用就是说明引入了"github.com/mattn/go-sqlite3"该包,但是不直接使用包里面的函数或变量,其中init函数也不自动调用。因此我们之后需要自己手动去调用init函数。
2》driver.Driver - 在database/sql/driver中
Driver是一个数据库驱动的接口,其定义了一个Open(name string)方法,该方法返回一个数据库的Conn接口:
type Driver interface { // Open返回一个新的与数据库的连接,参数name的格式是驱动特定的。 // // Open可能返回一个缓存的连接(之前关闭的连接),但这么做是不必要的; // sql包会维护闲置连接池以便有效的重用连接。 // // 返回的连接同一时间只会被一个go程使用。 Open(name string) (Conn, error) }
因为返回的连接同一时间只会被一个go程使用,所以返回的Conn只能用来进行一次goroutine操作,即不能把这个Conn应用于Go的多个goroutine中,否则会出现错误,如:
go goroutineA(Conn) //执行查询操作 go goroutineB(Conn) //执行插入操作
这样的代码会使Go不知某个操作到底是由哪个goroutine发起的从而导致数据混乱。即可能会讲goroutineA里面执行的查询操作的结果返回给goroutineB,从而让goroutineB将此结果当成自己执行的插入数据
3》driver.Conn - 在database/sql/driver中
Conn是一个数据连接的接口定义。这个Conn只能应用在一个goroutine中,如上所说。
type Conn interface { // Prepare返回一个准备好的、绑定到该连接的状态。 Prepare(query string) (Stmt, error) // Close作废并停止任何现在准备好的状态和事务,将该连接标注为不再使用。 // // 因为sql包维护着一个连接池,只有当闲置连接过剩时才会调用Close方法, // 驱动的实现中不需要添加自己的连接缓存池。 Close() error // Begin开始并返回一个新的事务。 Begin() (Tx, error) }
Prepare函数返回与当前连接相关的SQL语句的准备状态,可以进行查询、删除等操作
Close函数关闭当前的连接,执行释放连接拥有的资源等清理工作。因为驱动实现了database/sql中建议的conn pool,所以不用再去实现缓存conn之类的,这样会更容易引起问题
Begin函数返回一个代表事务处理的Tx,通过它你可以进行查询、更新等操作,或者对事务进行回滚、递交
4》driver.Stmt - 在database/sql/driver中
Stmt是一种准备好的状态,绑定到一个Conn中,并只能应用在一个goroutine中。
type Stmt interface { // Close关闭Stmt。 // // 和Go1.1一样,如果Stmt被任何查询使用中的话,将不会被关闭。 Close() error // NumInput返回占位参数的个数。 // // 如果NumInput返回值 >= 0,sql包会提前检查调用者提供的参数个数, // 并且会在调用Exec或Query方法前返回数目不对的错误。 // // NumInput可以返回-1,如果驱动占位参数的数量不知时。 // 此时sql包不会提前检查参数个数。 NumInput() int // Exec执行查询,而不会返回结果,如insert或update。 Exec(args []Value) (Result, error) // Query执行查询并返回结果,如select。 Query(args []Value) (Rows, error) }
Close函数关闭当前的连接状态,但是如果当前正在执行query,query还是会有效地返回rows数据
Exec函数执行Conn的Prepare准备好的sql,传入参数执行update/insert等操作,返回Result数据
Query函数执行Conn的Prepare准备好的sql,传入需要的参数执行select操作,返回Rows结果集
5》driver.Tx - 在database/sql/driver中
事务处理一般就两个过程,递交或回滚,即下面的两个函数:
type Tx interface { Commit() error Rollback() error }
6》driver.Execer - 在database/sql/driver中
这是一个Conn可选择实现的接口
type Execer interface { Exec(query string, args []Value) (Result, error) }
如果一个Conn未实现Execer接口,sql包的DB.Exec会首先准备一个查询(即调用Prepare返回Stmt),执行状态(即执行Stmt的Exec函数),然后关闭状态(即关闭Stmt)。Exec可能会返回ErrSkip。
7》driver.Result
这是是执行Update/insert等操作返回的结果接口定义
type Result interface { // LastInsertId返回insert等命令后数据库自动生成的ID LastInsertId() (int64, error) // RowsAffected返回被查询影响的行数 RowsAffected() (int64, error) }
8》driver.Rows
Rows是执行查询返回的结果集接口定义
type Rows interface { // Columns返回各列的名称,列的数量可以从切片长度确定。 // 如果某个列的名称未知,对应的条目应为空字符串。 Columns() []string // Close关闭Rows。 Close() error // 调用Next方法以将下一行数据填充进提供的切片中,即返回下一条数据,并把数据返回给dest。 // 提供的切片必须和Columns返回的切片长度相同。 // // 切片dest可能被填充同一种驱动Value类型,但字符串除外;即dest里面的元素必须是driver.Vlaue的值,除了string。 // 所有string值都必须转换为[]byte。 // // 当没有更多行时,Next应返回io.EOF。 Next(dest []Value) error }
Columns函数返回查询数据库表的字段信息,返回的slice和sql查询的字段一一对应,而不是返回整个表的所有字段
9》driver.RowsAffected
type RowsAffected int64
RowsAffected其实就是int64的别名,但是它实现了Result接口,用来底层实现Result的表示方式
RowsAffected实现了Result接口,用于insert或update操作,这些操作会修改零到多行数据。
10》driver.Value
type Value interface{}
Value其实就是一个空接口,它可以容纳任何数据
driver.Value是驱动必须能够操作的Value,所以Value要么是nil,要么是下面的任意一种:
int64 float64 bool []byte string [*] Rows.Next不会返回该类型值 time.Time
11》driver.ValueConverter
ValueConverter接口定义了一个如何把一个普通值转化成driver.Value的接口
type ValueConverter interface { // ConvertValue将一个值转换为驱动支持的Value类型 ConvertValue(v interface{}) (Value, error) }
ValueConverter接口提供了ConvertValue方法。
driver包提供了各种ValueConverter接口的实现,以保证不同驱动之间的实现和转换的一致性。ValueConverter接口有如下用途:
12》driver.Valuer
type Valuer interface { // Value返回一个驱动支持的Value类型值 Value() (Value, error) }
Valuer接口定义了一个返回driver.Value的方法
很多类型都实现了这个Value方法,用来实现自身与driver.Value的转换
一个驱动driver只要实现了上面的这些接口就能够完成增删改查等基本操作,剩下的就是与相应的数据库进行数据交互等细节问题了
2)使用MySQL数据库
1.MySQL驱动
Go中支持MySQL的驱动很多,有些支持database/sql标准,有些采用的是自己的实现接口。常用的有下面的几种:
在这里我们使用的是第一个驱动
首先可见该驱动源码中mysql/driver.go为:
import ( "database/sql" "database/sql/driver" "net" "sync" ) type MySQLDriver struct{} func init() { sql.Register("mysql", &MySQLDriver{}) } func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { ... } ...
当第三方开发者开发数据库驱动时,都会实现init函数来完成本驱动的注册,这样才能在Open时使用"mysql"作为其参数driverName的值,说明打开的是上面注册的mysql驱动
首先先在mysql中创建数据库test,并生成两个表,一个是用户表userinfo,一个是关联用户信息表userdetail。使用workbench进行创建,首先创建数据库test:
CREATE SCHEMA `test` DEFAULT CHARACTER SET utf8 ;
然后创建表:
use test; create table `userinfo` ( `uid` int(10) not null auto_increment, `username` varchar(64) null default null, `department` varchar(64) null default null, `created` date null default null, primary key (`uid`) ); create table `userdetail`( `uid` int(10) not null default '0', `intro` text null, `profile` text null, primary key (`uid`) );
接下来就示范怎么使用database/sql接口对数据库进行增删改查操作:
当然运行前首先需要下载驱动:
go get -u github.com/go-sql-driver/mysql