본문으로 건너뛰기

VS Code와 gopls

이 챕터에서 다루는 것

Go의 편집기 지원은 언어 서버 gopls 하나에 몰려 있다. 이 챕터는 gopls가 무엇을 해 주는지, VS Code에서 어떻게 붙이는지, 그리고 어떤 설정을 켜 두면 좋은지를 정리한다. 디버거 Delve도 함께 다룬다.

:::note 확인 범위에 대해 gopls와 Delve의 CLI 동작, 설정 키와 기본값은 이 기계에서 직접 실행해 확인했다 (gopls v0.23.0, Delve 1.27.1). 반면 VS Code의 화면 구성과 단축키는 확인할 수 없어서 공식 문서를 근거로 서술하고 그 사실을 그때그때 밝힌다. 버전에 따라 메뉴 위치는 달라질 수 있다. :::

gopls가 하는 일

gopls(고플리스로 읽는다)는 Go 팀이 관리하는 공식 언어 서버다. LSP(Language Server Protocol)를 말하므로 VS Code뿐 아니라 Neovim, Emacs, Zed, JetBrains 계열 일부에서도 같은 기능을 쓴다.

gopls가 없으면 편집기는 Go 파일을 그냥 텍스트로 본다. gopls가 붙으면 이런 게 생긴다.

  • 자동완성 — 타입을 알고 하는 완성. 인터페이스를 구현할 때 남은 메서드도 채워 준다.
  • 정의로 이동 / 참조 찾기 — 표준 라이브러리와 의존성까지 따라간다.
  • 리네임 — 프로젝트 전체에서 안전하게. 문자열 치환과 달리 같은 이름의 다른 심벌은 건드리지 않는다.
  • 실시간 진단 — 컴파일 에러와 go vet 수준의 문제를 저장 전에 보여 준다.
  • 코드 액션 — "import 정리", "이 부분을 함수로 추출", "이 에러를 처리하는 코드 채우기"
  • 포매팅 — 내부적으로 gofmt를 돌린다(다음 챕터에서 다룬다).
  • 인레이 힌트 — 타입과 매개변수 이름을 회색으로 겹쳐 보여 준다.

gopls는 CLI로도 쓸 수 있어서, 편집기 없이도 무슨 일을 하는지 확인할 수 있다.

gopls version
golang.org/x/tools/gopls v0.23.0
gopls check ./06-vet-demo/main.go
/Users/sgn04088/golang-lecture/examples/01-getting-started/06-vet-demo/main.go:17:9: WaitGroup.Add called from inside new goroutine

편집기에서 빨간 밑줄로 보이는 게 바로 이것이다.

설치

VS Code 확장

확장 마켓플레이스에서 Go(게시자 golang, 확장 ID golang.go)를 설치한다. Go 팀이 직접 관리하는 공식 확장이다.

code --install-extension golang.go

설치 후 .go 파일을 처음 열면 확장이 필요한 도구를 설치하겠느냐고 묻는다. Install All을 누르면 된다. 수동으로 하려면 명령 팔레트(Cmd+Shift+P / Ctrl+Shift+P)에서 Go: Install/Update Tools를 실행한다.

gopls를 직접 설치하기

VS Code를 쓰지 않거나 버전을 직접 관리하고 싶으면 이렇게 한다.

go install golang.org/x/tools/gopls@latest

$GOPATH/bin에 설치되므로 PATH에 들어 있어야 한다(1-3).

:::warning gopls는 프로젝트가 아니라 모듈 단위로 동작한다 VS Code에서 go.mod가 있는 디렉터리를 워크스페이스 루트로 열어야 한다. 상위 디렉터리를 열면 gopls가 모듈을 못 찾아서 자동완성이 통째로 죽는다. "왜 아무것도 안 되지?" 싶을 때 가장 먼저 확인할 것이 이것이다.

여러 모듈을 한 창에서 다뤄야 한다면 go.work를 쓴다(6-7). :::

자주 쓰는 기능

공식 문서 기준의 기본 단축키다. macOS / Windows·Linux 순으로 적었다.

기능단축키비고
정의로 이동F12Cmd/Ctrl + 클릭도 같다
정의 미리보기Option+F12 / Alt+F12창을 옮기지 않고 본다
참조 찾기Shift+F12
리네임F2프로젝트 전체
코드 액션Cmd+. / Ctrl+.노란 전구
심벌 검색Cmd+T / Ctrl+T파일 이름이 아니라 함수·타입 이름으로
문서 툴팁마우스 올리기go doc 내용이 그대로 뜬다

Go에서 특히 유용한 코드 액션 몇 가지를 적어 둔다.

  • Organize Imports — 안 쓰는 import를 지우고 필요한 걸 추가한다. goimports와 같은 일.
  • Extract to function / variable — 선택 영역을 추출한다.
  • Fill struct — 구조체 리터럴에 모든 필드를 제로값으로 채운다.
  • Add missing method — 인터페이스를 만족하지 않는 타입에 빈 메서드를 만들어 준다.
  • Implement error interfaceError() string을 추가한다.

