Scripting Go avec Expr Lang : les 10 pièges critiques à connaître absolument

Scripting Go avec Expr Lang : les 10 pièges critiques à connaître absolument

Table des matières

Expr est un moteur d’évaluation d’expressions rapide et sûr pour Go. Utilisez-le pour intégrer des filtres dynamiques, de la logique définie par l’utilisateur ou du scripting Go léger dans vos applications.

Lorsque j’ai utilisé Expr Lang pour la première fois, j’ai été immédiatement impressionné par la facilité avec laquelle il s’intégrait à un projet Go. Il offre une syntaxe claire et des performances solides, rendant l’intégration transparente.

Mais au fur et à mesure que je l’utilisais sérieusement, j’ai rencontré des limitations inattendues. Ces éléments n’étaient pas évidents d’après la documentation officielle. Certains comportements subtils m’ont rapidement fait trébucher. D’autres ont rendu le débogage plus difficile que prévu.

Cet article couvre les leçons que j’ai apprises à mes dépens. Si vous prévoyez d’utiliser Go Expr en production, ces conseils vous aideront à éviter les erreurs courantes et à en tirer le meilleur parti dès le premier jour.

Info

Éviter l’enfer de la dépendance est crucial pour toute librairie prête pour la production. C’est là qu’Expr excelle : il est sans dépendance externe (zero-dependency). Il ne repose que sur la librairie standard de Go.

Cela signifie que vous évitez d’intégrer d’énormes runtimes de scripting externes—pas de Lua, pas de JS, pas de packages externes. Vous évitez entièrement les conflits de dépendances transitives profondes.

Pour comprendre les risques importants que ce choix de conception permet d’éviter, consultez mon guide détaillé sur 5 Stratégies pour Éviter l’Enfer de la Dépendance.

1. Quel Type de Langage Est Expr ?

Le langage Expr lang n’est pas un langage de scripting complet. C’est un langage fonctionnel, basé uniquement sur des expressions avec des limitations clés.

1.1. Uniquement Basé sur des Expressions & Fonctionnel

Le programme entier dans Expr lang est une expression unique. Il n’y a pas de packages, pas de modules, et pas de définitions de fonctions.

Au lieu de cela, écrire un script Expr donne l’impression d’implémenter une seule fonction. Cette fonction prend des données en entrée et un ensemble d’identificateurs de fonctions appelables. Elle doit retourner une seule valeur.

  • Le mot-clé return n’existe pas.
  • Aucune réaffectation de variable n’est autorisée (plus de détails ci-dessous).
  • Aucune boucle n’est disponible.
  • Vous ne pouvez pas définir de fonctions utilisateur à l’intérieur du script.

L’absence de boucles garantit que les expressions se terminent toujours. Cela prévient les boucles infinies ou l’épuisement des ressources.

Warning

Faites cependant attention aux fonctions personnalisées qui sont appelées depuis Expr. Elles peuvent toujours introduire des effets secondaires ou des boucles infinies si vous ne les implémentez pas avec soin.

1.2. Intégration Transparente avec les Types Go

Vous n’avez pas besoin d’effectuer d’actions spéciales pour passer des variables Go dans Expr.

Vous pouvez même accéder aux méthodes de vos types Go. Par exemple, si vous avez une struct Go User avec une méthode IsAdmin(), vous pouvez appeler user.IsAdmin() directement dans votre script Expr.

L’option expr.Env vous permet de passer une entrée sous forme de map[string]any ou d’une struct. Expr rend automatiquement ses champs et méthodes disponibles dans le script.

Cela rend l’exposition de vos structures de données Go au moteur d’expression très pratique. Si vous passez une struct User avec un champ Name, par exemple, vous pouvez accéder directement à user.Name.

Cette intégration transparente simplifie l’accès et la manipulation des données. C’est un avantage significatif par rapport à d’autres alternatives de scripting Go nécessitant plus de code boilerplate.

C’est un argument de vente majeur pour le module Go Expr et l’une de ses fonctionnalités les plus puissantes.

Exemple d'interaction d'expr avec Go

