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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ vendor/

.vscode
coverage.txt

.claude
181 changes: 181 additions & 0 deletions GETTER_GENERICS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Getter型ジェネリクス実装: GetAs関数

## 概要

このドキュメントでは、Go 1.19のジェネリクス機能を活用してGetter型に実装した`GetAs`関数について解説します。この関数は、既存の型専用メソッド群(`String()`, `Int()`, `Bool()`など)を将来的に置き換えることを目的とした汎用的なフィールドアクセス機能を提供します。

## 実装仕様

### 関数シグネチャ

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

- **T**: 取得したい値の型(型パラメータ)
- **g**: Getterインスタンスのポインタ
- **name**: アクセスするフィールド名
- **戻り値**: (取得した値, 成功可否)

### 主要機能

1. **型安全性**: コンパイル時に型の安全性を保証
2. **汎用性**: 単一の関数で全てのGo基本型とユーザー定義型に対応
3. **互換性**: 既存の型専用メソッドと完全に同等の動作
4. **エラーハンドリング**: 不正なフィールド名や型変換失敗時の適切な処理

### 対応型

以下の全ての型に対応しています:

- **基本型**: `bool`, `string`, `byte`, `rune`
- **整数型**: `int`, `int8`, `int16`, `int32`, `int64`
- **符号なし整数型**: `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `uintptr`
- **浮動小数点型**: `float32`, `float64`
- **複素数型**: `complex64`, `complex128`
- **配列・スライス**: `[]T`, `[N]T`
- **マップ**: `map[K]V`
- **ポインタ**: `unsafe.Pointer`
- **関数**: `func(...) ...`
- **チャネル**: `chan T`
- **構造体**: ユーザー定義型
- **インターフェース**: `interface{}`

## 実装の特徴

### 1. 段階的な型変換戦略

`GetAs`関数は以下の順序で型変換を試行します:

1. **直接型アサーション**: 最も効率的なパス
2. **interface{}への変換**: 汎用的なインターフェース対応
3. **スライス特別処理**: `[]interface{}`への変換対応
4. **Reflection変換**: 型変換可能な場合の処理

### 2. プライベートフィールドの安全処理

unexportedフィールドへのアクセス時には、リフレクションのパニックを防ぐため`CanInterface()`チェックを実装しています。

### 3. エラー処理

- 存在しないフィールドへのアクセス → `(zero_value, false)`
- 型変換不可能な場合 → `(zero_value, false)`
- プライベートフィールドアクセス → `(zero_value, false)`

## 使用例

### 基本的な使用方法

```go
// 構造体の定義
type User struct {
Name string
Age int
IsAdmin bool
}

user := User{Name: "Alice", Age: 30, IsAdmin: true}
getter, _ := NewGetter(user)

// ジェネリクス関数を使用
name, ok := GetAs[string](getter, "Name") // "Alice", true
age, ok := GetAs[int](getter, "Age") // 30, true
admin, ok := GetAs[bool](getter, "IsAdmin") // true, true

// 存在しないフィールド
_, ok = GetAs[string](getter, "Unknown") // "", false

// 型が一致しない場合
_, ok = GetAs[int](getter, "Name") // 0, false
```

### 既存メソッドとの比較

```go
// 従来の方法
name, ok := getter.String("Name") // 型専用メソッド
age, ok := getter.Int("Age") // 型専用メソッド

// 新しい方法
name, ok := GetAs[string](getter, "Name") // ジェネリクス関数
age, ok := GetAs[int](getter, "Age") // ジェネリクス関数
```

### 複雑な型の処理

```go
// スライス
items, ok := GetAs[[]string](getter, "Items")

// マップ
config, ok := GetAs[map[string]interface{}](getter, "Config")

// 構造体
nested, ok := GetAs[NestedStruct](getter, "Nested")

