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.
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
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.
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)
}
Prepare
returns prepare status of corresponding SQL commands for querying and deleting, etc.Close
closes 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.Begin
returns a Tx that represents a affair handle, you can use it for querying, updating or affair roll back etc.
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)
}
Close
closes current connection, but it still returns rows data if it is doing query operation.NumInput
returns 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.Exec
executes SQL commands ofupdate/insert
that are prepared inPrepare
, returnsResult
.Query
executes SQL commands ofselect
that are prepared inPrepare
, returns rows data.
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
}
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.
This is the interface for result of update/insert
operations.
type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}
LastInsertId
returns auto-increment Id number after insert operation from database.RowsAffected
returns rows that affected after query operation.
This is the interface for result set of query operation.
type Rows interface {
Columns() []string
Close() error
Next(dest []Value) error
}
Columns
returns fields information of database tables, the slice is one-to-one correspondence to SQL query field, not all fields in that database table.Close
closes Rows iterator.Next
returns next data and assigns to dest, all string should be converted to byte array, and gets io.EOF error if no more data available.
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)
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
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
scan
function.
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.
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.