вторник, 7 июня 2016 г.

Emacs как IDE для Go

Цель статьи дать обзор инструментов в Emacs для работы с Go кодом. Настроить горячие клавиши, возможно добавить алиасы и сделать их удобнее для повседневного использования - на вашей совести. Хочу отметить, что поддержка языка в Emacs - на высоком уровне: подсветка кода, автодополнение, сниппеты, рефакторинг, подсветка ошибок, отображение документации, тестирование, компиляция и многое другое. Сразу оговорюсь, что проверял я только под Linux, под альтернативные OS могут быть особенности, которые тут не освещены.


 

Общая настройка Go

Надеюсь у вас уже стоит go, правильно настроены "GOPATH" и т.п., поскольку отдельные плагины чувствительны к подобного рода вещам. Не забудьте добавить "$GOPATH/bin" в PATH, что бы утилиты, который будем ставить, запускались без указания полного пути. Так же я рассчитываю, что с основами Emacs и Go вы знакомы.

 

Вся настройка в одном месте

Сначала коротко о настройке. Устанавливаем набор приложений, которые необходимы плагинам:
go get -u github.com/nsf/gocode
go get -u github.com/rogpeppe/godef
go get -u github.com/jstemmer/gotags
go get -u github.com/kisielk/errcheck
go get -u golang.org/x/tools/cmd/guru
go get -u github.com/golang/lint/golint
go get -u golang.org/x/tools/cmd/gorename
go get -u golang.org/x/tools/cmd/goimports
sudo go get -u golang.org/x/tools/cmd/godoc
Обратите внимание, что "godoc" нуждается в "sudo", т.к. ставятся в системную директорию, остальные будут установлены в локальный "GOPATH". Я бы посоветовал ставить "godoc" последним, что бы он не создавал директорий с правами root.
Из плагинов Emacs понадобятся следующие: go-mode, go-eldoc, company, company-go, yasnippet, go-rename, multi-compile, flycheck, gotest, go-scratch, go-direx, go-guru.
Полная настройка специфичная для go-mode режима выглядит у меня вот так:
(require 'company)
(require 'flycheck)
(require 'yasnippet)
(require 'multi-compile)
(require 'go-eldoc)
(require 'company-go)

(add-hook 'before-save-hook 'gofmt-before-save)
(setq-default gofmt-command "goimports")
(add-hook 'go-mode-hook 'go-eldoc-setup)
(add-hook 'go-mode-hook (lambda ()
                            (set (make-local-variable 'company-backends) '(company-go))
                            (company-mode)))
(add-hook 'go-mode-hook 'yas-minor-mode)
(add-hook 'go-mode-hook 'flycheck-mode)
(setq multi-compile-alist '(
    (go-mode . (
("go-build" "go build -v"
   (locate-dominating-file buffer-file-name ".git"))
("go-build-and-run" "go build -v && echo 'build finish' && eval ./${PWD##*/}"
   (multi-compile-locate-file-dir ".git"))))
    ))

 

Go mode

Теперь по порядку. Go-mode - базовый пакет, вокруг которого строится остальное. Кроме подсветки, он приносит с собой поддержку команд:
  • M-x godef-jump (C-c C-j) - перейти к реализации функции под курсором (вернуться назад, можно через M-*)
  • M-x godef-jump-other-window (C-x 4 C-c C-j) - аналогично "godef-jump" только открывается в новом окне
  • M-x godoc-at-point - покажет документацию по команде под курсором
  • M-x go-goto-imports (C-c C-f i) - перейти к секции "import" текущего файла
  • M-x go-goto-function (C-c C-f f) - перейти к началу функции, внутри которой находится курсор
  • M-x go-goto-arguments (C-c C-f a) - перейти к аргументам текущей функции
  • M-x go-goto-docstring (C-c C-f d) - перейти к комментариям функции
  • M-x go-goto-return-values (C-c C-f r) - перейти к описанию возвращаемого значения для функции
  • M-x beginning-of-defun (C-M-a) - перейти к началу функции
  • M-x end-of-defun (C-M-e) - перейти к концу функции
  • Есть базовая поддержка imenu для функций и типов
Форматирование исходников я делаю через "goimports", который установили выше. Он помимо собственно форматирования, умеет добавлять необходимые импорты в текущем файле и вычищать неиспользуемые. Удобно вызывать его автоматически, при сохранении:
(add-hook 'before-save-hook 'gofmt-before-save)
(setq-default gofmt-command "goimports")
Вручную вызывается через "M-x gofmt".
Вот как это выглядит:

 

Go eldoc

Плагин go-eldoc - умеет показывать в строке состояния информацию о переменной или аргументе\возвращаемом значении функции находящейся под курсором. Фактически документация по сигнатурам. Скриншот я нагло украл у автора:


Для настройки добавьте вот такие строки:
(require 'go-eldoc)
(add-hook 'go-mode-hook 'go-eldoc-setup)

 

Автодополнение

Я предпочитаю для автодополнения company, поэтому ставим ещё company-go и добавляем в конфиг:
(require 'company)
(require 'company-go)
(add-hook 'go-mode-hook (lambda ()
                            (set (make-local-variable 'company-backends) '(company-go))
                            (company-mode)))
Это единственная специфичная настройка для Go, сам "company", кто пользуется им, настраивать умеют.


Если вам нравится auto-complete, то для go понадобиться go-autocomplete, а настройка описана вот тут.

 

Сниппеты

Тут стандарт - yasnippet, который поставляется с поддержкой Go, если у вас "yasnippet" не включён глобально, достаточно добавить хук для go-mode:
(require 'yasnippet)
(yas-reload-all)
(add-hook 'go-mode-hook 'yas-minor-mode)
Доступные сниппеты смотрите в репозитории. Если вас они не удовлетворяют, существуют альтернативные наборы: yasnippet-go и go-snippets, но там, на мой вкус, ничего интересного.


 

Переименование функций и структур

Переименование самый используемый из методов рефакторинга. Он поддерживается плагином go-rename и вызывается при помощи "M-x go-rename". Из недостатков - go-rename не работает, если в проекте есть синтаксические ошибки.

 

Подсветка ошибок

Сильно ускоряет разработку - подсветка ошибок до компиляции. Для этого ставим flycheck и делаем общие настройки, по необходимости. Затем переключаемся в буфер в котором включен режим go-mode и проверяем, что flycheck видит все необходимые утилиты: "M-x flycheck-verify-setup", у меня получилось так:
  go-gofmt
    - predicate:  t
    - executable: Found at /usr/bin/gofmt
  go-golint
    - predicate:  t
    - executable: Found at /home/user/go/bin/golint
  go-vet
    - predicate:  t
    - executable: Found at /usr/bin/go
  go-build
    - predicate:  t
    - executable: Found at /usr/bin/go
  go-test
    - predicate:  nil
    - executable: Found at /usr/bin/go
  go-errcheck
    - predicate:  t
    - executable: Found at /home/user/go/bin/errcheck
Flycheck Mode is enabled.
Если что-то не найдено, то вы поставили не все утилиты из списка в начале статьи или не добавили их в PATH. go-test тут не включён, т.к. он срабатывает только для файлов имя которых оканчивается на "_test.go".
Настройка заключается в том, что бы включить "flycheck-mode" для режима go:
(require 'flycheck)
(add-hook 'go-mode-hook 'flycheck-mode)
Об общих настройках flycheck, я надеюсь рассказывать не нужно.

Поклонники flymake - сделают аналогичное при помощи пакета flymake-go (инструкции по настройке у автора в репозитории), а так же goflymake. При желании посмотрите на golint, go-errcheck и govet. Но на мой взгляд flycheck функциональнее.

 

Компиляция и запуск

Тут сразу две проблемы:
  • Стандартный модуль compile позволяет настроить только одну команду для режима. А я хочу меню с компиляцией, компиляцией и запуском, запуском тестов и т.п.
  • Я перерыл пол интернета и не нашёл способ обнаружения корня проекта на Go. Если у вас сложная иерархия проекта и открыт файл внутри этого проекта - найти место где нужно грубо говоря запустить build это та ещё задачка.
Первая проблема решается при помощи плагина multi-compile, подробнее читайте в этой статье. А вот вторая в каждом конкретном случае решается особо. Для себя я решил считать корнем проекта ту директорию, где лежит ".git", поскольку любой проект начинаю с "git init". А для экспериментов с языком - использую go-scratch.

Настройка для плагина multi-compile выглядит вот так:
(require 'multi-compile)
(setq multi-compile-alist '(
    (go-mode . (
("go-build" "go build -v"
   (locate-dominating-file buffer-file-name ".git"))
("go-build-and-run" "go build -v && echo 'build finish' && eval ./${PWD##*/}"
   (multi-compile-locate-file-dir ".git"))))
    ))
Да, на первый взгляд - сложно, но если разобраться, то не очень.

Тут написано что для файла открытого в режиме "go-mode" добавить два пункта меню "go-build" и "go-build-and-run" . Первый вызывает консольную команду "go build -v". Второй "go build -v && echo 'build finish' && eval ./${PWD##*/}".

Как рабочая директория у обеих команд устанавливается та, внутри которой лежит ".git", т.е. если у нас открыт файл "~/go/go1/go.go" плагин поищет директорию ".git" внутри "~/go/go1/", если не найдёт, ищет уровнем выше - в "~/go/". Как только нужная директория найдена - она считается рабочей из которой и вызваются скрипты.

Теперь о страшных bash командах: "go build -v" вопросов не вызовет - стандартная компиляция проекта. А вот вторая сложнее:
go build -v && echo 'build finish' && eval ./${PWD##*/}
Конструкция "${PWD##*/}" разворачивается в имя последнего элемента текущей директории. Н-р для "~/go/super_project", получим "super_project". Т.е. для этого примера, команда превращается вот в это:
go build -v && echo 'build finish' && eval ./super_project
Теперь читается проще - компилируем, пишем в консоль, что компиляция завершилась и запускаем получившийся бинарник. Go по умолчанию называет бинарник по имени директории, где находится проект, в нашем случае это "super_project".

Меню с командами компиляции вызывается вот так: M-x multi-compile.

Больше примеров настройки на github.

 

Тестирование

Я для тестирования пользуюсь goconvey, который умеет самостоятельно обнаруживать изменения в файлах, и перезапускать тесты. Но часто запуск всего набора тестов - лишнее действие, которое сильно грузит процессор и заставляет менять контекст переключаясь в браузер. Поэтому хочется иметь возможность запустить только один тест или все тесты в данном файле оставаясь в Emacs. В этом нам поможет пакет gotest.

Дополнительной настройки он не требует (ну может только горячие клавиши назначить). Умеет он следующее:
  • M-x go-test-current-test - запустить тест внутри которого находится курсор
  • M-x go-test-current-file - запустить тесты внутри текущего файла
  • M-x go-test-current-project - запустить тесты для текущего проекта
  • M-x go-test-current-benchmark - по аналогии - запустить бенчмарк внутри которого находится курсор
  • M-x go-test-current-file-benchmarks - запустить бенчмарки в файле
  • M-x go-test-current-project-benchmarks - запустить бенчмарки в проекте

 

Локальный Go Playground

Что-бы попробовать как работает та или иная часть Go, обычно пользуются play.golang.org, но у него большой недостаток - он работает в браузере, это не удобно. А локально создавать проект для каждой мелочи лень. Эту нишу покрывает плагин go-scratch. После команды "M-x go-scratch" откроется новый буфер с заготовкой функции main. Компиляция запускается сочетанием клавиш "C-c C-c".

Существует похожий плагин go-playground, но мне он не понравился, т.к. физически создаёт файл на диске, а чистить за ним не хочется. (в комментариях автор плагина объясняет почему так сделано и как с этим жить)

 

Отображение структуры кода

Если вас не устраивает imenu, поддержку которого добавляет go-mode, то воспользуйтесь go-direx, который, нужно отдать должное, покажет структуру текущего кода в гораздо более наглядном виде. Как-то настраивать его не нужно, достаточно вызвать "M-x go-direx-switch-to-buffer".

 

Go guru

Я не могу не упомянуть ещё об одном плагине - go-guru. Он умеет много интересных и полезных вещей, но в рамки статьи они не поместятся. Подробно почитать о них можно в oracle-user-manual, там правда речь идёт о предыдущем названии "go-oracle", сейчас проект переименован в "go-guru", но суть осталась прежней.

Для затравки покажу как найти места откуда вызывается функция. Вызываем:
M-x go-guru-set-scope
И вводим пакет с которым работает (в моём случае это "github.com/ReanGD/go-web-search"). После этого ставим курсор на интересующую функцию и вызываем
M-x go-guru-callers
В отдельном буфере отобразиться список мест откуда функция вызывается. На gif надеюсь понятнее:

Из недостатков - работает медленно, но в любом случае - руками делать подобные вещи дольше. И как я слышал, авторы сейчас серьёзно взялись за проект, надеюсь работать станет удобнее и быстрее.

Комментариев нет:

Отправить комментарий