在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
参考:https://studygolang.com/pkgdoc 导入方式: import "database/sql" sql包提供了保证SQL或类SQL数据库的泛用接口。 使用sql包时必须注入(至少)一个数据库驱动。相关可见go标准库的学习-database/sql/driver
1.空值 参考:https://yq.aliyun.com/articles/178898?utm_content=m_29337 当用户认为数据库中的某一列不会出现空值(即nil)而将该列设置为基本类型,然后在从数据库中接收写入数据时如果得到了空值nil,程序就会崩溃。 ⚠️空值(即nil)和零值是不同的,Go语言的每一个变量都有着默认零值,当数据的零值没有意义时,可以用零值来表示空值。 空值的解决办法有: 1)使用零值 如果数据本身从语义上就不会出现零值,或者根本不区分零值和空值,那么最简便的方法就是使用零值来表示空值 2)数据库层面解决办法 通过对列添加 3)自定义处理逻辑,如下 Scannertype Scanner interface { // Scan方法从数据库驱动获取一个值。 // // 参数src的类型保证为如下类型之一: // // int64 // float64 // bool // []byte // string // time.Time // nil - 表示NULL值 // // 如果不能不丢失信息的保存一个值,应返回错误。 Scan(src interface{}) error } Scanner接口会被Rows或Row等的Scan方法使用。 任何实现了Scanner接口的类型,都可以通过定义自己的Scan函数来处理空值问题,比如:
4)使用额外的标记字段,如下面的 Valid字段
NullStringtype NullString struct { String string Valid bool // 如果String不是NULL则Valid为真 } NullString代表一个可为NULL的字符串。NullString实现了Scanner接口,因此可以作为Rows/Row的Scan方法的参数保存扫描结果: var s NullString err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&s) //即将得到的name值转换成s.String类型并存储到&s中 ... if s.Valid {//如果name值非空值 // use s.String } else {//如果name值为空值 // NULL value } Scanfunc (ns *NullString) Scan(value interface{}) error
Scan实现了Scanner接口。 Valuefunc (ns NullString) Value() (driver.Value, error) Value实现了driver.Valuer接口。 其实现源码为: type NullString struct { String string Valid bool // Valid is true if String is not NULL } // Scan implements the Scanner interface. func (ns *NullString) Scan(value interface{}) error { if value == nil { //如果 ns.String, ns.Valid = "", false return nil } ns.Valid = true return convertAssign(&ns.String, value) } // Value implements the driver Valuer interface. func (ns NullString) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } return ns.String, nil } 其中使用到了一个函数convertAssign: //该函数的作用是将src的值复制到dest上,并将src的类型转换成dest的类型,可转换则返回nil;否则返回错误 //dest是一个指针类型 func convertAssign(dest, src interface{}) error 因此上面的Scan函数的作用就是将非nil的传入参数值value转换成ns.String类型,并存储在&ns.String中,同时设置ns.Valid为true NullBooltype NullBool struct { Bool bool Valid bool // 如果Bool不是NULL则Valid为真 } NullBool代表一个可为NULL的布尔值。NullBool实现了Scanner接口,因此可以作为Rows/Row的Scan方法的参数保存扫描结果,类似NullString。 Scanfunc (n *NullBool) Scan(value interface{}) error
Scan实现了Scanner接口。 Valuefunc (n NullBool) Value() (driver.Value, error) Value实现了driver.Valuer接口。 NullInt64type NullInt64 struct { Int64 int64 Valid bool // 如果Int64不是NULL则Valid为真 } NullInt64代表一个可为NULL的int64值。NullInt64实现了Scanner接口,因此可以作为Rows/Row的Scan方法的参数保存扫描结果,类似NullString。 Scanfunc (n *NullInt64) Scan(value interface{}) error
Scan实现了Scanner接口。 Valuefunc (n NullInt64) Value() (driver.Value, error) Value实现了driver.Valuer接口。 NullFloat64type NullFloat64 struct { Float64 float64 Valid bool // 如果Float64不是NULL则Valid为真 } NullFloat64代表一个可为NULL的float64值。NullFloat64实现了Scanner接口,因此可以作为Rows/Row的Scan方法的参数保存扫描结果,类似NullString。 Scanfunc (n *NullFloat64) Scan(value interface{}) error
Scan实现了Scanner接口。 Valuefunc (n NullFloat64) Value() (driver.Value, error) Value实现了driver.Valuer接口。
RawBytestype RawBytes []byte
RawBytes是一个字节切片,保管对内存的引用,为数据库自身所使用。在Scaner接口的Scan方法写入RawBytes数据后,该切片只在限次调用Next、Scan或Close方法之前合法。
2.DB Registerfunc Register(name string, driver driver.Driver)
Register注册并命名一个数据库,可以在Open函数中使用该命名启用该驱动。 如果 Register注册同一名称两次,或者driver参数为nil,会导致panic。 该函数用来注册数据库驱动。当第三方开发者开发数据库驱动时,都会实现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函数。这种使用方式仅让导入的包做初始化,而不使用包中其他功能
DBtype DB struct { // 内含隐藏或非导出字段 } DB是一个数据库(操作)句柄,代表一个具有零到多个底层连接的连接池。它可以安全的被多个go程同时使用。
sql包会自动创建和释放连接;它也会维护一个闲置连接的连接池。如果数据库具有单连接状态的概念,该状态只有在事务中被观察时才可信。 一旦调用了DB.Begin,返回的Tx会绑定到单个连接。当调用事务Tx的Commit或Rollback后,该事务使用的连接会归还到DB的闲置连接池中。 连接池的大小可以用SetMaxIdleConns方法控制。 Openfunc Open(driverName, dataSourceName string) (*DB, error)
Open打开一个dirverName指定的数据库,dataSourceName指定数据源,一般包至少括数据库文件名和(可能的)连接信息。 大多数用户会通过数据库特定的连接帮助函数打开数据库,返回一个*DB。Go标准库中没有数据库驱动。参见http://golang.org/s/sqldrivers获取第三方驱动。 Open函数不创建与数据库的连接,也不验证其参数。它可能会延迟到你第一次调用该数据库时才回去真正创建与数据库的连接。所以如果要立即检查数据源的名称是否合法,或者数据库是否实际可用,应调用返回值的Ping方法。 Pingfunc (db *DB) Ping() error Ping检查与数据库的连接是否仍有效,如果需要会创建连接。 Closefunc (db *DB) Close() error Close关闭数据库,释放任何打开的资源。一般不会关闭DB,因为DB句柄通常被多个go程共享,并长期活跃。
举例,正确是不会报错: package main import( "log" "database/sql" _ "github.com/go-sql-driver/mysql" ) func main() { db, err := sql.Open("mysql", "root:user78@/test") //后面格式为"user:password@/dbname" defer db.Close() if err != nil{ panic(err) } //使用Ping检查数据库是否实际可用 if err = db.Ping(); err != nil{ log.Fatal(err) } } 如果写错密码,则会返回: userdeMBP:go-learning user$ go run test.go 2019/02/20 19:51:00 Error 1045: Access denied for user 'root'@'localhost' (using password: YES) exit status 1 可见调用sql.Open()函数时并没有报错,是调用db.Ping()函数时才报出的密码错误
返回的DB可以安全的被多个go程同时使用,并会维护自身的闲置连接池。这样一来,Open函数只需调用一次。很少需要关闭DB,因为sql.DB对象是为了长连接设计的,不要频繁使用Open()和Close()函数,否则会导致各种错误。 因此应该为每个待访问的数据库创建一个sql.DB实例,并在用完前保留它。如果需要短连接使用,那么可以将其作为函数的参数传递给别的function的参数使用,而不是在这个function中调用Open()和Close()再建立已经创建的sql.DB实例,或者将其设置为全局变量。
Driverfunc (db *DB) Driver() driver.Driver Driver方法返回数据库下层驱动。
下面的四个函数用于进行数据库操作: Execfunc (db *DB) Exec(query string, args ...interface{}) (Result, error) Exec执行一次命令(包括查询、删除、更新、插入等),不返回数据集,返回的结果是Result, Queryfunc (db *DB) Query(query string, args ...interface{}) (*Rows, error) Query执行一次查询,返回多行结果(即Rows),一般用于执行select命令。参数args表示query中的占位参数。 上面两个的差别在与:Query会返回查询结果Rows,Exec不会返回查询结果,只会返回一个结果的状态Result所以一般进行不需要返回值的DDL和增删改等操作时会使用Exec,查询则使用Query。当然这主要还是取决于是否需要返回值 QueryRowfunc (db *DB) QueryRow(query string, args ...interface{}) *Row QueryRow执行一次查询,并期望返回最多一行结果(即Row)。QueryRow总是返回非nil的值,直到返回值的Scan方法被调用时,才会返回被延迟的错误。(如:未找到结果) Preparefunc (db *DB) Prepare(query string) (*Stmt, error)
Prepare创建一个准备好的状态用于之后的查询和命令,即准备一个需要多次使用的语句,供后续执行用。返回值可以同时执行多个查询和命令。
Beginfunc (db *DB) Begin() (*Tx, error) Begin开始一个事务。隔离水平由数据库驱动决定。
举一个简单例子: 首先先在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
当然,如果你连接的是sqlite3数据库,那么你要下载的驱动是: http://github.com/mattn/go-sqlite3 举例; package main
import(
"fmt"
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func checkErr(err error){
if err != nil{
panic(err)
}
}
func main() {
db, err := sql.Open("mysql", "root:user78@/test") //后面格式为"user:password@/dbname"
defer db.Close()
checkErr(err)
//插入数据
stmt, err := db.Prepare("insert userinfo set username = ?,department=?,created=?")
checkErr(err)
//执行准备好的Stmt
res, err := stmt.Exec("user1", "computing", "2019-02-20")
checkErr(err)
//获取上一个,即上面insert操作的ID
id, err := res.LastInsertId()
checkErr(err)
fmt.Println(id) //1
//更新数据
stmt, err =db.Prepare("update userinfo set username=? where uid=?")
checkErr(err)
res, err = stmt.Exec("user1update", id)
checkErr(err)
affect, err := res.RowsAffected()
checkErr(err)
fmt.Println(affect) //1
//查询数据
rows, err := db.Query("select * from userinfo")
checkErr(err)
for rows.Next() { //作为循环条件来迭代获取结果集Rows
返回: userdeMBP:go-learning user$ go run test.go
1
1
1 user1update computing 2019-02-20
1
上面代码使用的函数的作用分别是: 1.sql.Open()函数用来打开一个注册过的数据库驱动,go-sql-driver/mysql中注册了mysql这个数据库驱动,第二个参数是DNS(Data Source Name),它是go-sql-driver/mysql定义的一些数据库连接和配置信息,其支持下面的几种格式: user@unix(/path/to/socket)/dbname?charset=utf8
user:password@tcp(localhost:5555)/dbname?charset=utf8
user:password@/dbname
user:password@tcp([de:ad:be::ca:fe]:80)/dbname
2.db.Prepare()函数用来返回准备要执行的sql操作,然后返回准备完毕的执行状态 3.db.Query()函数用来直接执行Sql并返回Rows结果 4.stmt.Exec()函数用来执行stmt准备好的SQL语句,然后返回Result ⚠️sql中传入的参数都是=?对应的数据,这样做可以在一定程度上防止SQL注入
Resulttype Result interface { // LastInsertId返回一个数据库生成的回应命令的整数。 // 当插入新行时,一般来自一个"自增"列。 // 不是所有的数据库都支持该功能,该状态的语法也各有不同。 LastInsertId() (int64, error) // RowsAffected返回被update、insert或delete命令影响的行数。 // 不是所有的数据库都支持该功能。 RowsAffected() (int64, error) } Result是对已执行的SQL命令的总结。
SetMaxOpenConnsfunc (db *DB) SetMaxOpenConns(n int)
SetMaxOpenConns设置与数据库建立连接的最大数目。 如果n大于0且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大开启连接数的限制。 如果n <= 0,不会限制最大开启连接数,默认为0(无限制)。 SetMaxIdleConnsfunc (db *DB) SetMaxIdleConns(n int)
SetMaxIdleConns设置连接池中的最大闲置连接数。 如果n大于最大开启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的限制。 如果n <= 0,不会保留闲置连接。
3.Row 上面的DB的函数Query()和QueryRow()会返回*ROWs和*ROW,因此下面就是如何去得到返回结果的更多详细的信息Rowtype Row struct { // 内含隐藏或非导出字段 } QueryRow方法返回Row,代表单行查询结果。 Scanfunc (r *Row) Scan(dest ...interface{}) error
Scan将该行查询结果各列分别保存进dest参数指定的值中。如果该查询匹配多行,Scan会使用第一行结果并丢弃其余各行。如果没有匹配查询的行,Scan会返回ErrNoRows。 举例: 一开始数据库中为空,因此调用Scan会返回错误: package main import( "fmt" "log" "database/sql" _ "github.com/go-sql-driver/mysql" ) func main() { db, err := sql.Open("mysql", "root:user78@/test") //后面格式为"user:password@/dbname" defer db.Close() if err != nil{ panic(err) } //使用Ping检查数据库是否实际可用 if err = db.Ping(); err != nil{ log.Fatal(err) } //查询数据 var uid int var username, department, created string err = db.QueryRow("select * from userinfo").Scan(&uid, &username, &department, &created) switch { case err == sql.ErrNoRows: log.Printf("No user with that ID.") //返回 2019/02/21 10:38:33 No user with that ID. case err != nil: log.Fatal(err) default: fmt.Printf("Username is %s\n", username) } } 因此如果先插入数据再调用QueryRow则不会出错了: package main import( "fmt" "log" "database/sql" _ "github.com/go-sql-driver/mysql" ) func main() { db, err := sql.Open("mysql", "root:user78@/test") //后面格式为"user:password@/dbname" defer db.Close() if err != nil{ log.Fatal(err) } //使用Ping检查数据库是否实际可用 if err = db.Ping(); err != nil{ log.Fatal(err) } stmt, err := db.Prepare("insert userinfo set username =?,department=?,created=?") if err != nil{ log.Fatal(err) } _, err = stmt.Exec("testQueryRow", "computing", "2019-02-21") if err != nil{ log.Fatal(err) } //查询数据 var uid int var username, department, created string err = db.QueryRow("select * from userinfo").Scan(&uid, &username, &department, &created) switch { case err == sql.ErrNoRows: log.Printf("No user with that ID.") case err != nil: log.Fatal(err) default: fmt.Printf("Uid is %v, username is %s, department is %s, created at %s\n", uid, username, department, created) } } 返回: userdeMBP:go-learning user$ go run test.go Uid is 3, username is testQueryRow, department is computing, created at 2019-02-21
Rowstype Rows struct { // 内含隐藏或非导出字段 } Rows是查询的结果。它的游标指向结果集的第零行,使用Next方法来遍历各行结果: rows, err := db.Query("SELECT ...") ... defer rows.Close() for rows.Next() { var id int var name string err = rows.Scan(&id, &name) ... } err = rows.Err() // 在退出迭代后检查错误 ... Columnsfunc (rs *Rows) Columns() ([]string, error)
Columns返回列名。如果Rows已经关闭会返回错误。 Scanfunc (rs *Rows) Scan(dest ...interface{}) error
Scan将当前行各列结果填充进dest指定的各个值中,用于在迭代中获取一行结果。 如果某个参数的类型为*[]byte,Scan会保存对应数据的拷贝,该拷贝为调用者所有,可以安全的,修改或无限期的保存。如果参数类型为*RawBytes可以避免拷贝;参见RawBytes的文档获取其使用的约束。 如果某个参数的类型为*interface{},Scan会不做转换的拷贝底层驱动提供的值。如果值的类型为[]byte,会进行数据的拷贝,调用者可以安全使用该值。 Nextfunc (rs *Rows) Next() bool
Next准备用于Scan方法的下一行结果。如果成功会返回true,如果没有下一行或者出现错误会返回false。Err()方法应该被调用以区分这两种情况。 每一次调用Scan方法,甚至包括第一次调用该方法,都必须在前面先调用Next方法。 Closefunc (rs *Rows) Close() error Close关闭Rows,阻止对其更多的列举。 如果Next方法返回false,Rows会自动关闭,满足检查Err方法结果的条件。Close方法是幂等的(即多次调用不会出错),不影响Err方法的结果。 用于关闭结果集Rows。结果集引用了数据库连接,并会从中读取结果。读取完之后必须关闭它才能避免资源泄露。只要结果集仍然打开着,相应的底层连接就处于忙碌状态,不能被其他查询使用。 Errfunc (rs *Rows) Err() error Err返回可能的、在迭代时出现的错误,即用于在退出迭代后检查错误。Err需在显式或隐式调用Close方法后调用,即如果Next方法返回false,Rows会自动关闭,相当于调用了Close()。 正常情况下迭代退出是因为内部产生的EOF错误(即数据读取完毕),使得下一次 举例: 包括上面的例子,这里再插入一条数据,这样数据库中就有两条数据了 package main import( "fmt" "log" "database/sql" _ "github.com/go-sql-driver/mysql" ) func main() { db, err := sql.Open("mysql", "root:user78@/test") //后面格式为"user:password@/dbname" defer db.Close() if err != nil{ log.Fatal(err) } //使用Ping检查数据库是否实际可用 if err = db.Ping(); err != nil{ log.Fatal(err) } stmt, err := db.Prepare("insert userinfo set username =?,department=?,created=?") if err != nil{ log.Fatal(err) } _, err = stmt.Exec("testQuery", "data mining", "2019-02-21") if err != nil{ log.Fatal(err) } //查询数据 rows, err := db.Query("select * from userinfo") if err != nil{ log.Fatal(err) } defer rows.Close() //迭代结果 var uid int var username, department, created string for rows.Next() { if err = rows.Scan(&uid, &username, &department, &created); err != nil { log.Fatal(err) } fmt.Printf("Uid is %v, username is %s, department is %s, created at %s\n", uid, username, department, created) } //查看迭代时是否出错以及出的是什么错 if rows.Err() != nil { log.Fatal(err) } } 返回: userdeMBP:go-learning user$ go run test.go Uid is 3, username is testQueryRow, department is computing, created at 2019-02-21 Uid is 4, username is testQuery, department is data mining, created at 2019-02-21
4.Stmt 在调用db.Prepare()后会返回*Stmt,即准备好的语句,一般一个会多次进行查询的语句就应该将其设置为准备好的语句。 Stmt是和单个数据库直接绑定的。客户端会发送一个带有占位符,如?的SQL语句的Stmt到服务端,然后服务端会返回一个Stmt ID,说明给你绑定的连接是哪一个。然后之后当客户端要执行该Stmt时,就会发送ID和参数来绑定连接并执行操作。 要注意的是不能直接为Stmt绑定连接,连接只能与DB和Tx绑定,当我们生成一个Stmt时,首先它会自动在连接池中绑定一个空闲连接,然后Stmt会记住该连接,然后之后执行时尝试使用这个连接,如果不可用,如连接繁忙或关闭,则会重新准备语句并再绑定一个新的连接 Stmt中可以执行的方法与db中的方法十分类似 Stmttype Stmt struct { // 内含隐藏或非导出字段 } Stmt是准备好的状态。Stmt可以安全的被多个go程同时使用。 Execfunc (s *Stmt) Exec(args ...interface{}) (Result, error)
Exec使用提供的参数执行准备好的命令状态,返回Result类型的该状态执行结果的总结。 Queryfunc (s *Stmt) Query(args ...interface{}) (*Rows, error)
Query使用提供的参数执行准备好的查询状态,返回Rows类型查询结果。 QueryRowfunc (s *Stmt) QueryRow(args ...interface{}) *Row
QueryRow使用提供的参数执行准备好的查询状态。如果在执行时遇到了错误,该错误会被延迟,直到返回值的Scan方法被调用时才释放。返回值总是非nil的。如果没有查询到结果,*Row类型返回值的Scan方法会返回ErrNoRows;否则,Scan方法会扫描结果第一行并丢弃其余行。 示例用法: 全部评论
专题导读
上一篇:k8sclient-go源码分析informer源码分析(5)-Controller&Processor源码分析 ...发布时间:2022-07-10下一篇:go开发目录配置-gomod发布时间:2022-07-10热门推荐
热门话题
阅读排行榜
|
请发表评论