What is reflection?
In computer science, reflection is the ability of a computer program to examine and modify its own structure and behavior (specifically the values, meta-data, properties and functions) at runtime.
source: Wikipedia
Reflection can be used for observing and modifying program execution at runtime. A reflection-oriented program component can monitor the execution of an enclosure of code and can modify itself according to a desired goal related to that enclosure. This is typically accomplished by dynamically assigning program code at runtime.
In Golang
reflection allows inspection of struct, interfaces, fields and
methods at runtime without knowing the names of the interfaces, fields, methods
at compile time. It also allows instantiation of new objects and invocation of
methods.
Reflection in action
Reflection objects are used for obtaining type information at runtime. The
structs that give access to the metadata of a running program are in the
reflect
package. The package contains structs that allow you to obtain
information about the application and to dynamically emits types, values, and
objects to the program.
Even that reflection is not idiomatic for Golang. We will explore in details some
of reflect
package capabilities.
Example: QueryBuilder
Lets assume that we are developing an object-relation mapping packages like
gorm. We will implement QueryBuilder
struct
that is responsible for generating SQL
queries for update, delete and insert.
The QueryBuilder
has a field Type
that keep a metadata information about the
type that builder generates SQL
queries for:
type QueryBuilder struct {
Type reflect.Type
}
Typically the metadata for particular type could be accessed by instaciating the
reflect.Type
. Lets have the followin struct:
type Employee struct {
ID uint32
FirstName string
LastName string
Birthday time.Time
}
We need to instaciate reflection.Type
in order to access its type metadata.
It is the representation of a Go type. We should use the following code snippet:
t := reflect.TypeOf(&Employee{}).Elem()
builder := &QueryBuilder{Type: t}
Note in case of pointer type, we should retrieve the underlying actual type by
getting the result from the Elem
function. It panics if the type’s
Kind is not Array, Chan, Map, Ptr, or Slice.
Lets inspect the implementation of QueryBuilder
function CreateSelectQuery
:
func (qb *QueryBuilder) CreateSelectQuery() string {
buffer := bytes.NewBufferString("")
for index := 0; index < qb.Type.NumField(); index++ {
field := qb.Type.Field(index)
if index == 0 {
buffer.WriteString("SELECT ")
} else {
buffer.WriteString(", ")
}
column := field.Name
buffer.WriteString(column)
}
if buffer.Len() > 0 {
fmt.Fprintf(buffer, " FROM %s", qb.Type.Name())
}
return buffer.String()
}
The type NumField
function returns the struct type’s field count. The for-loop
interates over that count and obtain every field by index. The type’s Field
function
returns a StructField
value that describes the field owned by the underlying struct:
// A StructField describes a single field in a struct.
type StructField struct {
// Name is the field name.
// 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
Name string
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
}
Then we are appending the field name to the select query. The final implementation
produces the following result for Employee
struct:
SELECT ID, FirstName, LastName, Birthday FROM Employee
But how to handle the case when our field are represented with different names
in underlying database. Lets say that we want to represent ID
field as
id_pk
, FirstName
field as first_name
and LastName
field as last_name
.
We can implement that kind of mapping by using field tags.
The use of tags strongly depends on how your struct is used. A typical use is to add specifications or constraints for persistence or serialisation. For example, when using the JSON parser/encoder, tags are used to specify how the struct will be read from JSON or written in JSON, when the default encoding scheme (i.e. the name of the field) isn’t to be used.
Lets change the Employee
struct declaration to use tags that carries additional
information about how the field should be mapped into the underlying database:
type Employee struct {
ID uint32 `orm:"id_pk"`
FirstName string `orm:"first_name"`
LastName string `orm:"last_name"`
Birthday time.Time
}
Then we can access the associated tags by using field.Tag
field. It provides
a Get
function that allows access to any of the tags by name:
column := field.Name
if tag := field.Tag.Get("orm"); tag != "" {
column = tag
}
buffer.WriteString(column)
Then the generated select query would be:
SELECT id_pk, first_name, last_name, Birthday FROM Employee
Example: Validating fields
In the following example, we will explore how to read and validate fields values.
Lets assume that we have the following PaymentTransaction
struct:
type PaymentTransaction struct {
Amount float64 `validate:"positive"`
Description string `validate:"max_length:250"`
}
Like the previous example, we will use the tag annotation. The implementation of
Validate
function is the following code snippet:
func Validate(obj interface{}) error {
v := reflect.ValueOf(obj).Elem()
t := v.Type()
for index := 0; index < v.NumField(); index++ {
vField := v.Field(index)
tField := t.Field(index)
tag := tField.Tag.Get("validate")
if tag == "" {
continue
}
switch vField.Kind() {
case reflect.Float64:
value := vField.Float()
if tag == "positive" && value < 0 {
value = math.Abs(value)
vField.SetFloat(value)
}
case reflect.String:
value := vField.String()
if tag == "upper_case" {
value = strings.ToUpper(value)
vField.SetString(value)
}
default:
return fmt.Errorf("Unsupported kind '%s'", vField.Kind())
}
}
return nil
}
The reflect.Value
is the reflection interface to a Go value. It is used to
access all member for particular object (fields, function and interfaces). By
invoking the Kind
function we determine the field type. Then we could access
the actual value with the appropriate type function (such as Float
or String
).
To change the field value we should use some of the setters functions.
Example: Recognising interfaces and calling functions
The reflect
package can used to identify whether a particular interface is
implemented.
Lets have the Validator
interface which provide a Validate
function called
every time when an object is validated:
type Validator interface {
Validate() error
}
We will extend the implementation of PaymentTransaction
struct by implementing
the Validator
interface:
func (p *PaymentTransaction) Validate() error {
fmt.Println("Validating payment transaction")
return nil
}
In order to determine whether the PaymentTransaction
implements the interface,
we should call the reflect.Type
function Implements
. It returns true
if
the type obeys the interface signature.
To call a particular function, we could either case the object to the Validator
interface or retrieve the method via MethodByName
function:
func CustomValidate(obj interface{