One of the more sophisticated Go issues is reflection. In today's post, I will try to explain reflection in Golang with some detail examples
Getting started with golang reflect package
Reflection:
A program's ability to view its variables and values at runtime and determine their type is known as reflection.
reflect package
: Package reflect implements run-time reflection, allowing a program to manipulate objects with arbitrary types. The typical use is to take a value with static type interface{} and extract its dynamic type information by calling TypeOf, which returns a Type. A call to ValueOf returns a Value representing the run-time data. Zero takes a Type and returns a Value representing a zero value for that type. See "The Laws of Reflection" for an introduction to reflection in Go: https://golang.org/doc/articles/laws_of_reflection.html
Why we need reflection
Go uses static typing. Every variable has a static type, which is a type that is known and fixed at compile time and is one of the following: int, float32,... For example:
var i int
var f float64
var s string
Interface types, which express fixed sets of methods, are a crucial type category. Any concrete (non-interface) value may be stored in an interface variable as long as it implements the interface's methods. Io.Reader and Io.Writer, the types Reader and Writer from the io package, are a well-known pair of examples:
// Reader is the interface that wraps the basic Read method.
type Reader interface {
Read(p []byte) (n int, err error)
}
// Writer is the interface that wraps the basic Write method.
type Writer interface {
Write(p []byte) (n int, err error)
}
Any type that implements a Read (or Write) method with this signature is said to implement io.Reader
(or io.Writer
). For the sake of this article, that means that any value whose type has a Read method can be stored in a variable of type io.Reader:
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on
Noted that: Go is statically typed and the static type of r
is io.Reader
.
An extremely important example of an interface type is the empty interface:
interface{}
or:
any
Since every value contains zero or more methods, it represents the empty set of methods and is satisfied by any value at all. Here is an example of using empty interface:
package main
import (
"fmt"
)
func variableInfo(i interface{}) {
// use the verb %T to check type
fmt.Printf("The variable type is: %T\n", i)
// use the verb %v to print the value
fmt.Printf("The variable value is: %#v \n", i)
fmt.Println("-------------------------------------")
}
func main() {
var variable1 float64 = 10
variable2 := 10
variable3 := "Hello GolinuxCloud members!"
variableInfo(variable1)
variableInfo(variable2)
variableInfo(variable3)
}
Output:
The variable type is: float64
The variable value is: 10
-------------------------------------
The variable type is: int
The variable value is: 10
-------------------------------------
The variable type is: string
The variable value is: "Hello GolinuxCloud members!"
-------------------------------------
Conclusion: The data provided to these empty interfaces is frequently not primitive data. They might also be structs, for instance. Without knowing the nature or values of such data, we must run procedures on it. In this case, we need to know the kinds and the amount of fields contained in the struct in order to execute various actions on it, such as interpreting the data present in it to query a database or design a schema for a database. Reflection can be used at runtime to solve these issues.
Print type and value using reflect.TypeOf and reflect.ValueOf function
The concrete type of interface{} is represented by reflect.Type
and the underlying value is represented by reflect.Value.
Two functions reflect.TypeOf()
and reflect.ValueOf()
which return the reflect.Type
and reflect.Value
func TypeOf(i any) Type
: TypeOf returns the reflection Type that represents the dynamic type of i. If i is a nil interface value, TypeOf returns nil
func ValueOf(i any) Value
: ValueOf returns a new Value initialized to the concrete value stored in the interface i. ValueOf(nil) returns the zero Value.
Here is an example of how to use TypeOf()
and ValueOf()
functions:
package main
import (
"fmt"
"reflect"
)
type Student struct {
StudentId int
StudentName string
}
func printInfo(i interface{}) {
// return type and value of the interface
t := reflect.TypeOf(i)
v := reflect.ValueOf(i)
fmt.Println("Type of variable: ", t)
fmt.Println("Value of varialbe:", v)
}
func main() {
s1 := Student{
StudentId: 1,
StudentName: "Anna",
}
printInfo(s1)
println("-----")
printInfo(s1.StudentName)
}
Output:
Type of variable: main.Student
Value of varialbe: {1 Anna}
-----
Type of variable: string
Value of varialbe: Anna
Kind of variable in Golang
func (v Value) Kind() Kind
: Kind returns v's Kind. If v is the zero Value (IsValid returns false), Kind returns Invalid.
The types Kind
and Type
in the reflection package may appear to be similar, but they differ, as the program below will show:
package main
import (
"fmt"
"reflect"
)
type Student struct {
StudentId int
StudentName string
}
func printInfo(i interface{}) {
// return type and value of the interface
t := reflect.TypeOf(i)
v := reflect.ValueOf(i)
fmt.Println("Type of variable: ", t)
fmt.Println("Value of varialbe:", v)
// kind of value
k := t.Kind()
fmt.Println("Kind of variable:", k)
}
func main() {
s1 := Student{
StudentId: 1,
StudentName: "Anna",
}
printInfo(s1)
println("-----")
printInfo(s1.StudentName)
}
Output:
Type of variable: main.Student
Value of varialbe: {1 Anna}
Kind of variable: struct
-----
Type of variable: string
Value of varialbe: Anna
Kind of variable: string
The actual type of the interface, in this case main, is represented by main.Student and Kind stand for a certain kind of the type. It is a struct
in this instance.
NumField() and Field() functions
func (v Value) NumField() int
: NumField returns the number of fields in the struct v. It panics if v's Kind is not Struct.
func (v Value) Field(i int) Value
: Field returns the i'th field of the struct v. It panics if v's Kind is not Struct or i is out of range.
Here is an example of using NumField()
and Field()
function to get the struct information:
package main
import (
"fmt"
"reflect"
)
type Student struct {
StudentId int
StudentName string
}
func printInfo(i interface{}) {
if reflect.ValueOf(i).Kind() == reflect.Struct {
// check if value is a Struct
v := reflect.ValueOf(i)
fmt.Println("Number of fields", v.NumField())
for i := 0; i < v.NumField(); i++ {
//print type and value of each field
fmt.Printf("Field number:%d type:%T and value:%v\n", i, v.Field(i), v.Field(i))
}
} else {
t := reflect.TypeOf(i)
v := reflect.ValueOf(i)
fmt.Println("Type of variable: ", t)
fmt.Println("Value of varialbe:", v)
// kind of value
k := t.Kind()
fmt.Println("Kind of variable:", k)
}
}
func main() {
s1 := Student{
StudentId: 1,
StudentName: "Anna",
}
printInfo(s1)
println("-----")
printInfo(s1.StudentName)
}
Output:
Number of fields 2
Field number:0 type:reflect.Value and value:1
Field number:1 type:reflect.Value and value:Anna
-----
Type of variable: string
Value of varialbe: Anna
Kind of variable: string
Int(), String() and Float() functions
The reflect.VaXlue
can be extracted as an int64, string or float64 using the methods Int()
, String()
and Float()
, respectively:
func (v Value) Float() float64
: Float returns v's underlying value, as a float64. It panics if v's Kind is not Float32 or Float64
func (v Value) Int() int64
: Int returns v's underlying value, as an int64. It panics if v's Kind is not Int, Int8, Int16, Int32, or Int64.
func (v Value) String() string
: String returns the string v's underlying value, as a string. String is a special case because of Go's String method convention. Unlike the other getters, it does not panic if v's Kind is not String. Instead, it returns a string of the form "<T value>" where T is v's type.
Here is an example of using Int()
and String()
functions to print type and value of variable:
package main
import (
"fmt"
"reflect"
)
func main() {
intValue := 56
stringValue := "58"
intInfo := reflect.ValueOf(intValue).Int()
// no panic
intInfo2 := reflect.ValueOf(intValue).String()
fmt.Printf("Variale type:%T, value:%v\n", intInfo, intInfo)
fmt.Printf("Variale type:%T, value:%v\n", intInfo2, intInfo2)
fmt.Printf("Variale type:%T, value:%v\n", intValue, intValue)
stringInfo := reflect.ValueOf(stringValue).String()
fmt.Printf("Variale type:%T, value:%v\n", stringInfo, stringInfo)
fmt.Printf("Variale type:%T, value:%v\n", stringValue, stringValue)
}
Output:
Variale type:int64, value:56
Variale type:string, value:<int Value>
Variale type:int, value:56
Variale type:string, value:This is a string
Variale type:string, value:This is a string
Noted that, if we try to use Int()
for a string value, the program will raise a panic:
stringInfo := reflect.ValueOf(stringValue).String()
// panic
stringInfo2 := reflect.ValueOf(stringValue).Int()
Summary
Do you really need to reflect? Go's reflection concept is extremely powerful and sophisticated, thus it should be utilized with caution. Reflection makes it incredibly challenging to write understandable and maintainable code. It should only be used when absolutely required and avoided whenever possible. Hope with provided examples, you can understand about reflect concept in Golang
References
https://go.dev/blog/laws-of-reflection
https://pkg.go.dev/reflect
Amazing 😍