상세 컨텐츠

본문 제목

Section 5.6.28 Go Laws of Reflection

Go/Mastering Go

by Gopythor 2022. 3. 28. 03:48

본문

728x90
반응형
  • In the previous lesson, we had a healthy dosage of practical Go programming.
  • When we finished writing our chat client for the hydra spaceship.
  • In this section we'll begin to explore reflection in the Go language.

We are going to take a look at..

What is Refelction
Refresher on interfaces
First law of reflection
Second law of reflection
Third law of reflection
http://blog.golang.org/laws-of-reflection

  • We'll start by defining reflection then from there we will have a quick refresher on interfaces to remember how they were used.
  • From there we will proceed to cover the three major laws of reflection in the GO world.

What is Reflection

Advance feature that allows meta programming
Ability of a program to inspect itself
Identify types, and fields

  • For further reading, the article covering the three laws can be found in this link.

Refresher on Interfaces

Specifies a behavior of an object
Any user defined type can implement an interface
An interface value stores the concrete value assigned to the variable, and the value type descriptor
The interface value only allows access to methods that the interface implements
The empty interface is the parent of all types in Go

  • So do you remember interfaces?
  • There were special types that defined behaviors of objects.
  • An interface defines a number of functions which can then be implemented by any custom type created by us.
  • If a type implements an interface, its values can be assigned to a variable of the interface type.
  • An important remark about interface variables is that they store two values.
  • A concrete value and a descriptor of the value type.
  • Interface values only allow access to methods that the interface implements.
  • An empty interface is a parent of all types in Go.
  • Because it has zero methods and hence is implemented by any type which supports one or more methods.https://pkg.go.dev/net#Conn
  • type Conn
  • Let's see some code to clarify more.
  • So lets look at the net package for example.
  • And in there.
  • Let's look at the Conn type which is a connection type for the net package.
type Conn interface {
    // Read reads data from the connection.
    // Read can be made to time out and return an error after a fixed
    // time limit; see SetDeadline and SetReadDeadline.
    Read(b []byte) (n int, err error)

    // Write writes data to the connection.
    // Write can be made to time out and return an error after a fixed
    // time limit; see SetDeadline and SetWriteDeadline.
    Write(b []byte) (n int, err error)

    // Close closes the connection.
    // Any blocked Read or Write operations will be unblocked and return errors.
    Close() error

    // LocalAddr returns the local network address, if known.
    LocalAddr() Addr

    // RemoteAddr returns the remote network address, if known.
    RemoteAddr() Addr

    // SetDeadline sets the read and write deadlines associated
    // with the connection. It is equivalent to calling both
    // SetReadDeadline and SetWriteDeadline.
    //
    // A deadline is an absolute time after which I/O operations
    // fail instead of blocking. The deadline applies to all future
    // and pending I/O, not just the immediately following call to
    // Read or Write. After a deadline has been exceeded, the
    // connection can be refreshed by setting a deadline in the future.
    //
    // If the deadline is exceeded a call to Read or Write or to other
    // I/O methods will return an error that wraps os.ErrDeadlineExceeded.
    // This can be tested using errors.Is(err, os.ErrDeadlineExceeded).
    // The error's Timeout method will return true, but note that there
    // are other possible errors for which the Timeout method will
    // return true even if the deadline has not been exceeded.
    //
    // An idle timeout can be implemented by repeatedly extending
    // the deadline after successful Read or Write calls.
    //
    // A zero value for t means I/O operations will not time out.
    SetDeadline(t time.Time) error

    // SetReadDeadline sets the deadline for future Read calls
    // and any currently-blocked Read call.
    // A zero value for t means Read will not time out.
    SetReadDeadline(t time.Time) error

    // SetWriteDeadline sets the deadline for future Write calls
    // and any currently-blocked Write call.
    // Even if write times out, it may return n > 0, indicating that
    // some of the data was successfully written.
    // A zero value for t means Write will not time out.
    SetWriteDeadline(t time.Time) error
}
  • Conn type is a type that allows us to make IP connections like TCP and UDP.
  • It implements the IO reader interface as well as IO writer interface
  • And it does this by implementing the read method which is also defined in the IO reader interface.
  • And it also implements a write method which is also implemented in the IO writer interface.

