Golang Dynamic access to a property in a struct

eye-catch Golang

I needed dynamic access to all properties in a struct in unit tests. In my case, the struct has only boolean properties. I wanted to check if the properties are set to false except for a target property. Since I searched for the way, I wrote it in this post for future use.

Sponsored links

Get struct key names and the type as string

For dynamic access, we need to use reflect package. We use the following simple struct.

type person struct {
    Name   string
    Age    int   
    Gender string
}

By passing data to reflect.ValueOf(), it returns reflect.Value which is something like an abstract object. If the data is a struct, we can get the number of properties by reflect.ValueOf(data).Type().NumField(). By passing an index to reflect.ValueOf(data).Type().Field(index), we can access one of the properties in the struct.

We can also check the offset of the variable.

person := person{
    Name:   "Yuto",
    Age:    99,
    Gender: "Male",
}

value := reflect.ValueOf(person)
for i := 0; i < value.Type().NumField(); i++ {
    field := value.Type().Field(i)
    fmt.Printf("Key: %s, \ttype: %s, \toffset: %v\n", field.Name, field.Type, field.Offset)
}
// Key: Name,      type: string,   offset: 0
// Key: Age,       type: int,      offset: 16
// Key: Gender,    type: string,   offset: 24
Sponsored links

Chain Elem() if the target value is a pointer

If we pass a pointer to reflect.ValueOf(), we must provide the value instead of the address. Therefore, we need value.Elem() to provide the actual value before chaining .Type().

person := person{
    Name:   "Yuto",
    Age:    99,
    Gender: "Male",
}

pValue := reflect.ValueOf(&person)
for i := 0; i < pValue.Elem().Type().NumField(); i++ {
    field := pValue.Elem().Type().Field(i)
    fmt.Printf("Key: %s, \ttype: %s, \toffset: %v\n", field.Name, field.Type, field.Offset)
}
// Key: Name,      type: string,   offset: 0
// Key: Age,       type: int,      offset: 16
// Key: Gender,    type: string,   offset: 24

Get all values

It’s similar to getting key names. We don’t type anymore but need the values, so extract the field without .Type(). Then, get the value by .Interface(). We can also get the data type as string here.

person := person{
    Name:   "Yuto",
    Age:    99,
    Gender: "Male",
}

value := reflect.ValueOf(person)
for i := 0; i < value.Type().NumField(); i++ {
    field := value.Field(i)
    fmt.Printf("type: %s, \tvalue: %v\n", field.Type().Name(), field.Interface())
}
// type: string,   value: Yuto
// type: int,      value: 99
// type: string,   value: Male

Get value by key name

If we want to use a key name to get the value, use .FieldByname("name").

person := person{
    Name:   "Yuto",
    Age:    99,
    Gender: "Male",
}

value := reflect.ValueOf(person)
name := value.FieldByName("Name")
age := value.FieldByName("Age")
fmt.Printf("Name: %v, Age: %v\n", name.Interface(), age.Interface())
// Name: Yuto, Age: 99

How to handle data depending on the data type

I showed a way to get the data type as string but it’s not a good way to implement if we want to handle data differently depending on the data type. We should use .Type().Kind() with switch-case in this case.

person := person{
    Name:   "Yuto",
    Age:    99,
    Gender: "Male",
}

pValue := reflect.ValueOf(&person)
for i := 0; i < pValue.Elem().Type().NumField(); i++ {
    field := pValue.Elem().Field(i)
    switch field.Type().Kind() {
    case reflect.String:
        fmt.Printf("string data: %s\n", field.String())
    case reflect.Int:
        fmt.Printf("int data: %d\n", field.Int())
    default:
        fmt.Printf("interface data: %v\n", field.Interface())
    }
}
// string data: Yuto
// int data: 99
// string data: Male

Even if a function requires a specific data type, we can pass the correct data type in this way. .Interface() returns any data type and thus it can’t be used for a function that requires int. .Int() returns int64 for example, so we should use this when necessary.

If the target struct is not a pointer like this reflect.ValueOf(person), remove .Elem() function call.

How to get key and value

We can already implement it to get both key and value.

person := person{
    Name:   "Yuto",
    Age:    99,
    Gender: "Male",
}

value := reflect.ValueOf(person)
for i := 0; i < value.Type().NumField(); i++ {
    typeField := value.Type().Field(i)
    data := value.Field(i).Interface()

    fmt.Printf("Key: %s, \ttype: %s, \tvalue: %v\n", typeField.Name, typeField.Type, data)
}
// Key: Name,      type: string,   value: Yuto
// Key: Age,       type: int,      value: 99
// Key: Gender,    type: string,   value: Male

Beware of that the name can be accessed from .Type().Field().

How to handle a struct that contains a pointer and struct

How can we handle the case where a struct contains a pointer, array/slice, map, and struct as well?

type mixed struct {
    boolValue  bool
    intValue   int32
    intPointer *int
    person     *person
    arrayValue []int
    mapValue   map[string]int
}

It looks more complicated than previous example but we can handle it by checking the data type with .Kind().

func handleComplexStruct() {
    intValue := 99
    data := mixed{
        boolValue:  true,
        intValue:   55,
        intPointer: &intValue,
        person: &person{
            Name:   "Yuto",
            Age:    55,
            Gender: "Male",
        },
        arrayValue: []int{1, 2, 3, 4},
        mapValue:   map[string]int{"prop1": 11, "prop2": 22},
    }

    value := reflect.ValueOf(data)
    recursiveValue(value)
}

func recursiveValue(value reflect.Value) {
    valueType := value.Type()
    for i := 0; i < valueType.NumField(); i++ {
        field := value.Field(i)
        switch field.Kind() {
        case reflect.Bool:
            fmt.Printf("bool value (%s: %v)\n", valueType.Field(i).Name, field.Bool())
        case reflect.Int, reflect.Int32:
            fmt.Printf("int value (%s: %v)\n", valueType.Field(i).Name, field.Int())
        case reflect.String:
            fmt.Printf("string value (%s: %v)\n", valueType.Field(i).Name, field.String())
        case reflect.Pointer:
            if field.Elem().Kind() == reflect.Struct {
                recursiveValue(field.Elem())
            } else {
                fmt.Printf("pointer value. type: %s, (%s: %v)\n", field.Elem().Type().Name(), valueType.Field(i).Name, field.Elem())
            }
        case reflect.Array, reflect.Slice:
            fmt.Printf("array/slice value (%s: %v)\n", valueType.Field(i).Name, field.Slice(0, field.Len()))
        case reflect.Map:
            fmt.Printf("map value. Key: %s\n", valueType.Field(i).Name)
            iter := field.MapRange()
            for iter.Next() {
                fmt.Printf("  %s: %v\n", iter.Key(), iter.Value())
            }
        default:
            fmt.Printf("unsupported type: %s\n", field.Kind().String())
        }
    }
}
// bool value (boolValue: true)
// int value (intValue: 55)
// pointer value. type: int, (intPointer: 99)
// string value (Name: Yuto)
// int value (Age: 55)
// string value (Gender: Male)
// array/slice value (arrayValue: [1 2 3 4])
// map value. Key: mapValue
//   prop1: 11
//   prop2: 22

The way to handle pointer/array/map is a bit different but it’s not hard to understand. If it’s a struct, call the function again. Since person is a pointer to a struct, field.Elem() is specified to recursiveValue().

Related

Comments

Copied title and URL