热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Goweb编程学习笔记——未完待续

1.1).GOPATH设置先设置自己的GOPATH,可以在本机中运行$PATH进行查看:userdeMacBook-Pro:~user$$GOPATH-bash:Users

1.

1).GOPATH设置

先设置自己的GOPATH,可以在本机中运行$PATH进行查看:

userdeMacBook-Pro:~ user$ $GOPATH
-bash: /Users/user/go: is a directory

在这可见我的GOPATH是/Users/user/go,并在该目录下生成如下作用的三个子目录:

  • src:存放源代码(比如.go .c .h .s等)
  • pkg:编译后生成的文件(比如.a)
  • bin:编译后生成的可执行文件(为了方便可将此目录加入到$PATH中,本机已添加)

 

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应用包,之后的编译安装方法有两种:

  • 一是进入对应的应用包目录,即mymath目录,然后运行go install
  • 二是在任意目录下执行go install 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 本质上可以分成两步:

  • 通过源码工具clone代码到src下面
  • 然后自动执行go install

 使用方法就是:

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)对表单的输入进行验证

因为不能够信任任何用户的输入,因此我们需要对用户的输入进行有效性验证

主要有两方面的数据验证:

  • 页面端的js验证(使用插件库,比如ValidationJS插件)
  • 服务器端的验证,这里讲的就是这种

1》必填字段

确保从表单元素中能够得到一个值,如上面例子中的username字段,使用len()获取字符串长度:

if len(r.Form["username"][0]) == 0{
    //如果为0则说明该表单元素中没有值,即为空时要做出什么处理
}
  • 当r.Form中表单元素的类型是空文本框、空文本区域以及文件上传,表单元素为空值
  • 如果类型是未选中的复选框和单选按钮,那么就不会在r.Form中产生相应的条目,用这种方法来验证会报错。所以需要使用r.Form.Get()来获取这类表单元素的值,这样当该字段不存在时会返回。但是这种方法只能获取单个值,如果是map的值,还是要使用上面的方法

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》下拉菜单

判断表单的"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

func HTMLEscape(w io.Writer, b []byte)

函数向w中写入b的HTML转义等价表示。

func HTMLEscapeString

func HTMLEscapeString(s string) string

返回s的HTML转义等价表示字符串。

func HTMLEscaper

func HTMLEscaper(args ...interface{}) string

函数返回其所有参数文本表示的HTML转义等价表示字符串。

Template类型是text/template包的Template类型的特化版本,用于生成安全的HTML文本片段

func New

func New(name string) *Template

创建一个名为name的模板。

func (*Template) Parse

func (t *Template) Parse(src string) (*Template, error)

Parse方法将字符串text解析为模板。嵌套定义的模板会关联到最顶层的t。Parse可以多次调用,但只有第一次调用可以包含空格、注释和模板定义之外的文本。如果后面的调用在解析后仍剩余文本会引发错误、返回nil且丢弃剩余文本;如果解析得到的模板已有相关联的同名模板,会覆盖掉原模板。

func (*Template) ExecuteTemplate

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

推荐阅读
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • ZSI.generate.Wsdl2PythonError: unsupported local simpleType restriction ... [详细]
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • http:my.oschina.netleejun2005blog136820刚看到群里又有同学在说HTTP协议下的Get请求参数长度是有大小限制的,最大不能超过XX ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • Webmin远程命令执行漏洞复现及防护方法
    本文介绍了Webmin远程命令执行漏洞CVE-2019-15107的漏洞详情和复现方法,同时提供了防护方法。漏洞存在于Webmin的找回密码页面中,攻击者无需权限即可注入命令并执行任意系统命令。文章还提供了相关参考链接和搭建靶场的步骤。此外,还指出了参考链接中的数据包不准确的问题,并解释了漏洞触发的条件。最后,给出了防护方法以避免受到该漏洞的攻击。 ... [详细]
  • imx6ull开发板驱动MT7601U无线网卡的方法和步骤详解
    本文详细介绍了在imx6ull开发板上驱动MT7601U无线网卡的方法和步骤。首先介绍了开发环境和硬件平台,然后说明了MT7601U驱动已经集成在linux内核的linux-4.x.x/drivers/net/wireless/mediatek/mt7601u文件中。接着介绍了移植mt7601u驱动的过程,包括编译内核和配置设备驱动。最后,列举了关键词和相关信息供读者参考。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 本文介绍了在多平台下进行条件编译的必要性,以及具体的实现方法。通过示例代码展示了如何使用条件编译来实现不同平台的功能。最后总结了只要接口相同,不同平台下的编译运行结果也会相同。 ... [详细]
author-avatar
反反复复-念念不忘
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有