自从我开始使用 Golang 开发以来,我并没有真正使用过调试器。相反,我天真地在各处添加 fmt.Print 语句来验证我的代码?虽然打印语句和日志可能也是您的第一个调试本能,但在处理大型且复杂的代码库、复杂的运行时行为以及(当然!)似乎无法重现的复杂并发问题时,它们通常会出现不足。
开始处理更复杂的项目(比如这个:https://github.com/cloudoperators/heureka)后,我不得不强迫自己更深入地了解 delve(Golang 调试器)并了解 Emacs 提供的功能与它互动。虽然 Go 生态系统提供了出色的调试工具,但将它们集成到舒适的开发工作流程中可能具有挑战性。
在这篇文章中,我将详细阐述 Emacs、Delve 和 dape 的强大组合。这些工具共同创造了模仿(并且常常超越)传统 IDE 的调试体验,同时保留了 Emacs 闻名的灵活性和可扩展性。
这是你所期望的:
在这篇文章中,我假设您已经有一些 Emacs 经验,现在了解如何配置包和编写小的 Elisp 片段。我个人使用 Straight.el 作为包管理器,minimal-emacs.d 作为最小的普通 Emacs 配置(以及我自己的自定义),dape 作为调试适配器客户端,eglot 作为我的 LSP 客户端。
对于 Emacs 29 用户,eglot 是内置的。查看为 gopls 配置 eglot 和一些更高级的 gopls 设置。我们首先添加 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))
和运行模式:
(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"))
安装 Delve 和 gopls(LSP 服务器):
# Install Delve go install github.com/go-delve/delve/cmd/dlv@latest # Install gopls go install golang.org/x/tools/gopls@latest
此外,我还有一些我不时使用的其他工具:
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
然后需要配置相应的Emacs包:
(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)
我使用 dape 而不是 dap 没有什么特别的原因。当我还在使用 MinEmacs 时,它就是其中的一部分,我只是习惯了它。正如文档所述:
- Dape 不支持 launch.json 文件,如果需要每个项目配置,请使用 dir-locals 和 dape-command。
- Dape 允许用户使用选项修改或添加 PLIST 条目到现有配置,从而增强了迷你缓冲区内的人体工程学。
- 没有魔法,没有像 ${workspaceFolder} 这样的特殊变量。相反,函数和变量会在开始新会话之前解析。
- 尝试设想如果 vscode 不存在,如何在 Emacs 中实现调试适配器配置。
如果您曾经使用过 VSCode,您已经知道它使用 launch.json 来存储不同的调试配置文件:
(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))
您有不同的字段/属性,根据此页面,您可以在调试配置中进行调整:
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) |
现在让我们通过调试实现 REST API 的真实应用程序来将我们的知识付诸实践。
我们的示例是用于任务管理的 REST API,其结构如下:
(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))
让我们看看核心组件。
任务代表我们的核心领域模型:
(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"))
TaskStore 处理我们的内存数据操作:
# Install Delve go install github.com/go-delve/delve/cmd/dlv@latest # Install gopls go install golang.org/x/tools/gopls@latest
API 公开以下端点:
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
这是服务器实现:
(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)
让我们看看我们的主要功能:
{ "name": "Launch file", "type": "go", "request": "launch", "mode": "auto", "program": "${file}" }
让我们启动服务器:
taskapi/ ├── go.mod ├── go.sum ├── main.go ├── task_store.go └── task_test.go
现在从不同的终端创建一个新任务:
import ( "fmt" ) type Task struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Done bool `json:"done"` }
回应:
type TaskStore struct { tasks map[int]Task nextID int } func NewTaskStore() *TaskStore { return &TaskStore{ tasks: make(map[int]Task), nextID: 1, } }
让我们看看是否可以获取它:
// 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 }
回应:
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) }
下面是 TaskStore 的一些单元测试(用 Ginkgo 编写):
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
在 Emacs 中,我将调用 ginkgo-run-this-container,如以下屏幕截图所示:
为了调试我们的任务 API,我们有以下方法:
以下是不同请求类型的选项:
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 |
这是我们的第一个 .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))
?您可能希望对 command-cwd 使用不同的值。就我而言,我想在当前不是项目的目录中启动调试器。 default-directory 是一个变量,它保存当前所在缓冲区的工作目录。
开始调试:
使用此配置文件启动调试器后,您应该在 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"))
请注意,我们没有指定任何要调试的二进制文件/文件(我们在 .dir-locals.el 中有 :program ".")。 delve 将在启动应用程序之前自动构建二进制文件:
# Install Delve go install github.com/go-delve/delve/cmd/dlv@latest # Install gopls go install golang.org/x/tools/gopls@latest
让我们添加一个配置文件以连接到现有的调试会话:
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
现在让我们在 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)
现在在 Emacs 中,您可以启动 dape 并选择 go-attach-taskapi 配置文件:
在这种情况下,应用程序已经正在运行,但您想要附加调试器到它。首先启动应用程序:
{ "name": "Launch file", "type": "go", "request": "launch", "mode": "auto", "program": "${file}" }
找出其进程ID (PID):
taskapi/ ├── go.mod ├── go.sum ├── main.go ├── task_store.go └── task_test.go
让我们添加另一个调试配置文件:
import ( "fmt" ) type Task struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Done bool `json:"done"` }
我们需要一个辅助函数:
type TaskStore struct { tasks map[int]Task nextID int } func NewTaskStore() *TaskStore { return &TaskStore{ tasks: make(map[int]Task), nextID: 1, } }
现在我启动调试器:
如果我现在发送像这样的 POST 请求:
// 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 }
调试器应在设置的断点处自动停止:
能够在 Golang 中调试测试至关重要。为了运行 ginkgo 测试,我使用 ginkgo-mode,它具有以下几个功能:
作为输出,我得到:
(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))
这是调试 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"))
如果我选择 go-test-ginkgo 调试配置文件,我应该能够调试测试:
现在配置非常静态,因此您无法预先选择单元测试/容器。我们需要以某种方式使参数 -ginkgo.focus 动态化:
# Install Delve go install github.com/go-delve/delve/cmd/dlv@latest # Install gopls go install golang.org/x/tools/gopls@latest
之后,如果我查看 dape-configs 变量,我应该会看到这个值:
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
在 dape-repl 缓冲区中启动调试器(使用以调试为中心的测试配置文件)后,我得到:
(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)
?请注意,仅运行了“5 个规格中的 1 个”(❶),这意味着 ginkgo 只关注我们指定的容器 (❷)。
通过我的调试经验,我开始欣赏一些最佳实践:
以上是掌握 Emacs 中的 Golang 调试的详细内容。更多信息请关注PHP中文网其他相关文章!