Skip to content

Instantly share code, notes, and snippets.

@anjankow
Last active August 12, 2023 10:28
Show Gist options
  • Save anjankow/044ddcfcbf235eb75999588b2d7f7312 to your computer and use it in GitHub Desktop.
Save anjankow/044ddcfcbf235eb75999588b2d7f7312 to your computer and use it in GitHub Desktop.
UpdateStringValues of all struct fields (including nested fields) using reflection + benchmark for performance comparison
package reflectplayground
import (
"errors"
"fmt"
"reflect"
"reflect-playground/testdata"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// UpdateStringValues calls updateStringValueCb function on each string field
// existing in the struct. Nested structs are supported.
//
// Parameter structPtr must be a pointer to a struct.
// Parameter updateStringValueCb is a function called to update existing string values.
func UpdateStringValues(structPtr interface{}, updateStringValueCb func(s string) string) error {
// Verify if structPtr is a pointer to a struct
inputParamStructType := reflect.TypeOf(structPtr)
if inputParamStructType == nil ||
inputParamStructType.Kind() != reflect.Ptr ||
inputParamStructType.Elem().Kind() != reflect.Struct {
return errors.New("invalid input structPtr param: should be a pointer to a struct")
}
// ValueOf is used to get the struct data in the reflect.Value form
structValue := reflect.ValueOf(structPtr)
if structValue.CanSet() {
return errors.New("struct values can't be changed")
}
// Elem is used to get the value pointed by the struct pointer
structValue = structValue.Elem()
// update struct value calling the callback on each string field
if err := updateStringValues(&structValue, updateStringValueCb); err != nil {
return err
}
return nil
}
func updateStringValues(structValue *reflect.Value, updateStringValueCb func(s string) string) error {
for i := 0; i < structValue.NumField(); i++ {
field := structValue.Field(i)
if field.Kind() == reflect.Struct {
// call this function recursively on the nested fields
if err := updateStringValues(&field, updateStringValueCb); err != nil {
return fmt.Errorf("field error: %w", err)
}
continue
}
// we want to change only string values
if field.Kind() != reflect.String {
continue
}
// if the value can't be changed, continue with other fields
if !field.CanSet() {
continue
}
currentValue := field.String()
newValue := updateStringValueCb(currentValue)
field.SetString(newValue)
}
return nil
}
///////////////////////////////////////////////////
// TESTS
///////////////////////////////////////////////////
func TestUpdateStringValues(t *testing.T) {
// prepare test structure
var testData Outer
testData.Str1 = "original outer"
testData.I1.Str1 = "original inner1"
testData.I2.Str1 = "original inner2"
testData.I1.I2.Str1 = "original inner1 inner2"
testData.I2.strPrivate = "remains unchanged"
testData.I2.Slice = []string{"remains", "unchanged"}
// call the function
require.NoError(t, UpdateStringValues(&testData, func(s string) string {
// the callback function adds this prefix
// to all existing string fields
if s != "" {
return "updated: " + s
}
return s
}))
// assert that string fields have been updated
assert.Equal(t, "updated: original outer", testData.Str1)
assert.Equal(t, "updated: original inner1", testData.I1.Str1)
assert.Equal(t, "updated: original inner2", testData.I2.Str1)
assert.Equal(t, "updated: original inner1 inner2", testData.I1.I2.Str1)
// assert that private and non-string fields remained unchanged
assert.Equal(t, "remains unchanged", testData.I2.strPrivate)
assert.Equal(t, []string{"remains", "unchanged"}, testData.I2.Slice)
}
func TestUpdateStringValuesInvalidInput(t *testing.T) {
cb := func(s string) string { return s }
// nil
require.Error(t, UpdateStringValues(nil, cb))
// a struct passed by value and not by pointer
var testData Outer
require.Error(t, UpdateStringValues(testData, cb))
var str string
// string
require.Error(t, UpdateStringValues(str, cb))
// pointer to a string
require.Error(t, UpdateStringValues(&str, cb))
var ifc reflect.Type
// interface
require.Error(t, UpdateStringValues(ifc, cb))
// pointer to an interface
require.Error(t, UpdateStringValues(&ifc, cb))
}
type (
Outer struct {
Str1 string
Str2 string
I1 Inner1
I2 Inner2
}
Inner1 struct {
Str1 string
Str2 string
I2 Inner2
}
Inner2 struct {
Str1 string
Str2 string
strPrivate string
StrPtr *string
Int int
Slice []string
}
)
///////////////////////////////////////////////////
// BENCHMARK
///////////////////////////////////////////////////
func BenchmarkUpdateStringValues(b *testing.B) {
// allocating all the structs beforehand
// all of them created with the following script:
// https://gist.github.com/anjankow/7318d99381b44836374b961231188ca9
// for example:
//
// type BigStruct2 struct {
// Str0 string
// Nested0 NestedStruct2
// }
//
// type NestedStruct2 struct {
// Str0 string
// SubNested0 SubNestedStruct2
// }
//
// number after the name refers to the number of top level struct fields
var testData10 testdata.BigStruct10
var testData20 testdata.BigStruct20
var testData40 testdata.BigStruct40
var testData60 testdata.BigStruct60
var testData80 testdata.BigStruct80
var testData100 testdata.BigStruct100
// needed to invoke regular UpdateValues method
// for performance comparison
type TestDataIfc interface {
UpdateValues()
}
cb := func(s string) string { return "REFLECTION" }
var testCases = []struct {
testData TestDataIfc
// can't use testData as an interface directly to call UpdateStringValues
// - input needs to be a pointer to a struct,
// can't be wrapped in interface
updateWithReflection func() error
}{
{
testData: &testData10,
updateWithReflection: func() error {
return UpdateStringValues(&testData10, cb)
},
},
{
testData: &testData20,
updateWithReflection: func() error {
return UpdateStringValues(&testData20, cb)
},
},
{
testData: &testData40,
updateWithReflection: func() error {
return UpdateStringValues(&testData40, cb)
},
},
{
testData: &testData60,
updateWithReflection: func() error {
return UpdateStringValues(&testData60, cb)
},
},
{
testData: &testData80,
updateWithReflection: func() error {
return UpdateStringValues(&testData80, cb)
},
},
{
testData: &testData100,
updateWithReflection: func() error {
return UpdateStringValues(&testData100, cb)
},
},
}
for _, tc := range testCases {
testData := tc.testData
testDataNumFields := reflect.TypeOf(testData).Elem().NumField()
b.Run(fmt.Sprintf("BigStruct%d", testDataNumFields), func(b *testing.B) {
n := b.N
b.ResetTimer()
b.StartTimer()
for i := 0; i < n; i++ {
require.NoError(b, tc.updateWithReflection())
}
b.StopTimer()
reflectElapsed := b.Elapsed()
b.ResetTimer()
b.StartTimer()
for i := 0; i < n; i++ {
testData.UpdateValues()
}
b.StopTimer()
noReflectElapsed := b.Elapsed()
b.Logf(`{"n":%d, "fields": %d, "reflect_elapsed_ns": %d, "no_reflect_elapsed_ns": %d}`, n, testDataNumFields, reflectElapsed.Nanoseconds(), noReflectElapsed.Nanoseconds())
})
}
}
/*********************** benchmark output ***********************
*
*
goos: linux
goarch: amd64
pkg: reflect-playground
cpu: AMD Ryzen 7 PRO 5850U with Radeon Graphics
BenchmarkUpdateStringValues/BigStruct10-16 500 118.5 ns/op 0 B/op 0 allocs/op
--- BENCH: BenchmarkUpdateStringValues/BigStruct10-16
string_values_test.go:240: {"n":1, "fields": 10, "reflect_elapsed_ns": 21251, "no_reflect_elapsed_ns": 1934}
string_values_test.go:240: {"n":500, "fields": 10, "reflect_elapsed_ns": 5971705, "no_reflect_elapsed_ns": 59243}
BenchmarkUpdateStringValues/BigStruct20-16 500 2070 ns/op 0 B/op 0 allocs/op
--- BENCH: BenchmarkUpdateStringValues/BigStruct20-16
string_values_test.go:240: {"n":1, "fields": 20, "reflect_elapsed_ns": 96093, "no_reflect_elapsed_ns": 24928}
string_values_test.go:240: {"n":500, "fields": 20, "reflect_elapsed_ns": 35444555, "no_reflect_elapsed_ns": 1034893}
BenchmarkUpdateStringValues/BigStruct40-16 500 68586 ns/op 0 B/op 0 allocs/op
--- BENCH: BenchmarkUpdateStringValues/BigStruct40-16
string_values_test.go:240: {"n":1, "fields": 40, "reflect_elapsed_ns": 639559, "no_reflect_elapsed_ns": 144326}
string_values_test.go:240: {"n":500, "fields": 40, "reflect_elapsed_ns": 457829120, "no_reflect_elapsed_ns": 34292799}
BenchmarkUpdateStringValues/BigStruct60-16 500 216362 ns/op 0 B/op 0 allocs/op
--- BENCH: BenchmarkUpdateStringValues/BigStruct60-16
string_values_test.go:240: {"n":1, "fields": 60, "reflect_elapsed_ns": 2956497, "no_reflect_elapsed_ns": 832077}
string_values_test.go:240: {"n":500, "fields": 60, "reflect_elapsed_ns": 1626863626, "no_reflect_elapsed_ns": 108180989}
BenchmarkUpdateStringValues/BigStruct80-16 500 472729 ns/op 0 B/op 0 allocs/op
--- BENCH: BenchmarkUpdateStringValues/BigStruct80-16
string_values_test.go:240: {"n":1, "fields": 80, "reflect_elapsed_ns": 4696705, "no_reflect_elapsed_ns": 1460785}
string_values_test.go:240: {"n":500, "fields": 80, "reflect_elapsed_ns": 4394384544, "no_reflect_elapsed_ns": 236364722}
BenchmarkUpdateStringValues/BigStruct100-16 500 1257375 ns/op 0 B/op 0 allocs/op
--- BENCH: BenchmarkUpdateStringValues/BigStruct100-16
string_values_test.go:240: {"n":1, "fields": 100, "reflect_elapsed_ns": 14515314, "no_reflect_elapsed_ns": 3321282}
string_values_test.go:240: {"n":500, "fields": 100, "reflect_elapsed_ns": 8104683179, "no_reflect_elapsed_ns": 628687526}
PASS
ok reflect-playground 15.689s
*
*
/***************************************************************/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment