Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions GETTER_GENERICS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Generic `GetAs` for `Getter`

This document explains the design and implementation of the generic `GetAs` function
that complements the non-generic `Getter` type in Go 1.19 environments.

## Background

In Go 1.19, methods on non-generic types **cannot** have their own type parameters. Although Go 1.18 introduced generics,
the language spec only allows type parameters on functions or on generic types. Attempting to define

```go
func (g *Getter) GetAs[T any](name string) (T, bool) { … }
```

results in a compilation error:

```
method must have no type parameters
```

To work around this in Go 1.19, we provide `GetAs` as a **free function** rather than a method.

## Implementation

```go
// GetAs retrieves the field named "name" from g and attempts to cast its value to T.
// The boolean result reports whether the field exists and the cast to T succeeded.
func GetAs[T any](g *Getter, name string) (T, bool) {
var zero T
gf, ok := g.getSafely(name)
if !ok {
return zero, false
}
res, ok := gf.intf.(T)
return res, ok
}
```

Here, `g.getSafely` checks presence of the named field and returns its stored `interface{}` value.
The type assertion `gf.intf.(T)` then performs the cast to the generic type parameter.

## Usage Examples

Below snippets show how to use `GetAs` in practice:

```go
g, _ := structil.NewGetter(myStructPtr)

// Get an int field
if age, ok := structil.GetAs[int](g, "Age"); ok {
fmt.Println("Age is", age)
}

// Get a string slice
if tags, ok := structil.GetAs[[]string](g, "Tags"); ok {
fmt.Println("Tags", tags)
}

// Attempt nonexistent or mismatched type
if _, ok := structil.GetAs[bool](g, "Name"); !ok {
fmt.Println("Name is not a bool or does not exist")
}
```

## Future Outlook

When the Go language eventually permits methods with type parameters on non-generic types (e.g., Go 1.21+),
the generic `GetAs` free function can be migrated to a method on `Getter` for more ergonomic calls.
12 changes: 12 additions & 0 deletions getter.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,18 @@ func (g *Getter) Get(name string) (interface{}, bool) {
return nil, false
}

// GetAs returns the original struct field named "name" cast to type T.
// The second return value reports whether the field exists and its value is of type T.
func GetAs[T any](g *Getter, name string) (T, bool) {
var zero T
gf, ok := g.getSafely(name)
if !ok {
return zero, false
}
res, ok := gf.intf.(T)
return res, ok
}

// ToMap returns a map converted from this Getter.
func (g *Getter) ToMap() map[string]interface{} {
m := make(map[string]interface{})
Expand Down
127 changes: 126 additions & 1 deletion getter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,132 @@ func TestGetterToMap(t *testing.T) {
}
}

testGetSeries(t, true, false, assertionFunc)
testGetSeries(t, true, false, assertionFunc)
}

