상세 컨텐츠

본문 제목

[5.7.7] Reflection on Structs and Interfaces

Go/Mastering Go

by Gopythor 2022. 4. 11. 03:21

본문

728x90
반응형

Introduction

Reflection and structs
Reflection and struct tags
Reflections and interfaces

  • In the previous, we took a big step in understanding how reflection works in the Go language.
  • Now we will dive deeper into the topic by covering how to use reflection with structs and interfaces.
  • In this lecture, we will start by explaining how reflection needs structs
  • And then we'll explore struct tags which are an important way to send messages from a struct to a refleciton object.
  • And then finally we will conclude by discovering the techniques of using reflection on interfaces.

Reflection on Structs

Use type reflect object to get metadata of the struct and it's fields(types, name.. etc)
Use value reflect object to get the actual values of the struct and it's fields (values of fields)

  • So in order to inspect a struct via reflection object, we need to draw the distinction between the information provided by the reflect.value struct type versus the information provided by the reflect.type interface.
  • So those are two types that we find in the reflect object.
  • So our reflection object that implements subtype interface will help us get meta information about our struct and its fields like the field types the field names and etc..
  • On the other hand an object that implements the reflect.value struct will help us get to the actual values of the fields and will even allow us to change the values of the struct fields.
  • So let's explore further this code to see how that is done.
func main() {
    type myStruct struct {
        field1 int
        field2 string
        field3 float64
    }
    mys := myStruct{2, "Hello", 2.4}

    mysRValue := reflect.ValueOf(mys)
    mysRType := reflect.TypeOf(mys)

    for i := 0; i < mysRType.NumField(); i++ {
        fieldRType := mysRType.Field(i) //datatype: StructField
        fieldRValue := mysRValue.Field(i)

}
  • So let's go to the Go playground to play with some code.
  • So we will import the reflect package to explore reflection on structs and then we'll create a struct for our tests.
  • So we'll call the struct myStruct and it will host three fields.
  • field1 of type int
  • field2 of type string
  • And then field3 with by float64.
  • Now we'll create a new variable called mys which stands for my struct.
  • And this variable will basically use a struct literal statement to define and use struct of type myStruct.
  • So we fill the different fields with different data types.
  • Now let's play around with the reflect package and myStruct.
  • So I'll create a value called mysRValue which will basically be the reflect.Value object.
  • And we get that by calling reflect.ValueOf with mys.
  • We'll create another variable called mysRType which stands for my struct reflection type.
  • And it will be the type object of mys.
  • So it'll be reflect.TypeOf as a call we need to use.
  • And we'll fill it the mys variable as an argument.
  • Now let's see how we can use a mysRType variable.
  • So first thing the reflect type object can provide for us is a method called NumField.
  • This method returns a number of fields of the underlying object that the reflection objects here represents.

https://www.codetd.com/ko/article/12196207

  • So since mysRType represents the reflection type object of the mys variable.
  • And mys variable is of the type myStruct.
  • So that means that NumField should be 3.
  • That is because we have three fields here in this struct type.
  • So see here the distinction how the type object or reflection type object returns information about the actual type of our object.
  • So we can use this number inside the for loop.
  • In order to be able to perform some operations on each field.
  • So the for loop here will help us navigate through the different fields.
  • Now to get the reflection type object for each field.
  • We can call mysRType field which is a method the type object provides us.
  • And this method takes a number as an argument.
  • And that number will indicate the field ID.
  • So this should be field zero.
  • This should be field 1.
  • And this should be field 2.
  • Now what if we want to get that value object of each field or the reflection value objects In other words.
  • For this we'll create a new variable called fieldRValue in this field we use.
  • So the field here will use a method called field.
  • But this time the methods will belong to mysRValue.
  • So mysRValue again was a main reflection value objects that we created from myStruct.
  • The field method on the mysRValue or the reflection value object also takes a number for presents a field.
  • So by doing this, we get the type object of the field and the value object of the field.
  • The fieldRType object is an object that belongs to a data type called struct field which will explore very shortly.
  • So this object belongs to a datatype called StructField.
  • The fieldRValue on the other hand belongs to the normal reflect.value objects.
  • So fieldRValue will belong to the same type as mysRValue.
  • Just for fieldRValue will be more focused on the actual field as opposed to the whole struct.
  • So struct field is a type that we haven't discussed in the past.
  • So let's have a look at.

StructField

type StructField struct {
    // Name is the field name.
    Name string

    // PkgPath is the package path that qualifies a lower case (unexported)
    // field name. It is empty for upper case (exported) field names.
    // See https://golang.org/ref/spec#Uniqueness_of_identifiers
    PkgPath string

    Type      Type      // field type
    Tag       StructTag // field tag string
    Offset    uintptr   // offset within struct, in bytes
    Index     []int     // index sequence for Type.FieldByIndex
    Anonymous bool      // is an embedded field
}
  • So StructField is a type that exists in the reflect package.
  • And it basically contains some mata data that we need for each field inside a struct.
  • So we can get the field Name
  • And we can get the PkgPath.
  • We can get the field Type.
  • We can get the StructTag.
  • This is very important ETC.
  • You can even tell if it's an embedded field.
  • A StructField is only provided when we call the field method.
  • This one of the type struct.
  • So because this type object represented a go struct that field method on that object returned a StructField type.
  • If we call that field method on a reflect.value object on the other hand like this one.
  • One that we obtain by calling a respect.ValueOf with myStruct.
  • We'll get another reflect.value object.
  • So same type here.
  • Now let's import the fmt package and then use it to write down some information to the standard output to help us see what we obtained.
  • So use field.
  • So fmt.Printf so that we can print a formatted string and then you will format the string.
  • So we'll say Field Name.
  • And then we use the pecent and s to indicate that a string will be expected here.
  • Field type another percent s.
  • Field value percent v.
  • Then we'll do to a new line using slash n.
  • Then let's show this data.
  • So for field name, we will find the field name would be more far type meta data piece of info.
  • So we should find it in the fieldRType object.
  • So this should be the fieldRType.
  • Then the field would be called name.
  • Then to get the field type.
  • We also get this information from a field RType object but this time we will get Type.
  • So because fieldRType is a struct by the end of the date.
  • The struct we can access its field directory.
  • And that's exactly what I'm doing.
  • So I'm showing the name.
  • I'm showing the type.
  • And then finally we need to get the value of this field.
  • So for this, we will use fieldRValue object.
  • Because this is where our value information is stored.
  • And if you remember from the previous lesson, to get this information, we called the Interface method.
  • This will give us the actual value.
  • Now what happens if we try to run this code.
func main() {
    type myStruct struct {
        field1 int
        field2 string
        field3 float64
    }
    mys := myStruct{2, "Hello", 2.4}

    mysRValue := reflect.ValueOf(mys)
    mysRType := reflect.TypeOf(mys)

    for i := 0; i < mysRType.NumField(); i++ {
        fieldRType := mysRType.Field(i) //datatype: StructField
        fieldRValue := mysRValue.Field(i)
        fmt.Printf("Field Name: '%s', field type: '%s', field value: '%v' \n", fieldRType.Name, fieldRType.Type, fieldRValue.Interface())
    }
}

Result

panic: reflect.Value.Interface: cannot return value obtained from unexported field or method.

https://go.dev/play/p/Eg0JSPgZRQ5

  • We'll get the panic.
  • So why is that.
  • It's important to know the reason in order to be able to properly design reflection code.
  • So if you look here at the message on the panic, it says cannot return value obtained from unexported field or method.
  • So what does that really mean.
  • It basically means that the fields in our struct here need to be exportable so that our reflection code here can access it.
  • So to fix this, we need to make those fields exportable.
    type myStruct struct {
        Field1 int
        Field2 string
        Field3 float64
    }

https://go.dev/play/p/QuwFnvTJe9n

  • And we do that by making the first letter an uppercase letter.
  • Now if I try to run, the code works.

result

Field Name: 'Field1', field type: 'int', field value: '2' 
Field Name: 'Field2', field type: 'string', field value: 'Hello' 
Field Name: 'Field3', field type: 'float64', field value: '2.4' 
  • So let's see what we got.
  • We can see each one of our three fields correctly represented by its name, its field type and its value.
  • 2, "Hello", 2.4
  • All in order.
  • So as we can see, our struct is now exposed to the reflection objects providing us a power to understand a struct type even if it was previously unknown to us.
  • So in real life, this piece of code should probably live in a separate function.
  • Let's call it InspectStructType and it will take an empty interface to allow any type of struct to enter then it will host our inspection code.
func main() {
    type myStruct struct {
        Field1 int
        Field2 string
        Field3 float64
    }
    mys := myStruct{2, "Hello", 2.4}
    InspectStructType(mys)
}

func InspectStructType(i interface{}) {
    mysRValue := reflect.ValueOf(i)
    mysRType := reflect.TypeOf(i)

    for i := 0; i < mysRType.NumField(); i++ {
        fieldRType := mysRType.Field(i) //datatype: StructField
        fieldRValue := mysRValue.Field(i)
        fmt.Printf("Field Name: '%s', field type: '%s', field value: '%v' \n", fieldRType.Name, fieldRType.Type, fieldRValue.Interface())
    }
}

https://go.dev/play/p/QuwFnvTJe9n

  • So typically when we write reflection code like this, we would encapsulated in an isolated function which in a production environment would not know the type getting passed to it.
  • And hence why it needs to be inspected.
  • And that is why it takes an empty interface.
  • Now for even more powerful code, we should do another check inside this function to ensure the empty interface object pass is of type struct and we do that By calling mysRValue.kind.
  • Kind was a method we discussed in the previous lesson which returns type of value.
  • Here with struct type.
  • It's not struct, we just return for now.

Struct tag

    type myStruct struct {
        Field1 int `alias:"f1" desc:"field number 1"`
        Field2 string `alias:"f2" desc:"field number 2"`
        Field3 float64 `alias:"f3" desc:"field number 3"`
    }
func InspectStructType(i interface{}) {
    mysRValue := reflect.ValueOf(i)
    mysRType := reflect.TypeOf(i)

    for i := 0; i < mysRType.NumField(); i++ {
        fieldRType := mysRType.Field(i) //datatype: StructField
        fieldRValue := mysRValue.Field(i)
        fmt.Printf("Field Name: '%s', field type: '%s', field value: '%v' \n", fieldRType.Name, fieldRType.Type, fieldRValue.Interface())
        fmt.Println("Struct tags, alias:", fieldRType.Tag.Get("alias"), "description:", fieldRType.Tag.Get("desc"))
    }
}
  • Now let's discuss struct tags.
  • Struck tags are ways to embed messages into our struct fields.
  • To write struct tags, We use symbols and inside we would write a key value pair.
  • There will be our key or the name of our struct tag and this will be its value.
  • So let's assume that we want to tell whatever code does inspecting this struct type.
  • That field one is also known as f1 as an alias.
  • Let's also say we would like to describe the stack as field number 1.
  • So notice here how I use the white space to separate two struct tags.
  • The key or the title of the tag will be followed by column then the value of the tag.
  • Now let's repeat.
  • with the other two fields.
  • Alias for Field2 will be f2.
  • Alias for Field3 will be f3.
  • And description will be a field number2.
  • And field number 3.
  • Now So in order to get a struct tag in order to access a struct tag in our reflection objects, we use a method called Tag.Get.
  • And this method will belong to the fieldRType value object or the struct field type object.
  • And that is because struct tags is a piece of meta data is not the actual value of the field.
  • But it's like a label on the field.
  • So let's get a piece of code here to restruct tags for each one of those struct fields.
  • So let's call this struct tags, this struct is alias.
  • And then we will call fieldRType object for the struct field and .Tag.Get
  • And get takes argument which is basically the tag name or the tag key.
  • So this is for Alias.
  • So Alias here.
  • Then let's get the description.
  • This will take fieldRType.Tag.Get
  • Argument here will be the name of the second struct tag which is desc stands for description.

result

Field Name: 'Field1', field type: 'int', field value: '2' 
Struct tags, alias: f1 description: field number 1
Field Name: 'Field2', field type: 'string', field value: 'Hello' 
Struct tags, alias: f2 description: field number 2
Field Name: 'Field3', field type: 'float64', field value: '2.4' 
Struct tags, alias: f3 description: field number 3

https://go.dev/play/p/fSnSzgtROc0

func main() {
    type myStruct struct {
        Field1 int     `alias:"f1" desc:"field number 1"`
        Field2 string  `alias:"f2" desc:"field number 2"`
        Field3 float64 `alias:"f3" desc:"field number 3"`
    }
    mys := myStruct{2, "Hello", 2.4}
    InspectStructType(&mys)
}

func InspectStructType(i interface{}) {
    mysRValue := reflect.ValueOf(i)
    if mysRValue.Kind() != reflect.Ptr {
        return
    }
    mysRValue = mysRValue.Elem()
    if mysRValue.Kind() != reflect.Struct {
        return
    }
    mysRValue.Field(0).SetInt(15)
    mysRType := mysRValue.Type()

    for i := 0; i < mysRType.NumField(); i++ {
        fieldRType := mysRType.Field(i) //datatype: StructField
        fieldRValue := mysRValue.Field(i)
        fmt.Printf("Field Name: '%s', field type: '%s', field value: '%v' \n", fieldRType.Name, fieldRType.Type, fieldRValue.Interface())
        fmt.Println("Struct tags, alias:", fieldRType.Tag.Get("alias"), "description:", fieldRType.Tag.Get("desc"))
    }
}

https://go.dev/play/p/ofKhTgqfpt6

  • We were able to access our struct tags via reflection.
  • If struct tag name or title does not exist, This should return an empty string.
  • Because there is no value.
  • Struct tags is basically a very powerful feature in go.
  • Strucks which allow us to add information and messages to our struct fields.
  • This information is typically used for serialization and deserialzation protocols.
  • Now what if we want to change values in this struct via reflection.
  • For this, we can use the pointer dereference trick which we demoed in the last lesson.
  • So the idea of this approach is to create the reflection objects on a reference of our objects or a pointer to our object.
  • And then in here we first need to make sure that what's getting passed is a pointer.
  • So if it's not a pointer would return
  • Otherwise it's a pointer, set mysRValue again to be the elements for the dereference of the original mysRValue pointer.
  • So Elem method will give us access to the original object which lived inside that pointer.
  • Because it will access its memory address.
  • Let's do a quick test here.
  • So let's try to change the value of let's say the first field from to approach to .
  • So we can do something like this.
  • mysRValue.Field(0).Set(15)
  • We should change this as well because we no longer are looking to the reflection type object of i.
  • Instead we want the reflection type object of the value represented by mysRValue.
  • That is because mysRValue currently presents the dereferenced i.
  • You can simply kill you on mysRValue or my ctruct value dot type which will return the type object of this value.

Result

Field Name: 'Field1', field type: 'int', field value: '15' 
Struct tags, alias: f1 description: field number 1
Field Name: 'Field2', field type: 'string', field value: 'Hello' 
Struct tags, alias: f2 description: field number 2
Field Name: 'Field3', field type: 'float64', field value: '2.4' 
Struct tags, alias: f3 description: field number 3
  • As you can see here the first field value is now 15 instead of 2.
  • So we successfully changed the value.
package main

import (
    "fmt"
    "reflect"
)

type Printer interface {
    Print(s string)
}

type pStruct struct {
    s string
}

func (p *pStruct) Print(s string) {
    p.s = s
    fmt.Println(s)
}

func main() {
    p := new(pStruct)
    inspectType(p)
}

func inspectType(obj interface{}) {
    v := reflect.ValueOf(obj)
    t := v.Type()
    myInterface := reflect.TypeOf((*Printer)(nil)).Elem()

    fmt.Println("obj implements Printer?", t.Implements(myInterface))

    // if t.Implements(myInterface) {
    //     printFunc := v.MethodByName("Print")
    //     args := []reflect.Value{reflect.ValueOf("Printing Hello")}
    //     printFunc.Call(args)
    // }
}

https://go.dev/play/p/82frjd8x-7D

  • Now it's time to talk about reflections on interfaces.
  • So let's create an interface called Printer which includes a print method that takes an argument of type string.
  • Then we'll create a structure type called pStruct which implements the Print method or the pointer to it implements the print method.
  • And for this, pStruct will become a child of the printer interface.
  • Because it satisfies the print method of the printer interface.
  • So the code is simple.
  • We just set an internal struct field here called s which is the string argument passed.
  • And then we print it to the standard output.
  • So it doesn't matter what the function does.
  • Our main focus here is the fact that this is an interface and this is a type that implements that interface.
  • So we'll use go's reflection to explore the relationship between pStruct and Printer from within our code.
  • And for that we'll use a function called inspectType which we will explore right now.
  • So the inspect type of function will take an empty interface object as an argument.
  • Because similarly to any function that is expected to perform some reflection work.
  • Chances are it doesn't know the object type getting passed to it.
  • So we start by creating variable v which is a reflected value object of obj.
  • And we created variable t which is reflect.Type Object of v.
  • So v is a reflection value object or as t is a reflection type object.
  • Next we will use a known trick to get a reflection object that holds a specific interface type.
  • We do that by calling reflect.TypeOf.
  • Because we're looking to obtain reflection type object.
  • Then in there we would typecast a pointer to the interface type we see which in this case would be the Printer type
  • And the type cast will just type cast nil value.
  • And the reason why we do this casting is to obtain a Printer pointer which points to a memory location that is empty.
  • We can then call .Elem to obtain the reflection type object which will represent type Printer.
  • So this line here is a trick that could be used to get a reflection object to a type without caring about the value that the type holds.
  • So why would we need to do that.
  • We need to do that in order to do something like this.
  • The implements method basically checks if the provided argument is implemented by the type were presented by the reflection type object.
  • So in other words see here is the reflection type object of p.
  • And p is a pointer to pStruct and the pointer to pStruct implements the Printer interface.
  • So when I call t.implements and then I pass this value(myInterface) as an argument.
  • I can check whether t.Implements that Printer interface.
  • And that is why we did not care here about the value being held in the Printer interface type.
  • We only wanted to get reflection type object that represents a printer interface type.
  • So let me just comment out here.
  • And then run this code really quick and we'll see here that it's true.

Result

obj implements Printer? true
  • That means that p here actually implements a Printer interface.
  • That's how we can use some reflection methods to test that out.
  • Shows you yet another feature that go reflection allow us.

MethodByName

func (v Value) MethodByName(name string) Value
  • You can even expand further and call methods of a Type using reflection.
  • We can do this by leveraging??? them MethodByName method which belongs to the reflect.Value object of the reflection value object.
  • This method will return another reflection value object which we will represent the method with the provided name.

Call

func (v Value) Call(in []Value) []Value
  • Then when we get a value object or reflection value object that presents a method, we can call another method called Call here
  • And method call will invoke the function or the method.
  • We're trying to execute using the list of arguments that will provide.
  • A list of arguments are all provided as reflection value objects.
  • Because they're all values.
  • Of course Those values represents the original arguments of the invoked method or function.
  • Now the return value will be another slice of reflection value objects which is basically ours ???? types.
func inspectType(obj interface{}) {
    v := reflect.ValueOf(obj)
    t := v.Type()
    myInterface := reflect.TypeOf((*Printer)(nil)).Elem()

    fmt.Println("obj implements Printer?", t.Implements(myInterface))

    if t.Implements(myInterface) {
        printFunc := v.MethodByName("Print")
        args := []reflect.Value{reflect.ValueOf("Printing Hello from a refleciton object!!!")}
        printFunc.Call(args)
    }
}

https://go.dev/play/p/gGuzHtJi5MT

  • Let's see how that looks like in code.
  • So going back here to our code so enable back the code commented out.
  • So in here we check again if t actually does implements this interface type and if it does we'll try to call the Print method from our reflection object.
  • So we do that by calling the v.MethodByName which we just covered and then we pass the method name which is Print.
  • So by doing this we obtain an object, a reflection value of object to be specific that represents this method.
  • So the print Func object is basically the reflection object of this method.
  • And then let's create the arguments that we need to pass in order to call this function.
  • So the method types are all reflective value objects or reflection value objects and as seen here.
  • The method takes a single argument of type string.
  • So our case, we will take a string then converted into a reflection value object by calling a reflect.ValueOf.
  • And then have that as a single item in this slice.
  • Now we have our arguments so we can just use a call method from the printFunc object as a call?????
  • Call method which we just covered and pass the arguments to it.
  • If this works correctly, this should be printed.
  • The string???? got passed.
  • The string here says Printing Hello.
  • It's making even more fancy.
  • Let's say Printing Hello from a reflection object.
  • Let's run it.

Result

obj implements Printer? true
Printing Hello from a refleciton object!!!
  • And sure enough it works.
  • This shows you the powers that the refelction features in go can provide you in your journey to build master applications in the go language.

Summary

  • We drove deeper into the power of reflection in go by exposing structs and interfaces to go's reflection objects
  • Reflection is a powerful feature in go.
  • That needs to be used with caution.
  • Because it has performance penalties.
  • In the next, we will build a custom configuration file reader for the hydra space ship micro services utilizing our new practical knowledge of reflection in go.
728x90
반응형

관련글 더보기

댓글 영역