侧边栏壁纸
博主头像
张种恩的技术小栈博主等级

行动起来,活在当下

  • 累计撰写 748 篇文章
  • 累计创建 65 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录
Go

[转载]Go操作MySQL

zze
zze
2020-09-26 / 0 评论 / 0 点赞 / 503 阅读 / 13418 字

不定期更新相关视频,抖音点击左上角加号后扫一扫右方侧边栏二维码关注我~正在更新《Shell其实很简单》系列

MySQL 是业界常用的关系型数据库,本文介绍了 Go 语言如何操作 MySQL 数据库。

连接

Go 语言中的 database/sql 包提供了保证 SQL 或类 SQL 数据库的泛用接口,并不提供具体的数据库驱动。使用 database/sql 包时必须注入(至少)一个数据库驱动。

我们常用的数据库基本上都有完整的第三方实现。例如:MySQL 驱动

下载依赖

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

下载太慢的话可参考:【go get 太慢使用本地代理】。

使用 MySQL 驱动

func Open(driverName, dataSourceName string) (*DB, error)

Open 函数用来打开一个 dirverName 指定的数据库,dataSourceName 指定数据源,一般至少包括数据库文件名和其它连接必要的信息。

package main

import (
	"database/sql"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	// DSN:Data Source Name 格式:user:password@tcp(127.0.0.1:3306)/dbname
	dsn := "root:root1234@tcp(192.168.0.27:3306)/test01_db"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		panic(err)
	}
	defer db.Close() // 注意这行代码要写在上面 err 判断的下面
}

初始化连接

Open 函数可能只是验证其参数格式是否正确,实际上并不创建与数据库的连接。如果要检查数据源的名称是否真实有效,应该调用 Ping 方法。

返回的 DB 对象可以安全地被多个 goroutine 并发使用,并且维护其自己的空闲连接池。因此,Open 函数应该仅被调用一次,很少需要关闭这个 DB 对象。

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
)

// 定义一个全局对象db
var db *sql.DB

// 定义一个初始化数据库的函数
func initDB() (err error) {
	// DSN:Data Source Name
	dsn := "root:root1234@tcp(192.168.0.27:3306)/test01_db?charset=utf8mb4&parseTime=True"
	// 不会校验账号密码是否正确
	// 注意!!!这里不要使用:=,我们是给全局变量赋值,然后在main函数中使用全局变量db
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		return err
	}
	// 尝试与数据库建立连接(校验dsn是否正确)
	err = db.Ping()
	if err != nil {
		return err
	}
	return nil
}

func main() {
	err := initDB() // 调用输出化数据库的函数
	if err != nil {
		fmt.Printf("init db failed,err:%v\n", err)
		return
	}
	fmt.Println("连接数据库成功~~~")
}

其中 sql.DB 是表示连接的数据库对象(结构体实例),它保存了连接数据库相关的所有信息。它内部维护着一个具有零到多个底层连接的连接池,它可以安全地被多个 goroutine 同时使用。

SetMaxOpenConns

func (db *DB) SetMaxOpenConns(n int)

SetMaxOpenConns 设置与数据库建立连接的最大数目。 如果 n 大于 0 且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大开启连接数的限制。 如果 n<=0,不会限制最大开启连接数,默认为 0(无限制)。

SetMaxIdleConns

func (db *DB) SetMaxIdleConns(n int)

SetMaxIdleConns 设置连接池中的最大闲置连接数。 如果 n 大于最大开启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的限制。 如果 n<=0,不会保留闲置连接。

CRUD

建库建表

我们先在 MySQL 中创建一个名为 sql_test 的数据库,并创建一张用于测试的数据表:

# 建库
mysql> CREATE DATABASE sql_test;
Query OK, 1 row affected (0.01 sec)

