Embarquer des ressources avec embed

Les outils d’inclusion de ressources comme embed permettent d’embarquer dans l’exécutable aussi bien les assets statiques que des fichiers non publiés, comme les templates.

Voyons comment.

Pourquoi utiliser un “bundler” comme embed ?

Pour préserver la facilité de déploiement des applications Go sous la forme d’un unique fichier exécutable, sans avoir à copier également ses ressources statiques comme les fichiers CSS, JS, ou les templates, de multiples mécanismes d’inclusion des ressources ont été élaborés par la communauté des développeurs Go au fil des ans, dont rakyll/statik, markbates/pkger, et bien d’autres.

Face à la prolifération de ces solutions et leurs degrés variables d’efficacité et de simplicité d’utilisation, le projet Go a fini par inclure ce mécanisme directement au niveau du langage et de sa bibliothèque standard, au moyen du paquet embed.

Dans un article précédent, nous avons vu comment embarquer des templates avec pkger. Le nouveau mécanisme standard avec embed est l’aboutissement du ticket Github logo35950 évoqué dans cet article, toujours pertinent quant aux motivations des mécanismes d’inclusion.

Nous allons donc voir comment convertir à l’utilisation d'embed l’exemple d’inclusion de templates présenté dans l’article précédent.

Comment utiliser embed dans son projet

À la différence de pkger et d’autres paquets d’inclusion de ressources:

  • le nouveau mécanisme ne nécessite plus aucune commande spécifique, mais uniquement un SDK Go 1.16 ou plus récent
  • il ne distingue plus entre un mode développement et un mode exécution
  • l’API n’est plus nécessaire pour charger une ressource simple comme un fichier de template
  • l’API se limite à trois méthodes pour charger une arborescence de ressources.

Les projets compilés n’ont aucune dépendance à l’exécution.

Pour permettre à embed d’inclure les ressources dans le bundle (exécutable, bibliothèque, greffon), les sources du module doivent être rédigés en conséquence. Voyons comment.

Les exemples illustrés dans cet article sont extraites du mini-projet github.com/fgm/pkger_demo V21 qui est prêt à compiler: vous pouvez le récupérer pour suivre plus facilement dans votre IDE:

$ GO111MODULE=off go get github.com/fgm/pkger_demo
$ cd $GOPATH/src/github.com/fgm/pkger_demo/v2

Charger une ressource isolée

Avec embed, charger une ressource isolée se limite à déclarer une variable dans la portée paquet, étiquetée d’une directive //go:embed et de type string ou []byte.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import _ "embed"

//go:embed hello.txt
var hello string

func main() {
	println(hello)
}

L’API de embed: embed.FS

Pour manipuler une arborescence embarquée, et charger sélectivement ses divers contenus, la directive d’inclusion reste la même, par exemple: //go:embed templates, mais la variable déclarée sera cette fois du type embed.FS.

Ce type, introduit en Go 1.16 implémente 3 nouvelles interfaces du nouveau paquet io/fs de la bibliothèque standard, issues de la rationalisation des entrées/sorties fichier dans cette version.

Méthodeembed.FSfs.FSfs.ReadDirFSfs.ReadFileFS
Open(name string) (fs.File, error)XXvia fs.FSvia fs.FS
ReadDir(name string) ([]fs.DirEntry, error)XX
ReadFile(name string) ([]byte, error)XX

Cette API est donc considérablement plus simple que celle des paquets contribués antérieures, permettant l’emploi des fonctions de la bibliothèque standard sur les ressources énumérées dans un //go:embed.

Les inclusions peuvent comporter un astérisque pour accepter les fichiers par motif, et peuvent être multiples dans une même directive, soit sur la même ligne, séparées par des espaces, soit sur des lignes distinctes adjacentes

  • ce qui est plutôt plus lisible et maintenable dans le temps.

Tous les fichiers ainsi décrits dans un même bloc de //go:embed sont alors placés dans la variable embed.FS sous leur chemin d’accès relatif.

Chargement de templates dans une arborescence

