저는 Golang으로 개발을 시작한 이후로 디버거를 실제로 사용하지 않았습니다. 대신 나는 내 코드를 검증하기 위해 순진하게 fmt.Print 문을 모든 곳에 추가했습니다. 인쇄 문과 로그는 첫 번째 디버깅 본능일 수도 있지만 정교한 런타임 동작과 (물론!) 재현이 불가능해 보이는 복잡한 동시성 문제로 인해 크고 복잡한 코드 기반을 처리할 때 종종 부족합니다.
보다 복잡한 프로젝트 작업을 시작한 후(예: https://github.com/cloudoperators/heureka) delve(Golang 디버거)에 대해 더 자세히 살펴보고 Emacs가 무엇을 제공하는지 확인해야 했습니다. 그것과 상호 작용합니다. Go 생태계는 뛰어난 디버깅 도구를 제공하지만 이를 편안한 개발 워크플로에 통합하는 것은 어려울 수 있습니다.
이번 게시물에서는 Emacs, Delve 및 dape의 강력한 조합에 대해 자세히 설명하겠습니다. 이러한 도구를 함께 사용하면 Emacs의 유명한 유연성과 확장성을 유지하면서 기존 IDE를 모방하는(종종 능가하는) 디버깅 환경을 만들 수 있습니다.
다음과 같은 결과를 기대할 수 있습니다.
이 게시물에서는 여러분이 이미 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"))
LSP 서버인 Delve와 gopls를 설치하세요.
# 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)
dap 대신 dape를 사용하는 특별한 이유는 없습니다. 내가 여전히 MinEmacs를 사용하고 있을 때 그것은 그것의 일부였고 나는 그것에 익숙해졌습니다. 문서에 따르면:
- Dape는 launch.json 파일을 지원하지 않습니다. 프로젝트별 구성이 필요한 경우 dir-locals 및 dape-command를 사용하세요.
- Dape는 사용자가 옵션을 사용하여 기존 구성에 PLIST 항목을 수정하거나 추가할 수 있도록 하여 미니버퍼 내의 인체공학성을 향상시킵니다.
- 마법도 없고 ${workspaceFolder}와 같은 특수 변수도 없습니다. 대신 새 세션을 시작하기 전에 함수와 변수가 해결됩니다.
- vscode가 존재하지 않는 경우 Emacs에서 디버그 어댑터 구성이 어떻게 구현될지 상상해 봅니다.
VSCode를 사용해 본 적이 있다면 이 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를 호출합니다.
Task 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에서 테스트를 디버그할 수 있는 능력은 매우 중요합니다. 은행나무 테스트를 실행하기 위해 다음과 같은 여러 기능이 있는 은행나무 모드를 사용합니다.
그리고 결과는 다음과 같습니다.
(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 중국어 웹사이트의 기타 관련 기사를 참조하세요!