Maison >développement back-end >Golang >Maîtriser le débogage Golang dans Emacs
Depuis que j'ai commencé à développer en Golang, je n'ai pas vraiment utilisé le débogueur. Au lieu de cela, j'ajoutais naïvement des instructions fmt.Print partout pour valider mon code ?. Bien que les instructions d'impression et les journaux puissent également être votre premier instinct de débogage, ils échouent souvent lorsqu'il s'agit d'une base de code volumineuse et complexe, avec un comportement d'exécution sophistiqué et (bien sûr !) des problèmes de concurrence complexes qui semblent impossibles à reproduire.
Après avoir commencé à travailler sur des projets plus complexes (comme celui-ci : https://github.com/cloudoperators/heureka) j'ai dû me forcer à approfondir delve (le débogueur Golang) et voir ce que propose Emacs interagir avec lui. Bien que l'écosystème Go offre d'excellents outils de débogage, les intégrer dans un flux de développement confortable peut s'avérer difficile.
Dans cet article, je vais élaborer la puissante combinaison d'Emacs, Delve et dape. Ensemble, ces outils créent une expérience de débogage qui imite (et dépasse souvent) les IDE traditionnels, tout en préservant la flexibilité et l'extensibilité qui font la renommée d'Emacs.
Voici ce à quoi vous pouvez vous attendre :
Dans cet article, je suppose que vous avez déjà une certaine expérience avec Emacs et que vous savez maintenant comment configurer des packages et écrire de petits extraits Elisp. J'utilise personnellement Straight.el comme gestionnaire de paquets, minimal-emacs.d comme configuration minimale d'Emacs Vanilla (avec mes propres personnalisations), dape comme client adaptateur de débogage et eglot comme mon client LSP.
Pour les utilisateurs d'Emacs 29, eglot est intégré. Découvrez la configuration d'eglot pour les gopls et quelques paramètres gopls plus avancés. Nous allons d'abord ajouter dape :
(use-package dape :straight t :config ;; Pulse source line (performance hit) (add-hook 'dape-display-source-hook 'pulse-momentary-highlight-one-line) ;; To not display info and/or buffers on startup ;; (remove-hook 'dape-start-hook 'dape-info) (remove-hook 'dape-start-hook 'dape-repl))
Et go-mode :
(use-package go-mode :straight t :mode "\.go\'" :hook ((before-save . gofmt-before-save)) :bind (:map go-mode-map ("M-?" . godoc-at-point) ("M-." . xref-find-definitions) ("M-_" . xref-find-references) ;; ("M-*" . pop-tag-mark) ;; Jump back after godef-jump ("C-c m r" . go-run)) :custom (gofmt-command "goimports"))
Installez Delve et gopls, le serveur LSP :
# Install Delve go install github.com/go-delve/delve/cmd/dlv@latest # Install gopls go install golang.org/x/tools/gopls@latest
De plus, j'ai un tas d'autres outils que j'utilise de temps en temps :
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest go install github.com/onsi/ginkgo/v2/ginkgo@latest go install -v golang.org/x/tools/cmd/godoc@latest go install -v golang.org/x/tools/cmd/goimports@latest go install -v github.com/stamblerre/gocode@latest go install -v golang.org/x/tools/cmd/gorename@latest go install -v golang.org/x/tools/cmd/guru@latest go install -v github.com/cweill/gotests/...@latest go install -v github.com/davidrjenni/reftools/cmd/fillstruct@latest go install -v github.com/fatih/gomodifytags@latest go install -v github.com/godoctor/godoctor@latest go install -v github.com/haya14busa/gopkgs/cmd/gopkgs@latest go install -v github.com/josharian/impl@latest go install -v github.com/rogpeppe/godef@latest
Ensuite, vous devez configurer les packages Emacs correspondants :
(use-package ginkgo :straight (:type git :host github :repo "garslo/ginkgo-mode") :init (setq ginkgo-use-pwd-as-test-dir t ginkgo-use-default-keys t)) (use-package gotest :straight t :after go-mode :bind (:map go-mode-map ("C-c t f" . go-test-current-file) ("C-c t t" . go-test-current-test) ("C-c t j" . go-test-current-project) ("C-c t b" . go-test-current-benchmark) ("C-c t c" . go-test-current-coverage) ("C-c t x" . go-run))) (use-package go-guru :straight t :hook (go-mode . go-guru-hl-identifier-mode)) (use-package go-projectile :straight t :after (projectile go-mode)) (use-package flycheck-golangci-lint :straight t :hook (go-mode . flycheck-golangci-lint-setup)) (use-package go-eldoc :straight t :hook (go-mode . go-eldoc-setup)) (use-package go-tag :straight t :bind (:map go-mode-map ("C-c t a" . go-tag-add) ("C-c t r" . go-tag-remove)) :init (setq go-tag-args (list "-transform" "camelcase"))) (use-package go-fill-struct :straight t) (use-package go-impl :straight t) (use-package go-playground :straight t)
Il n'y a aucune raison particulière pour laquelle j'utilise du dape au lieu de dap. Quand j'utilisais encore MinEmacs, cela en faisait partie et je m'y suis juste habitué. Comme l'indique la documentation :
- Dape ne prend pas en charge les fichiers launch.json, si une configuration par projet est nécessaire, utilisez dir-locals et dape-command.
- Dape améliore l'ergonomie au sein du mini-tampon en permettant aux utilisateurs de modifier ou d'ajouter des entrées PLIST à une configuration existante à l'aide d'options.
- Pas de magie, pas de variables spéciales comme ${workspaceFolder}. Au lieu de cela, les fonctions et les variables sont résolues avant de démarrer une nouvelle session.
- Essaye d'imaginer comment les configurations de l'adaptateur de débogage seraient implémentées dans Emacs si vscode n'existait jamais.
Si vous avez déjà travaillé avec VSCode, vous savez déjà qu'il utilise un launch.json pour stocker différents profils de débogage :
(use-package dape :straight t :config ;; Pulse source line (performance hit) (add-hook 'dape-display-source-hook 'pulse-momentary-highlight-one-line) ;; To not display info and/or buffers on startup ;; (remove-hook 'dape-start-hook 'dape-info) (remove-hook 'dape-start-hook 'dape-repl))
Vous disposez de différents champs/propriétés que, selon cette page, vous pouvez modifier dans votre configuration de débogage :
Property | Description |
---|---|
name | Name for your configuration that appears in the drop down in the Debug viewlet |
type | Always set to "go". This is used by VS Code to figure out which extension should be used for debugging your code |
request | Either of launch or attach. Use attach when you want to attach to an already running process |
mode | For launch requests, either of auto, debug, remote, test, exec. For attach requests, use either local or remote |
program | Absolute path to the package or file to debug when in debug & test mode, or to the pre-built binary file to debug in exec mode |
env | Environment variables to use when debugging. Example: { "ENVNAME": "ENVVALUE" } |
envFile | Absolute path to a file containing environment variable definitions |
args | Array of command line arguments that will be passed to the program being debugged |
showLog | Boolean indicating if logs from delve should be printed in the debug console |
logOutput | Comma separated list of delve components for debug output |
buildFlags | Build flags to be passed to the Go compiler |
remotePath | Absolute path to the file being debugged on the remote machine |
processId | ID of the process that needs debugging (for attach request with local mode) |
Mettons maintenant nos connaissances en pratique en déboguant une application réelle implémentant une API REST.
Notre exemple est une API REST pour la gestion des tâches avec la structure suivante :
(use-package dape :straight t :config ;; Pulse source line (performance hit) (add-hook 'dape-display-source-hook 'pulse-momentary-highlight-one-line) ;; To not display info and/or buffers on startup ;; (remove-hook 'dape-start-hook 'dape-info) (remove-hook 'dape-start-hook 'dape-repl))
Jetons un coup d'œil aux composants de base.
La tâche représente notre modèle de domaine principal :
(use-package go-mode :straight t :mode "\.go\'" :hook ((before-save . gofmt-before-save)) :bind (:map go-mode-map ("M-?" . godoc-at-point) ("M-." . xref-find-definitions) ("M-_" . xref-find-references) ;; ("M-*" . pop-tag-mark) ;; Jump back after godef-jump ("C-c m r" . go-run)) :custom (gofmt-command "goimports"))
Le TaskStore gère nos opérations de données en mémoire :
# Install Delve go install github.com/go-delve/delve/cmd/dlv@latest # Install gopls go install golang.org/x/tools/gopls@latest
L'API expose les points de terminaison suivants :
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest go install github.com/onsi/ginkgo/v2/ginkgo@latest go install -v golang.org/x/tools/cmd/godoc@latest go install -v golang.org/x/tools/cmd/goimports@latest go install -v github.com/stamblerre/gocode@latest go install -v golang.org/x/tools/cmd/gorename@latest go install -v golang.org/x/tools/cmd/guru@latest go install -v github.com/cweill/gotests/...@latest go install -v github.com/davidrjenni/reftools/cmd/fillstruct@latest go install -v github.com/fatih/gomodifytags@latest go install -v github.com/godoctor/godoctor@latest go install -v github.com/haya14busa/gopkgs/cmd/gopkgs@latest go install -v github.com/josharian/impl@latest go install -v github.com/rogpeppe/godef@latest
Voici l'implémentation du serveur :
(use-package ginkgo :straight (:type git :host github :repo "garslo/ginkgo-mode") :init (setq ginkgo-use-pwd-as-test-dir t ginkgo-use-default-keys t)) (use-package gotest :straight t :after go-mode :bind (:map go-mode-map ("C-c t f" . go-test-current-file) ("C-c t t" . go-test-current-test) ("C-c t j" . go-test-current-project) ("C-c t b" . go-test-current-benchmark) ("C-c t c" . go-test-current-coverage) ("C-c t x" . go-run))) (use-package go-guru :straight t :hook (go-mode . go-guru-hl-identifier-mode)) (use-package go-projectile :straight t :after (projectile go-mode)) (use-package flycheck-golangci-lint :straight t :hook (go-mode . flycheck-golangci-lint-setup)) (use-package go-eldoc :straight t :hook (go-mode . go-eldoc-setup)) (use-package go-tag :straight t :bind (:map go-mode-map ("C-c t a" . go-tag-add) ("C-c t r" . go-tag-remove)) :init (setq go-tag-args (list "-transform" "camelcase"))) (use-package go-fill-struct :straight t) (use-package go-impl :straight t) (use-package go-playground :straight t)
Regardons notre fonction principale :
{ "name": "Launch file", "type": "go", "request": "launch", "mode": "auto", "program": "${file}" }
Démarrons le serveur :
taskapi/ ├── go.mod ├── go.sum ├── main.go ├── task_store.go └── task_test.go
Maintenant depuis un autre terminal créez une nouvelle tâche :
import ( "fmt" ) type Task struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Done bool `json:"done"` }
Réponse :
type TaskStore struct { tasks map[int]Task nextID int } func NewTaskStore() *TaskStore { return &TaskStore{ tasks: make(map[int]Task), nextID: 1, } }
Voyons si nous pouvons le récupérer :
// CreateTask stores a given Task internally func (ts *TaskStore) CreateTask(task Task) Task { task.ID = ts.nextID ts.tasks[task.ID] = task ts.nextID++ return task } // GetTask retrieves a Task by ID func (ts *TaskStore) GetTask(id int) (Task, error) { task, exists := ts.tasks[id] if !exists { return Task{}, fmt.Errorf("task with id %d not found", id) } return task, nil } // UpdateTask updates task ID with a new Task object func (ts *TaskStore) UpdateTask(id int, task Task) error { if _, exists := ts.tasks[id]; !exists { return fmt.Errorf("task with id %d not found", id) } task.ID = id ts.tasks[id] = task return nil }
Réponse :
package main import ( "encoding/json" "fmt" "net/http" ) // Server implements a web application for managing tasks type Server struct { store *TaskStore } func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var task Task if err := json.NewDecoder(r.Body).Decode(&task); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } createdTask := s.store.CreateTask(task) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(createdTask) } func (s *Server) handleGetTask(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } id := 0 fmt.Sscanf(r.URL.Query().Get("id"), "%d", &id) task, err := s.store.GetTask(id) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(task) }
Vous trouverez ci-dessous quelques tests unitaires (écrits en Ginkgo) pour le TaskStore :
package main import ( "log" "net/http" ) func main() { store := NewTaskStore() server := &Server{store: store} http.HandleFunc("/task/create", server.handleCreateTask) http.HandleFunc("/task/get", server.handleGetTask) log.Printf("Starting server on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) }
go build -o taskapi *.go ./taskapi 2024/11/14 07:03:48 Starting server on :8080
Dans Emacs, j'appellerais alors ginkgo-run-this-container comme indiqué dans cette capture d'écran :
Afin de déboguer notre API de tâches, nous avons les approches suivantes :
Voici les options pour différents types de demandes :
request | mode | required | optional |
---|---|---|---|
launch | debug | program | dlvCwd, env, backend, args, cwd, buildFlags, output, noDebug |
test | program | dlvCwd, env, backend, args, cwd, buildFlags, output, noDebug | |
exec | program | dlvCwd, env, backend, args, cwd, noDebug | |
core | program, corefilePath | dlvCwd, env | |
replay | traceDirPath | dlvCwd, env | |
attach | local | processId | backend |
remote |
Voici notre premier profil de débogage pour .dir-locals.el :
(use-package dape :straight t :config ;; Pulse source line (performance hit) (add-hook 'dape-display-source-hook 'pulse-momentary-highlight-one-line) ;; To not display info and/or buffers on startup ;; (remove-hook 'dape-start-hook 'dape-info) (remove-hook 'dape-start-hook 'dape-repl))
? Vous souhaiterez peut-être utiliser une valeur différente pour command-cwd. Dans mon cas, je voulais démarrer le débogueur dans un répertoire qui n'est actuellement pas un projet. default-directory est une variable qui contient le répertoire de travail du tampon actuel dans lequel vous vous trouvez actuellement.
Démarrer le débogage :
Après avoir démarré le débogueur avec ce profil, vous devriez voir dans le tampon dape-repl :
(use-package go-mode :straight t :mode "\.go\'" :hook ((before-save . gofmt-before-save)) :bind (:map go-mode-map ("M-?" . godoc-at-point) ("M-." . xref-find-definitions) ("M-_" . xref-find-references) ;; ("M-*" . pop-tag-mark) ;; Jump back after godef-jump ("C-c m r" . go-run)) :custom (gofmt-command "goimports"))
Notez que nous n'avons spécifié aucun binaire/fichier à déboguer (nous avions :program "." dans .dir-locals.el). delve construira automatiquement le binaire avant de lancer l'application :
# Install Delve go install github.com/go-delve/delve/cmd/dlv@latest # Install gopls go install golang.org/x/tools/gopls@latest
Ajoutons un profil pour se connecter à une session de débogage existante :
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest go install github.com/onsi/ginkgo/v2/ginkgo@latest go install -v golang.org/x/tools/cmd/godoc@latest go install -v golang.org/x/tools/cmd/goimports@latest go install -v github.com/stamblerre/gocode@latest go install -v golang.org/x/tools/cmd/gorename@latest go install -v golang.org/x/tools/cmd/guru@latest go install -v github.com/cweill/gotests/...@latest go install -v github.com/davidrjenni/reftools/cmd/fillstruct@latest go install -v github.com/fatih/gomodifytags@latest go install -v github.com/godoctor/godoctor@latest go install -v github.com/haya14busa/gopkgs/cmd/gopkgs@latest go install -v github.com/josharian/impl@latest go install -v github.com/rogpeppe/godef@latest
Démarrons maintenant le débogueur sur la CLI :
(use-package ginkgo :straight (:type git :host github :repo "garslo/ginkgo-mode") :init (setq ginkgo-use-pwd-as-test-dir t ginkgo-use-default-keys t)) (use-package gotest :straight t :after go-mode :bind (:map go-mode-map ("C-c t f" . go-test-current-file) ("C-c t t" . go-test-current-test) ("C-c t j" . go-test-current-project) ("C-c t b" . go-test-current-benchmark) ("C-c t c" . go-test-current-coverage) ("C-c t x" . go-run))) (use-package go-guru :straight t :hook (go-mode . go-guru-hl-identifier-mode)) (use-package go-projectile :straight t :after (projectile go-mode)) (use-package flycheck-golangci-lint :straight t :hook (go-mode . flycheck-golangci-lint-setup)) (use-package go-eldoc :straight t :hook (go-mode . go-eldoc-setup)) (use-package go-tag :straight t :bind (:map go-mode-map ("C-c t a" . go-tag-add) ("C-c t r" . go-tag-remove)) :init (setq go-tag-args (list "-transform" "camelcase"))) (use-package go-fill-struct :straight t) (use-package go-impl :straight t) (use-package go-playground :straight t)
Maintenant dans Emacs, vous pouvez lancer dape et sélectionner le profil go-attach-taskapi :
Dans ce scénario, l'application est déjà en cours d'exécution mais vous souhaitez y attacher le débogueur. Lancez d’abord l’application :
{ "name": "Launch file", "type": "go", "request": "launch", "mode": "auto", "program": "${file}" }
Découvrez son ID de processus (PID) :
taskapi/ ├── go.mod ├── go.sum ├── main.go ├── task_store.go └── task_test.go
Ajoutons un autre profil de débogage :
import ( "fmt" ) type Task struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Done bool `json:"done"` }
Nous aurons besoin d'une fonction d'assistance :
type TaskStore struct { tasks map[int]Task nextID int } func NewTaskStore() *TaskStore { return &TaskStore{ tasks: make(map[int]Task), nextID: 1, } }
Maintenant, je démarre le débogueur :
Si j'envoie maintenant une requête POST comme celle-ci :
// CreateTask stores a given Task internally func (ts *TaskStore) CreateTask(task Task) Task { task.ID = ts.nextID ts.tasks[task.ID] = task ts.nextID++ return task } // GetTask retrieves a Task by ID func (ts *TaskStore) GetTask(id int) (Task, error) { task, exists := ts.tasks[id] if !exists { return Task{}, fmt.Errorf("task with id %d not found", id) } return task, nil } // UpdateTask updates task ID with a new Task object func (ts *TaskStore) UpdateTask(id int, task Task) error { if _, exists := ts.tasks[id]; !exists { return fmt.Errorf("task with id %d not found", id) } task.ID = id ts.tasks[id] = task return nil }
Le débogueur devrait s'arrêter automatiquement au point d'arrêt défini :
Être capable de déboguer les tests dans Golang est crucial. Pour exécuter des tests de ginkgo, j'utilise le mode ginkgo qui possède plusieurs fonctionnalités :
Et en sortie j'obtiens :
(use-package dape :straight t :config ;; Pulse source line (performance hit) (add-hook 'dape-display-source-hook 'pulse-momentary-highlight-one-line) ;; To not display info and/or buffers on startup ;; (remove-hook 'dape-start-hook 'dape-info) (remove-hook 'dape-start-hook 'dape-repl))
Voici la configuration de base pour le débogage des tests Ginkgo :
(use-package go-mode :straight t :mode "\.go\'" :hook ((before-save . gofmt-before-save)) :bind (:map go-mode-map ("M-?" . godoc-at-point) ("M-." . xref-find-definitions) ("M-_" . xref-find-references) ;; ("M-*" . pop-tag-mark) ;; Jump back after godef-jump ("C-c m r" . go-run)) :custom (gofmt-command "goimports"))
Si j'ai choisi le profil de débogage go-test-ginkgo, je devrais pouvoir déboguer les tests :
Maintenant, la configuration est assez statique et vous ne pouvez donc pas présélectionner le test unitaire/conteneur. Nous devons en quelque sorte rendre le paramètre -ginkgo.focus dynamique :
# Install Delve go install github.com/go-delve/delve/cmd/dlv@latest # Install gopls go install golang.org/x/tools/gopls@latest
Ensuite, si je regarde la variable dape-configs, je devrais voir cette valeur :
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest go install github.com/onsi/ginkgo/v2/ginkgo@latest go install -v golang.org/x/tools/cmd/godoc@latest go install -v golang.org/x/tools/cmd/goimports@latest go install -v github.com/stamblerre/gocode@latest go install -v golang.org/x/tools/cmd/gorename@latest go install -v golang.org/x/tools/cmd/guru@latest go install -v github.com/cweill/gotests/...@latest go install -v github.com/davidrjenni/reftools/cmd/fillstruct@latest go install -v github.com/fatih/gomodifytags@latest go install -v github.com/godoctor/godoctor@latest go install -v github.com/haya14busa/gopkgs/cmd/gopkgs@latest go install -v github.com/josharian/impl@latest go install -v github.com/rogpeppe/godef@latest
Après avoir démarré le débogueur (avec le profil debug-focused-test) dans le tampon dape-repl, j'obtiens :
(use-package ginkgo :straight (:type git :host github :repo "garslo/ginkgo-mode") :init (setq ginkgo-use-pwd-as-test-dir t ginkgo-use-default-keys t)) (use-package gotest :straight t :after go-mode :bind (:map go-mode-map ("C-c t f" . go-test-current-file) ("C-c t t" . go-test-current-test) ("C-c t j" . go-test-current-project) ("C-c t b" . go-test-current-benchmark) ("C-c t c" . go-test-current-coverage) ("C-c t x" . go-run))) (use-package go-guru :straight t :hook (go-mode . go-guru-hl-identifier-mode)) (use-package go-projectile :straight t :after (projectile go-mode)) (use-package flycheck-golangci-lint :straight t :hook (go-mode . flycheck-golangci-lint-setup)) (use-package go-eldoc :straight t :hook (go-mode . go-eldoc-setup)) (use-package go-tag :straight t :bind (:map go-mode-map ("C-c t a" . go-tag-add) ("C-c t r" . go-tag-remove)) :init (setq go-tag-args (list "-transform" "camelcase"))) (use-package go-fill-struct :straight t) (use-package go-impl :straight t) (use-package go-playground :straight t)
?Remarquez que seules "1 des 5 spécifications" (❶) ont été exécutées, ce qui signifie que le ginkgo s'est uniquement concentré sur le conteneur que nous avons spécifié (❷).
Tout au long de mon expérience de débogage, j'ai appris à apprécier plusieurs bonnes pratiques :
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!