Go语言MySql基本操作

基本使用

首先是 导入 驱动包

go get -u github.com/go-sql-driver/mysql

然后连接本地的mysql

package main

import (
   "database/sql"
   // 匿名导入,代表 import的时候 会自动执行 这个库里面的init方法
   _ "github.com/go-sql-driver/mysql"
)

func main() {
   dsn := "root:1234qwer@tcp(127.0.0.1:3306)/dbname"
   db, error := sql.Open("mysql", dsn)
// defer db.Close() 这个地方defer 不能写在error判断的前面
//  因为 如果open失败 db一般都是nil nil的close一般就报错了 而且打印不出关键的数据库
//  失败信息
   if error != nil {
      panic(error)
   }
   // 做完错误检查之后  确保db不为nil 在close()
   defer db.Close()
}
复制代码

但是大家要注意了 上述的代码 并不会真正的链接一个数据库

open函数 只是验证参数格式 是不是正确, 要真正的链接数据库 要使用对应的ping方法

package main

import (
   "database/sql"
   "fmt"

   // 匿名导入,代表 import的时候 会自动执行 这个库里面的init方法
   _ "github.com/go-sql-driver/mysql"
)


func main() {
   dsn := "root:1234qwer@tcp(127.0.0.1:3306)/dbname"
   db, error := sql.Open("mysql", dsn)
   // defer db.Close() 这个地方defer 不能写在error判断的前面
   //  因为 如果open失败 db一般都是nil nil的close一般就报错了 而且打印不出关键的数据库
   //  失败信息
   if error != nil {
      panic(error)
   }
   // 做完错误检查之后  确保db不为nil 在close()
   defer db.Close()

   error = db.Ping()
   if error != nil {
      fmt.Println("db connect failed")
      panic(error)
   }
   fmt.Println("db connect success")
}
复制代码

image.png

看一下,明显的可以看到 这里连接数据库失败了,因为我们没有这个名为dbname的数据库

改一下 ,链接到我们本地的go test数据库 这样就正确了

image.png

修改一下 上述的代码 抽出来一个函数来做 init sql的操作

package main

import (
   "database/sql"
   "fmt"

   // 匿名导入,代表 import的时候 会自动执行 这个库里面的init方法
   _ "github.com/go-sql-driver/mysql"
)

// 定义一个全局对象 这是并发安全的对象
var db *sql.DB

func initMysql() (err error) {
   dsn := "root:1234qwer@tcp(127.0.0.1:3306)/go_test"
   db, err = sql.Open("mysql", dsn)
   // defer db.Close() 这个地方defer 不能写在error判断的前面
   //  因为 如果open失败 db一般都是nil nil的close一般就报错了 而且打印不出关键的数据库
   //  失败信息
   if err != nil {
      return err
   }

   err = db.Ping()
   if err != nil {
      return err
   }
   return nil
}

func main() {
   err := initMysql()
   if err != nil {
      panic(err)
   }
   defer db.Close()
   fmt.Println("db connect success")
}
复制代码

当然一般情况下 我们还要去根据业务的具体情况 去配置一下db的连接数 属性

// 连接的最大时间 
db.SetConnMaxLifetime(time.Second * 10)
// 设置最大的连接数 默认是无限制 如果超出限制了 就会排队等待
db.SetMaxOpenConns(200)
// 设置最大的空闲连接数 默认是无限制 业务量小的时候 可以把多余的连接释放掉,只保留一定数量的连接数
db.SetMaxIdleConns(10)
复制代码

sql驱动工作流程 简单介绍

前面我们提到过 匿名导包的概念 提到有一个init函数 会默默的帮我们做一下初始化的操作

image.png

image.png
上图可以看出来 这个register函数 里面 有一些锁的操作 我们看看这个锁是啥?

这里能够明显的看出来 这里声明了一个全局的对象,有点类似于 java中的单例模式
一个是读写锁, 另外一个是一个map 这个map的key是string类型 value是一个driver类型的接口

image.png

显然map类型 是不支持并发的,所以我们需要这个driversMu这个全局锁 来让我们操作map的时候可以做到
并发安全

那看完这个我们再看看前面我们调用的open函数

image.png

image.png

整体代码其实不难理解,最终我们还是生成了db这个结构体。

这里着重要注意的是,作为标准库中的sql

image.png

最重要的就是这个driver的文件,里面就是一堆接口,标准库的sql 操作中 操作的都是接口,
任何想要实现 具体数据库驱动的开发者 只要实现对应的接口 就可以了