Regardons un exemple.

 1package main
 2
 3import (
 4  "fmt"
 5  "github.com/expr-lang/expr"
 6)
 7// Définir une struct Go et une méthode
 8type User struct {
 9  Name string
10  Age  int
11}
12func (u User) IsAdmin() bool {
13  return u.Name == "admin"
14}
15// Définir une fonction Go externe
16func double(x int) int {
17  return x * 2
18}
19
20func main() {
21  user := User{Name: "Alice", Age: 30}
22  env := map[string]any{"user": user, "double": double}
23
24	program, _ := expr.Compile(
25    // Accéder aux champs de la struct, appeler une fonction externe, appeler une méthode Go
26    `user.Name + " is not " + string(double(user.Age)) + " years old and is admin: " + string(user.IsAdmin())`, 
27    expr.Env(env),
28  )
29	output, _ := expr.Run(program, env)
30	fmt.Println(output) // [Output]: Alice is not 60 years old and is admin: false
31
32	user = User{Name: "Bob", Age: 25}
33	env = map[string]any{"user": user, "double": double}
34	output, _ = expr.Run(program, env)
35	fmt.Println(output) // [Output]: Bob is not 50 years old and is admin: false
36}

2. Comportement du Langage Expr et Pièges

Voici les surprises courantes au niveau du langage que vous rencontrerez lorsque vous travaillerez avec Expr.

2.1. Les Clés des Maps Locales Doivent Être des Littéraux de Chaîne

Les maps créées dans le script Expr ne prennent en charge que les clés de type chaîne de caractères (string keys) lors de l’initialisation.

Si vous utilisez des clés int, Expr convertit implicitement les clés en chaînes.

Vous ne pouvez pas utiliser de variables ou d’expressions comme clés. Tout nom est traité littéralement comme une clé de chaîne.

Vous pouvez accéder aux valeurs de la map en utilisant des variables comme clés. Cependant, vous ne pouvez pas y accéder directement en utilisant des clés entières.

1let k = "mykey";
2let data = {k: 12, 25: 'Hi'};
3
4data[k] // ❌ Invalide, la clé est littéralement "k", sera nil
5data["mykey"] // ❌ Idem que k
6data["k"] // ✅ Fonctionne et vaut 12
7
8data[25] // ❌ Erreur de compilation
9data["25"] // ✅ Fonctionne et vaut 'Hi'
Exemple des pièges des clés de Maps Locales
 1package main
 2
 3import (
 4  "fmt"
 5  "github.com/expr-lang/expr"
 6)
 7
 8func main() {
 9  env := map[string]any{}
10
11  program, _ := expr.Compile(`
12    let k = "mykey";
13    // Les clés k, 2, et "1" sont toutes traitées comme des littéraux de chaîne
14    let data = {k: 12, 2: 34, "1": 56};
15    // data["k"] fonctionne, data[k] et data["mykey"] échouent car la clé littérale est "k"
16    [data[k], data["mykey"], data["k"], data["2"], data["1"]]
17  `, expr.Env(env))
18  output, _ := expr.Run(program, env)
19  fmt.Println(output)
20
21  program, err := expr.Compile(`
22    let data = {1: 12};
23    data[1]
24  `, expr.Env(env))
25  if err != nil {
26    fmt.Println("COMPILE ERROR:", err)
27  }
28}

Sortie :

[<nil> <nil> 12 34 56]
COMPILE ERROR: cannot use int to get an element from map[string]interface {} (3:10)
 |     data[1]
 | .........^

2.2. Les Clés de Maps d’Environnement Conservent les Types Go Stricts

Contrairement aux maps créées dans le script Expr, les maps Go passées à Expr conservent leurs types de clés d’origine.

Dans le script, si la clé de la map Go est un entier, y accéder en utilisant une clé de type chaîne échouera.

1env := map[string]any{
2  "data": map[any]any{
3    1: "one",
4  },
5}
1data[1] // ✅ Fonctionne
2data["1"] // ❌ Échoue (si la clé de la map Go est int)
Exemple de Types de Clés de Map à partir de l'environnement
 1package main
 2
 3import (
 4  "fmt"
 5  "github.com/expr-lang/expr"
 6)
 7
 8func main() {
 9  env := map[string]any{
10    "data": map[int]string{1: "one"}, // La map Go utilise des clés int
11  }
12
13  program, _ := expr.Compile(`data[1]`, expr.Env(env)) // Accès avec int fonctionne
14  output, _ := expr.Run(program, env)
15  fmt.Println(output)
16
17  _, err := expr.Compile(`data["1"]`, expr.Env(env)) // Accès avec string échoue
18  if err != nil {
19    fmt.Println("COMPILE ERROR:", err)
20  }
21}