mysql> use sql_test;
Database changed
# 建表
mysql> CREATE TABLE `user` (
    ->     `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    ->     `name` VARCHAR(20) DEFAULT '',
    ->     `age` INT(11) DEFAULT '0',
    ->     PRIMARY KEY(`id`)
    -> )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
Query OK, 0 rows affected (0.12 sec)
# 插入数据
mysql> insert into user(name, age) value('zhangsan',12),('ls',23);
Query OK, 2 rows affected (0.02 sec)
Records: 2  Duplicates: 0  Warnings: 0

查询

为了方便查询,我们事先定义好一个结构体来存储 user 表的数据。

单行查询

单行查询 db.QueryRow() 执行一次查询,并期望返回最多一行结果(即 Row)。QueryRow 总是返回非 nil 的值,直到返回值的 Scan 方法被调用时,才会返回被延迟的错误(如:未找到结果)。

func (db *DB) QueryRow(query string, args ...interface{}) *Row

具体示例代码:

// 查询单条数据示例
func queryRowDemo() {
	sqlStr := "select id, name, age from user where id=?"
	var u user
	// 非常重要:确保QueryRow之后调用Scan方法,否则持有的数据库链接不会被释放
	err := db.QueryRow(sqlStr, 1).Scan(&u.id, &u.name, &u.age)
	if err != nil {
		fmt.Printf("scan failed, err:%v\n", err)
		return
	}
	fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
	/*
	id:1 name:zhangsan age:12
	*/
}

多行查询

多行查询 db.Query() 执行一次查询,返回多行结果(即 Rows),一般用于执行 select 命令。参数 args 表示 query 中的占位参数。

func (db *DB) Query(query string, args ...interface{}) (*Rows, error)

具体示例代码:

// 查询多条数据示例
func queryMultiRowDemo() {
	sqlStr := "select id, name, age from user where id > ?"
	rows, err := db.Query(sqlStr, 0)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", err)
		return
	}
	// 非常重要:关闭rows释放持有的数据库链接
	defer rows.Close()

	// 循环读取结果集中的数据
	for rows.Next() {
		var u user
		err := rows.Scan(&u.id, &u.name, &u.age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
	}
	/*
	id:1 name:zhangsan age:12
	id:2 name:ls age:23
	*/
}

插入数据

插入、更新和删除操作都使用 Exec 方法。

func (db *DB) Exec(query string, args ...interface{}) (Result, error)

Exec 执行一次命令(包括查询、删除、更新、插入等),返回的 Result 是对已执行的 SQL 命令的总结。参数 args 表示 query 中的占位参数。

具体插入数据示例代码如下:

// 插入数据
func insertRowDemo() {
	sqlStr := "insert into user(name, age) values (?,?)"
	ret, err := db.Exec(sqlStr, "wangwu", 38)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	theID, err := ret.LastInsertId() // 新插入数据的id
	if err != nil {
		fmt.Printf("get lastinsert ID failed, err:%v\n", err)
		return
	}
	fmt.Printf("insert success, the id is %d.\n", theID)
	/*
	insert success, the id is 3.
	*/
}

更新数据

具体更新数据示例代码如下:

// 更新数据
func updateRowDemo() {
	sqlStr := "update user set age=? where id = ?"
	ret, err := db.Exec(sqlStr, 39, 3)
	if err != nil {
		fmt.Printf("update failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影响的行数
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("update success, affected rows:%d\n", n)
	/*
	update success, affected rows:1
	*/
}

删除数据

具体删除数据的示例代码如下:

// 删除数据
func deleteRowDemo() {
	sqlStr := "delete from user where id = ?"
	ret, err := db.Exec(sqlStr, 3)
	if err != nil {
		fmt.Printf("delete failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影响的行数
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("delete success, affected rows:%d\n", n)
	/*
	delete success, affected rows:1
	*/
}

MySQL 预处理

什么是预处理?

普通 SQL 语句执行过程:

  1. 客户端对 SQL 语句进行占位符替换得到完整的 SQL 语句。
  2. 客户端发送完整 SQL 语句到 MySQL 服务端。
  3. MySQL 服务端执行完整的 SQL 语句并将结果返回给客户端。

预处理执行过程:

  1. 把 SQL 语句分成两部分,命令部分与数据部分。
  2. 先把命令部分发送给 MySQL 服务端,MySQL 服务端进行 SQL 预处理。
  3. 然后把数据部分发送给 MySQL 服务端,MySQL 服务端对 SQL 语句进行占位符替换。
  4. MySQL 服务端执行完整的 SQL 语句并将结果返回给客户端。

为什么要预处理?

  1. 优化 MySQL 服务器重复执行 SQL 的方法,可以提升服务器性能,提前让服务器编译,一次编译多次执行,节省后续编译的成本。
  2. 避免 SQL 注入问题。

Go 实现 MySQL 预处理

database/sql 中使用下面的 Prepare 方法来实现预处理操作。

func (db *DB) Prepare(query string) (*Stmt, error)

Prepare 方法会先将 sql 语句发送给 MySQL服务端,返回一个准备好的状态用于之后的查询和命令。返回值可以同时执行多个查询和命令。

查询操作的预处理示例代码如下:

// 预处理查询示例
func prepareQueryDemo() {
	sqlStr := "select id, name, age from user where id > ?"
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	defer stmt.Close()
	rows, err := stmt.Query(0)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", err)
		return
	}
	defer rows.Close()
	// 循环读取结果集中的数据
	for rows.Next() {
		var u user
		err := rows.Scan(&u.id, &u.name, &u.age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
	}
}

插入、更新和删除操作的预处理十分类似,这里以插入操作的预处理为例:

// 预处理插入示例
func prepareInsertDemo() {
	sqlStr := "insert into user(name, age) values (?,?)"
	stmt, err := db.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	defer stmt.Close()
	_, err = stmt.Exec("小王子", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	_, err = stmt.Exec("沙河娜扎", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	fmt.Println("insert success.")
}

SQL 注入问题

我们任何时候都不应该自己拼接SQL语句!

这里我们演示一个自行拼接SQL语句的示例,编写一个根据name字段查询user表的函数如下:

// sql注入示例
func sqlInjectDemo(name string) {
	sqlStr := fmt.Sprintf("select id, name, age from user where name='%s'", name)
	fmt.Printf("SQL:%s\n", sqlStr)
	var u user
	err := db.QueryRow(sqlStr).Scan(&u.id, &u.name, &u.age)
	if err != nil {
		fmt.Printf("exec failed, err:%v\n", err)
		return
	}
	fmt.Printf("user:%#v\n", u)
}

此时以下输入字符串都可以引发 SQL 注入问题:

sqlInjectDemo("xxx' or 1=1#")
sqlInjectDemo("xxx' union select * from user #")
sqlInjectDemo("xxx' and (select count(*) from user) <10 #")

占位符

不同的数据库中,SQL语句使用的占位符语法不尽相同。

数据库占位符语法
MySQL?
PostgreSQL$1, $2
SQLite?$1
Oracle:name

Go 使用 MySQL 事务

事务相关介绍可参考【数据库事务了解一下】。

事务相关方法

Go 语言中使用以下三个方法实现 MySQL 中的事务操作。

  • 开始事务:
func (db *DB) Begin() (*Tx, error)
  • 提交事务:
func (tx *Tx) Commit() error
  • 回滚事务:
func (tx *Tx) Rollback() error

事务示例

下面的代码演示了一个简单的事务操作,该事物操作能够确保两次更新操作要么同时成功要么同时失败,不会存在中间状态。

// 事务操作示例
func transactionDemo() {
	tx, err := db.Begin() // 开启事务
	if err != nil {
		if tx != nil {
			tx.Rollback() // 回滚
		}
		fmt.Printf("begin trans failed, err:%v\n", err)
		return
	}
	sqlStr1 := "Update user set age=30 where id=?"
	ret1, err := tx.Exec(sqlStr1, 2)
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec sql1 failed, err:%v\n", err)
		return
	}
	affRow1, err := ret1.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
		return
	}

	sqlStr2 := "Update user set age=40 where id=?"
	ret2, err := tx.Exec(sqlStr2, 3)
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec sql2 failed, err:%v\n", err)
		return
	}
	affRow2, err := ret2.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
		return
	}

	fmt.Println(affRow1, affRow2)
	if affRow1 == 1 && affRow2 == 1 {
		fmt.Println("事务提交啦...")
		tx.Commit() // 提交事务
	} else {
		tx.Rollback()
		fmt.Println("事务回滚啦...")
	}

	fmt.Println("exec trans success!")
}

练习

结合 net/httpdatabase/sql 实现一个使用 MySQL 存储用户信息的注册及登陆的简易 web 程序。

sql_test 库创建用户表保存用户信息:

CREATE TABLE `userinfo` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(20) DEFAULT '',
    `password` VARCHAR(20) DEFAULT '0',
    PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

代码参见:https://github.com/zze326/go_learning/tree/master/20200926-mysql

转自:

0

评论区