Monday 27 January 2020

OOP & Reflection in Go??

Converting DaggerTech.Data to Go is proving to be more of a challenge that I realised.

After over twenty years of object oriented programming, I am having to unlearn a great deal to work the Go way. Whilst Go does have a form of object orientation, to do what the ORM library needs to do, I need to re-work a lot of the code to a non-OOP fashion.
The main issue I am facing, is that I am so used to inheritance. The ability to call a method of a base class, with that base method being able to view the properties of the derived class (when using reflection). Unfortunately, this is not possible with Go’s method of embedding structs. The answer is it use interfaces and have functions that do not have a receiver, but the interface as the parameter. Not sure if that makes sense, but I will illustrate:

The following code is how I would have performed the reflection in a normal OOP manner. In fact, DaggerTech.Data does this, but in the C# version:

package main

import (
    "fmt"
    "reflect"
)

type Base struct {
    Name string
}

type Derived struct {
    Base
    Age int
}

func (b Base) DoReflection() {
    t := reflect.TypeOf(b)
    for i := 0; i < t.NumField(); i++ {
        fmt.Println(t.Field(i).Name)
    }
}

func main() {
    b := Base{}
    b.DoReflection() // OK. Displays the field Name
    d := Derived{}
    d.DoReflection() // Not OK. Only displays the field Name
}

Result:

Base Name
Base Name

The problem with this is that the call to DoReflection is only dealing with the Base struct, even though the DoReflection is elevated when the Derived struct embeds it. What is actually happening, is the compiler sees d.DoReflection() and actually replaces it with d.Base.DoReflection(), therefore passing the Base as the receiver.

To get around this, we need to make the DoReflection method a function receiving an interface:

package main

package main

import (
    "fmt"
    "reflect"
)

type Base struct {
    Name string
}

type Derived struct {
    Base
    Age int
}

func DoReflection(b interface{}) {
    t := reflect.TypeOf(b)
    for i := 0; i < t.NumField(); i++ {
        fmt.Println(t.Name(), t.Field(i).Name)
    }
}

func main() {
    b := Base{}
    DoReflection(b) // OK. Displays the field Name
    d := Derived{}
    DoReflection(d) // Not OK. Displays Base and Age
}

Result:

Base Name
Derived Base
Derived Age

But now we have a different issue. When the Derived struct is inspected, it sees the Base struct and just reports that, not the ‘inherited’ fields. For this we need to employ a little recursion:

package main

import (
    "fmt"
    "reflect"
)

type Base struct {
    Name string
}

type Derived struct {
    Base
    Age int
}

func DoReflection(b interface{}) {
    t := reflect.TypeOf(b)
    v := reflect.ValueOf(b)
    for i := 0; i < t.NumField(); i++ {
        if v.Field(i).Kind() == reflect.Struct {
            DoReflection(v.Field(i).Interface())
        } else {
            fmt.Println(t.Name(), t.Field(i).Name)
        }
    }
}

func main() {
    b := Base{}
    DoReflection(b) // OK. Displays the field Name
    fmt.Println("") // Added an empty line for readability in the results
    d := Derived{}
    DoReflection(d) // OK. Displays Name and Age
}

Result:

Base Name

Base Name
Derived Age

As you can see in the results, we now have both of the fields from the Derived struct. As a bonus, we can even see in which struct the field was declared.

In the database library, I need to also get the field type and any tags that are applied to the fields, but that is trivial compared to re-working my brain for this. I also need to put in special conditioning for the Time struct (equivalent to the C# DateTime).

No comments:

Post a Comment