前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站https://www.captainbed.cn/kitie。
前言
对数据库的CURD是现代应用程序的必备功能,Go语言当然也对数据库的操作提供了非常完善的支持。
尽管在Go语言社区中有很多优秀的ORM库或框架(比如GORM,后面也会发文)能让我们更方便地操作数据库,不过要更好地使用ORM库,掌握Go原生操作数据库database/sql包的使用还是有必要的。
所以,在这篇文章中,我们先来学习一下database/sql包的使用吧。
database/sql简介
我们可以把标准库中的database/sql包看作一个数据库操作抽象层,因为database/sql并不直接连接并操作数据库,而是为不同的数据库驱动提供了统一的API,database/sql包与驱动包(driver)的关系如下图所示:
对数据库的具体操作由对应数据库的驱动包来完成,访问不同的类型的数据库需要导入不同的驱动包,而所有驱动包都必须实现database/sql/driver包下的相关接口,因此database/sql、database/sql/driver以及驱动包的关系如下图所示:
这样做的好处在于,如果某一天我们想迁移数据库,比如从MySQL切换为Postgres,只需要更换驱动包即可,而不需要把数据库操作的代码重写一遍。
连接数据库
连接到数据库,获取一个数据库操作句柄,简单来说可以分为三步:
选择对应数据库的驱动,并导入。
配置连接数据库所需的DSN。
使用sql.Open()函数打开数据连接,获得*sql.DB对象。
DSN是Data Source Name,包含了连接数据库所需的参数,比如用户,密码、数据编码、数据库名称等。
下面我们以MySQL,Postgres,SQLite为例介绍在Go语言中如何连接到数据库。
MySQL
连接MySQL驱动包推荐用github.com/go-sql-driver/mysql库,其完整的DSN为如以所示,可以看到MySQL的DSN各个部分都是可选的:
[username[:password]@][protocol[host[:port]]]/dbname[?param1=value1&...¶mN=valueN]
- username:用户名。
- password:密码,与用户名之间要用冒号分隔。
- protocol:网络协议,默认用tcp即可。
- host:数据库地址。
- port:端口号,默认为3306
- dbname:数据库名称
- param1~paramN:可选参数。
package main import ( "database/sql" // 匿名导入 _ "github.com/go-sql-driver/mysql" ) func main(){ //db为*sql.DB //DSN要根据实际替换 db, err := sql.Open("mysql", "user:password@/dbname") if err != nil { panic(err) } }
Postgres
连接Postgres数据库推荐用github.com/lib/pq,其连接DSN为:
host=192.168.0.1 user=postgres dbname=postgres port=5432 password=123456 sslmode=disable
- user:用户名。
- password:密码,与用户名之间要用冒号分隔。
- dbname:数据库名称。
- sslmode:是否使用ss模式,其值可以是disable,required,verify-ca ,verify-full等。
- host:主机
- port:端口号
package main import ( "context" "fmt" "os" "github.com/lib/pq" ) func main(){ dsn := "user=postgres dbname=postgres password=123456 sslmode=disable" db, err := sql.Open("postgres", dsn) if err != nil { log.Fatal(err) } defer db.Close() }
SQLite
连接SQLite数据库推荐用github.com/mattn/go-sqlite3,SQLite是嵌入式数据库,因此它的DSN是数据库文件所在的路径:
import ( "database/sql" _ "github.com/mattn/go-sqlite3" ) func main(){ dsn := "./test.db" db,err := sql.Open("sqlite3",dsn) }
关闭连接
打开数据库连接要记得关闭数据连接,一般在defer语句后面调用sql.DB的Close()方法:
import ( "database/sql" _ "github.com/mattn/go-sqlite3" ) func main(){ dsn := "./test.db" db,err := sql.Open("sqlite3",dsn) if err != nil{ panic(err) } //关闭数据库连接 defer db.Close() }
连接池设置
sql.DB对象内包含一个数据库连池,并支持通过以下几个方法设置连接池的相关配置:
db.SetConnMaxLifetime(0) //最大打开的连接时间 db.SetMaxIdleConns(50) //最大闲置连接数 db.SetMaxOpenConns(50) //最大打开的连接数
示例数据表
为了后面更好的进行实例演示,我们需要一个演示数据库,这里以Mysql数据库为例,数据库名称为test,创建一个users数据表:
CREATE DATABASE IF NOT EXISTS test; USE test; CREATE TABLE IF NOT EXISTS users( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, gender TINYINT NOT NULL DEFAULT 0, mobile VARCHAR(255) NOT NULL DEFAULT '' );
sql.DB
前面我们调用sql.Open()获得了sql.DB对象,这是操作数据的句柄,也可以理解为一个数据库连接池,可以通过sql.DB的Conn()可以获得数据库连接池里的单个连接对象sql.Conn:
ctx, _ := context.WithCancel(context.Background()) conn, err := db.Conn(ctx)
上面的方法执行后返回sql.Result接口的实例,这个接口只有两个方法:
type Result interface { //执行insert语句时,返回自增id LastInsertId() (int64, error) //影响行数 RowsAffected() (int64, error) }
示例代码:
package main import ( "database/sql" "fmt" _ "github.com/mattn/go-sqlite3" ) func main() { db, err := sql.Open("sqlite3", "../test.db") if err != nil { panic(err) } defer db.Close() insertSql := "INSERT INTO users(name,gender,mobile) VALUES(?,?,?),(?,?,?)" insertResult, err := db.Exec(insertSql, "小白", 2, "166xxxxxxxx", "小张", 1, "13493023333") if err != nil { panic(err) } lastInsertId, _ := insertResult.LastInsertId() fmt.Printf("最新记录id:%d\n", lastInsertId) rowsAffected, _ := insertResult.RowsAffected() fmt.Println("影响行数:", rowsAffected) updateSql := "UPDATE users SET mobile = ? WHERE id = ?" updateResult, err := db.Exec(updateSql, "136xxxxxxxx", 1) if err != nil { panic(err) } rowsAffected, _ = updateResult.RowsAffected() fmt.Printf("影响行数:%d\n", rowsAffected) deleteSql := "DELETE FROM users WHERE id = ?" deleteResult, err := db.Exec(deleteSql, 14) if err != nil { panic(err) } rowsAffected, _ = deleteResult.RowsAffected() fmt.Printf("影响行数:%d\n", rowsAffected) }
如果你只想从数据表查询一行数据,可以调用QueryRow()和QueryRowContext()方法:
func (db *DB) QueryRow(query string, args ...any) *Row func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row
上面的方法返回的是sql.Row对象,代表查询的那一行数据,这个对象只有Scan方法可以获取对象里的数据,调用Scan方法时传进去参数个数必须与查询返回的数据列数一致:
package main import ( "database/sql" "fmt" _ "github.com/mattn/go-sqlite3" ) func main() { db, err := sql.Open("sqlite3", "./test.db") if err != nil { panic(err) } defer db.Close() selectOne := "SELECT * FROM users WHERE id = ?" row := db.QueryRow(selectOne, 1) var ( id int name string gender uint8 mobile string ) //扫描数据 err = row.Scan(&id, &name, &gender, &mobile) if err != nil { panic(err) } genderText := "未知" switch gender { case 1: genderText = "男" case 2: genderText = "女" } fmt.Printf("用户:%s | 性别:%s | 手机:%s\n", name, genderText, mobile) }
然而,更多的时候,我们需要查询多行数据,可以调用Query()和QueryContext()方法:
func (db *DB) Query(query string, args ...any) (*Rows, error) func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
上面两个方法返回的是sql.Rows对象,代表查询回来的多行数据,可以在for循环语句中通过Next()和Scan()方法扫描每一行数据,传给Scan方法的参数数量必须与查询回来的列数相同,另外,要记得调用Close()方法关闭sql.Rows对象:
package main import ( "database/sql" "fmt" "log" _ "github.com/mattn/go-sqlite3" ) func main() { db, err := sql.Open("sqlite3", "./test.db") if err != nil { panic(err) } defer db.Close() selectMany := "SELECT * FROM users" rows, err := db.Query(selectMany) if err != nil { panic(err) } defer rows.Close() for rows.Next() { var ( id int name string gender uint8 mobile string ) if err := rows.Scan(&id, &name, &gender, &mobile); err != nil { log.Fatal(err) } genderText := "未知" switch gender { case 1: genderText = "男" case 2: genderText = "女" } fmt.Printf("用户:%s | 性别:%s | 手机:%s\n", name, genderText, mobile) } }
sql.Stmt
SQL语句预编译机制允许先把带有参数占位符的SQL语句发送给数据库,数据库会提前对SQL语句进行编译,之后我们再发送对应占位符的参数给数据库 。
SQL语句的好处在于:
- 预防SQL注入攻击。
- 复杂的SQL语句,提前编译提高SQL语句执行效率。
调用sql.DB、sql.Conn和sql.Tx(下面会讲到)的Prepare()方法或者PrepareContext()会返回一个sql.Stmt:
stmt, err := db.Prepare("INSERT INTO users(name,) VALUES(?,?,?),(?,?,?)")
因为SQL语句已经预发送给数据库,因此调用sql.Stmt的ExecXXX()和QueryXXX()时,只需要发送参数即可:
Stmt.Exec("张三",2,"10086","李四",1,"10086111")
sql.Stmt对象里的方法返回值与sql.DB一样是sql.Result,具体使用参考前面的例子。
sql.Stmt对象里的方法返回值与sql.DB一样是sql.Result,具体使用参考前面的例子。
sql.Tx
sql.Tx表示一个数据库事务对象,调用sql.DB或者sql.Conn对象的Begin()或者BeginTx()方法会返回一个sql.Tx:
tx,err := db.Begin()
sql.Tx与sql.DB执行CURD的方法基本相同,所不同的是,通过sql.Tx执行的语句,最后要调用sql.Tx的Commit()方法提交事务,如果执行时有错误发生,则应该调用Rollback()方法回滚事务:
package main import ( "database/sql" _ "github.com/mattn/go-sqlite3" ) func main() { db, err := sql.Open("sqlite3", "./test.db") if err != nil { panic(err) } defer db.Close() tx, err := db.Begin() if err != nil { panic(err) } if _, err := tx.Exec("INSERT INTO users VALUES(?,?,?,?)", 1, "小龙", 1, "137xxxxxxxx"); err != nil { tx.Rollback() } if _, err := tx.Exec("INSERT INTO users VALUES(?,?,?,?)", 2, "小明", 1, "137xxxxxxxx"); err != nil { tx.Rollback() } tx.Commit() }
总结
通过本文的学习,我们能够掌握Go语言数据库编程的基本技能,并能够独立进行数据库应用程序的开发。Go语言数据库编程是一个实践性很强的领域,需要不断地学习和实践。希望本文能够为你的学习之路提供指导和帮助。
简单总结一下:
sql.DB对象表示一个数据库句柄,其中包含一个数据库连接池。
sql.Conn对象表示一个连接池里的普通数据库连接。
sql.Tx表示一个数据库事务对象,通过该对象执行的SQL语句要调用Commit()方法才会生效,如果执行过程发生错误,要调用Rollback()方法回滚事务。
sql.Stmt表示一个预编译对象,通过这个对象执行的SQL语句会先发送给数据库,之后再发送参数,这样可以避免SQL注入以及提前编译语句,提高执行效率。
sql.Rows是调用QueryXXX这类方法返回的表示多行数据的对象,内置迭代器,可以调用for语句迭代查询回来的数据。
sql.Row是调用QueryRowXXX这类方法返回的对象,代表一行数据。
- sql.Result是调用ExecXXX这类方法进行update,delete,insert操作之后返回的结果。