Seit ich angefangen habe, in Golang zu entwickeln, habe ich den Debugger nicht mehr wirklich verwendet. Stattdessen habe ich naiverweise überall fmt.Print-Anweisungen hinzugefügt, um meinen Code zu validieren? Während Druckanweisungen und Protokolle möglicherweise auch Ihr erster Debugging-Instinkt sind, versagen sie oft, wenn Sie mit einer großen und komplexen Codebasis arbeiten, mit ausgefeiltem Laufzeitverhalten und (natürlich!) komplexen Parallelitätsproblemen, die scheinbar unmöglich zu reproduzieren sind.
Nachdem ich begonnen hatte, an komplexeren Projekten zu arbeiten (wie diesem: https://github.com/cloudoperators/heureka), musste ich mich dazu zwingen, einen tieferen Blick auf delve (den Golang-Debugger) zu werfen und zu sehen, was Emacs zu bieten hat damit interagieren. Obwohl das Go-Ökosystem hervorragende Debugging-Tools bietet, kann es eine Herausforderung sein, diese in einen komfortablen Entwicklungsworkflow zu integrieren.
In diesem Beitrag werde ich die leistungsstarke Kombination von Emacs, Delve und Dape näher erläutern. Zusammen schaffen diese Tools ein Debugging-Erlebnis, das herkömmliche IDEs nachahmt (und oft übertrifft), während gleichzeitig die Flexibilität und Erweiterbarkeit erhalten bleibt, für die Emacs berühmt ist.
Das können Sie erwarten:
In diesem Beitrag gehe ich davon aus, dass Sie bereits Erfahrung mit Emacs haben und nun wissen, wie man Pakete konfiguriert und kleine Elisp-Snippets schreibt. Ich persönlich verwende Straight.el als Paketmanager, minimal-emacs.d als minimale Vanilla-Emacs-Konfiguration (zusammen mit meinen eigenen Anpassungen), dape als Debug-Adapter-Client und eglot als meinen LSP-Client.
Für Emacs 29-Benutzer ist eglot integriert. Schauen Sie sich die Konfiguration von eglot für gopls und einige erweiterte Gopls-Einstellungen an. Wir fügen zuerst dape:
hinzu
(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))
Und Go-Modus:
(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"))
Installieren Sie Delve und gopls, den LSP-Server:
# Install Delve go install github.com/go-delve/delve/cmd/dlv@latest # Install gopls go install golang.org/x/tools/gopls@latest
Außerdem habe ich eine Reihe anderer Tools, die ich von Zeit zu Zeit verwende:
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
Dann müssen Sie die entsprechenden Emacs-Pakete konfigurieren:
(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)
Es gibt keinen besonderen Grund, warum ich Dape anstelle von Dap verwende. Als ich noch MinEmacs benutzte, gehörte es dazu und ich habe mich einfach daran gewöhnt. In der Dokumentation heißt es:
- Dape unterstützt keine launch.json-Dateien. Wenn eine Projektkonfiguration erforderlich ist, verwenden Sie dir-locals und dape-command.
- Dape verbessert die Ergonomie innerhalb des Minipuffers, indem es Benutzern ermöglicht, PLIST-Einträge mithilfe von Optionen zu ändern oder zu einer vorhandenen Konfiguration hinzuzufügen.
- Keine Magie, keine speziellen Variablen wie ${workspaceFolder}. Stattdessen werden Funktionen und Variablen aufgelöst, bevor eine neue Sitzung gestartet wird.
- Versucht sich vorzustellen, wie Debug-Adapterkonfigurationen in Emacs implementiert würden, wenn vscode nie existieren würde.
Wenn Sie jemals mit VSCode gearbeitet haben, wissen Sie bereits, dass es eine launch.json verwendet, um verschiedene Debugging-Profile zu speichern:
(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))
Sie haben verschiedene Felder/Eigenschaften, die Sie gemäß dieser Seite in Ihrer Debugging-Konfiguration anpassen können:
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) |
Lassen Sie uns nun unser Wissen in die Praxis umsetzen, indem wir eine echte Anwendung debuggen, die eine REST-API implementiert.
Bei unserem Beispiel handelt es sich um eine REST API zur Aufgabenverwaltung mit folgendem Aufbau:
(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))
Werfen wir einen Blick auf die Kernkomponenten.
Die Aufgabe stellt unser Kerndomänenmodell dar:
(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"))
Der TaskStore verwaltet unsere In-Memory-Datenoperationen:
# Install Delve go install github.com/go-delve/delve/cmd/dlv@latest # Install gopls go install golang.org/x/tools/gopls@latest
Die API stellt folgende Endpunkte bereit:
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
Hier ist die Serverimplementierung:
(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)
Schauen wir uns unsere Hauptfunktion an:
{ "name": "Launch file", "type": "go", "request": "launch", "mode": "auto", "program": "${file}" }
Lass uns den Server starten:
taskapi/ ├── go.mod ├── go.sum ├── main.go ├── task_store.go └── task_test.go
Jetzt von einem anderen Terminal aus eine neue Aufgabe erstellen
:
import ( "fmt" ) type Task struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Done bool `json:"done"` }
Antwort:
type TaskStore struct { tasks map[int]Task nextID int } func NewTaskStore() *TaskStore { return &TaskStore{ tasks: make(map[int]Task), nextID: 1, } }
Mal sehen, ob wir es holen können:
// 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 }
Antwort:
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) }
Im Folgenden finden Sie einige Unit-Tests (geschrieben in Ginkgo) für den 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
In Emacs würde ich dann ginkgo-run-this-container aufrufen, wie in diesem Screenshot gezeigt:
Um unsere Task-API zu debuggen, haben wir folgende Ansätze:
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 |
Hier ist unser erstes Debugging-Profil für .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))
? Möglicherweise möchten Sie einen anderen Wert für command-cwd verwenden. In meinem Fall wollte ich den Debugger in einem Verzeichnis starten, das derzeit kein Projekt ist. Standardverzeichnis ist eine Variable, die das Arbeitsverzeichnis für den aktuellen Puffer enthält, in dem Sie sich gerade befinden.
Debuggen starten:
Nachdem Sie den Debugger mit diesem Profil gestartet haben, sollten Sie im dape-repl-Puffer Folgendes sehen:
(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"))
Beachten Sie, dass wir keine Binärdatei/Datei zum Debuggen angegeben haben (wir hatten :program „.“ in .dir-locals.el). delve erstellt die Binärdatei automatisch, bevor die Anwendung gestartet wird:
# Install Delve go install github.com/go-delve/delve/cmd/dlv@latest # Install gopls go install golang.org/x/tools/gopls@latest
Fügen wir ein Profil hinzu, um eine Verbindung zu einer vorhandenen Debugging-Sitzung herzustellen:
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
Jetzt starten wir den Debugger auf der 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)
Jetzt können Sie in Emacs dape starten und das Profil go-attach-taskapi auswählen:
In diesem Szenario wird die Anwendung bereits ausgeführt, aber Sie möchten den Debugger daran anhängen. Starten Sie zuerst die Anwendung:
{ "name": "Launch file", "type": "go", "request": "launch", "mode": "auto", "program": "${file}" }
Finden Sie die Prozess-ID (PID) heraus:
taskapi/ ├── go.mod ├── go.sum ├── main.go ├── task_store.go └── task_test.go
Fügen wir ein weiteres Debug-Profil hinzu:
import ( "fmt" ) type Task struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Done bool `json:"done"` }
Wir benötigen eine Hilfsfunktion:
type TaskStore struct { tasks map[int]Task nextID int } func NewTaskStore() *TaskStore { return &TaskStore{ tasks: make(map[int]Task), nextID: 1, } }
Jetzt starte ich den Debugger:
Wenn ich jetzt eine POST-Anfrage wie diese sende:
// 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 }
Der Debugger sollte automatisch am festgelegten Haltepunkt anhalten:
Es ist von entscheidender Bedeutung, Tests in Golang debuggen zu können. Zum Durchführen von Ginkgo-Tests verwende ich den Ginkgo-Modus, der mehrere Funktionen bietet:
Und als Ausgabe erhalte ich:
(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))
Dies ist die Grundkonfiguration zum Debuggen von Ginkgo-Tests:
(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"))
Wenn ich das Debug-Profil go-test-ginkgo wähle, sollte ich in der Lage sein, die Tests zu debuggen:
Jetzt ist die Konfiguration recht statisch und daher kann man den Unit-Test/Container nicht vorab auswählen. Wir müssen den Parameter -ginkgo.focus irgendwie dynamisch machen:
# Install Delve go install github.com/go-delve/delve/cmd/dlv@latest # Install gopls go install golang.org/x/tools/gopls@latest
Wenn ich mir danach die Variable dape-configs anschaue, sollte ich diesen Wert sehen:
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
Nachdem ich den Debugger (mit dem debug-focused-test-Profil) im Dape-Repl-Puffer gestartet habe, erhalte ich Folgendes:
(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)
?Beachten Sie, dass nur „1 von 5 Spezifikationen“ (❶) ausgeführt wurde, was bedeutet, dass sich Ginkgo nur auf den von uns angegebenen Container konzentriert hat (❷).
Während meiner Debugging-Erfahrung habe ich mehrere Best Practices schätzen gelernt:
Das obige ist der detaillierte Inhalt vonGolang-Debugging in Emacs beherrschen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!