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.
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
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()
.
Comments