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 programs have 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 to examine, introspect, 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

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.

For more info feel free to check out our video about reflection



Ready for your next Golang Job? Join other fellow Gophers, submit your profile and get hired