Sortie :

one
COMPILE ERROR: cannot use string to get an element from map[int]string (1:6)
 | data["1"]
 | .....^

2.3. Les Variables Sont Immuables (Lecture Seule)

Une fois que vous avez affecté une valeur à une variable dans Expr lang (à partir de l’env ou d’une déclaration de variable avec let), vous ne pouvez pas la modifier.

1let x = 10;
2x = 20; // ❌ Erreur : affectation à une constante

Même les maps se comportent comme immuables :

1let myMap = {"a": 1};
2myMap["a"] = 2; // ❌ Erreur : la map est en lecture seule

Vous ne pouvez muter aucune valeur à l’intérieur de l’expression. L’environnement (env) est strictement en lecture seule.

Illustration de l'immuabilité des Variables
 1package main
 2
 3import (
 4  "fmt"
 5  "github.com/expr-lang/expr"
 6)
 7
 8func main() {
 9  env := map[string]any{
10    "data": map[string]int{"a": 1},
11  }
12  // Tenter d'affecter une valeur dans l'expression provoque une erreur de compilation
13  _, err := expr.Compile(`data["a"] = 2`, expr.Env(env))
14  if err != nil {
15    fmt.Println("COMPILE ERROR:", err)
16  }
17}

Sortie :

COMPILE ERROR: unexpected token Operator("=") (1:11)
 | data["a"] = 2
 | ..........^

2.4. Le Piège d’Accès : Les Structs Sont Strictes, Les Maps Sont Indulgentes

L’un des aspects les plus intéressants et initialement déroutants de Go Expr est que les structs et les maps partagent la même syntaxe d’accès.

Que vous utilisiez la notation par point (data.field) ou la notation par crochets (data["field"]), les deux fonctionnent pour les structs et les maps :

1user.Name
2user["Name"]

Cette symétrie donne l’impression que les structs et les maps se comportent de manière identique. Ce n’est pas le cas.

La différence apparaît lorsque vous essayez d’accéder à un champ ou une clé de map inexistant.

  • Pour les maps, Expr agit comme Go : l’accès à une clé manquante retourne la valeur zéro du type attendu (par exemple, "", 0, nil, etc.).
  • Pour les structs, Expr agit strictement : l’accès à un champ manquant déclenche une erreur de compilation d’expression.
Exemple d'Accès Struct vs Map
 1package main
 2
 3import (
 4    "fmt"
 5    "github.com/expr-lang/expr"
 6)
 7
 8type User struct {
 9    Name string
10}
11
12func main() {
13    // Configuration de l'environnement avec une struct Go et une map Go
14    env := map[string]any{
15        "user": User{Name: "Alice"},
16        "data": map[string]string{"Name": "Bob"},
17    }
18
19    // L'accès à une clé de map manquante retourne la valeur zéro ("")
20    programMap, errMap := expr.Compile(`'"' + data.Missing + '"'`, expr.Env(env))
21    outputMap, errMap := expr.Run(programMap, env)
22    fmt.Println("MAP: output is", outputMap, "error is", errMap)
23
24    // L'accès à un champ de struct manquant provoque une erreur de compilation
25    _, errStruct := expr.Compile(`'"' + user.Missing + '"'`, expr.Env(env))
26    fmt.Println("STRUCT: COMPILE ERROR IS:", errStruct)
27}

Sortie :

MAP: output is "" error is <nil>
STRUCT: COMPILE ERROR IS: type main.User has no field Missing (1:12)
 | '"' + user.Missing + '"'
 | ...........^

Remarquez ce qui se passe :

  • L’accès à user.Missing sur la struct provoque une erreur.
  • L’accès à data.Missing sur la map retourne la valeur zéro, une chaîne vide (""), et ne provoque pas d’erreur.

