Créer des applications Go pour plusieurs cibles
Cet article couvrira le processus et les composants impliqués dans la construction d’un Interface de ligne de commande (CLI) [1] application utilisant le langage de programmation Go. Nous couvrirons les bibliothèques requises, la structure des répertoires, les fichiers de configuration, les tests et le processus de construction multiplateforme.
CLI signifie Interface de ligne de commande [1]. L’application CLI reçoit une entrée de l’utilisateur, effectue certaines tâches de calcul et produit une sortie. Comparé à un Interface utilisateur graphique (GUI) [2]les applications CLI nécessitent moins de ressources système car les interactions avec celles-ci n’impliquent pas de graphiques.
L’une des choses intéressantes (du moins à mon avis) à propos des applications CLI est que lorsqu’elles sont conçues avec la composition à l’esprit et avec une interface In/Out bien définie. Ces applications peuvent être composées ensemble (similaire à la composition de fonctions) en une seule solution.
Par exemple, si nous avons deux applications CLI, A et B, nous pouvons créer une solution composée de AB ayant l’entrée de A et la sortie de B.
Multiplateforme[5] les applications sont conçues pour fonctionner sur plus d’une plate-forme informatique. Par exemple, nous pouvons créer le même logiciel mais l’exécuter sur des appareils Linux, Windows et Android. Ces applications sont également appelées multiplateformes, indépendantes de la plateforme ou indépendantes de la plateforme.
C’est un concept très cool. Parce que qui veut maintenir plus de logiciels que nécessaire ? Mais cela doit également être pris en compte dans la conception du logiciel. Les applications multiplateformes doivent tenir compte de toutes les spécificités du système d’exploitation (OS). Nous en verrons des exemples concernant le système de fichiers local dans les sections ci-dessous.
Nous allons utiliser Cobra [3] CLI cadre. Cobra est un framework très puissant, extensible et agréable avec lequel travailler. Vous ne le regretterez pas, promis !
Version Go utilisée :
$ go version
go version go1.19 darwin/arm64
Initialiser notre projet :
$ go mod init github.com/Pavel-Durov/cli-demo
go: creating new go.mod: module github.com/Pavel-Durov/cli-demo
Installer Cobra
et CobraCLI
. CobraCLI
va créer notre application et ajouter des commandes CLI.
$ go get -u github.com/spf13/cobra/cobra
$ go install github.com/spf13/cobra-cli@latest
Maintenant, nous pouvons utiliser CobraCLI
to initialise our CLI
application :
$ cobra-cli init
C’est ça. Nous avons une application qui fonctionne ! Il doit avoir la structure suivante :
$ tree
├── cmd
│ └── root.go
├── go.mod
├── go.sum
└── main.go
Nous avons le main.go
fichier, qui est le point d’entrée principal de notre application. Et un seul CLI
commande appelée root
c’est le principal point d’entrée du framework Cobra. C’est aussi une convention générale d’avoir des points d’entrée d’application pour les projets Go dans le cmd
annuaire.
Exécutez notre tout nouveau CLI
application :
$ go run ./main.go
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
Rien de comparable à voir pour le moment ; nous obtenons le message par défaut. Ajoutons quelques fonctionnalités.
Pour cette démo, nous allons construire une calculatrice CLI
application. Je sais, très excitant !
Ajout de notre première commande
//file: ./cmd/add.go
package cmdimport (
"github.com/spf13/cobra"
)
var addCmd = &cobra.Command{
Use: "add",
Short: "Add operator",
Long: `Add operator, adds two integers and prints the result.`,
Run: func(cmd *cobra.Command, args []string) {
num1, _ := cmd.Flags().GetInt32("n1")
num2, _ := cmd.Flags().GetInt32("n2")
cmd.Printf("%d + %d = %d\n", num1, num2, num1+num2)
},
}
func init() {
addCmd.Flags().Int32("n1", 0, "--n1 1")
addCmd.Flags().Int32("n2", 0, "--n1 2")
addCmd.MarkFlagRequired("n1")
addCmd.MarkFlagRequired("n2")
}
Nous pourrions utiliser CobraCLI
pour ça aussi. Mais j’ai décidé d’aller manuel ici.
Notez que nous avons défini le Utilisation propriété, ce qui signifie que pour utiliser add
commande, nous devons d’abord spécifier add
dans notre CLI
paramètres.
Fil CLI
commandes ensemble :
//file: ./cmd/root.go
package cmdimport (
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "[command]",
Short: "A CLI calculator",
Long: `A CLI calculator that can add and subtract two numbers.`,
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
rootCmd.AddCommand(addCmd) // adding add command to root
}
Étant donné que nos commandes font partie du même package appelé cmd
— l’importation et la configuration sont très simples.
Réexécutez notre commande, cette fois avec -h
drapeau:
$ go run ./main.go -h
A CLI calculator that can add and subtractwo numbers.Usage:
calc [command]
Available Commands:
add Add operator
completion Generate the autocompletion script for the specified shell
help Help about any command
Flags:
-h, --help help for calc
Use "calc [command] --help" for more information about a command.
Comme vous pouvez le voir, cobra a fait beaucoup de travail pour nous. Il a configuré comment analyser les drapeaux, les messages d’aide, etc.
Exécutez la commande réelle avec les paramètres spécifiés :
$ go run ./main.go add --n1=1 --n2=3
1 + 3 = 4
Nous avons un CLI
application qui peut additionner deux nombres et imprimer le résultat sur le stdout
.
Ajoutons une autre commande ! Cette fois, nous ajouterons la substitution.
Ce sera aussi simple que :
//file: ./cmd/sub.go
package cmdimport (
"github.com/spf13/cobra"
)
var subCmd = &cobra.Command{
Use: "sub",
Short: "Sub operator",
Long: `Sub operator, subtracts two integers and prints the result.`,
Run: func(cmd *cobra.Command, args []string) {
num1, _ := cmd.Flags().GetInt32("n1")
num2, _ := cmd.Flags().GetInt32("n2")
cmd.Printf("%d - %d = %d\n", num1, num2, num1-num2)
},
}
func init() {
subCmd.Flags().Int32("n1", 0, "--n1 1")
subCmd.Flags().Int32("n2", 0, "--n1 2")
subCmd.MarkFlagRequired("n1")
subCmd.MarkFlagRequired("n2")
}
Comme précédemment, ajoutez la nouvelle commande au racine:
...
rootCmd.AddCommand(subCmd)
...
Essayez:
$ go run ./main.go sub - n1=10 - n2=4
10–4 = 6
Cela fonctionne exactement comme prévu ! Vous pouvez imaginer comment, en utilisant le même processus, nous pouvons étendre davantage notre application CLI.
J’aime les tests, et je pense que vous devriez aussi. L’ajout de tests unitaires dans Go est très simple ; cependant, tester des commandes CLI comme Cobra peut être un peu délicat. C’est pourquoi j’ai voulu montrer comment faire.
Ajout du test à la commande CLI racine :
// file: cmd/root_test.go
func TestTypeLocal(t *testing.T) {
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetArgs([]string{"sub", "--n1=10", "--n2=4"})err := rootCmd.Execute()
if err != nil {
fmt.Println(err)
}
if buf.String() != "10 - 4 = 6\n" {
t.Errorf("Expected 10 - 4 = 6, got %s", buf.String())
}
}
Ici, nous définissons un tampon comme flux de sortie pour la commande cobra et passons CLI arguments (alias drapeaux), puis nous affirmons le résultat de sortie – rien d’extraordinaire.
Exécutez les tests :
$ go test ./…
ok github.com/Pavel-Durov/cli-demo/cmd 0.207s
Que faire si nous voulons stocker la configuration de l’application à travers les sessions, ou peut-être voulons-nous avoir des secrets tels que les clés API définies en dehors de notre code d’application ? Quelle que soit la raison, le cobra est là pour vous ! Réellement, Vipère [4] vous avez le dos. Viper est un outil de gestion de configuration pour les applications Go. Viper et Cobra fonctionnent très bien ensemble.
Installer Vipère:
$ go get github.com/spf13/viper
Configurez Viper dans notre fonction init, root cmd :
// file: cmd/root.go
func initConfig() {
home, err := os.UserHomeDir()
cobra.CheckErr(err)
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".calc")
viper.ReadInConfig()
}func init() {
cobra.OnInitialize(initConfig)
...
}
Si nous créons une section locale YAML
fichier appelé .calc
dans notre $HOME
répertoire (car c’est ce que nous avons configuré) avec le contenu :
$ cat ~/.calc
username: kimchi
Nous pouvons maintenant lire ces valeurs dans notre application :
username := viper.Get("username")
if username != nil {
fmt.Println("Hello", username)
}
Nous n’avons pas à utiliser YAML
ou la $HOME
annuaire; cette configuration peut être configurée de plusieurs façons.
Remarque sur le répertoire $HOME
Remarquez comment nous avons utilisé os.UserHomeDir()
pour obtenir le répertoire personnel de l’utilisateur. Ceci est important si nous voulons construire un Multiplateforme[6] application. Nous aurions pu coder en dur le chemin d’accès au fichier. Mais pourquoi devrions-nous? Go a un excellent support de bibliothèque indépendant de la plate-forme – os.UserHomeDir()
retournera le chemin vers le $HOME
répertoire spécifique à la machine sur laquelle il s’exécute sans que nous modifiions une seule ligne de code !
- Sous Unix (y compris macOS), il renvoie le
$HOME
variable d’environnement - Sous Windows, il renvoie
%USERPROFILE%
- Sur le plan 9, il renvoie le
$HOME
variable d’environnement
Go a un système de construction incroyable qui contient tout ce dont nous avons besoin.
Nous pouvons facilement construire notre application pour plusieurs architectures et systèmes d’exploitation (OS) :
Version cible Linux
$ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o out/linux-arm64-calc -ldflags="-extldflags=-static" # linux, arm64 arch
$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out/linux-amd64-calc -ldflags="-extldflags=-static" # linux, amd64 arch
Version cible Mac (alias darwin)
$ CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o out/darwin-arm64-calc -ldflags="-extldflags=-static" # mac, arm64 arch
$ CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o out/darwin-amd64-calc -ldflags="-extldflags=-static" # mac, amd64 arch
Version cible Windows
$ CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -o out/windows-arm64-calc -ldflags="-extldflags=-static" # windows, arm64 arch
$ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o out/windows-amd64-calc -ldflags="-extldflags=-static" # windows, amd64 arch
si nous exécutons toutes ces commandes de construction, nous obtiendrons ces binaires :
$ ls -l ./out/
-rwxr-xr-x 1 ... darwin-amd64-calc
-rwxr-xr-x 1 ... darwin-arm64-calc
-rwxr-xr-x 1 ... linux-amd64-calc
-rwxr-xr-x 1 ... linux-arm64-calc
-rwxr-xr-x 1 ... windows-amd64-calc
-rwxr-xr-x 1 ... windows-arm64-calc
Variables d’environnement
GOOS
Vous avez probablement remarqué que la seule chose qui a changé entre les cibles de construction est le GOOS
variables d’environnement. Et c’est tout ce dont vous avez besoin pour changer avec l’outillage go-build ! C’est vraiment aussi facile à utiliser !
GOARCH
C’est ici que nous spécifions le CPU l’architecture que nous ciblons.
Voir tous les supports GOOS
et GOARCH
combinés :
$ go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
....The list goes on
CGO_ENABLED
Nous avons également utilisé CGO_ENABLED
variables d’environnement. CGO_ENABLED=1
conduit à des constructions plus rapides et plus petites – il permet de charger dynamiquement les bibliothèques natives du système d’exploitation hôte. Cependant, il repose sur un OS hôte, une dépendance que nous aimerions éviter ! Sinon, le comportement de notre code peut différer d’une machine à l’autre si notre code repose sur les bibliothèques hôtes.
Drapeaux de l’éditeur de liens – ldflags
Nous avons utilisé des drapeaux appelés ldflags
— ld
représente lieur [6] Donc ldflags
représente les drapeaux de l’éditeur de liens. Un éditeur de liens est un programme qui « lie » les morceaux du code source compilé dans le résultat binaire. Nous passons extldflags
à notre lien. Selon la documentation de l’outil de liaison, ces drapeaux sont transmis à l’éditeur de liens externe. Pour faire court, nous utilisons ces drapeaux pour indiquer à l’outil de construction Go d’inclure toutes les dépendances dans le binaire et de ne pas compter sur celles fournies par l’environnement dans lequel il s’exécute. Nous définissons le drapeau comme -static
, indiquant que le binaire doit inclure toutes ses dépendances. S’il n’est pas spécifié, notre binaire serait lié dynamiquement. Nous voudrions l’éviter ici pour les mêmes raisons qu’avec CGO_ENABLED
.
Nous avons vu comment configurer à partir de zéro des applications CLI basées sur Cobra. Nous avons abordé les propriétés des applications multiplateformes, telles que les chemins de système de fichiers indépendants de la plate-forme. Nous avons ajouté des tests et brièvement présenté Linker et les outils de construction Go avec différentes configurations cibles.
Construire des applications Go pour plusieurs cibles est simple et amusant une fois que vous avez acquis les bases.
Cet article était pour mon propre bien de comprendre et d’organiser mes pensées car il s’agissait de partage de connaissances. J’espère que cela vous a été utile
Le code source complet peut être trouvé ici.
[1] https://en.wikipedia.org/wiki/Command-line_interface
[2] https://en.wikipedia.org/wiki/Graphical_user_interface
[4] https://github.com/spf13/viper