然后就可以利用init函数 把自己的驱动 注入到 这个sql 标注库的 执行过程里

是一种构思很巧妙的 面向接口编程的写法

简单的crud 操作

单行查询:

func queryTest() {
   sqlStr := "select id,age,name from user where id=?"
   var u user
   row := db.QueryRow(sqlStr, 1)
   //queryRow 之后 一定要执行scan  否则 持有的数据库连接 不会释放
   err := row.Scan(&u.id, &u.age, &u.name)
   if err != nil {
      fmt.Println(err)
   }
   fmt.Println("result:", u)
}
复制代码

这里一定要注意scan方法哟 否则会一直持有一个数据库连接, 当连接数打满以后 就是线上故障了

所以一般我们这么写: 链式调用


func queryTest() {
   sqlStr := "select id,age,name from user where id=?"
   var u user
   //queryRow 之后 一定要执行scan  否则 持有的数据库连接 不会释放
   err := db.QueryRow(sqlStr, 1).Scan(&u.id, &u.age, &u.name)
   if err != nil {
      fmt.Println(err)
   }
   fmt.Println("result:", u)
}
复制代码

多行查询:

func queryRows() {
   sqlStr := "select id,age,name from user where id>?"
   rows, err := db.Query(sqlStr, 0)
   if err != nil {
      fmt.Println(err)
   }
   // 这个关闭连接的操作 一定不要忘记
   defer rows.Close()
   // 实际上 next 函数 到最后也会释放连接的,但是有时候我们for循环可能会跳出
   // 所以 为了保险 我们是用defer 来保证 连接一定会被释放
   for rows.Next() {
      var u user
      err := rows.Scan(&u.id, &u.age, &u.name)
      if err != nil {
         fmt.Println(err)
      }
      fmt.Println(u)
   }
}
复制代码

add操作:

func insert() {
   sqlStr := "insert into user(name,age) values(?,?)"
   ret, err := db.Exec(sqlStr, "马云", 99)
   if err != nil {
      fmt.Println(err)
      return
   }
   id, err2 := ret.LastInsertId()
   if err2 != nil {
      fmt.Println("get lastid error:", err2)
      return
   }
   fmt.Println("insert success,id: ", id)
}
复制代码

update 操作 和 delete操作 和 add操作 其实一样, 没有任何区别,都是一样的模版写法,

无非就是sql 语句改一下,然后 返回一个受影响的行数

mysql 预处理

上面的crud例子,就是一个普通的sql语句 执行的过程,

首先 我们 对sql语句中的占位符 进行处理 得到一个完整的sql语句
然后 客户端把完整的sql语句 发给mysql
最后 mysql 服务端 执行完整的sql 语句 并将结果 返回给客户端

预处理则和上面不同

客户端先把 sql语句的命令部分 发给服务端
然后mysql 预处理 这个命令部分
客户端 再发 数据部分,mysql来对sql语句的占位符 进行替换
结果返回

预处理的好处:

优化mysql服务器重复执行sql的方法,提升mysql的性能,一次编译 多次执行

另外还可以避免sql注入问题

来看个例子:

func prepareTest() {
   sqlStr := "select id,age,name from user where id>?"
   stmt, err := db.Prepare(sqlStr)
   if err != nil {
      fmt.Println(err)
      return
   }
   defer stmt.Close()
   rows, err2 := stmt.Query(0)
   if err2 != nil {
      fmt.Println(err2)
      return
   }
   defer rows.Close()
   for rows.Next() {
      var u user
      err := rows.Scan(&u.id, &u.age, &u.name)
      if err != nil {
         fmt.Println(err)
      }
      fmt.Println(u)
   }
}
复制代码

mysql 事务

func transactionDemo() {
   tx, err := db.Begin()
   if err != nil {
      if tx != nil {
         tx.Rollback()
      }
      fmt.Println("begin trans failed")
      return
   }

   sqlstr1 := "update user set age=30 where id=?"
   _, err2 := tx.Exec(sqlstr1, 1)
   if err2 != nil {
      tx.Rollback()
      fmt.Println(err2)
      return
   }

   sqlstr2 := "update user set age=30 where id=?"
   _, err3 := tx.Exec(sqlstr2, 2)
   if err3 != nil {
      tx.Rollback()
      fmt.Println(err3)
      return
   }

   err4 := tx.Commit()
   if err4 != nil {
      tx.Rollback()
      fmt.Println(err4)
      return
   }

}
复制代码

其实只要关注3个操作就可以了 分别是begin commit 和rollback