今天水群看到个奇怪的需求,要求用go去查数据库,需求就是用户执行任意sql然后把对应数据查询出来用表的形式展示,表类型,字段数量和类型都是不确定的,这种需求用动态语言python很容易实现,用go的话,有点麻烦也不太难,有同学说了动态增加结构体字段,这样的话好像不行。go也没有tuple这种类型就是说,但是可以用[]interface{}
来模拟一下,再结合反射,就好了
go底层一般都是database/sql
这个结构去操作mysql的,提供的接口是db.Query
,看一下
go
// Query executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
//
// Query uses context.Background internally; to specify the context, use
// QueryContext.
func (db *DB) Query(query string, args ...any) (*Rows, error) {
return db.QueryContext(context.Background(), query, args...)
}
// *****************************************************************
// QueryContext executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error) {
var rows *Rows
var err error
err = db.retry(func(strategy connReuseStrategy) error {
rows, err = db.query(ctx, query, args, strategy)
return err
})
return rows, err
}
// ******************************************************************
func (db *DB) query(ctx context.Context, query string, args []any, strategy connReuseStrategy) (*Rows, error) {
dc, err := db.conn(ctx, strategy)
if err != nil {
return nil, err
}
return db.queryDC(ctx, nil, dc, dc.releaseConn, query, args)
}
//*****************************************************************
// queryDC executes a query on the given connection.
// The connection gets released by the releaseConn function.
// The ctx context is from a query method and the txctx context is from an
// optional transaction context.
func (db *DB) queryDC(ctx, txctx context.Context, dc *driverConn, releaseConn func(error), query string, args []any) (*Rows, error) {
queryerCtx, ok := dc.ci.(driver.QueryerContext)
var queryer driver.Queryer
if !ok {
queryer, ok = dc.ci.(driver.Queryer)
}
if ok {
var nvdargs []driver.NamedValue
var rowsi driver.Rows
var err error
withLock(dc, func() {
nvdargs, err = driverArgsConnLocked(dc.ci, nil, args)
if err != nil {
return
}
rowsi, err = ctxDriverQuery(ctx, queryerCtx, queryer, query, nvdargs)
})
if err != driver.ErrSkip {
if err != nil {
releaseConn(err)
return nil, err
}
// Note: ownership of dc passes to the *Rows, to be freed
// with releaseConn.
rows := &Rows{
dc: dc,
releaseConn: releaseConn,
rowsi: rowsi,
}
rows.initContextClose(ctx, txctx)
return rows, nil
}
}
var si driver.Stmt
var err error
withLock(dc, func() {
si, err = ctxDriverPrepare(ctx, dc.ci, query)
})
if err != nil {
releaseConn(err)
return nil, err
}
ds := &driverStmt{Locker: dc, si: si}
rowsi, err := rowsiFromStatement(ctx, dc.ci, ds, args...)
if err != nil {
ds.Close()
releaseConn(err)
return nil, err
}
// Note: ownership of ci passes to the *Rows, to be freed
// with releaseConn.
rows := &Rows{
dc: dc,
releaseConn: releaseConn,
rowsi: rowsi,
closeStmt: ds,
}
rows.initContextClose(ctx, txctx)
return rows, nil
}
其实上面也不用看太多,只需要知道这个Row结构就好
go// Rows 是查询的结果集。其光标位于结果集的第一行之前,可以使用 Next 方法逐行移动。
type Rows struct {
dc *driverConn // 拥有的数据库连接;在关闭时必须调用 releaseConn 进行释放
releaseConn func(error)
rowsi driver.Rows // 实现 driver.Rows 接口的对象,用于与底层数据库驱动通信,获取数据
cancel func() // 在关闭 Rows 时调用的函数;可能为 nil
closeStmt *driverStmt // 如果非 nil,则表示在关闭 Rows 时需要关闭的语句
// closemu 防止在有活动的流式结果时关闭 Rows。在非关闭操作期间,它被用于读,而在关闭操作期间,它是独占的。
// closemu 保护了 lasterr 和 closed。
closemu sync.RWMutex
closed bool
lasterr error // 仅在 closed 为 true 时非 nil
// lastcols 仅在 Scan、Next 和 NextResultSet 方法中使用,预期不会并发调用。
lastcols []driver.Value
}
//****************************************************************
// driver.Rows
// Rows is an iterator over an executed query's results.
type Rows interface {
// Columns returns the names of the columns. The number of
// columns of the result is inferred from the length of the
// slice. If a particular column name isn't known, an empty
// string should be returned for that entry.
Columns() []string
// Close closes the rows iterator.
Close() error
// Next is called to populate the next row of data into
// the provided slice. The provided slice will be the same
// size as the Columns() are wide.
//
// Next should return io.EOF when there are no more rows.
//
// The dest should not be written to outside of Next. Care
// should be taken when closing Rows not to modify
// a buffer held in dest.
Next(dest []Value) error
}
sql.Rows是实现了
Columns`方法的
go// Columns returns the column names.
// Columns returns an error if the rows are closed.
func (rs *Rows) Columns() ([]string, error) {
rs.closemu.RLock()
defer rs.closemu.RUnlock()
if rs.closed {
return nil, rs.lasterrOrErrLocked(errRowsClosed)
}
if rs.rowsi == nil {
return nil, rs.lasterrOrErrLocked(errNoRows)
}
rs.dc.Lock()
defer rs.dc.Unlock()
return rs.rowsi.Columns(), nil
}
// **************************************************************************
拿到一行数据的Scan方法
// Scan 将当前行中的列复制到由 dest 指向的值。dest 中的值的数量必须与 Rows 中的列数相同。
//
// Scan 将从数据库读取的列转换为以下通用 Go 类型和 sql 包提供的特殊类型:
//
// *string
// *[]byte
// *int, *int8, *int16, *int32, *int64
// *uint, *uint8, *uint16, *uint32, *uint64
// *bool
// *float32, *float64
// *interface{}
// *RawBytes
// *Rows(游标值)
// 实现 Scanner 接口的任何类型(请参阅 Scanner 文档)
//
// 在最简单的情况下,如果源列的值的类型为整数、布尔或字符串类型 T,而 dest 的类型为 *T,
// 则 Scan 只是通过指针分配值。
//
// Scan 还在字符串和数值类型之间进行转换,只要不会丢失信息。虽然 Scan 将从数值数据库列扫描的所有数字都转换为 *string,
// 但对数值类型的扫描会检查溢出。例如,float64 值为 300 或字符串值为 "300" 可以扫描到 uint16,但不能扫描到 uint8,
// 而 float64(255) 或 "255" 可以扫描到 uint8。有一个例外,即将一些 float64 数字扫描为字符串时可能会在字符串化时丢失信息。
// 通常情况下,将浮点列扫描到 *float64。
//
// 如果 dest 参数的类型为 *[]byte,则 Scan 会在该参数中保存相应数据的副本。该副本由调用方拥有,可以被修改并无限期保持。
// 通过使用类型为 *RawBytes 的参数,可以避免该副本;请参阅 RawBytes 文档以获取其使用限制。
//
// 如果参数的类型为 *interface{},则 Scan 将在没有转换的情况下复制底层驱动程序提供的值。
// 当从类型为 []byte 的源值扫描到 *interface{} 时,会创建切片的副本,调用者拥有该结果。
//
// 类型为 time.Time 的源值可以扫描到类型为 *time.Time、*interface{}、*string 或 *[]byte 的值。
// 在转换为后两者时,将使用 time.RFC3339Nano。
//
// 类型为 bool 的源值可以扫描到类型为 *bool、*interface{}、*string、*[]byte 或 *RawBytes 的值。
//
// 对于扫描到 *bool,源值可以是 true、false、1、0 或可以由 strconv.ParseBool 解析的字符串输入。
//
// Scan 还可以将从查询返回的游标(例如 "select cursor(select * from my_table) from dual")
// 转换为可以进行扫描的 *Rows 值。如果父查询的 *Rows 关闭,则父查询将关闭任何游标 *Rows。
//
// 如果实现 Scanner 接口的第一个参数之一返回错误,则该错误将被包装在返回的错误中。
func (rs *Rows) Scan(dest ...any) error {
rs.closemu.RLock()
if rs.lasterr != nil && rs.lasterr != io.EOF {
rs.closemu.RUnlock()
return rs.lasterr
}
if rs.closed {
err := rs.lasterrOrErrLocked(errRowsClosed)
rs.closemu.RUnlock()
return err
}
rs.closemu.RUnlock()
if rs.lastcols == nil {
return errors.New("sql: Scan called without calling Next")
}
if len(dest) != len(rs.lastcols) {
return fmt.Errorf("sql: expected %d destination arguments in Scan, not %d", len(rs.lastcols), len(dest))
}
for i, sv := range rs.lastcols {
err := convertAssignRows(dest[i], sv, rs)
if err != nil {
return fmt.Errorf(`sql: Scan error on column index %d, name %q: %w`, i, rs.rowsi.Columns()[i], err)
}
}
return nil
}
还有Next
方法,置于底层,还是调用的driver.Rows.Next
,我们只要知道这两个方法,就可以。
go// Next 准备下一行结果以便使用 Scan 方法读取。它在成功时返回 true,如果没有下一行结果或者在准备结果时发生错误则返回 false。
// 应该参考 Err 来区分这两种情况。
//
// 每次调用 Scan,即使是第一次调用,都必须在之前调用 Next。
func (rs *Rows) Next() bool {
var doClose, ok bool
// 使用锁保护对 nextLocked 方法的调用
withLock(rs.closemu.RLocker(), func() {
doClose, ok = rs.nextLocked()
})
// 如果需要关闭,则调用 Close 方法
if doClose {
rs.Close()
}
// 返回结果
return ok
}
还有数据,上面看到了字段columns
是字符串切片,数据其实是
go// If the driver supports cursors, a returned Value may also implement the Rows interface
// in this package. This is used, for example, when a user selects a cursor
// such as "select cursor(select * from my_table) from dual". If the Rows
// from the select is closed, the cursor Rows will also be closed.
type Value any
空切片
捋清一下,我们通过Query查出一个Rows对象,调用该对象的Columns
能得到表示列名的string[]
,然后可以调用Scan方法和Next来拿到每一行数据,我们用[]interface{}
来保存数据,一开始可以不在乎类型,先拿到数据再处理
接下来是代码
go rows, err := db.Query(sqlQuery)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// 获取列信息
cols, err := rows.Columns()
if err != nil {
log.Fatal(err)
}
// 打印列名
for _, col := range cols {
fmt.Printf("%s\t", col)
}
fmt.Println()
很简单,接下来是拿到数据,用空接口切片保存
go// 准备存储结果的切片
values := make([]interface{}, len(cols))
//values := make([]string, len(cols))
for i := range values {
var v interface{}
values[i] = &v
}
// 遍历结果集
for rows.Next() {
// 将每一行的数据加载到values切片中
if err := rows.Scan(values...); err != nil {
log.Fatal(err)
}
// 打印每一行的数据
for _, value := range values {
fmt.Printf("%v\t", convertToString(*value.(*interface{})))
}
fmt.Println()
}
这里写了个converToString
为了处理类型方便,用反射
go
func convertToString(value interface{}) string {
switch v := value.(type) {
case nil:
return "<nil>"
case []byte:
return fmt.Sprintf("[%s]", string(v))
default:
return fmt.Sprintf("%v", v)
}
}
返回数据给前端照样可以用一个二维的string切片,一个一个append就好。
gopackage main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"log"
)
func main() {
// 连接数据库
db, err := sql.Open("mysql", "root:0503@tcp(106.52.78.230:3306)/java_demo")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 获取用户输入的SQL语句
fmt.Print("Enter SQL Query: ")
//var sqlQuery string
//fmt.Scan(&sqlQuery)
sqlQuery := "select * from or_tag;"
// 执行SQL查询
rows, err := db.Query(sqlQuery)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// 获取列信息
cols, err := rows.Columns()
if err != nil {
log.Fatal(err)
}
// 准备存储结果的切片
values := make([]interface{}, len(cols))
//values := make([]string, len(cols))
for i := range values {
var v interface{}
values[i] = &v
}
// 打印列名
for _, col := range cols {
fmt.Printf("%s\t", col)
}
fmt.Println()
var results [][]string
results = append(results, cols)
// 遍历结果集
for rows.Next() {
// 将每一行的数据加载到values切片中
if err := rows.Scan(values...); err != nil {
log.Fatal(err)
}
var res []string
// 打印每一行的数据
for _, value := range values {
res = append(res, convertToString(*value.(*interface{})))
fmt.Printf("%v\t", convertToString(*value.(*interface{})))
}
results = append(results, res)
fmt.Println()
}
fmt.Println(results)
// 检查错误
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
// Convert byte slice to string representation
func convertToString(value interface{}) string {
switch v := value.(type) {
case nil:
return "<nil>"
case []byte:
return fmt.Sprintf("[%s]", string(v))
default:
return fmt.Sprintf("%v", v)
}
}
本文作者:yowayimono
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!