1

I am working on a project where I need to build out the partial updates for all of the methods that will be supported. Each partial update will require a different struct, with different fields and number of fields, and not knowing which ones will be present or not. I decided on going over each struct field, and if it's present adding it to an array to return at the end. I also took some time to Benchmark a couple of functions that seemed most realistic to approach this problem, and to help make a decision.

All of the structs fields will be pointers. With that in mind, these are the functions I wrote.

Note: I can't create a playground example for this, because it doesn't support Benchmarks. I'll link the full classes, and put the explanation above them.

  1. Create a mapping function for each partial update struct, where I would check each field separately. If the field is not nil, I will put the value in a 2D array storing in a [key,value] format. After the struct has been processed, return the array.
  2. Create a single mapping function that uses Generics and Reflection to do the same as above.
// main.go
package main

import (
    "reflect"
    "strings"
    "time"
)

type updateRequest struct {
    FieldOne   *string    `json:"field_one,omitempty"`
    FieldTwo   *string    `json:"field_two,omitempty"`
    FieldThree *string    `json:"field_three,omitempty"`
    FieldFour  *string    `json:"field_four,omitempty"`
    FieldFive  *string    `json:"field_five,omitempty"`
    FieldSix   *time.Time `json:"field_six,omitempty" time_format:"2006-01-02"`
}

// Mapping function that would need to be recreated for each partial update struct.
func ManualUpdateRequestMapping(req *updateRequest) [][]string {
    vals := make([][]string, 0, 6)
    if req.FieldOne != nil {
        vals = append(vals, []string{"field_one", *req.FieldOne})
    }

    if req.FieldTwo != nil && req.FieldThree != nil {
        vals = append(vals, []string{"field_two", *req.FieldTwo}, []string{"field_three", *req.FieldThree})
    }

    if req.FieldFour != nil {
        vals = append(vals, []string{"field_four", *req.FieldFour})
    }

    if req.FieldFive != nil {
        vals = append(vals, []string{"field_five", *req.FieldFive})
    }

    if req.FieldSix != nil {
        vals = append(vals, []string{"field_six", req.FieldSix.Format(time.RFC3339)})
    }

    return vals
}

// Generics and Reflection function
func ReflectUpdateRequestMapping[T *updateRequest](str T) [][]string {
    valHolder := reflect.ValueOf(*str)
    if valHolder.Kind() != reflect.Struct {
        return nil
    }
    vals := make([][]string, 0, valHolder.NumField())
    for i := 0; i < valHolder.NumField(); i++ {
        if valHolder.Field(i).IsNil() {
            continue
        }
        spl := strings.Split(valHolder.Type().Field(i).Tag.Get("json"), ",")

        if valHolder.Field(i).Elem().Type() != reflect.TypeOf(time.Time{}) {
            vals = append(vals, []string{spl[0], valHolder.Field(i).Elem().String()})
        } else {
            vals = append(vals, []string{spl[0], valHolder.Field(i).Interface().(*time.Time).Format(time.RFC3339)})
        }
    }
    return vals
}

This is the benchmark method I ran with:

// main_test.go
package main

import (
    "testing"
    "time"
)

func BenchmarkBoth(b *testing.B) {

    field1 := "testfield1"
    field2 := "testfield2"
    field3 := "testfield3"
    field4 := "testfield4"
    field5 := "testfield5"
    date1, _ := time.Parse(time.RFC3339, "2004-10-16T12:40:53.00Z")

    str := &updateRequest{
        FieldOne:   &field1,
        FieldTwo:   &field2,
        FieldThree: &field3,
        FieldFour:  &field4,
        FieldFive:  &field5,
        FieldSix:   &date1,
    }
    b.Run("ManualUpdateRequestMapping", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = ManualUpdateRequestMapping(str)
        }
    })

    b.Run("ReflectUpdateRequestMapping", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = ReflectUpdateRequestMapping(str)
        }
    })
}

Below is the CPU used and the results that come from the test:

cpu: 12th Gen Intel(R) Core(TM) i9-12900KF
BenchmarkBoth/ManualUpdateRequestMapping-24              3560083           331.9 ns/op       368 B/op          8 allocs/op
BenchmarkBoth/ReflectUpdateRequestMapping-24             1393377           866.7 ns/op       648 B/op         21 allocs/op
PASS
ok      com.example.stack   3.745s

I expected the Reflection function to be slower, but not ~2.5x slower. It also seems to allocate ~2.5x more resources per iteration. Did I botch something in the code above, or is Reflection just that much slower?

If there are any recommendations to make the code above more efficient, I am open to all suggestions. I've been working with Go for about 3 months now, so please go easy on me if I committed any Golang treason in the code above. :)

JDev
  • 73
  • 1
  • 8

1 Answers1

1

Reflection is slower than non-reflection code. Here's an improvement. Some notes:

  • Reduce reflect calls by getting the field as a regular typed value and working from there.
  • There's no need for that new-fangled type parameter stuff.
  • Speaking of Golang treason, the name of the language is Go.

With that out of the way, here's the code:

func UpdateRequestMapping(p any) [][]string {
    v := reflect.ValueOf(p).Elem()
    if v.Kind() != reflect.Struct {
        return nil
    }
    t := v.Type()
    result := make([][]string, t.NumField())
    for i := 0; i < t.NumField(); i++ {
        var ok bool
        var value string
        switch f := v.Field(i).Interface().(type) {
        case *string:
            if f != nil {
                ok = true
                value = *f
            }
        case *time.Time:
            if f != nil {
                ok = true
                value = (*f).Format(time.RFC3339)
            }
        }
        if ok {
            name, _, _ := strings.Cut(t.Field(i).Tag.Get("json"), ",")
            result[i] = []string{name, value}
        }
    }
    return result
}
  • I made a tweak to the manual function, I thought I had it fixed when I copied it over. It needs to print the values that would be fed to the struct that will be used when the call to update the database is made. Your version is a lot faster, and that was initially how I was trying to structure my Reflection method. I was in a tossup between trying to use an array, or trying to use a map, but thought I should figure out the performance thing first. Map would enable me to store a key:generic to have string, int32, int64, time.Time etc be stored. If that makes any sense. – JDev Oct 22 '22 at 03:35
  • @JDev Your two functions still do different things for the time field. – not a Penny more Oct 22 '22 at 03:43
  • I haven't actually figured out how to make the generic function produce the correct result yet. I wasn't going to continue with the reflect route if it wasn't possible to improve the performance in a scalable way. – JDev Oct 22 '22 at 03:53
  • I updated the other method to give the same output. – JDev Oct 22 '22 at 04:48
  • @JDev I updated to match your functions. – not a Penny more Oct 22 '22 at 16:13
  • I think this gives me what I need to move forward with a decision. Thank you. :) – JDev Oct 25 '22 at 00:54