7.6 KiB
5.1 database/sql interface
Go doesn't provide any official database driver which PHP does, but it does have some database driver interface standards for developers develop database drivers. There is an advantage that if your code is developed according to these interface standards, you will not change any code when your database changes. Let's see what these database interface standards are.
sql.Register
This function is in package database/sql for registering database drivers when you use third-party database drivers. In all those drivers, they should call function Register(name string, driver driver.Driver) in init() function in order to register their drivers.
Let's take a look at corresponding code in drivers of mymysql and sqlite3:
//https://github.com/mattn/go-sqlite3 driver
func init() {
sql.Register("sqlite3", &SQLiteDriver{})
}
//https://github.com/mikespook/mymysql driver
// 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)
}
We see that all third-party database drivers implemented this function to register themselves, and Go uses a map to save user drivers inside of databse/sql.
var drivers = make(map[string]driver.Driver)
drivers[name] = driver
Therefore, this register function can register drivers as many as you want with all different name.
We always see following code when we use third-party drivers:
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)
Here the blank _ is quite confusing for many beginners, and this is a great design in Go. We know this identifier is for discarding values from function return, and you have to use all imported packages in your code in Go. So when the blank is used with import means you need to execute init() function of that package without directly using it, which is for registering database driver.
driver.Driver
Driver is a interface that has a method Open(name string) that returns a interface of Conn.
type Driver interface {
Open(name string) (Conn, error)
}
This is a one-time Conn, which means it can be used only once in one goroutine. The following code will occur errors:
...
go goroutineA (Conn) // query
go goroutineB (Conn) // insert
...
Because Go has no idea about which goroutine does what operation, so the query operation may get result of insert, vice-versa.
All third-party drivers should have this function to parse name of Conn and return results correctly.
driver.Conn
This is a database connection interface with some methods, and as I said above, same Conn can only be used in one goroutine.
type Conn interface {
Prepare(query string) (Stmt, error)
Close() error
Begin() (Tx, error)
}
Preparereturns prepare status of corresponding SQL commands for querying and deleting, etc.Closecloses current connection and clean resources. Most of third-party drivers implemented some kinds of connection pool, so you don't need to cache connections unless you want to have unexpected errors.Beginreturns a Tx that represents a affair handle, you can use it for querying, updating or affair roll back etc.
driver.Stmt
This is a ready status and is corresponding with Conn, so it can only be used in one goroutine like Conn.
type Stmt interface {
Close() error
NumInput() int
Exec(args []Value) (Result, error)
Query(args []Value) (Rows, error)
}
Closecloses current connection, but it still returns rows data if it is doing query operation.NumInputreturns the number of obligate arguments, database drivers should check caller's arguments when the result is greater than 0, and it returns -1 when database drivers don't know any obligate argument.Execexecutes SQL commands ofupdate/insertthat are prepared inPrepare, returnsResult.Queryexecutes SQL commands ofselectthat are prepared inPrepare, returns rows data.
driver.Tx
Generally, affair handle only have submit or roll back, and database drivers only need to implement these two methods.
type Tx interface {
Commit() error
Rollback() error
}
driver.Execer
This is an optional interface.
type Execer interface {
Exec(query string, args []Value) (Result, error)
}
If the driver doesn't implement this interface, then when you call DB.Exec, it automatically calls Prepare and returns Stmt, then executes Exec of Stmt, then closes Stmt.
driver.Result
This is the interface for result of update/insert operations.
type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}
LastInsertIdreturns auto-increment Id number after insert operation from database.RowsAffectedreturns rows that affected after query operation.
driver.Rows
This is the interface for result set of query operation.
type Rows interface {
Columns() []string
Close() error
Next(dest []Value) error
}
Columnsreturns fields information of database tables, the slice is one-to-one correspondence to SQL query field, not all fields in that database table.Closecloses Rows iterator.Nextreturns next data and assigns to dest, all string should be converted to byte array, and gets io.EOF error if no more data available.
diriver.RowsAffected
This is a alias of int64, but it implemented Result interface.
type RowsAffected int64
func (RowsAffected) LastInsertId() (int64, error)
func (v RowsAffected) RowsAffected() (int64, error)
driver.Value
This is a empty interface that can contain any kind of data.
type Value interface{}
The Value must be somewhat that drivers can operate or nil, so it should be one of following types:
int64
float64
bool
[]byte
string [*] Except Rows.Next which cannot return string
time.Time
driver.ValueConverter
This defines a interface for converting normal values to driver.Value.
type ValueConverter interface {
ConvertValue(v interface{}) (Value, error)
}
This is commonly used in database drivers and has many good features:
- Convert driver.Value to corresponding database field type, for example convert int64 to uint16.
- Convert database query results to driver.Value.
- Convert driver.Value to user defined value in
scanfunction.
driver.Valuer
This defines a interface for returning driver.Value.
type Valuer interface {
Value() (Value, error)
}
Many types implemented this interface for conversion between driver.Value and itself.
At this point, you should have concepts about how to develop a database driver. Once you implement about interfaces for operations like add, delete, update, etc. There only left problems about communicating with specific database.
database/sql
databse/sql defines more high-level methods above database/sql/driver for more convenient operations with databases, and it suggests you to implement a connection pool.
type DB struct {
driver driver.Driver
dsn string
mu sync.Mutex // protects freeConn and closed
freeConn []driver.Conn
closed bool
}
As you can see, Open function returns a DB that has a freeConn, and this is the simple connection pool. Its implementation is very simple or ugly, it uses defer db.putConn(ci, err) in function Db.prepare to put connection into connection pool. Every time you call Conn function, it checks length of freeCoon, if it's greater than 0 means there is a reusable connection and directly returns to you, otherwise it creates a new connection and returns.