Ainsi, même si vous pouvez utiliser la même syntaxe, rappelez-vous cette règle :

Les structs sont strictes ; les maps sont indulgentes.

Cette distinction passe facilement inaperçue. Elle est particulièrement délicate lorsque votre environnement mélange des types struct et des maps dynamiques.

Vérifiez toujours vos types de données avant de supposer que l’accès au « champ » se comportera de la même manière.

3. Curiosités d’Intégration de Go Expr

Examinons maintenant les surprises qui surviennent lors de l’intégration d’Expr avec le code Go.

3.1. Les Fonctions/Objets Doivent Être Passés à la Fois à la Compilation et à l’Exécution

Si vous définissez une fonction d’environnement personnalisée comme isAdmin, vous devez la fournir à la fois au moment de la compilation (compile-time) et au moment de l’exécution (run-time).

Si vous ne passez la fonction qu’au moment de l’exécution, cela ne fonctionnera pas. Il en va de même si vous ne la passez qu’au moment de la compilation.

Ce comportement diffère de certains autres moteurs de templates qui autorisent l’injection uniquement à l’exécution.

Pour les objets d’environnement (comme user), une règle similaire s’applique. Vous devez les passer au moment de la compilation. Si vous ne les passez pas à l’exécution, ils prennent la valeur par défaut nil.

Exemple de l'Exigence d'Environnement Double (Compilation + Exécution)
 1package main
 2
 3import (
 4  "fmt"
 5  "github.com/expr-lang/expr"
 6)
 7
 8type User struct {
 9  Role string
10}
11
12func main() {
13  env := map[string]any{
14    "isAdmin": func(u User) bool { return u.Role == "admin" },
15    "user":    &User{Role: "admin"},
16  }
17
18  // Utilisation correcte : Compilation et Exécution avec env contenant à la fois la fonction et l'objet
19  program, _ := expr.Compile(`isAdmin(user)`, expr.Env(env))
20  output, _ := expr.Run(program, env)
21  fmt.Println(output)
22  
23  // 'user' prend la valeur par défaut nil car il est manquant dans la map d'environnement
24  output, err := expr.Run(program, map[string]any{})
25  if err != nil {
26    fmt.Println("RUNTIME ERROR:", err)
27  }
28  output, _ = expr.Run(program, map[string]any{"isAdmin": func (a any) bool { fmt.Println("a =", a);return false }} ) // ❌ user manquant
29  fmt.Println(output)
30
31// Échoue à la compilation car 'isAdmin' est manquant de l'option Env
32  program, err = expr.Compile(`isAdmin(user)`, expr.Env(map[string]any{})) // ❌ isAdmin manquant
33  if err != nil {
34    fmt.Println("COMPILE ERROR FUNC:", err)
35  }
36  // Échoue à la compilation car 'user' est manquant de l'option Env
37  program, err = expr.Compile(`user`, expr.Env(map[string]any{})) // ❌ user manquant
38  if err != nil {
39    fmt.Println("COMPILE ERROR OBJECT:", err)
40  }
41}

La sortie est :

true
RUNTIME ERROR: reflect: call of reflect.Value.Type on zero Value (1:1)
 | isAdmin(user)
 | ^
a = <nil>
false
COMPILE ERROR FUNC: unknown name isAdmin (1:1)
 | isAdmin(user)
 | ^
COMPILE ERROR OBJECT: unknown name user (1:1)
 | user
 | ^

3.2. Seuls les Champs de Structs Exportés (Publics) Sont Accessibles

Lorsque vous passez une struct Go à Expr, seuls ses champs exportés (publics) sont accessibles dans l’expression. Les champs non exportés (privés) sont ignorés.

Il s’agit d’une règle standard de visibilité de Go. N’oubliez pas cela lorsque vous concevez vos structs pour les utiliser avec Expr.

Si votre script Expr exige qu’un champ de votre struct Go commence par une minuscule, que ce soit pour des raisons de compatibilité ou de convention, vous pouvez utiliser des tags de struct (expr:"fieldName") pour créer des alias. Par exemple :

1type User struct {
2  Name string `expr:"name"`
3}

Dans ce cas, vous ne pouvez pas accéder au champ Name d’origine, seulement à l’alias name car le champ d’origine est désormais masqué par l’étiquette.

