Last active
August 12, 2023 10:28
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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