5.6-1.go

func main() {
    c, _ := net.Dial("tcp", ":2100")
    var r io.Reader
    r = c // r now stores (value:c, type descriptor: net.Conn)
    // that's why we can also do this:
    if _, ok := r.(io.Writer); ok {
        /*
            even though r in theory is only of type io.Reader,
            the underlying value stored also implements the io.writer interface
        */
        fmt.Println("We didn't forget there is a writer inside value c")
    }
}
  • Let's explore a simple program here that showcases some important interface features in the Go language.
  • So this program creates a TCP connection to local port :2100.
  • So since C here is of type Conn which gets provided via the net.Dial function call.
  • C then could be assigned to a variable of type reader.
  • We can simply see that here so if I say var r io.Reader.
  • I defined a variable called r of type io.Reader.
  • Then this code becomes legal which is where I assigned C into r.
  • Even though in theory they have different types.
  • Now r in this case, we'll store the value of c as well as a type descriptor of c.
  • And the type descriptor of C will be the Conn type which would be net.Conn.
  • Conn is a type and net would be the package.
  • So those two pieces of information are now stored in r.
  • However since r is of type io.Reader

  • It will only allow access to a read method as we see here.
  • Only the read method could be allowed.
  • However does that mean we lost access to other methods implemented by C.
  • Actually we didn't.
  • We can simply use type assertion here to grab more of the original features of the object stored in a variable r.
  • Now because objects stored in variable r is type Conn.
  • And because Conn also implements IO writer, We can do something like that.
  • So by doing that we switched the concrete type inside r into io.writer.
  • This is made possible again because r still knows that the value it hosts originated from net.conn.
  • So in this could be example.
  • I didn't capture the value thing via the type assertion.
  • Because this piece of code only confirms that r could indeed convert to io.Writer.
if w, ok := r.(io.Writer); ok {
        /*
            even though r in theory is only of type io.Reader,
            the underlying value stored also implements the io.writer interface
        */
        fmt.Println("We didn't forget there is a writer inside value c")
    }
  • However if I capture the writer by doing this.

  • We see here that we can now call the method write.
  • Should the conversion be successful.
  • So I ran a TCP server listening on a local port and for our program.
  • I will run our client program to test out our code.
  • Indeed get a message said "We didn't forget there is a writer inside value c" which is the message we have here inside the if statement which confirms subtype assertion of variable r to type io.Writer.
  • So this gives you a glimpse into how interfaces can work behind the scenes.
  • And this is an important feature that is used for reflection.

Reflect package

https://pkg.go.dev/reflect

  • Now it's time to see how we can use interfaces to allow reflection in Go.
  • So first we need to cover a very important package for reflection which is the reflect package
  • The reflect package is the key get to in your reflection or work in the GO world.
  • So there are two main reflection objects which we need to understand in the reflect package.
  • The type interface and the value struct will cover them one by one.
  • The interface contains a lot of the meta data information that we need in order to explore the properties of a variable of unknown origin.
  • We can tell the type name whether the type implements methods or we can also figure out if the type has fields as in struct fields.
  • The value struct on the other hand provides the information we need to know about the value of the mysterious variable.
  • We can use it to set a new value to the variable or simply get the value.
  • We'll see real examples on how we can do that as we go.

First Law of Reflection

Reflection goes from interface value to reflection object
At a basic level, reflection examines the type and value pair stored inside an interface variable
reflect.TypeOf to get the type, reflect.ValueOf to get the value struct

func main() {
    var x1 float32 = 5.7

    inspectIfTypeFloat(x1)
}

func inspectIfTypeFloat(i interface{}) {
    v := reflect.ValueOf(i)
    fmt.Println("Type:", v.Type()) //same as reflect.TypeOf(i)
    fmt.Println("Is type float64?", v.Kind() == reflect.Float64)
    fmt.Println("Float Value:", v.Float())
}

Result