Exemple de champ de Struct non exporté avec étiquette
 1package main
 2
 3import (
 4  "fmt"
 5  "github.com/expr-lang/expr"
 6)
 7
 8type User struct {
 9    Name string `expr:"name"` // Champ exporté, avec alias 'name' dans expr
10    age  int    // Champ non exporté, non accessible
11}
12
13func main() {
14    user := User{Name: "Alice", age: 30}
15    env := map[string]any{"user": user}
16
17    program, _ := expr.Compile(`user.name`, expr.Env(env))
18    output, _ := expr.Run(program, env)
19    fmt.Println(output) // Output: Alice
20
21    _, err := expr.Compile(`user.Name`, expr.Env(env))
22    fmt.Println(err) // Error Output: type main.User has no field Name (1:6)
23}

Sortie :

Alice
type main.User has no field Name (1:6)
 | user.Name
 | .....^

3.3. Vous Ne Pouvez Pas Construire de Structs Go dans le Script

Vous ne pouvez pas construire une struct Go dans le script Expr :

1let u = User{name: "John", age: 42}; // ❌ Invalide

Si vous avez besoin qu’Expr retourne une struct Go, vous devez :

  1. Définir une fonction Go comme newUser(name, age) dans votre environnement.
  2. Retourner la struct à partir de cette fonction.

C’est une limitation clé du langage qui est uniquement basé sur des expressions et sandboxé.

Exemple de Limitation de Construction de Struct
 1package main
 2
 3import (
 4  "fmt"
 5  "github.com/expr-lang/expr"
 6)
 7
 8type User struct {
 9  Name string
10  Age  int
11}
12// Fonction d'aide pour créer la struct dans Go
13func newUser(name string, age int) User {
14  return User{Name: name, Age: age}
15}
16
17func main() {
18  env := map[string]any{
19    "newUser": newUser,
20  }
21
22  // Tenter de construire la struct dans expr échoue à la compilation
23  program, err := expr.Compile(`User{name: "John", age: 42}`, expr.Env(env))
24  if err != nil {
25    fmt.Printf("COMPILE FAILED STRUCT: %v\n", err)
26  }
27
28  // L'appel de la fonction d'aide Go fonctionne
29  program, _ = expr.Compile(`newUser("Alice", 30)`, expr.Env(env))
30  output, _ := expr.Run(program, env)
31  fmt.Println(output)
32}

Sortie :

COMPILE FAILED STRUCT: unexpected token Bracket("{") (1:5)
 | User{name: "John", age: 42}
 | ....^
{Alice 30}

Tip

Une autre solution consiste à renvoyer une map depuis Expr et à la charger dans une structure (struct) en utilisant une bibliothèque de mappage de structures comme mapstructure.

Cette bibliothèque est très pratique pour charger des configurations dans des structures Go.

3.4. Les Méthodes Doivent Retourner une Valeur

Lorsque vous exposez une méthode Go à Expr, l’expression s’attend implicitement à ce qu’elle retourne une valeur qu’elle peut utiliser.

Si la signature de la méthode a un retour vide (i.e., elle ne retourne rien), l’appel depuis Expr entraîne une erreur de compilation (erreur d’exécution dans les anciennes versions, où l’expression tentait de lire une valeur de retour inexistante).

Si vous avez besoin d’une méthode avec seulement un effet secondaire, assurez-vous qu’elle retourne une valeur de substitution comme bool ou nil. Cette valeur peut ensuite être ignorée en toute sécurité dans l’expression.

Exemple d'Exigence de Valeur de Retour de Méthode
 1package main
 2
 3import (
 4  "fmt"
 5  "github.com/expr-lang/expr"
 6)
 7
 8type Executor struct {
 9  Count int
10}
11// La méthode ne retourne rien 
12func (e *Executor) Increment() { 
13  e.Count++
14}
15
16func main() {
17  env := map[string]any{"exec": &Executor{Count: 10}}
18  
19  // L'expression appelle la méthode sans retour, ce qui provoque un échec de compilation
20  expression := `exec.Increment() == nil` 
21
22  _, err := expr.Compile(expression, expr.Env(env))
23  if err != nil {
24    fmt.Printf("COMPILE FAILED: %v\n", err)
25  }
26}

