Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a function that accepts two different objects that have the same method

Tags:

go

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.

My Original Code

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
}

Desired Refactor

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?

Update/Solution

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
}
like image 949
Elliot Larson Avatar asked May 28 '16 23:05

Elliot Larson


3 Answers

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.

like image 84
seh Avatar answered Oct 19 '22 22:10

seh


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
}
like image 34
dlamblin Avatar answered Oct 19 '22 22:10

dlamblin


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)
}
like image 37
Kaveh Shahbazian Avatar answered Oct 19 '22 22:10

Kaveh Shahbazian