권장 설정

VS Code의 settings.json에 넣는다. Go 관련 설정은 두 층이다. go.*는 확장의 설정, gopls 블록 안은 gopls 자체의 설정이다.

.vscode/settings.json
{
"[go]": {
"editor.defaultFormatter": "golang.go",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"gopls": {
"ui.diagnostic.staticcheck": true,
"ui.completion.usePlaceholders": true,
"ui.semanticTokens": true,
"formatting.gofumpt": true,
"ui.inlayhint.hints": {
"assignVariableTypes": true,
"compositeLiteralFields": true,
"constantValues": true,
"functionTypeParameters": true,
"parameterNames": true,
"rangeVariableTypes": true
}
}
}

각 항목이 무엇인지 보자. 괄호 안은 gopls의 기본값이며, 이 기계의 gopls v0.23.0에서 gopls api-json으로 확인한 값이다.

  • ui.diagnostic.staticcheck (기본 false) — staticcheck 검사를 편집기 진단에 포함한다. go vet보다 훨씬 많이 잡는다. 다음 챕터에서 무엇을 잡는지 본다.
  • ui.completion.usePlaceholders (기본 false) — 함수를 자동완성할 때 매개변수 자리를 채워 준다. Tab으로 옮겨 다니며 채운다.
  • ui.semanticTokens (기본 false) — 문법 강조를 gopls의 타입 정보 기반으로 바꾼다. 타입 이름과 변수 이름이 실제로 구분돼 보인다.
  • formatting.gofumpt (기본 false) — gofmt보다 엄격한 gofumpt로 포매팅한다. 팀 합의가 필요한 항목이다(1-6에서 다룬다).
  • ui.inlayhint.hints (기본 {}) — 타입과 매개변수 이름 힌트. Go는 :=로 타입을 생략하는 일이 많아서 assignVariableTypes가 특히 유용하다.
  • build.directoryFilters (기본 ["-**/node_modules"]) — 스캔에서 제외할 디렉터리. 거대한 저장소에서 gopls가 느리면 여기에 추가한다.

확장 쪽 설정도 몇 개 알아 두자. 아래 기본값은 공식 문서 기준이다.

  • go.useLanguageServer (기본 true) — gopls 사용 여부. 끌 이유가 없다.
  • go.formatTool (기본 "default") — "default"면 gopls가 포매팅한다. gofumpt를 쓰고 싶다면 이 값을 바꾸는 대신 위처럼 formatting.gofumpt를 켜는 쪽이 낫다. 두 군데서 다르게 설정하면 충돌한다.
  • go.lintTool (기본 없음) — gopls 진단과 별도로 돌릴 린터. golangci-lint를 지정할 수 있다.
  • go.toolsManagement.autoUpdate (기본 false) — 도구 자동 업데이트.

:::tip 설정을 어디에 둘 것인가 .vscode/settings.json(프로젝트별)과 사용자 전역 설정 중 하나를 고를 수 있다. 포매팅과 린터 설정은 프로젝트에 두고 커밋하는 편이 낫다. 팀원마다 포매팅이 다르면 diff가 지저분해진다. 인레이 힌트나 테마 같은 취향 문제는 전역에 두자. :::

Delve로 디버깅하기

Go의 디버거는 Delve(dlv)다. GDB로도 Go 프로그램을 볼 수 있지만, 고루틴과 채널을 제대로 이해하는 건 Delve뿐이다.

go install github.com/go-delve/delve/cmd/dlv@latest
dlv version
Delve Debugger
Version: 1.27.1
Build: $Id: 38e5cb2261db4e94a735fecc8bc5ccdc1226f2ef $

VS Code의 Go 확장은 Delve를 DAP(Debug Adapter Protocol)로 직접 몰기 때문에, 확장이 설치돼 있으면 별도 설정 없이 F5로 디버깅이 시작된다. 세밀하게 제어하려면 .vscode/launch.json을 만든다.

.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "현재 패키지 실행",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}"
},
{
"name": "인자를 주고 실행",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/04-first-program",
"args": ["세연"]
},
{
"name": "현재 파일의 테스트",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${fileDirname}"
}
]
}

mode가 핵심이다. 공식 문서 기준으로 launch 요청에서 쓸 수 있는 값은 auto, debug, test, exec, replay, core다. auto는 열려 있는 파일이 테스트 파일이면 test, 아니면 debug를 고른다. attach 요청에서는 local(실행 중인 프로세스에 붙기)과 remote(원격 디버그 서버에 붙기)를 쓴다.

Go 특유의 디버깅 화면 요소도 알아 두자. 브레이크포인트에 멈추면 VS Code의 Call Stack 패널에 실행 중인 고루틴 목록이 뜬다. 고루틴을 클릭하면 그 고루틴의 스택으로 전환된다. 동시성 버그를 잡을 때 이게 결정적이다(Part 7에서 다시 쓴다).

터미널에서 쓰는 Delve

편집기 없이도 된다.

