可以将golang的db.Query()输出转储为字符串吗?

huangapple go评论89阅读模式
英文:

Is it possible to dump golang db.Query() output to a string?

问题

我有一个小的Heroku应用程序,在查询执行后,我从每一行打印出姓名和年龄。

我想避免使用循环的rows.Next()和Scan(),只想在查询执行后显示数据库返回的数据或错误。

我们可以直接将数据转储到字符串中以进行打印吗?

rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)

if err != nil {
    log.Fatal(err)
}
for rows.Next() {
    var name string
    if err := rows.Scan(&name); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s is %d\n", name, age)
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}
英文:

I have a small Heroku app in which i print out name and age from each rows after query execution.

I want to avoid looping rows.Next(),Scan().. and just want to show what database returned after query execution which may be some data or error.

Can we directly dump data to a string for printing?

rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)

if err != nil {
    log.Fatal(err)
}
for rows.Next() {
    var name string
    if err := rows.Scan(&name); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s is %d\n", name, age)
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

答案1

得分: 7

基本上:不。

Query方法将返回一个指向Rows结构的指针:

func (db *DB) Query(query string, args ...interface{}) (*Rows, error)

如果你打印它(fmt.Printf("%#v\n", rows)),你会看到如下内容:

&sql.Rows{dc:(*sql.driverConn)(0xc8201225a0), releaseConn:(func(error))(0x4802c0), rowsi:(*pq.rows)(0xc820166700), closed:false, lastcols:[]driver.Value(nil), lasterr:error(nil), closeStmt:driver.Stmt(nil)}

...可能不是你想要的。

这些对应于sql包中的Rows结构(你会注意到这些字段没有被导出):

type Rows struct {
    dc          *driverConn // owned; must call releaseConn when closed to release
    releaseConn func(error)
    rowsi       driver.Rows
    
    closed    bool
    lastcols  []driver.Value
    lasterr   error       // non-nil only if closed is true
    closeStmt driver.Stmt // if non-nil, statement to Close on close
}

你会看到[]driver.Value(来自driver包的接口),看起来我们可以在其中找到一些有用的、甚至是可读的数据。但是直接打印它时,它似乎没有用,甚至是空的...所以你必须以某种方式获取底层信息。sql包给了我们一个起点,即Next方法:

Next准备下一行结果以便使用Scan方法读取。如果成功,返回true;如果没有下一行结果或者在准备过程中发生错误,则返回false。应该通过Err来区分这两种情况。

每次调用Scan(即使是第一次调用)之前,必须先调用Next。

Next会创建一个与我拥有的列数相同大小的[]driver.Value,它可以通过driver.Rows(即rowsi字段)在sql包内部访问,并用查询结果中的值填充它。

在调用rows.Next()之后,如果你再次执行相同的fmt.Printf("%#v\n", rows),你应该会看到[]driver.Value不再为空,但它仍然不会是你可以读取的内容,更可能是类似于[]driver.Value{[]uint8{0x47, 0x65...的东西。

由于该字段没有被导出,你甚至不能尝试将其转换为更有意义的内容。但是sql包给了我们一种处理数据的方法,即Scan。

Scan方法非常简洁,有着冗长的注释,我不会在这里粘贴,但真正重要的部分是它遍历来自Next方法的当前行中的列,并调用convertAssign(dest[i], sv),你可以在这里看到它:
https://golang.org/src/database/sql/convert.go

它非常长,但实际上相对简单,它基本上根据源和目标的类型进行转换,并在可能的情况下进行转换,从源复制到目标;函数注释告诉我们:

convertAssign将src中的值复制到dest中,如果可能的话进行转换。如果复制会导致信息丢失,则返回错误。dest应该是一个指针类型。

所以现在你有了一个可以直接调用的方法(Scan),它会返回转换后的值。你上面的代码示例是正确的(除了可能在Scan错误上调用Fatal())。

重要的是要意识到,sql包必须与特定的驱动程序一起工作,而这个驱动程序又是针对特定的数据库软件实现的,所以背后有很多工作正在进行。

如果你想要隐藏/泛化整个Query() ---> Next() ---> Scan()的习惯用法,我认为你最好将它放入另一个函数中,在函数的背后进行处理...编写一个包,在其中抽象出这个更高级别的实现,就像sql包抽象出一些特定于驱动程序的细节、转换和复制、填充Rows等一样。

英文:

Pretty much: No.

The Query method is going to return a pointer to a Rows struct:

func (db *DB) Query(query string, args ...interface{}) (*Rows, error)

If you print that (fmt.Printf("%#v\n", rows)) you'll see something such as:

&sql.Rows{dc:(*sql.driverConn)(0xc8201225a0), releaseConn:(func(error)(0x4802c0), rowsi:(*pq.rows)(0xc820166700), closed:false, lastcols:[]driver.Value(nil), lasterr:error(nil), closeStmt:driver.Stmt(nil)}

...probably not what you want.

Those correspond to the Rows struct from the sql package (you'll notice the fields are not exported):

type Rows struct {
    dc          *driverConn // owned; must call releaseConn when closed to release
  	releaseConn func(error)
  	rowsi       driver.Rows
  	
  	closed    bool
  	lastcols  []driver.Value
  	lasterr   error       // non-nil only if closed is true
 	closeStmt driver.Stmt // if non-nil, statement to Close on close
  	}

You'll see []driver.Value (an interface from the driver package), that looks like where we can expect to find some useful, maybe even human readable data. But when directly printed it doesn't appear useful, it's even empty... So you have to somehow get at the underlying information. The sql package gives us the Next method to start with:

> Next prepares the next result row for reading with the Scan method.
> It returns true on success, or false if there is no next
> result row or an error happened while preparing it. Err
> should be consulted to distinguish between the two cases.
>
>Every call to Scan, even the first one, must be preceded by a call to Next.

Next is going to make a []driver.Value the same size as the number of columns I have, which is accessible (within the sql package) through driver.Rows (the rowsi field) and populate it with values from the query.

After calling rows.Next() if you did the same fmt.Printf("%#v\n", rows) you should now see that []diver.Value is no longer empty but it's still not going to be anything that you can read, more likely something resembling:[]diver.Value{[]uint8{0x47, 0x65...

And since the field isn't exported you can't even try and convert it to something more meaningful. But the sql package gives us a means to do something with the data, which is Scan.

The Scan method is pretty concise, with lengthy comments that I won't paste here, but the really important bit is that it ranges over the columns in the current row you get from the Next method and calls convertAssign(dest[i], sv), which you can see here:
https://golang.org/src/database/sql/convert.go

It's pretty long but actually relatively simple, it essentially switches on the type of the source and destination and converts where it can, and copies from source to destination; the function comments tell us:

>convertAssign copies to dest the value in src, converting it if possible. An error is returned if the copy would result in loss of information. dest should be a pointer type.

So now you have a method (Scan) which you can call directly and which hands you back converted values. Your code sample above is fine (except maybe the call to Fatal() on a Scan error).

It's important to realize that the sql package has to work with a specific driver, which is in turn implemented for specific database software, so there is quite some work going on behind the scenes.

I think your best bet if you want to hide/generalize the whole Query() ---> Next() ---> Scan() idiom is to drop it into another function which does it behind the scenes... write a package in which you abstract away that higher level implementation, as the sql package abstracts away some of the driver-specific details, the converting and copying, populating the Rows, etc.

huangapple
  • 本文由 发表于 2016年4月19日 19:12:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/36716699.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定