#5.1 database/sql接口 Go和PHP不同的地方是,他没有官方提供数据库驱动,而是定义了一些标准接口用来开发数据库驱动,第三方用户可以根据定义的接口来开发相应的数据库驱动,其实这样做有一个好处,我们按照标准接口开发的代码,将来迁移数据库是相当方便的,换一个数据库驱动,但是底层的接口没有任何变化。那么Go都定义了那些标准接口呢?让我们来详细的分析一下 ##sql.Register 这个函数是database/sql里面用来注册数据库驱动的,当第三方开发者开发完毕一个数据库驱动,都会在驱动里面有一个init函数,里面都会调用这个`Register(name string, driver driver.Driver)` 我们来看一下mymysql、sqlite3的驱动里面都是怎么调用的: //https://github.com/mattn/go-sqlite3驱动 func init() { sql.Register("sqlite3", &SQLiteDriver{}) } //https://github.com/mikespook/mymysql驱动 // Driver automatically registered in database/sql var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"} func init() { Register("SET NAMES utf8") sql.Register("mymysql", &d) } 我们看到第三方驱动都是通过调用这个函数来注册自己的驱动名称以及相应的driver。database/sql内部是一个map类型用来存储相应的驱动。 var drivers = make(map[string]driver.Driver) drivers[name] = driver 因此通过database/sql的注册函数可以同时注册多个数据库驱动,只要不重复。 >在我们使用database/sql接口和第三方库的时候经常看到如下的import > > "database/sql" > _ "github.com/mattn/go-sqlite3" > >新手都会被这个`_`所迷惑,其实这个就是Go设计的巧妙之处,我们知道在变量赋值的时候看到过这个,是用来忽略变量的意思,那么这个包引入也是,这个只是引入包而不直接使用这个pkg的调用,我们在2.3流程和函数里面介绍过init函数的初始化过程,包在引入的时候会去初始化包内部的init函数,那么我们引入上面的数据库驱动包之后会去调用init函数,然后在init函数里面注册了这个数据库驱动,这样我们就可以在接下来的代码中直接使用这个数据库驱动了。 ##driver.Driver 数据库驱动是一个接口定义,他定义了一个method: Open(name string),这个方法返回一个数据库的Conn接口,这个Conn只能用来进行一次goroutine的操作。 type Driver interface { Open(name string) (Conn, error) } 每个驱动的这个函数都是通过name参数用来解析,然后初始化一个Conn返回。 ##driver.Conn 数据库连接是一个接口定义,他定义了一系列方法,这个Conn只能应用在一个goroutine里面,不能使用在多个goroutine里面。 type Conn interface { Prepare(query string) (Stmt, error) Close() error Begin() (Tx, error) } Prepare函数返回与当前连接相关的准备好Sql语句的状态,可以进行查询、删除等操作。 Close函数关闭使得当前的连接失效,以及和当前连接相关的状态、处理。因为database/sql里面实现了建议的conn pool,所以你不要再自己去实现缓存conn之类的,这样容易引起问题。 Begin函数返回一个事务处理Tx,可以进行事物操作,回滚、递交等操作。 ##drive.Stmt Stmt是一种准备好的状态,和Conn相关联,而且是只能应用于一个goroutine中,不能应用在多个goroutine中。 type Stmt interface { Close() error NumInput() int Exec(args []Value) (Result, error) Query(args []Value) (Rows, error) } Close函数关闭当前的链接状态,但是如果当前正在执行query,query还是有效返回raws数据。 NumInput函数返回当前预留参数的个数,当返回>=0时数据库驱动就会智能检查调用者的参数。当数据库驱动包不知道预留参数的时候,返回-1。 Exec函数执行Prepare准备好的sql,传入参数执行update/insert等操作,返回Result数据 Query函数执行Prepare准备好的sql,传入需要的参数执行select操作,返回Rows结果集 ##drive.Tx 事务处理一般就两个过程,递交或者回滚。数据库驱动里面也只需要实现这两个函数就可以 type Tx interface { Commit() error Rollback() error } 这两个函数一个用来递交一个事务,一个用来回滚事务。 ##drive.Execer 这个接口是Conn可选的可实现可不实现的接口 type Execer interface { Exec(query string, args []Value) (Result, error) } 如果这个接口没有定义,那么在调用DB.Exec,就会首先调用Prepare返回Stmt,然后执行Stmt的Exec,然后关闭Stmt。 ##drive.Result 这个是执行Update/Insert等操作返回的结果接口定义 type Result interface { LastInsertId() (int64, error) RowsAffected() (int64, error) } LastInsertId函数返回插入数据库之后返回自动增长字段的ID,Insert之后返回的数据 RowsAffected函数返回通过query影响的数据库条数。 ##drive.Rows Rows是执行查询返回的结果集接口定义 type Rows interface { Columns() []string Close() error Next(dest []Value) error } Columns函数返回查询数据库表的字段信息,这个返回的slice和你sql查询的字段一一对应,而不是返回整个表的所有字段。 Close函数用来关闭Rows迭代器。 Next函数用来返回下一条数据,把数据赋值给dest。dest里面的元素必须是drive.Value的值除了string,所有的string必须转换成[]byte.如果最后没数据了,Next函数最后返回io.EOF。 ##drive.RowsAffected RowsAffested其实就是一个int64的别名,但是他实现了Result接口,用来底层实现Result的表示方式 type RowsAffected int64 func (RowsAffected) LastInsertId() (int64, error) func (v RowsAffected) RowsAffected() (int64, error) ##drive.Value Value其实就是一个空接口,他可以容纳任何的数据 type Value interface{} Value的值必须所有的驱动里面控制的,Value要么是nil,要么是下面的任意一种 int64 float64 bool []byte string [*]除了Rows.Next返回的不能是string. time.Time ##drive.ValueConverter ValueConverter接口定义了如何把一个普通的值转化成drive.Value的接口 type ValueConverter interface { ConvertValue(v interface{}) (Value, error) } ValueConverter对于各种各种的值的实现必须保持高度的一致性,在驱动包里面很多地方会使用到这个转化接口 - 转化drive.value到数据库表相应的字段,例如int64的数据如何转化成数据库表uint16字段 - 把数据库查询结果转化成drive.Value值 - 在scan函数里面如何吧dirve.Value值转化成用户定义的值 ##drive.Valuer Valuer接口定义了返回如何返回一个drive.Value值 type Valuer interface { Value() (Value, error) } 很多类型上面都定义了这个Value方法,用来实现每个相应类型如何转化为drive.Value类型。 通过上面的讲解,对于驱动的开发有了细致的了解,一个驱动基本上就是实现上面的这些接口就能完成我们平常的增删改所有的操作了,底层就是和相应的数据库进行数据交互就可以了。 ##database/sql database/sql里面定义了基于这个驱动之上的一些方法,这些方法都是基于drive接口来封装的,同时内部还实现了一个建议的conn pool。 type DB struct { driver driver.Driver dsn string mu sync.Mutex // protects freeConn and closed freeConn []driver.Conn closed bool } 我们可以看到Open函数返回的是DB对象,里面有一个freeConn,这个就是那个简易的连接池。它的实现也是就是很简单,当执行Db.prepare的时候最后会defer:`db.putConn(ci, err)`,也就是放入连接池,而每次调用conn的时候也会优先判断freeConn的长度是否大于0,大于0说明有使用过的conn,直接拿出来就用了。 ## links * [目录]() * 上一节: [访问数据库](<5.md>) * 下一节: [使用MySQL数据库](<5.2.md>) ## LastModified * $Id$