// インターフェース
value, ok := GetAs[interface{}](getter, "Value")
```

## 実装上の制約とGo 1.19での対応

### メソッドレベルジェネリクスの制限

Go 1.19では、メソッド自体に型パラメータを定義することができません。そのため、以下のような実装は不可能です:

```go
// これは Go 1.19 では不可能
func (g *Getter) GetAs[T any](name string) (T, bool)
```

この制限を回避するため、以下のアプローチを採用しました:

1. **関数ベースの実装**: `GetAs[T](getter, name)` 形式
2. **型パラメータを関数レベルで定義**: より明示的で理解しやすい

### 利点

1. **明示性**: 関数シグネチャがより明確
2. **一貫性**: 他のジェネリクス関数とのAPI一貫性
3. **将来性**: Go言語の進化に対応しやすい設計

## テスト実装

`GetAs`関数の信頼性を保証するため、以下の網羅的なテストを実装しました:

- **全基本型のテスト**: 20以上の基本型でのテスト
- **複雑な型のテスト**: 配列、スライス、マップ、構造体
- **エラーケースのテスト**: 存在しないフィールド、型不一致
- **エッジケースのテスト**: プライベートフィールド、nil値

テスト関数数: 25個以上
テストケース数: 全ての既存型専用メソッドと同等の網羅性

## パフォーマンス考慮

`GetAs`関数は以下のパフォーマンス最適化を実装しています:

1. **早期リターン**: 直接型アサーションによる高速パス
2. **段階的フォールバック**: 効率的な順序での変換試行
3. **リフレクション最小化**: 必要最小限のリフレクション使用

## 今後の展望

### 段階的移行戦略

1. **Phase 1** (現在): `GetAs`関数の導入と安定化
2. **Phase 2**: 既存メソッドに`@deprecated`マーク追加
3. **Phase 3**: 既存メソッドの削除とAPI簡素化

### メリット

- **コード量削減**: 500行以上のボイラープレートコード削減
- **保守性向上**: 単一関数による一元的な型処理
- **型安全性**: コンパイル時型チェックによるランタイムエラー削減
- **開発者体験**: より直感的で使いやすいAPI

## 結論

Go 1.19のジェネリクスを活用した`GetAs`関数により、Getter型ライブラリの大幅な改善を実現しました。既存の機能との完全な互換性を保ちながら、より型安全で保守しやすいAPIを提供することができました。

この実装は、Go言語のジェネリクス機能を実際のライブラリ開発に適用した実践的な例となり、今後の類似ライブラリ開発の参考となることが期待されます。
60 changes: 60 additions & 0 deletions getter.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,3 +637,63 @@ func (g *Getter) MapGet(name string, f func(int, *Getter) (interface{}, error))

return res, nil
}

// GetAs returns the value of the original struct field named name, typed as T.
// 2nd return value will be false if the original struct does not have a "name" field.
// 2nd return value will be false if type of the original struct "name" field cannot be converted to T.
func GetAs[T any](g *Getter, name string) (T, bool) {
var zero T
gf, ok := g.getSafely(name)
if !ok {
return zero, false
}

// Try direct type assertion first
if val, ok := gf.intf.(T); ok {
return val, true
}

// Handle special cases based on reflect.Value for more complex conversions
v := gf.indirect
if !v.IsValid() {
return zero, false
}

// If T is an interface{}, return the interface directly
var interfaceType interface{} = (*interface{})(nil)
if reflect.TypeOf(zero) == reflect.TypeOf(interfaceType).Elem() {
return any(gf.intf).(T), true
}

// For slice types, handle special conversion cases
targetType := reflect.TypeOf(zero)
if targetType.Kind() == reflect.Slice {
if v.Kind() == reflect.Slice {
// Handle []interface{} conversion for slices
if targetType.Elem().Kind() == reflect.Interface && targetType.Elem().Name() == "" {
// Target is []interface{}
len := v.Len()
result := make([]interface{}, len)
for i := 0; i < len; i++ {
result[i] = v.Index(i).Interface()
}
if sliceVal, ok := any(result).(T); ok {
return sliceVal, true
}
}
}
}

// Try to convert via reflection if types are convertible
if v.Type().ConvertibleTo(targetType) {
converted := v.Convert(targetType)
// Check if we can safely call Interface() - avoid panic on unexported fields
if converted.CanInterface() {
if val, ok := converted.Interface().(T); ok {
return val, true
}
}
}

return zero, false
}
Loading
Loading