dlv debug ./04-first-program -- 세연

-- 뒤가 프로그램에 전달할 인자다. 대화형 프롬프트에서 쓰는 명령은 다음과 같다.

명령
break main.greet (b)함수에 브레이크포인트
continue (c)다음 브레이크포인트까지 실행
next (n)한 줄 실행 (함수 안으로 안 들어감)
step (s)한 줄 실행 (함수 안으로 들어감)
print name (p)변수 값 출력
args / locals인자 / 지역 변수 전부
goroutines고루틴 목록
goroutine 7그 고루틴으로 전환
quit (q)종료

:::warning macOS에서 Delve가 안 뜬다면 이 기계에서 실제로 dlv debug를 실행하면 이렇게 실패했다.

could not launch process: stub exited while waiting for connection: exit status 0

--backend=native를 주면 원인이 더 분명해진다.

could not launch process: native backend disabled during compilation

darwin/arm64 빌드의 Delve는 네이티브 백엔드 없이 LLDB의 debugserver를 통해 동작하는데, 그걸 xcrun으로 찾는다. 이 기계에는 Command Line Tools만 깔려 있어서 실패한다.

xcrun -f debugserver
xcrun: error: unable to find utility "debugserver", not a developer tool or in PATH

debugserver 바이너리 자체는 존재하지만(/Library/Developer/CommandLineTools/Library/PrivateFrameworks/LLDB.framework/Resources/debugserver) xcrun이 등록된 개발자 도구로 인식하지 못한다. 해결하려면 전체 Xcode를 설치하고 sudo xcode-select -s /Applications/Xcode.app으로 전환한 뒤 xcrun -f debugserver가 경로를 반환하는지 확인하면 된다.

Delve 없이도 이 강의는 전부 따라올 수 있다. 실제로 Go 개발에서 디버거보다 fmt.Println과 테스트를 훨씬 많이 쓴다. 그래도 동시성 코드(Part 7)에 들어가기 전에는 갖춰 두면 좋다. :::

gopls가 이상하게 굴 때

  • 자동완성이 전혀 안 된다 → 워크스페이스 루트에 go.mod가 있는지 확인. 없으면 go.mod가 있는 디렉터리를 다시 열자.
  • 의존성을 못 찾는다 → 터미널에서 go mod tidy를 돌리고, 명령 팔레트에서 Go: Restart Language Server.
  • 엄청 느리다 → 거대 저장소라면 build.directoryFilters에 제외 경로를 추가한다. gopls는 워크스페이스 전체를 타입 체크하므로 규모에 비례해 메모리를 쓴다.
  • 에러가 실제와 다르다 → gopls 버전이 낡았을 수 있다. Go: Install/Update Tools에서 gopls를 갱신하자. gopls는 Go 툴체인과 별개로 릴리스된다.
  • 무슨 일이 벌어지는지 보고 싶다 → 출력 패널의 gopls (server) 채널에 로그가 쌓인다.

다른 편집기

  • Neovim — 내장 LSP 클라이언트 + nvim-lspconfiggopls 설정. 위 gopls 블록의 키를 그대로 settings 테이블에 넣으면 된다.
  • Zed — Go 지원이 내장돼 있고 gopls를 쓴다.
  • GoLand — gopls를 쓰지 않고 자체 분석 엔진을 쓴다. 유료지만 리팩터링과 디버깅 UI가 강하다.
  • Emacseglot(내장) 또는 lsp-mode.

어느 쪽이든 설정 키 이름은 동일하다. gopls 설정은 편집기가 아니라 gopls의 것이기 때문이다.

정리

  • Go의 편집기 지능은 전부 gopls 하나에서 나온다. 편집기가 바뀌어도 설정 키는 같다.
  • VS Code에서는 golang.go 확장을 깔면 끝난다. go.mod가 있는 디렉터리를 열 것.
  • 켜 두면 좋은 것: ui.diagnostic.staticcheck, ui.completion.usePlaceholders, ui.semanticTokens, 인레이 힌트.
  • go.*는 확장 설정, gopls 블록은 gopls 설정. 포매터를 두 군데서 지정하면 충돌한다.
  • 디버거는 Delve. VS Code는 F5로 바로 붙는다. Call Stack 패널의 고루틴 목록이 Go 디버깅의 핵심 기능이다.

연습문제

  1. 04-first-program/main.go를 편집기로 열고 greetF2(리네임)로 다른 이름을 줘 보자. main 안의 호출부도 같이 바뀌는가? 문자열 안에 있는 같은 단어는 어떻게 되는가?

  2. ui.inlayhint.hints를 켜고 main 함수를 보자. name := ""에 어떤 힌트가 붙는가? greet(name) 호출에는 무엇이 보이는가?

  3. gopls checkexamples/01-getting-started 아래 모든 파일에 돌려 보자 (힌트: gopls check ./...가 되는지 확인하고, 안 되면 셸의 find로 파일 목록을 만들자). go vet ./...의 결과와 비교해 보면 무엇이 다른가?