I'm learning Golang and want to understand the "Go way" of solving this problem.
Specifically, I'm using the sql
package and I'm seeing some redundant functionality in my code that I'd like to pull out into a function.
I have, 1) a user struct:
type User struct {
ID int
FirstName string
LastName string
}
2) a function for getting one user by ID from the database (Postgresql):
func GetUserById(id int) (user User) {
sql := `
SELECT id, first_name, last_name
FROM users
WHERE id = $1
`
row := db.QueryRow(sql, id)
err := row.Scan(&user.ID, &user.FirstName, &user.LastName)
if err != nil {
panic(err)
}
return
}
and, 3) a function for getting all users from the database:
func GetUsers() (users []User) {
sql := `
SELECT id, first_name, last_name
FROM users
ORDER BY last_name
`
rows, err := db.Query(sql)
if err != nil {
panic(err)
}
for rows.Next() {
user := User{}
err := rows.Scan(&user.ID, &user.FirstName, &user.LastName)
if err != nil {
panic(err)
}
users = append(users, user)
}
rows.Close()
return
}
With only 3 fields in the user record, this is a trivial example. But, with a few more fields, the rows.Scan(...)
piece of both data access functions would be nice to move to a function that both could call:
func ScanUserFromRow(row *sql.Row) (user User) {
err := row.Scan(&user.ID, &user.FirstName, &user.LastName)
if err != nil {
panic(err)
}
return
}
Then the updated database access functions would look something like:
func GetUserById(id int) (user User) {
sql := `
SELECT id, first_name, last_name
FROM users
WHERE id = $1
`
row := db.QueryRow(sql, id)
user = ScanUserFromRow(row)
return
}
func GetUsers() (users []User) {
sql := `
SELECT id, first_name, last_name
FROM users
ORDER BY last_name
`
rows, err := db.Query(sql)
if err != nil {
panic(err)
}
for rows.Next() {
user := ScanUserFromRow(rows)
users = append(users, user)
}
rows.Close()
return
}
However, in the case of the GetUserById
function, I'm dealing with an *sql.Row
struct pointer. In the case of the GetUsers
function, I'm dealing with an *sql.Rows
struct pointer. The two are different... obviously, but similar in that they both have a Scan
method.
It seems the type system won't let me create a method that will accept one or the other. Is there a way to do this utilizing interface{}
, or is there some other more idiomatic Go solution for this?
With this question, I'm saying both sql.Row
and sql.Rows
are ducks that "quack" with Scan
. How can I use a function argument that allows both?
@seh provided an answer below that allows the sort of duck-typing I was hoping for by making the argument a custom interface. Here is the resulting code:
type rowScanner interface {
Scan(dest ...interface{}) error
}
func ScanPlayerFromRow(rs rowScanner) (u User) {
err := rs.Scan(&u.ID, &u.FirstName, &u.LastName)
if err != nil {
panic(err)
}
return
}
...or, as @Kaveh pointed out below, the definition of the interface can be inlined in the function argument:
func ScanPlayerFromRow(rs interface {
Scan(des ...interface{}) error
}) (u User) {
err := rs.Scan(&u.ID, &u.FirstName, &u.LastName)
if err != nil {
panic(err)
}
return
}
Both sql.Rows
and sql.Row
have a Scan
method. There's no interface containing that method in the standard library, but it's possible to define it yourself:
type rowScanner interface {
Scan(dest ...interface{}) error
}
You can then write a function that operates on a rowScanner
rather than a *sql.Row
or a *sql.Rows
:
import "database/sql"
type rowScanner interface {
Scan(dest ...interface{}) error
}
func handleRow(scanner rowScanner) error {
var i int
return scanner.Scan(&i)
}
func main() {
var row *sql.Row
handleRow(row) // Crashes due to calling on a nil pointer.
var rows *sql.Rows
handleRow(rows) // Crashes due to calling on a nil pointer.
}
I didn't mock up using a real *sql.Row
or *sql.Rows
, but that should give you the idea. Your desired ScanUserFromRow
function would demand a rowScanner
instead of *sql.Row
.
In this case I'd refactor both to Query()
with sql.rows
and have both return users[]
. Then add a convenience function to return the first item in that array if we know ids are unique.
Something like:
type User struct {
ID int
FirstName string
LastName string
}
func GetUser(id int) (user User) {
users := getUsers(id)
if (len(users) > 0) {
user = users[0]
}
return
}
func GetUsers() []User {
return getUsers(0)
}
func getUsers(id int) (users []User) {
rows := getUserRows(id)
for rows.Next() {
user := User{}
err := rows.Scan(&user.ID, &user.FirstName, &user.LastName)
if err != nil {
panic(err)
}
users = append(users, user)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
rows.Close()
return
}
func getUserRows(id int) (rows sql.Rows) {
sqlSelect := `
SELECT id, first_name, last_name
FROM users`
sqlWhere := `
WHERE id = $1`
sqlOrder := `
ORDER BY last_name`
var err error
if (0 == id) {
rows, err = db.Query(sqlSelect + sqlOrder)
} else {
rows, err = db.Query(sqlSelect + sqlWhere + sqlOrder, id)
}
if err != nil {
panic(err)
}
return
}
You can enforce the argument of your function to obey a specific interface - in this case, having a Scan(dest ...interface{}) error
method:
func sampleHandler(ru interface {
Scan(dest ...interface{}) error
}) error {
var data []interface{}
// result of some action in your logic
return ru.Scan(data)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With