While working on Gorp with Indexes i had to solve the problem of how to write into a slice inside a struct passed as an interface{}. A new struct with the filled slice should be returned. Both the struct and the slice are not known at compile time, i only have the fieldname of the slice passed to me at runtime. After some serious headscratching and reading through the reflection code i luckily found a way to do it.
The basic method is:
- Convert the incoming interface i to a reflect.Type t
- Dereference until we have a t which is a Struct
- Create a new reflect.Value v from the Type t
- From v we now can ask for the FieldName of the slice, getting back another reflect.Value s
- As we now have the slice s as a reflect.Value, we need to get the slice elements type
- From the slice elements type we create a new instance of it (reflect.New)
- Write to fields in this newItem using FieldByName (Hardcoded in the example)
- Append the newItem to the slice (the set append was the hard part to find out for me)
- Return the reflect.Value v as an interface
- ???
- Profit - Heureka!!
Output:
Input Type main.Post:
Slice Type []*main.Comment:
Slice Elem Type main.Comment:
Comment 0, Body XYZ 0, PostId 0
Comment 1, Body XYZ 1, PostId 2
Comment 2, Body XYZ 2, PostId 4
Comment 3, Body XYZ 3, PostId 6
Comment 4, Body XYZ 4, PostId 8
Erfolg: Prozess beendet mit Rückgabewert 0.
// This is a demo to show how to convert from a normal struct
// to a reflection type and back to a struct without knowing
// the original one. Input is passed as an Interface and the
// output will be an interface, too.
// Bonus points for writing into an embedded slice
// (= the embedded Comment struct slice in Post)
package main
import (
"errors"
"fmt"
"os"
"reflect"
)
type Post struct {
Id uint64
Title string
Comments []*Comment
}
// holds a single comment bound to a post
type Comment struct {
Id uint64
PostId uint64
Body string
}
func CreateAndFillSlice(i interface{}, sliceName string) (interface{}, error) {
// Convert the interface i to a reflect.Type t
t := reflect.TypeOf(i)
// Check if the input is a pointer and dereference it if yes
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
// Check if the input is a struct
if t.Kind() != reflect.Struct {
return nil, errors.New("Input param is not a struct")
}
fmt.Printf("Input Type %v:\n", t)
// Create a new Value from the input type
// this will be returned to the caller
v := reflect.New(t).Elem()
// Get the field named "sliceName" from the input struct, which should be a slice
s := v.FieldByName(sliceName)
if s.Kind() == reflect.Slice {
st := s.Type()
fmt.Printf("Slice Type %s:\n", st)
// Get the type of a single slice element
sliceType := st.Elem()
// Pointer?
if sliceType.Kind() == reflect.Ptr {
// Then dereference it
sliceType = sliceType.Elem()
}
fmt.Printf("Slice Elem Type %v:\n", sliceType)
for i := 0; i < 5; i++ {
// Create a new slice element
newitem := reflect.New(sliceType)
// Set some field in it
newitem.Elem().FieldByName("Body").SetString(fmt.Sprintf("XYZ %d", i))
newitem.Elem().FieldByName("PostId").SetUint(uint64(i * 2))
// This is the important part here - append and set
// Append the newitem to the slice in "v" which will be the output
s.Set(reflect.Append(s, newitem))
}
} else {
return nil, fmt.Errorf("Field %s is not a slice\n", sliceName)
}
// IMPORTANT
// Cast back to the empty interface type
// So the cast back to Post outside will work
return v.Interface(), nil
}
func main() {
var err error
p := Post{Id: 1, Title: "Title 1"}
result, err := CreateAndFillSlice(p, "Comments")
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
// Cast the returned interface to a Post
post := result.(Post)
for i, c := range post.Comments {
fmt.Printf("Comment %d, Body %s, PostId %d\n", i, c.Body, c.PostId)
}
}