编辑
2023-12-02
后端
00
请注意,本文编写于 525 天前,最后修改于 525 天前,其中某些信息可能已经过时。

目录

获取列名
完整代码

今天水群看到个奇怪的需求,要求用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) } }

image.png

返回数据给前端照样可以用一个二维的string切片,一个一个append就好。

完整代码

go
package 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 许可协议。转载请注明出处!