Réflexion en Go pour la manipulation dynamique des champs de structures

Introduction

En Go, la réflexion offre la capacité de manipuler dynamiquement les champs des structures, comparable à getattr et setattr en Python. Le système de types strict de Go nécessite une gestion rigoureuse lors de l'attribution de valeurs via la réflexion, en particulier pour les types aliasés.

Problème de type lors de l'attribution

Lors de l'utilisation de la réflexion pour définir un champ, une incompatibilité de type peut survenir. Par exemple, avec un type alias comme TaskState dérivé de int, l'attribution directe d'un entier échoue car les types Go sont distincts. Cela requiert une approche pour gérer les conversions de type.

Solution avec vérification de Kind et conversion

Pour résoudre ce problème, on peut exploiter la notion de Kind dans la réflexion, qui représente la catégorie de type (comme Int, Struct). Les types de base et leurs alias partagent le même Kind. Ensuite, la méthode Convert permet d'effectuer des conversions de type lorsque nécessaire.

Voici une implémentation réécrite des fonctions pour gérer l'attribution de manière flexible et stricte.

Fonctions de validation

package hreflect

import (
    "fmt"
    "reflect"
)

// fetchFieldFromObject valide l'objet et récupère le champ spécifié.
func fetchFieldFromObject(obj interface{}, name string) (objVal reflect.Value, fieldVal reflect.Value, err error) {
    if obj == nil {
        err = fmt.Errorf("opération impossible sur un objet nil")
        return
    }
    objType := reflect.TypeOf(obj)
    if objType.Kind() != reflect.Struct && objType.Kind() != reflect.Ptr {
        err = fmt.Errorf("l'objet doit être une structure ou un pointeur, type reçu : %T", obj)
        return
    }
    objVal = reflect.Indirect(reflect.ValueOf(obj))
    fieldVal = objVal.FieldByName(name)
    if !fieldVal.IsValid() {
        err = fmt.Errorf("champ '%s' introuvable dans l'objet %T", name, obj)
    }
    return
}

// RetrieveValue récupère la valeur d'un champ par son nom.
func RetrieveValue(obj interface{}, fieldName string) (interface{}, error) {
    _, field, err := fetchFieldFromObject(obj, fieldName)
    if err != nil {
        return nil, err
    }
    return field.Interface(), nil
}

Attribution avec gestion de type

// AssignValue définit un champ avec une conversion de type flexible.
func AssignValue(obj interface{}, fieldName string, value interface{}) error {
    _, field, err := fetchFieldFromObject(obj, fieldName)
    if err != nil {
        return err
    }
    if !field.CanSet() {
        return fmt.Errorf("le champ '%s' n'est pas modifiable dans l'objet %T", fieldName, obj)
    }

    val := reflect.ValueOf(value)
    // Tentative de conversion directe
    if converted, convErr := convertSafely(field.Type(), val); convErr == nil {
        return setSafely(field, converted)
    }
    // Vérification des Kind si la conversion échoue
    if kindErr := compareKinds(obj, fieldName, field, val); kindErr != nil {
        return kindErr
    }
    return fmt.Errorf("échec de la conversion de %T vers %s", value, field.Type())
}

// AssignValueStrict définit un champ en imprimant une correspondance stricte des Kind.
func AssignValueStrict(obj interface{}, fieldName string, value interface{}) error {
    _, field, err := fetchFieldFromObject(obj, fieldName)
    if err != nil {
        return err
    }
    if !field.CanSet() {
        return fmt.Errorf("le champ '%s' n'est pas modifiable dans l'objet %T", fieldName, obj)
    }

    val := reflect.ValueOf(value)
    if kindErr := compareKinds(obj, fieldName, field, val); kindErr != nil {
        return kindErr
    }
    return tryConvertAndAssign(field, val)
}

// compareKinds vérifie si les Kind des types correspondent.
func compareKinds(obj interface{}, fieldName string, field, val reflect.Value) error {
    if field.Type().Kind() != val.Type().Kind() {
        return fmt.Errorf("Kind incompatibles : %s (Kind: %s) et %s (Kind: %s) pour le champ '%s' dans %T",
            val.Type(), val.Type().Kind(), field.Type(), field.Type().Kind(), fieldName, obj)
    }
    return nil
}

// convertSafely tente une conversion de type avec récupération d'erreur.
func convertSafely(targetType reflect.Type, val reflect.Value) (reflect.Value, error) {
    defer func() {
        if r := recover(); r != nil {
            // Le panic est capturé et ignoré ici, retourné via l'erreur.
        }
    }()
    converted := val.Convert(targetType)
    return converted, nil
}

// setSafely définit une valeur de champ avec gestion des panics.
func setSafely(field, val reflect.Value) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("erreur d'attribution : %v", r)
        }
    }()
    field.Set(val)
    return nil
}

// tryConvertAndAssign combine conversion et attribution.
func tryConvertAndAssign(field, val reflect.Value) error {
    if converted, err := convertSafely(field.Type(), val); err != nil {
        return setSafely(field, converted)
    }
    return fmt.Errorf("impossible de convertir %s vers %s", val.Type(), field.Type())
}

Exemple d'application

type TaskState int
type Task struct {
    State TaskState
}

func main() {
    task := &Task{}
    // Attribution flexible : la conversion de int en TaskState est autorisée
    if err := AssignValue(task, "State", 5); err != nil {
        fmt.Println("Erreur (flexible) :", err)
    }

    // Attribution stricte : les Kind doivent correspondre
    if err := AssignValueStrict(task, "State", 10); err != nil {
        fmt.Println("Erreur (stricte) :", err)
    }
}

Considérations

Cette méthode offre deux modes : l'un flexible permettant des conversions implicites (comme float vers int), et l'autre strict limiatnt les attributions aux correspondances exactes de Kind. Le choix dépend des besoins de typage et de sécurité du code.

Étiquettes: Go reflect types structs type-conversion

Publié le 28 juin à 23h28