Access struct property by name

Here is a simple go program that is not working :

package main
import "fmt"

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2}
    fmt.Println(getProperty(&v, "X"))
}

func getProperty(v *Vertex, property string) (string) {
    return v[property]
}

Error:

prog.go:18: invalid operation: v[property] (index of type *Vertex)

What I want is to access the Vertex X property using its name. If I do v.X it works, but v["X"] doesn't.

Can someone tell me how to make this work ?


Solution 1:

Most code shouldn't need this sort of dynamic lookup. It's inefficient compared to direct access (the compiler knows the offset of the X field in a Vertex structure, it can compile v.X to a single machine instruction, whereas a dynamic lookup will need some sort of hash table implementation or similar). It's also inhibits static typing: the compiler has no way to check that you're not trying to access unknown fields dynamically, and it can't know what the resulting type should be.

But... the language provides a reflect module for the rare times you need this.

package main

import "fmt"
import "reflect"

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2}
    fmt.Println(getField(&v, "X"))
}

func getField(v *Vertex, field string) int {
    r := reflect.ValueOf(v)
    f := reflect.Indirect(r).FieldByName(field)
    return int(f.Int())
}

There's no error checking here, so you'll get a panic if you ask for a field that doesn't exist, or the field isn't of type int. Check the documentation for reflect for more details.

Solution 2:

You now have the project oleiade/reflections which allows you to get/set fields on struct value or pointers.
It makes using the reflect package less tricky.

s := MyStruct {
    FirstField: "first value",
    SecondField: 2,
    ThirdField: "third value",
}

fieldsToExtract := []string{"FirstField", "ThirdField"}

for _, fieldName := range fieldsToExtract {
    value, err := reflections.GetField(s, fieldName)
    DoWhatEverWithThatValue(value)
}


// In order to be able to set the structure's values,
// a pointer to it has to be passed to it.
_ := reflections.SetField(&s, "FirstField", "new value")

// If you try to set a field's value using the wrong type,
// an error will be returned
err := reflection.SetField(&s, "FirstField", 123)  // err != nil

Solution 3:

With getAttr, you can get and set easy.

package main

import (
    "fmt"
    "reflect"
)

func getAttr(obj interface{}, fieldName string) reflect.Value {
    pointToStruct := reflect.ValueOf(obj) // addressable
    curStruct := pointToStruct.Elem()
    if curStruct.Kind() != reflect.Struct {
        panic("not struct")
    }
    curField := curStruct.FieldByName(fieldName) // type: reflect.Value
    if !curField.IsValid() {
        panic("not found:" + fieldName)
    }
    return curField
}

func main() {
    type Point struct {
        X int
        y int  // Set prefix to lowercase if you want to protect it.
        Z string
    }

    p := Point{3, 5, "Z"}
    pX := getAttr(&p, "X")

    // Get test (int)
    fmt.Println(pX.Int()) // 3

    // Set test
    pX.SetInt(30)
    fmt.Println(p.X)  // 30

    // test string
    getAttr(&p, "Z").SetString("Z123")
    fmt.Println(p.Z)  // Z123

    py := getAttr(&p, "y")
    if py.CanSet() { // The necessary condition for CanSet to return true is that the attribute of the struct must have an uppercase prefix
        py.SetInt(50) // It will not execute here because CanSet return false.
    }
    fmt.Println(p.y) // 5
}

Run it on 👉 Go Playground

Reference

  • CanSet
  • A Good example of the reflex: https://stackoverflow.com/a/6396678