Alex
Alex

Reputation: 6099

How can I use interfaces to allow for more than 1 struct type to make code more useable?

I have a struct that looks like this:

type BetaKey struct {
    Id           int64  `json:"-"`
    IssuerId     int64  `json:"-"          db:"issuer_id"`
    SubjectEmail string `json:"-"          db:"subject_email"`
    IssuedTime   int64  `json:"-"          db:"issued_time"`
    ExpiryTime   int64  `json:"expiryTime" db:"expiry_time"`
    Betakey      string `json:"accessKey"`
    Description  string `json:"description"`
}

And within the same package I have a function that returns a slice of BetaKey:

func buildResults(query string) ([]BetaKey, error) {
    results := []BetaKey{}
    rows, err := sql.DB.Queryx(query)
    if err != nil {
        return results, err
    }
    defer rows.Close()
    for rows.Next() {
        var bk BetaKey
        err := rows.StructScan(&bk)
        if err != nil {
            return results, err
        }
        results = append(results, bk)
    }
    err = rows.Err()
    if err != nil {
        return results, err
    }
    return results, nil
}

Is it possible for me to rewrite this function so that it takes in a query string but also a type of BetaKey as interface{}, and returns a slice of interface{} so that I can reuse the code instead of copy pasting this into every package because it is literally the same but the only difference is the name of the struct that changes.

Is this possible? And also is this advised? If not, then why?

Upvotes: 2

Views: 140

Answers (2)

icza
icza

Reputation: 417642

Generics could be used to implement such thing, but Go does not support generics. To do what you want in Go, you need to use reflection.

You may change your function to take 1 additional parameter, a reflect.Type for example which designates type of values the individual rows should be loaded into.

Then you can use reflect.New() to create a new value of this type and acquire a pointer to it. You can use Value.Interface() to obtain the pointer value as a type of interface{} from the reflect.Value value. This interface{} wrapping the pointer can now be passed to Rows.StructScan().

And if you want the result slice to hold non-pointer values, you can use reflect.Indirect() to get the pointed value (and another reflect.Interface() to extract the struct value as an interface{}).

Example code:

func buildResults(query string, t reflect.Type) ([]interface{}, error) {
    results := []interface{}{}
    rows, err := sql.DB.Queryx(query)
    if err != nil {
        return results, err
    }
    defer rows.Close()
    for rows.Next() {
        val := reflect.New(t)
        err := rows.StructScan(val.Interface())
        if err != nil {
            return results, err
        }
        i_ := reflect.Indirect(val)
        result = append(result, i_.Interface())
    }
    err = rows.Err()
    if err != nil {
        return results, err
    }
    return results, nil
}

The heart of it is the for block:

val := reflect.New(t)                   // A pointer to a new value (of type t)
err := rows.StructScan(val.Interface()) // Pass the pointer to StructScan
if err != nil {
    return results, err
}
i_ := reflect.Indirect(val)             // Dereference the pointer
result = append(result, i_.Interface()) // And add the non-pointer to the result slice

Here's how you can test it:

type BetaKey struct {
    Id   string
    Name string
}

type AlphaKey struct {
    Id     string
    Source string
}

r, err := buildResults("", reflect.TypeOf(AlphaKey{}))
fmt.Printf("%T %+v %v\n", r[0], r, err)

r, err = buildResults("", reflect.TypeOf(BetaKey{}))
fmt.Printf("%T %+v %v\n", r[0], r, err)

Output:

main.AlphaKey [{Id:aa Source:asource} {Id:aa Source:asource} {Id:aa Source:asource}] <nil>
main.BetaKey [{Id:aa Name:aname} {Id:aa Name:aname} {Id:aa Name:aname}] <nil>

Try it on the Go Playground.

Notes:

The above solution will return a value of type of []interface{} whose elements will be of static type interface{} and their dynamic type will be the one specified by the reflect.Type argument. So for example if you call it with type:

bt := reflect.TypeOf(BetaKey{})

The values in the result slice will have dynamic type BetaKey so you can safely type assert them like this:

results, err := buildResults("some query", bt)
if err == nil {
    for _, v := range results {
        key := v.(BetaKey)
        // key is of type BetaKey, you may use it like so
    }
} else {
    // handle error
}

To learn more about reflection, read the blog post:

The Go Blog: The Laws of Reflection

Upvotes: 1

Apin
Apin

Reputation: 2668

I write a little example using json, instead of sql rows. You can try to develop from this code :

package main

import (
    "fmt"
    "reflect"
    "encoding/json"
)

type A struct {
    AField int `json:"a"`
}

type B struct {
    BField string `json:"b"`
}

func build(str string, typ reflect.Type) interface{} {
    results := reflect.MakeSlice(reflect.SliceOf(typ), 0, 10)
    for i:=0; i < 5; i++ {
        res := reflect.New(typ)
        json.Unmarshal([]byte(str), res.Interface())
        results = reflect.Append(results, res.Elem())
    }
    return results.Interface();
}

func main() {
    a := build("{ \"a\": 15 }", reflect.TypeOf(&A{}))
    fmt.Printf("%T : %V\n", a, a)
    b := build("{ \"b\": \"my string\" }", reflect.TypeOf(&B{}))
    fmt.Printf("%T : %V\n", b, b)
}

Golang Playground

Upvotes: 1

Related Questions