La sortie sera similaire à :

COMPILE FAILED: func Increment doesn't return value (1:6)
 | exec.Increment() == nil
 | .....^

3.5. Aucune Vérification de Signature pour les Fonctions d’Option

Les fonctions expr.Function n’ont pas de validation automatique de signature. Il s’agit des fonctions passées via l’option expr.Function(...), et non de celles dans l’env.

Cela signifie que :

  • Vous n’obtiendrez pas d’erreur de compilation si vous les appelez avec les mauvais arguments.
  • L’erreur se produira à l’exécution, ou elle pourrait échouer silencieusement, selon l’utilisation.

Testez toujours minutieusement vos fonctions d’option personnalisées.

Exemple de l'Absence de Vérification de Signature pour les Fonctions d'option
 1// Exemple d'une fonction sans vérification de signature
 2package main
 3
 4import (
 5  "fmt"
 6  "strconv"
 7  "github.com/expr-lang/expr"
 8)
 9
10type User struct {
11  Role string
12}
13
14func (u *User) MyMethod(arg1 string) bool {
15  return u.Role == arg1
16}
17
18func MyCustomFunc(u User, arg2 string) bool {
19  return u.Role == arg2
20}
21
22func main() {
23  env := map[string]any{
24    "MyCustomFunc": MyCustomFunc,
25    "user":         &User{Role: "admin"},
26  }
27
28  // La vérification au moment de la compilation fonctionne pour les méthodes et les fonctions d'environnement :
29  // Celles-ci échouent à la compilation car un argument est manquant :
30  program, err := expr.Compile(`user.MyMethod()`, expr.Env(env))
31  if err != nil {
32    fmt.Printf("COMPILE FAILED METHOD: %v\n", err)
33  }
34
35  // Ceci échouera à la compilation car arg2 est manquant
36  program, err = expr.Compile(`MyCustomFunc(user)`, expr.Env(env))
37  if err != nil {
38    fmt.Printf("COMPILE FAILED FUNCTION: %v\n", err)
39  }
40
41  // Fonction 'atoi' personnalisée ajoutée via l'option expr.Function()
42  atoi := expr.Function(
43    "atoi",
44    func(params ...any) (any, error) {
45      return strconv.Atoi(params[0].(string)) // Accède à params[0] sans vérifier la longueur
46    },
47  )
48
49	program, err = expr.Compile(`atoi()`, expr.Env(env), atoi)
50	if err != nil {
51		fmt.Printf("COMPILE FAILED FUNCTION OPTION: %v\n", err)
52	}
53
54  output, err := expr.Run(program, env)
55  if err != nil {
56    fmt.Printf("RUN FAILED FUNCTION OPTION: %v\n", err)
57  }
58
59  fmt.Println(output)
60}

La sortie est :

COMPILE FAILED METHOD: not enough arguments to call MyMethod (1:6)
 | user.MyMethod()
 | .....^
COMPILE FAILED FUNCTION: not enough arguments to call MyCustomFunc (1:1)
 | MyCustomFunc(user)
 | ^
RUN FAILED FUNCTION OPTION: runtime error: index out of range [0] with length 0 (1:1)
 | atoi()
 | ^
<nil>

3.6. Ne Passez Pas de Pointeur vers une Map (*map[K]V)

Expr déréférence automatiquement les pointeurs vers les structs (par exemple, *User). Cependant, passer un pointeur vers une map (*map[string]string) conduit à un comportement inattendu et déroutant lorsque vous accédez aux clés.

Le moteur Go Expr déréférence correctement le pointeur pour obtenir l’objet map.

Si vous tentez d’accéder à une clé inexistante, Expr retourne par erreur l’objet map déréférencé entier au lieu de la valeur zéro de la map (par exemple, une chaîne vide "" ou nil).

L’expression aurait dû retourner la valeur zéro pour un élément de map de type chaîne (qui est ""). Au lieu de cela, elle a retourné la map entière !

Ceci est incorrect car le type de retour aurait dû être une chaîne vide (""), la valeur zéro pour le type d’élément.

