Golang Reflection Example
Go (Golang) Reflection’s is a powerful feature that allows us to bypass the Go type system and do very interesting things.
What is Reflection?
Reflection is a programming concept that derives from Meta-Programming. In order to understand reflection in Go, it’s going to be useful for us to see what meta-programming and reflection is in general.
Metaprogramming is a programming technique in which computer programshave the ability to treat other programs as their data.
And Reflection is more specifically is defined as follows
reflection programming is the ability of a process "Process (computing)") to examine, introspect "Introspection (computer science)"), and modify its own structure and behavior
Golang Reflection Example
Sometimes, our programs need to understand and read their own structure. For structure we intend the variables and types that compose our code. Today we are going to go through an concrete example which is inspired directly from the Go standard library.
Let’s take the following example, we have a simple JSON encoding operation. As you may know the json marshal function takes an argument which can be any type and returns the respective encoded JSON string as output
func Marshal(v interface{}) ([]byte, error)
Here’s the full example
package main
import "encoding/json"
import "log"
type User struct {
Name string `json:"username"`
Age int `json:"age"`
}
func main() {
u := User{Name: "bob", Age: 10}
r, _ := json.Marshal(u)
log.Println(string(r))
}
Expected Output
2009/11/10 23:00:00 {"username":"bob","age":10}
Now here there are a couple of questions
- How does the json encode know what json keys to print?
- How does the json encoder know what how to print the json values? (Remeber integers, floats, strings and objects are “printed” in different ways in JSON)
The answer is: reflection!
Let’s try implement a basic JSON marshaller by using the reflect package from the Go standard library.
Values Vs Types
In Go (Golang) each variable has a high level structure and can be represented as a pair of (value, type). The value represents all value and typing information of that instanced variable and the type anything relevant to the type that variable belongs to. For example
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string
Age int
}
func main() {
u := User{Name: "bob", Age: 10}
s := "hello"
fmt.Printf("(%+v, %+v)\n", reflect.ValueOf(u), reflect.TypeOf(u))
fmt.Printf("(%+v, %+v)", reflect.ValueOf(s), reflect.TypeOf(s))
}
Expected output
({Name:bob Age:10}, main.User)
(hello, string)
This is useful because now the program can read what type a certain variable is and what value that variable holds. We can also read more other interesting information such as the kind of type a certain variable is.
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string
Age int
}
func main() {
u := User{Name: "bob", Age: 10}
s := "hello"
fmt.Printf("(%+v, %+v, %+v)\n", reflect.ValueOf(u), reflect.TypeOf(u), reflect.TypeOf(u).Kind())
fmt.Printf("(%+v, %+v, %+v)", reflect.ValueOf(s), reflect.TypeOf(s), reflect.TypeOf(s).Kind())
}
({Name:bob Age:10}, main.User, struct)
(hello, string, string)
The Kind is useful when we have type alias and non basic types, such as the User struct in this example. So there are effectively only the following Kind values in Go
Invalid
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Ptr
Slice
String
Struct
UnsafePointer
So each variable has 3 things which we need to keep track of: value, type, kind. These are also exported types present in the Go reflect package. Namely reflect.Type, reflect.Value, reflect.Kind. Each of which has interesting and useful characteristics which are useful when implementing our JSON encoder. The JSON encoder will need to be able to
- Determine the reflection Type of the incoming variable
- Determine the reflection Kind from it
- For each Kind there will be a different way to “encode” the string, for example, string values are enclosed with double quotes, integer values are not, array are enclosed with square brackets, and so on…
- Determine the variable Value
- Write the final string
Now let’s see our basic implementation
package main
import (
"bytes"
"fmt"
"reflect"
"strconv"
"strings"
)
type User struct {
Name string `json:"name"`
Age int64 `json:"age"`
}
type City struct {
Name string `json:"name"`
Population int64 `json:"pop"`
GDP int64 `json:"gdp"`
Mayor string `json:"mayor"`
}
func main() {
var u User = User{"bob", 10}
res, err := JSONEncode(u)
if err != nil {
panic(err)
}
fmt.Println(string(res))
c := City{"sf", 5000000, 567896, "mr jones"}
res, err = JSONEncode(c)
if err != nil {
panic(err)
}
fmt.Println(string(res))
}
func JSONEncode(v interface{}) ([]byte, error) {
refObjVal := reflect.ValueOf(v)
refObjTyp := reflect.TypeOf(v)
buf := bytes.Buffer{}
if refObjVal.Kind() != reflect.Struct {
return buf.Bytes(), fmt.Errorf(
"val of kind %s is not supported",
refObjVal.Kind(),
)
}
buf.WriteString("{")
pairs := []string{}
for i := 0; i < refObjVal.NumField(); i++ {
structFieldRefObj := refObjVal.Field(i)
structFieldRefObjTyp := refObjTyp.Field(i)
tag := structFieldRefObjTyp.Tag.Get("json")
switch structFieldRefObj.Kind() {
case reflect.String:
strVal := structFieldRefObj.Interface().(string)
pairs = append(pairs, `"`+tag+`":"`+strVal+`"`)
case reflect.Int64:
intVal := structFieldRefObj.Interface().(int64)
pairs = append(pairs, `"`+tag+`":`+strconv.FormatInt(intVal, 10))
default:
return buf.Bytes(), fmt.Errorf(
"struct field with name %s and kind %s is not supprted",
structFieldRefObjTyp.Name,
structFieldRefObj.Kind(),
)
}
}
buf.WriteString(strings.Join(pairs, ","))
buf.WriteString("}")
return buf.Bytes(), nil
}
Expected Output
{"name":"bob","age":10}
{"name":"sf","pop":5000000,"gdp":567896,"mayor":"mr jones"}
You can also run it on the Go playground https://play.golang.org/p/8TGnjYt0jFR
Bear in mind that this is a very basic JSON encoder and only supports strings, integers and struct types and does not support nested structs! It could be a fun exercise to try and extend this to support more Go Kinds