Type: float32
Is type float64? false
Float Value: 5.699999809265137

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

  • Now that we talk about the values struct and type interface.
  • It's time to explore the laws of reflection.
  • Reflection goes from an interface value to a reflection object.
  • That means that we can use the reflect package to convert a variable of unknown origins or properties to the type interface or the values struct equivalent.
  • And we know the type interface and the values struct belong to the reflect package.
  • So reflect.typeOf could be used to get the type interface from a value of unknown origins.
  • Or as reflect.ValueOf is to retrieve the value struct from a variable of unknown origins.
  • Let's go to go playground to test out some code.
  • So I'll start by creating a variable called x1 which is of type float.
  • Actually to be more specific do something like this.
  • To see here the x1 is of type float32.
  • Now let's import the reflect package to our code here.
  • And let's use it.
  • So x1 is of type float 32.
  • Let's use a bunch of code happened in between and another developer came in.
  • And for some reason they need to write some code to identify the origins meaning that type and the value of x1.
  • To do that, let's start retrieving reflect value of x1
  • And we do that by calling reflect.ValueOf will host x1, We will take X-1 as an input.
  • So this will provide us the value struct of x1.
  • Now let's say we want to identify the type of x1.
  • So we can just use fmt.Println to print out.
  • Let's say type
  • We can call v which is a variable that stores reflect.value struct.
  • Then from the value structure, we will call a method called type.
  • This method would return the actual type of this stored value.
  • It's also equivalent to calling reflect.TypeOf.
  • So if you would do something like this
    //same as reflect.TypeOf(x1)
  • then pass its input.
  • Now let's explore more features in the reflect values struct
  • So let's say we want to confirm it is float64 for example.
  • This could be done by calling a method called kind from the value struct.
  • You can say kind equals
  • Then we can compare it with type directly.
  • So this will compare the type of the value contained in V which is basically the float32 value with the type of float 64.
  • they should be false.
  • Finally Let's say we want to obtain the float value of our reflection objects.
  • So we can do this.
  • We would call method Float from the value objects.
  • More real use case for this code.
  • If we package it in a function that we do the type inspection.
  • So we'll call function inspectIfTypeFloat.
  • In the function needs to take a type as an argument that is generic and would accept any other type.
  • That is a definition of the empty interface.
  • It's the type that presents any other type in the Go language.
  • So next we will copy over this code to here.
  • So this function because it takes an empty interface as an argument.
  • Then it will accept any variable of any type and then it will inspect the value accordingly.
  • This should be i here and there.
  • Because i is now our arguments.
  • Now let's run our new function.
  • This Function with x1.
  • So x1 get passed to inspectIfTypeFloat function.
  • In there we'll be able to print the type of the passed value.
  • And we'll be able to confirm that this is not Float64.
  • And at the end we'll be able to obtain the float value.
  • An important remark here is that when we use the float method from the reflect a value object type.
  • Then it will return a float 64 value instead of 32.
  • That is because it will return the maximum float type that could be returned.
  • And maximum float type is logically float 64.
  • This was a design decision made in the reflect package for the sake of simplicity.
  • The same rule applies to in 64 and in 32 are any smaller in type.
  • So to prove my point here.
func inspectIfTypeFloat(i interface{}) {
    v := reflect.ValueOf(i)
    fmt.Println("Type:", v.Type()) //same as reflect.TypeOf(i)
    fmt.Println("Float Value:", v.Float())
    x2 := v.Float()
    v2 := reflect.ValueOf(x2)
    fmt.Println("Is type float64?", v.Kind() == reflect.Float64)
    fmt.Println("Type of v2", v2.Type())

}
  • So let's capture v.Float and new variable called x2 and x2 will be equal to v.Float.
  • So let's get reflect value object type of x2.
  • You can do that by calling reflect.ValueOf(x2)
  • Then let's change our log messages here little bit.
  • Type of v2, v2.Type
  • Let's see ??????
  • And we see here that v2 is of type float 64 that is due to how .float method works.
func main() {
    //var x1 float32 = 5.7
    //inspectIfTypeFloat(x1)

    type myFloat float64
    var x3 myFloat = 5.7
    v := reflect.ValueOf(x3)
    fmt.Println(v.Type())
}

