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.
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
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())
}
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
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)
}
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.
댓글 영역