Skip to content

Commit

Permalink
move StructLookup to standalone package
Browse files Browse the repository at this point in the history
  • Loading branch information
hsiafan committed Sep 8, 2024
1 parent 6db5f00 commit 1a220ac
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 80 deletions.
128 changes: 128 additions & 0 deletions internal/reflects/struct_lookup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package reflects

import (
"errors"
"reflect"
"sync"
"unsafe"
)

// StructLookup used to deal reflect operations for struct fields and methods.
// The lookup instance should be reused to improve the performance.
type StructLookup struct {
cache sync.Map //map[reflect.Type]structInfo
}

// structInfo contains struct meta cache to accelerate the reflect operations.
type structInfo struct {
fields map[string]int // The field name to index map
// The embed fields name to index map. It is a many-to-one map.
// The names is retrieve recursively from all embed type fields;
// The values is the indexes for each level' struct field, from the inner to outer.
embedFields map[string][]int
}

var defaultStructLookup = sync.OnceValue(func() *StructLookup {
return NewStructLookup()
})

// DefaultStructLookup returns the default StructLookup instance.
func DefaultStructLookup() *StructLookup {
return defaultStructLookup()
}

// NewStructLookup returns a new created StructLookup instance.
func NewStructLookup() *StructLookup {
return &StructLookup{
cache: sync.Map{},
}
}

// Field get the field value for the giving struct value and field name.
// It will look into embed struct fields if no match Field is found.
//
// param v: a reflect contains a struct value, or contains a pointer to struct value.
func (s *StructLookup) Field(v reflect.Value, name string) (reflect.Value, bool) {
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
panic(errors.New("not a struct value"))
}

t := v.Type()
si := s.loadStructFields(t)
if idx, ok := si.fields[name]; ok {
fv := v.Field(idx)
if !fv.CanInterface() {
// try to read unexported fields
p := (*reflectValue)(unsafe.Pointer(&fv))
p.flag = p.flag & ^flagRO
}
return fv, true
}

if indexes, ok := si.embedFields[name]; ok {
fv := v
for i := len(indexes) - 1; i >= 0; i-- {
fv = fv.Field(indexes[i])
if fv.Kind() == reflect.Pointer {
fv = fv.Elem()
}
}
if !fv.CanInterface() {
// try to read unexported fields
p := (*reflectValue)(unsafe.Pointer(&fv))
p.flag = p.flag & ^flagRO
}
return fv, true
}

var zero reflect.Value
return zero, false
}

func (s *StructLookup) loadStructFields(t reflect.Type) structInfo {
v, ok := s.cache.Load(t)
if ok {
return v.(structInfo)
}

var embeds = map[string][]int{}
var fields = map[string]int{}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if !f.Anonymous {
fields[f.Name] = i
}
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
ft := f.Type
if f.Anonymous {
if ft.Kind() == reflect.Pointer {
ft = ft.Elem()
}
if ft.Kind() == reflect.Struct {
si := s.loadStructFields(ft)
for name, idx := range si.fields {
if _, ok := fields[name]; !ok {
embeds[name] = []int{idx, i}
}
}
for name, indexes := range si.embedFields {
if _, ok := fields[name]; !ok {
embeds[name] = append(indexes, i)
}
}
}
}
}
si := structInfo{
fields: fields,
embedFields: embeds,
}

s.cache.Store(t, si)
return si
}
71 changes: 71 additions & 0 deletions internal/reflects/struct_lookup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package reflects

import (
"reflect"
"testing"

"github.com/stretchr/testify/assert"
)

func TestStructLookup_GetField(t *testing.T) {

v := reflect.ValueOf(mockTestStruct())
lookup := DefaultStructLookup()
name, ok := lookup.Field(v, "Name")
assert.True(t, ok)
assert.Equal(t, "john", name.String())

age, ok := lookup.Field(v, "age")
assert.True(t, ok)
assert.Equal(t, 18, int(age.Int()))

male, ok := lookup.Field(v, "male")
assert.True(t, ok)
assert.Equal(t, true, male.Bool())

weight, ok := lookup.Field(v, "weight")
assert.True(t, ok)
assert.Equal(t, 10.0, weight.Float())

rich, ok := lookup.Field(v, "rich")
assert.True(t, ok)
assert.Equal(t, true, rich.Bool())
}

func mockTestStruct() any {
return &testStruct{
Name: "john",
age: 18,
testEmbedStruct2: &testEmbedStruct2{
weight: 10,
testEmbedStruct21: testEmbedStruct21{
weight: 11,
rich: true,
},
},
testEmbedStruct: testEmbedStruct{
male: true,
},
}
}

type testStruct struct {
testEmbedStruct
*testEmbedStruct2
Name string
age int
}

type testEmbedStruct struct {
male bool
}

type testEmbedStruct2 struct {
weight float32
testEmbedStruct21
}

type testEmbedStruct21 struct {
weight float32
rich bool
}
17 changes: 17 additions & 0 deletions internal/reflects/value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package reflects

import "unsafe"

type flag uintptr

const (
flagStickyRO flag = 1 << 5 // unexported not embedded field
flagEmbedRO flag = 1 << 6 // unexported embedded field
flagRO flag = flagStickyRO | flagEmbedRO
)

type reflectValue struct {
typ_ unsafe.Pointer
ptr unsafe.Pointer
flag
}
11 changes: 9 additions & 2 deletions strings2/string_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"strconv"
"strings"
"unicode/utf8"

"github.com/hsiafan/go-utils/internal/reflects"
)

// Format format a string use python-PEP3101 style, with positional arguments.
Expand Down Expand Up @@ -85,9 +87,14 @@ func FormatNamed2(pattern string, values any) string {
}

f := formatter{pattern: pattern}
lookup := newStructLookup()
lookup := reflects.DefaultStructLookup()
s, err := f.formatNamed(func(name string) (any, bool) {
return lookup.lookup(rv, name)
v, ok := lookup.Field(rv, name)
if ok {
return v.Interface(), true
} else {
return nil, false
}
})
if err != nil {
panic(err)
Expand Down
78 changes: 0 additions & 78 deletions strings2/struct_lookup.go

This file was deleted.

0 comments on commit 1a220ac

Please sign in to comment.