Passez toujours les maps par valeur (c’est-à-dire map[string]string) à Expr. Cela garantit la récupération correcte de la valeur zéro pour les clés inexistantes.

Exemple du Bug du Pointeur vers une Map
 1package main
 2
 3import (
 4  "fmt"
 5  "github.com/expr-lang/expr"
 6)
 7
 8func main() {
 9  data := map[string]string{"key1": "value1"}
10  
11  // Pointeur vers la map est passé dans l'environnement
12  env := map[string]any{"dataPtr": &data}
13  
14  // L'expression tente d'accéder à une clé INEXISTANTE
15  expression := `dataPtr["nonexistent"]` 
16
17  program, _ := expr.Compile(expression, expr.Env(env))
18  output, err := expr.Run(program, env)
19  
20  if err != nil {
21    fmt.Println("Error:", err)
22    return
23  }
24  
25  fmt.Printf("Expression: %s\n", expression)
26  // La sortie affiche le type de la map entière, et non le type de l'élément (string)
27  fmt.Printf("Result Type: %T\n", output) 
28  fmt.Printf("Result Value: %v\n", output)
29}

La sortie montre le bug :

Expression: dataPtr["nonexistent"]
Result Type: map[string]string
Result Value: map[]

4. Dernières Réflexions et Conseils de Pro

Expr est un moteur d’expressions puissant, mais ce n’est pas un langage de scripting complet. Connaître ses contraintes vous aidera à éviter les bugs, la confusion et le temps perdu.

Voici un bref résumé de ce qu’il faut garder à l’esprit pour le scripting Go avec Expr :

  • Contrainte : Il est basé uniquement sur des expressions—n’attendez pas de packages, modules ou définitions de fonctions.
  • Immuabilité : Aucune mutation de variable n’est prise en charge—cela inclut les mises à jour de maps.
  • Piège de Clé de Map : Les clés de map sont interprétées différemment, selon qu’elles sont définies dans Expr Lang ou dans Go.
  • Accès Struct/Map : L’accès aux structs est strict (erreur de compilation sur champ manquant) ; l’accès aux maps est indulgent (retourne la valeur zéro).
  • Double Passage d’Environnement : Les fonctions et objets doivent être passés à la fois au moment de la compilation et à l’exécution.
  • Visibilité : Seuls les champs de structs exportés (publics) de Go sont accessibles.
  • Limite de Construction : Vous ne pouvez pas créer de structs Go en ligne dans les scripts.
  • Retour de Méthode : Les méthodes doivent retourner une valeur utilisable (pas de méthodes avec seulement un effet secondaire).
  • Risque de Fonction d’option : Les appels expr.Function n’ont pas de validation automatique des arguments.

Si vous venez de langages comme Go, JavaScript ou Python, ces contraintes peuvent être surprenantes au début.

Une fois que vous en êtes conscient, vous pouvez utiliser Expr efficacement pour la logique dynamique, les règles utilisateur, les filtres, et plus encore dans vos applications Go.

Tip

💡 Conseil de pro : coloration syntaxique Pour une meilleure expérience dans votre éditeur, utilisez la coloration syntaxique de Rust (.rs). Les symboles et les éléments de flux logique dans Expr sont largement compatibles avec la syntaxe standard de Rust.

Expr Lang n’est pas seulement pris en charge pour Go. Une implémentation Rust est actuellement disponible. Cela vous permet d’utiliser la même logique implémentée dans différents services écrits en Go et Rust.

🔗 Liens utiles :

Articles Connexes

Qu'est-ce qu'un Programme Informatique et un Langage de Programmation

Qu’est-ce qu’un Programme Informatique et un Langage de Programmation

À quoi peut-on comparer les programmes informatiques? Pour moi, ils sont comme des manuels d’instruction. D’un point de vue fonctionnel, …

Lire la suite
L'Enfer des Dépendances : 5 Stratégies pour Gérer les Risques Open Source (Le Dilemme de la Dépendance)

L’Enfer des Dépendances : 5 Stratégies pour Gérer les Risques Open Source (Le Dilemme de la Dépendance)

La question est : Faut-il importer ou non ? Une bibliothèque économise des semaines de codage, mais introduit des risques d’Enfer des …

Lire la suite