func inspectIfTypeFloat(i interface{}) {
    v := reflect.ValueOf(i)
    fmt.Println("Type:", v.Type()) //same as reflect.TypeOf(i)
    fmt.Println("Float Value:", v.Float())
    x2 := v.Float()
    v2 := reflect.ValueOf(x2)
    fmt.Println("Is type float64?", v.Kind() == reflect.Float64)
    fmt.Println("Type of v2", v2.Type())

}
main.myFloat

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

  • What's ?????? a couple of steps deeper.
  • What if we create our own custom type.
  • So let's create the type called myFloat which is basically maybe float 64 underneath.
  • And then let's create a new variable.
  • Let's call it x3.
  • Then x3 will be a variable of type myFloat and must assign a value.
  • And then let's try to get the reflect value object By calling relfect.ValueOf x3.
  • So let's print out here.
  • And indeed we see our custom type here.
  • It's main.myFloat
type myFloat float64

func main() {
    //var x1 float32 = 5.7
    //inspectIfTypeFloat(x1)

    var x3 myFloat = 5.7
    v := reflect.ValueOf(x3)
    fmt.Println(v.Type())
}

func inspectIfTypeFloat(i interface{}) {
    v := reflect.ValueOf(i)
    fmt.Println("Type:", v.Type()) //same as reflect.TypeOf(i)
    fmt.Println("Float Value:", v.Float())
    x2 := v.Float()
    v2 := reflect.ValueOf(x2)
    fmt.Println("Is type float64?", v.Kind() == reflect.Float64)
    fmt.Println("Type of v2", v2.Type())

Result

main.myFloat

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

  • Whereas main is our package name and myFloat is the name of a custom type we just created.
  • What if we define myFloat outside of the main function with a return the same type format here.
  • My float.
  • The answer is yes that is because we still belong to the main package.
type myFloat float64

func main() {
    //var x1 float32 = 5.7
    //inspectIfTypeFloat(x1)

    var x3 myFloat = 5.7
    v := reflect.ValueOf(x3)
    fmt.Println(v.Type())
    fmt.Println(v.Kind() == reflect.Float64)
}

func inspectIfTypeFloat(i interface{}) {
    v := reflect.ValueOf(i)
    fmt.Println("Type:", v.Type()) //same as reflect.TypeOf(i)
    fmt.Println("Float Value:", v.Float())
    x2 := v.Float()
    v2 := reflect.ValueOf(x2)
    fmt.Println("Is type float64?", v.Kind() == reflect.Float64)
    fmt.Println("Type of v2", v2.Type())

}

Result

main.myFloat
true

https://go.dev/play/p/9RDPgO-8GIj

  • But what if we try to compare the type or the kind of v using the kind method with reflection.Float64
  • You think it will work.
  • It actually does work.
  • And that is because if we use some kind of method, it will return the original Type of myFloat which was float64.
  • This comes in handy if you want to confirm the underlying type.
  • Before let's say for example doing a calculation in case of a number type.
  • So this year adds a lot of flexibility to what we can do is custom types.

Second Law of Reflection

Reflection goes from reflection object to interface value
value.Interface() to get the interface value from the reflect object
Interface() is the inverse of reflect.ValueOf()

  • Now it's time for the second lot of reflection.
  • The last said that reflection goes from reflection objects to an interface value.
  • Meaning that the direction of the reflection could go opposite to the first law of reflection which also means that we can retrieve a regular object from a reflection object.
  • So if we use the interface method of the value struct type, it'll return a value outside of the realm of reflection objects
  • This in effect does the opposite of reflect.ValueOf().
type myFloat float64

func main() {
    var x1 float32 = 5.7
    inspectIfTypeFloat(x1)

    /*
        var x3 myFloat = 5.7
        v := reflect.ValueOf(x3)
        fmt.Println(v.Type())
        fmt.Println(v.Kind() == reflect.Float64)
    */
}

func inspectIfTypeFloat(i interface{}) {
    v := reflect.ValueOf(i)
    fmt.Println("Type:", v.Type()) //same as reflect.TypeOf(i)
    fmt.Println("Float Value:", v.Float())
    x2 := v.Float()
    v2 := reflect.ValueOf(x2)
    fmt.Println("Is type float64?", v.Kind() == reflect.Float64)
    fmt.Println("Type of v2", v2.Type())

    interfaceValue := v.Interface()

    switch t := interfaceValue.(type) {
    case float32:
        fmt.Println("Original float32 value", t)
    }
}

Result

Type: float32
Float Value: 5.699999809265137
Is type float64? false
Type of v2 float64
Original float32 value 5.7

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

  • Let's see how we can put that to use.
  • To test out the second of reflection, need to modify some code.
  • I will reactivate instpectIfTypeFloat function
  • Inside inspectIfTypeFloat, we will add a bunch of code to test out the second law reflection.
  • So v is basically the reflect value objects that we retrieve of the i interface.
  • And i interface was basically the carrier of x1 float32 type value.
  • So whatif we want to reverse the process here where we converted a value to reflection objects.
  • For this, we will use a method called interface like this(v.Interface()).
  • This method returns an empty interface which will host the concrete value that was represented by the reflection object.
  • So to be more specific, let's make use of the interface.
  • Because this is an empty interface, we can very simply use a type switch here to retrieve the type of the interface.
  • So we can just say interface.(type)
  • And then we can compare against the original type.
  • So we can check whether this was float32 which it was.
  • Just say "Original float32 value"
  • The end here received the fact that we converted v back into the original value that it represented.
  • The original value I mean a value that is not part of the reflection object.
  • So by calling v.Interface and then doing a type switch, we went to an opposite direction to this line of code where we converted an object type that was not of a reflection type into a refleciton object type.
  • We took refleciton object called the interface method and converted to an empty interface type which is not a reflection object type.
  • And then via this empty interface We were able to go back to the float32 value.

Third Law of Reflection

To modify a reflection object, the value must be settable
value.CanSer() to inspect if the reflect object value is settable

  • Now it's time to discuss the third law of reflection which basically states that the value must be settable
  • Before we can modify through a reflection object.
  • We can use value.CanSet which is a method that belongs to the values struct type to inspect if the values is settable or not.
  • Let's see how that is done.
  • Now to test third law of reflection.
import (
    "reflect"
)

func main() {
    var x1 float32 = 5.8
    v := reflect.ValueOf(x1)
    v.SetFloat(2.2)
}

Result

panic: reflect: reflect.Value.SetFloat using unaddressable value

goroutine 1 [running]:
reflect.flag.mustBeAssignableSlow(0x408ec5?)
    /usr/local/go-faketime/src/reflect/value.go:262 +0x85
reflect.flag.mustBeAssignable(...)
    /usr/local/go-faketime/src/reflect/value.go:249
reflect.Value.SetFloat({0x461d40?, 0xc000120000?, 0x0?}, 0x400199999999999a)
    /usr/local/go-faketime/src/reflect/value.go:2147 +0x49
main.main()
    /tmp/sandbox4152637133/prog.go:10 +0xb1

https://go.dev/play/p/VuAv-svsffi

  • Let's go to a clean go playground session.
  • So we'll import reflect package.
  • It again.
  • Let's do something similar to what we did before.
  • So create a variable called x1 which was of type float32.
  • And it will hold a value.
  • And then create a variable called v which is basically a reflection value object of x1.
  • And then we will learn about a new method that belongs to the value struct type which is called SetFloat.
  • So this method could be used to set a value on the reflection object.
  • So it does exactly as it's named to set a float value.
  • Since a currently not using the fmt package then I can remove it from here.
  • And then let's try to run the code, you think it will work?
func main() {
    var x1 float32 = 5.8
    v := reflect.ValueOf(x1)
    //v.SetFloat(2.2)
    fmt.Println("v settable?", v.CanSet())
}

Result

v settable? false

https://go.dev/play/p/lb3Py-nSz3p

  • That it will not work and it will end up with a panic which will stop our program.
  • So why is that.
  • This is simply is because v is currently a non settable reflection object.
  • Now we will use fmt package.
  • If i try to call v.CanSet method, it should return false.
  • So if I check here the import option and then click on format, the fmt package will get added.
  • That is because we're using it here.
  • So if I hit run now, we see here the CanSet method of the value object returns false which makes it known that we cannot set a value on v.
  • So whenever the can set method returns false avoid to set a value on your reflection object.
  • But how come v is not a settable reflection object.
  • That is because it holds a copy of the value of x1 which is what happens when we pass x1 here as a argument.
func main() {
    var x1 float32 = 5.8
    v := reflect.ValueOf(&x1)
    //v.SetFloat(2.2)
    fmt.Println("v settable?", v.CanSet())
}

Result

v settable? false

https://go.dev/play/p/2z_5OSSfjJm

  • However in order to make v settable, we need to pass a pointer or reference to the reflect.ValueOf call when we create v.
  • And that is because when we pass by reference, we'll be able to change the value of the original variable.
  • Since a pointer will point to the original variable which would be x1 in this case.
  • So try that out.
  • Passing a reference or pointer to x1 is as easy as adding and simple here to the reflect.ValueOf call.
  • Then if we hit to run, it still says that v is not settable.
func main() {
    var x1 float32 = 5.8
    v := reflect.ValueOf(&x1) // *float32 ==> x1
    //v.SetFloat(2.2)
    fmt.Println("v settable?", v.CanSet())
    vpElem := v.Elem() //x1
    fmt.Println("vpElem settable?", vpElem.CanSet())
}

Result

v settable? false
vpElem settable? true

https://go.dev/play/p/9RNRKysgXt9

  • But why that?
  • The answer is actually quite simple.
  • V is still not suitable because when we call CanSet on v, It basically says that we cannot change the pointer value of x1 and not x1 itself.
  • Meaning that the value that v currently represents is a pointer of type float32 which points to x1.
  • And the pointer value is a part that is currently not settable.
  • But luckily we're not interested in changing the value of the actual pointer.
  • Quite the opposite.
  • We need this value in order to access x1.
  • x1 is the variable that we would like to set to a different value.
  • So how do we access x1 now from the pointer that we passed here as an argument.
  • For this, we need to call a new method.
  • So new method called Elem which stands for element.
  • So call the Elem method of object v, And then store this value into a new variable called vpElem.
  • Elem will actually deal referance the pointer value inside v into the variable that points to
  • Meaning that was Elem We should get x1 or rather the piece of memory where x1 currently lives.
  • Now if we check if vpElem is settable using the same code.
  • Then run again, we find that it actually is.
func main() {
    var x1 float32 = 5.8
    v := reflect.ValueOf(&x1) // *float32 ==> x1
    //v.SetFloat(2.2)
    fmt.Println("v settable?", v.CanSet())
    vpElem := v.Elem() //x1
    fmt.Println("vpElem settable?", vpElem.CanSet())
    vpElem.SetFloat(2.2)
}

Result

v settable? false
vpElem settable? true

https://go.dev/play/p/86uyE_entGJ

  • So we finally obtained the reflection object that we can use to change the value of the underlying variable
  • Now let's try to call SetFloat again, but this time we will call it on vpElem sends for the pointer element.
  • And we get no panics.
func main() {
    var x1 float32 = 5.8
    v := reflect.ValueOf(&x1) // *float32 ==> x1
    //v.SetFloat(2.2)
    fmt.Println("v settable?", v.CanSet())
    vpElem := v.Elem() //x1
    fmt.Println("vpElem settable?", vpElem.CanSet())
    vpElem.SetFloat(2.2)
    fmt.Println(x1)
}

Result

v settable? false
vpElem settable? true
2.2

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

  • Now what if I try to print out the value of x1.
  • That actually changed.
  • So it's not 5.8 anymore.
  • Now it's 2.2 perfect.
func main() {
    X := 5
    ChangeX(&X) //*int => X
    fmt.Println(X)
}

func ChangeX(X *int) {
    *X = 10 // X from *int
}

Result

10

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

  • The concepts that made this possible are the same as this.
  • This is much simpler.
  • So what we did was to pass a pointer value as an argument to a function.
  • So and percent X here is basically a pointer of type int which points to where X lives in memory.
  • And then the function, the ChangeX function took the pointer as a pointer.
  • And then deal reference and by dereferencing it
  • It obtained X from integer pointer and when we dereferenced it, we obtained the piece of memory where x lives.
  • So when we change the value of what we obtain from dereference, we change the original X which was seen by the call a function - And then now if I run this code, we see here that X is now 10.
  • Even though it stared as 5
  • We went to where it lives in memory and then assign it to 10.
  • Same concept.

Summary

  • we took a good look at reflection support in the go language and covered the three laws of reflection
  • And how they look in code
  • In the next we will dive into reflection even more by discussing reflections on structs and interfaces.
728x90
반응형

관련글 더보기

댓글 영역