// TestGetAs tests the generic GetAs free function for various field types.
func TestGetAs(t *testing.T) {
t.Parallel()

testStructPtr := newGetterTestStructPtr()
g, err := NewGetter(testStructPtr)
if err != nil {
t.Fatalf("NewGetter() unexpected error: %v", err)
}

// Byte
if got, ok := GetAs[byte](g, "Byte"); !ok || got != testStructPtr.Byte {
t.Errorf("GetAs[byte](g, \"Byte\") = %v, %v; want %v, true", got, ok, testStructPtr.Byte)
}
// Bytes
if got, ok := GetAs[[]byte](g, "Bytes"); !ok || !cmp.Equal(got, testStructPtr.Bytes) {
t.Errorf("GetAs[[]byte](g, \"Bytes\") = %v, %v; want %v, true", got, ok, testStructPtr.Bytes)
}
// String and Stringptr
if got, ok := GetAs[string](g, "String"); !ok || got != testStructPtr.String {
t.Errorf("GetAs[string](g, \"String\") = %v, %v; want %v, true", got, ok, testStructPtr.String)
}
if got, ok := GetAs[string](g, "Stringptr"); !ok || got != *testStructPtr.Stringptr {
t.Errorf("GetAs[string](g, \"Stringptr\") = %v, %v; want %v, true", got, ok, *testStructPtr.Stringptr)
}
// Ints
if got, ok := GetAs[int](g, "Int"); !ok || got != testStructPtr.Int {
t.Errorf("GetAs[int](g, \"Int\") = %v, %v; want %v, true", got, ok, testStructPtr.Int)
}
if got, ok := GetAs[int8](g, "Int8"); !ok || got != testStructPtr.Int8 {
t.Errorf("GetAs[int8](g, \"Int8\") = %v, %v; want %v, true", got, ok, testStructPtr.Int8)
}
if got, ok := GetAs[int16](g, "Int16"); !ok || got != testStructPtr.Int16 {
t.Errorf("GetAs[int16](g, \"Int16\") = %v, %v; want %v, true", got, ok, testStructPtr.Int16)
}
if got, ok := GetAs[int32](g, "Int32"); !ok || got != testStructPtr.Int32 {
t.Errorf("GetAs[int32](g, \"Int32\") = %v, %v; want %v, true", got, ok, testStructPtr.Int32)
}
if got, ok := GetAs[int64](g, "Int64"); !ok || got != testStructPtr.Int64 {
t.Errorf("GetAs[int64](g, \"Int64\") = %v, %v; want %v, true", got, ok, testStructPtr.Int64)
}
// Uints
if got, ok := GetAs[uint](g, "Uint"); !ok || got != testStructPtr.Uint {
t.Errorf("GetAs[uint](g, \"Uint\") = %v, %v; want %v, true", got, ok, testStructPtr.Uint)
}
if got, ok := GetAs[uint8](g, "Uint8"); !ok || got != testStructPtr.Uint8 {
t.Errorf("GetAs[uint8](g, \"Uint8\") = %v, %v; want %v, true", got, ok, testStructPtr.Uint8)
}
if got, ok := GetAs[uint16](g, "Uint16"); !ok || got != testStructPtr.Uint16 {
t.Errorf("GetAs[uint16](g, \"Uint16\") = %v, %v; want %v, true", got, ok, testStructPtr.Uint16)
}
if got, ok := GetAs[uint32](g, "Uint32"); !ok || got != testStructPtr.Uint32 {
t.Errorf("GetAs[uint32](g, \"Uint32\") = %v, %v; want %v, true", got, ok, testStructPtr.Uint32)
}
if got, ok := GetAs[uint64](g, "Uint64"); !ok || got != testStructPtr.Uint64 {
t.Errorf("GetAs[uint64](g, \"Uint64\") = %v, %v; want %v, true", got, ok, testStructPtr.Uint64)
}
if got, ok := GetAs[uintptr](g, "Uintptr"); !ok || got != testStructPtr.Uintptr {
t.Errorf("GetAs[uintptr](g, \"Uintptr\") = %v, %v; want %v, true", got, ok, testStructPtr.Uintptr)
}
// Floats
if got, ok := GetAs[float32](g, "Float32"); !ok || got != testStructPtr.Float32 {
t.Errorf("GetAs[float32](g, \"Float32\") = %v, %v; want %v, true", got, ok, testStructPtr.Float32)
}
if got, ok := GetAs[float64](g, "Float64"); !ok || got != testStructPtr.Float64 {
t.Errorf("GetAs[float64](g, \"Float64\") = %v, %v; want %v, true", got, ok, testStructPtr.Float64)
}
// Bool
if got, ok := GetAs[bool](g, "Bool"); !ok || got != testStructPtr.Bool {
t.Errorf("GetAs[bool](g, \"Bool\") = %v, %v; want %v, true", got, ok, testStructPtr.Bool)
}
// Complex
if got, ok := GetAs[complex64](g, "Complex64"); !ok || got != testStructPtr.Complex64 {
t.Errorf("GetAs[complex64](g, \"Complex64\") = %v, %v; want %v, true", got, ok, testStructPtr.Complex64)
}
if got, ok := GetAs[complex128](g, "Complex128"); !ok || got != testStructPtr.Complex128 {
t.Errorf("GetAs[complex128](g, \"Complex128\") = %v, %v; want %v, true", got, ok, testStructPtr.Complex128)
}
// UnsafePointer
if got, ok := GetAs[unsafe.Pointer](g, "Unsafeptr"); !ok || got != testStructPtr.Unsafeptr {
t.Errorf("GetAs[unsafe.Pointer](g, \"Unsafeptr\") = %v, %v; want %v, true", got, ok, testStructPtr.Unsafeptr)
}
// String slice and array
if got, ok := GetAs[[]string](g, "Stringslice"); !ok || !cmp.Equal(got, testStructPtr.Stringslice) {
t.Errorf("GetAs[[]string](g, \"Stringslice\") = %v, %v; want %v, true", got, ok, testStructPtr.Stringslice)
}
if got, ok := GetAs[[2]string](g, "Stringarray"); !ok || got != testStructPtr.Stringarray {
t.Errorf("GetAs[[2]string](g, \"Stringarray\") = %v, %v; want %v, true", got, ok, testStructPtr.Stringarray)
}
// Map
if got, ok := GetAs[map[string]interface{}](g, "Map"); !ok || !cmp.Equal(got, testStructPtr.Map) {
t.Errorf("GetAs[map[string]interface{}](g, \"Map\") = %v, %v; want %v, true", got, ok, testStructPtr.Map)
}
// Func
if got, ok := GetAs[func(string) interface{}](g, "Func"); !ok || reflect.ValueOf(got).Pointer() != reflect.ValueOf(testStructPtr.Func).Pointer() {
gp := reflect.ValueOf(got).Pointer()
wp := reflect.ValueOf(testStructPtr.Func).Pointer()
t.Errorf("GetAs[func(string) interface{}](g, \"Func\") gp=%#x, ok=%v; want wp=%#x, true", gp, ok, wp)
}
// Chan
if got, ok := GetAs[chan int](g, "ChInt"); !ok || !cmp.Equal(got, testStructPtr.ChInt) {
t.Errorf("GetAs[chan int](g, \"ChInt\") = %v, %v; want %v, true", got, ok, testStructPtr.ChInt)
}
// Nested struct and slices
if got, ok := GetAs[GetterTestStruct2](g, "GetterTestStruct2"); !ok || !cmp.Equal(got, testStructPtr.GetterTestStruct2) {
t.Errorf("GetAs[GetterTestStruct2](g, \"GetterTestStruct2\") = %v, %v; want %v, true", got, ok, testStructPtr.GetterTestStruct2)
}
if got, ok := GetAs[GetterTestStruct2](g, "GetterTestStruct2Ptr"); !ok || !cmp.Equal(got, *testStructPtr.GetterTestStruct2Ptr) {
t.Errorf("GetAs[GetterTestStruct2](g, \"GetterTestStruct2Ptr\") = %v, %v; want %v, true", got, ok, *testStructPtr.GetterTestStruct2Ptr)
}
if got, ok := GetAs[[]GetterTestStruct4](g, "GetterTestStruct4Slice"); !ok || !cmp.Equal(got, testStructPtr.GetterTestStruct4Slice) {
t.Errorf("GetAs[[]GetterTestStruct4](g, \"GetterTestStruct4Slice\") = %v, %v; want %v, true", got, ok, testStructPtr.GetterTestStruct4Slice)
}
if got, ok := GetAs[[]*GetterTestStruct4](g, "GetterTestStruct4PtrSlice"); !ok || !cmp.Equal(got, testStructPtr.GetterTestStruct4PtrSlice) {
t.Errorf("GetAs[[]*GetterTestStruct4](g, \"GetterTestStruct4PtrSlice\") = %v, %v; want %v, true", got, ok, testStructPtr.GetterTestStruct4PtrSlice)
}
// Private or not exist
if _, ok := GetAs[string](g, "privateString"); ok {
t.Errorf("GetAs[string](g, \"privateString\") ok = %v; want false", ok)
}
if _, ok := GetAs[string](g, "NotExist"); ok {
t.Errorf("GetAs[string](g, \"NotExist\") ok = %v; want false", ok)
}
}

func TestByte(t *testing.T) {
Expand Down
Loading