Le programme de démo pkger_demo V2 est une application Web basique, renvoyant une page mise en forme par deux templates, reflétant une disposition courante.

templates/page.gohtml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{{ define "page" }}
{{- /*gotype: github.com/fgm/pkger_demo/v2.PageData*/ -}}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{ .Path }} callback</title>
</head>
<body>
  <p>Called on {{ .Path }}</p>
  {{ template "layout/footer" . }}
</body>
</html>
{{ end }}

templates/layout/footer.gohtml

1
2
3
4
5
6
{{define "layout/footer"}}
{{- /*gotype: github.com/fgm/pkger_demo/v2.PageData*/ -}}
  <footer>
    &copy; {{ .Year }} Frederic G. MARAND for OSInet
  </footer>
{{end}}

Examinons le source du fichier main.go point par point. Les lignes 1 à 13 contiennent le préambule du programme (package, import). Le paquet embed y est importé pour pouvoir être référencé plus bas. Rien de particulier sur ce point.

Aux lignes 14-16 se trouve l’exemple de déclaration d’une inclusion de fichier isolé, automatiquement chargé dans une variable string:

14
15
16
// To embed a random file, just go:embed it as a string or []byte
//go:embed hello.txt
var hello string

Tandis qu’aux lignes 18-20 se trouve l’exemple de déclaration d’une inclusion d’arborescence, automatiquement chargée dans une variable embed.FS

18
19
20
// To use an embed hierarchy, use go:embed with an embed.FS.
//go:embed templates
var templates embed.FS

Le programme utilise les templates présents dans le répertoire templates et tous ses sous-répertoires. Il va donc parcourir l’arborescence située sous /templates, et ouvrir les fichiers templates qu’il y trouvera:

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
func compileTemplates(dir string) (*template.Template, error) {
	const fun = "compileTemplates"
	tpl := template.New("")
	// Since filepath.Walk only handles filesystem directories, we use the new
	// and optimized fs.WalkDir introduced in Go 1.16, which takes an fs.FS.
	err := fs.WalkDir(templates, dir, func(path string, info fs.DirEntry, err error) error {
		// Skip non-templates.
		if info.IsDir() || !strings.HasSuffix(path, ".gohtml") {
			return nil
		}
		// Load file from embed virtual file, or use the shortcut
		// templates.ReadFile(path).
		f, _ := templates.Open(path)
		// Now read it.
		sl, _ := io.ReadAll(f)
		// It can now be parsed as a string.
		tpl.Parse(string(sl))
		return nil
	})
	return tpl, err
}
  • ligne 33: le fonctionnement est similaire à celui de filepath.Walk: la fonction WalkDir parcourt l’arborescence et invoque une fonction de rappel walkFn pour chaque fichier et sous-répertoire rencontré. Dans notre exemple, c’est la fonction anonyme des lignes 33-46.
  • lignes 34-37: la fonction ignore les fichiers ne correspondant pas au format des noms de templates attendus.
  • ligne 40, elle ouvre le fichier avec templates.Open, obtenant un fs.File
  • ligne 42, elle en lit le contenu avec la fonction io.ReadAll du runtime. Ces lignes 40/42 pourraient aussi être remplacées par un seul appel à la méthode templates.ReadFile()
  • ligne 44, elle compile la chaîne ainsi obtenu en tant que template Go.

En fin d’exécution, le résultat est un jeu de templates compilés valide, ou une erreur.

Une fois le jeu de templates compilé obtenu à la ligne 57, les handlers de requête HTTP peuvent l’utiliser comme si de rien n’était:

59
60
61
62
63
64
65
66
	// Now serve pages
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		ctx := PageData{Path: r.URL.Path, Year: time.Now().Local().Year()}
		tpl.ExecuteTemplate(w, "page", ctx)
	})
	log.Printf("Listening on %s", addr)
	log.Fatal(http.ListenAndServe(addr, nil))
}

Ressources additionnelles


  1. la V1 du module est celle de l’article précédent, qui utilise markbates/pkger↩︎