Как начинать проект на Go в 2023 году
- среда, 26 июля 2023 г. в 00:00:16
Когда-то я написал статью о том, как начинать проект на Go в 2018 году. С тех пор многое изменилось, и я захотел написать обновлённую версию статьи. В ней я расскажу всё, что нужно новичку, чтобы приступить к работе с Go.
Первым делом нужно скачать и установить Go. Рекомендую всегда устанавливать его с самого вебсайта Go https://golang.org/ и выполнить инструкции для соответствующей операционной системы. За исключением релиза Go 1.18 (в который включены дженерики), у меня никогда не было проблем с установкой последней версии Go и компиляцией. Обещания обратной совместимости выполняются. Настолько, что если в файле go.mod проекта написано 1.20, но он не использует функциональность 1.20, то, вероятно, вы всё равно сможете скомпилировать его в более раннем релизе.
В старых руководствах рекомендовалось настроить $GOPATH. В 2023 году об этом можно спокойно забыть. Если вам любопытно, то прочитайте мой предыдущий пост. Однако всё переместилось или перемещается в модули, поэтому воспринимайте это как то, чему вам не нужно учиться.
Единственное, что я рекомендую — обновить путь своих машин, чтобы он указывал на папку bin стандартного $GOPATH, export PATH=$PATH:$(go env GOPATH)/bin, чтобы можно было установить всё, с чем вы работаете, и всё это было доступно отовсюду. Например, на моей машине это выглядит так:
# boyter @ Bens-MacBook-Air in ~/go/bin [10:04:57]
$ tree
.
├── boltbrowser
├── cs
├── dcd
├── goreleaser
├── gow
├── hashit
├── lc
├── scc
В моём dotfile для получения показанного выше указано export PATH=/Users/boyter/go/bin:$PATH.
Благодаря экспортированному пути я могу выполнять scc, dcd and lc из любого места, сначала запустив go install для проекта, над которым работаю. В Windows я ещё и делаю при помощи WSL $GOPATH общим между Windows и Linux, чтобы можно было работать над одним кодом в обеих ОС, поэтому вы увидите файлы .exe. Для этого достаточно выполнить симлинк внутри WSL на папку в Windows.
Судя по последнему опросу разработчиков на Go, большинство людей кодит на Go в Visual Studio Code или Jetbrains Goland, поэтому я буду рассматривать только эти редакторы.
Goland работает практически без настройки и именно в нём я пишу код каждый день. Если у вас установлен Go, то он должен его найти и начать работать. Самая большая проблема возникает, когда выходят новые релизы Go. Это сбивает Goland с толку и может что-нибудь поломать, например, отладчик. Чтобы избежать этой проблемы, обновляйте релиз примерно спустя неделю, чтобы команда Jetbrains успела внести обновления в редактор.
Иногда в macOS эти обновления приводят к бесконечному циклу команды clang, требующей инструментов разработчика командной строки. Об устранении этой проблемы я писал; всё сводится к запуску xcodebuild -runFirstLaunch.
Visual Studio Code проделал большой путь и стал гораздо лучше, чем когда я начинал работать с Go. Установите последнюю версию, а затем расширение Go in Visual Studio Code. Вы получите intellisense, реформатирование и нужную вам функциональность автоматического импорта/удаления импорта. Предположительно, отладка сейчас тоже работает, но я не могу сказать об этом с уверенностью.
Я по-прежнему предпочитаю платить и пользоваться Goland, потому что это похоже на совместную работу с прекрасным инженером, который никогда не спит и почти никогда не ошибается. Его способность генерировать табличные тесты и выполнять отдельные тесты экономит кучу времени, а его инструменты рефакторинга великолепны. Однако ради этого поста я попробовал несколько часов поработать в Visual Studio Code и был очень впечатлён, поэтому без проблем могу его рекомендовать.
Чтобы начать проект, достаточно создать новую папку и выполнить go mod init NAMEHERE, где NAMEHERE — имя пакета, который вы хотите для своего проекта. Раньше оно должно было совпадать с расположением вашего репозитория, например, github.com/boyter/scc, но теперь можно использовать любое имя. Однако всё равно будет неплохой идеей использовать полный URL репозитория, и я по-прежнему предпочитаю его для большинства проектов.
Для получения пакета достаточно практически знать его путь и использовать go get URL, чтобы скачать его в локальную систему. Обычно я выполняют vendor зависимостей, чтобы с моим проектом хранилась копия, позволяя создавать воспроизводимые сборки. Кроме того, это позволяет мне с лёгкостью патчить баги в зависимостях, пока ожидаются апстрим-исправления. Для этого достаточно выполнить go mod vendor, эта команда подтянет всё в папку vendor. Если вы будете так делать, то рекомендую настроить файл .ignore с vendor в нём.
Получение пакетов может запутывать только тогда, когда мейнтейнер пакета перешёл с семантической версии 1 на версию 2 или выше. В таком случае вам нужно будет добавить в конце нужную вам версию и подтянуть её.
Например, мой проект scc имеет версию 3.1.0. Если бы я хотел импортировать его без указания версии:
$ go get github.com/boyter/scc/
go: added github.com/boyter/scc v2.12.0+incompatible
То получил бы пакет версии 2.12, что может сбить с толку новичков в Go. Нужно добавить последнюю версию:
$ go get github.com/boyter/scc/v3
go: added github.com/boyter/scc/v3 v3.1.0
Тогда, как вы видите, подтянется нужная версия, как и ожидалось.
В руководстве Just tell me how to use Go Modules и его обсуждении на Hacker News это достаточно неплохо объяснено.
Время от времени при попытке запуска go после работы с пакетами он сообщает, что необходимо выполнить go mod tidy. Не особо беспокойтесь об этом, просто выполните go mod tidy и всё, что хотели сделать, прежде чем сможете двигаться дальше. Подробнее о происходящем можно прочитать в Go Modules Reference.
Кэшированные артефакты из системы сборки Go могут храниться в локальной системе и занимать приличный объём (на момент написания статьи мой кэш занимал примерно 1 ГБ). Чтобы очистить его, выполните go clean -cache.
Освоить Go можно достаточно просто при помощи обучающих туториалов на go.dev. Там вы научитесь писать код и узнаете необходимый синтаксис. Однако для того, чтобы узнать, как структурировать собственное HTTP-приложение, которые и разрабатывает большинство людей, я крайне рекомендую книгу https://lets-go.alexedwards.net/. Она не бесплатна, зато позволяет ускорить процесс обучения.
У меня есть пример того, что лично я использую для подготовки новых HTTP-проектов. Пример можно найти на github: https://github.com/boyter/go-http-template.
Крайне рекомендую прочитать пост 50 Shades of Go. В нём раскрыто множество тонкостей Go, с которыми вы скорее всего столкнётесь. Эти тонкости проверяют при найме многие компании, поэтому знание этих вопросов — хороший показатель опыта работы с Go.
При поиске любой информации о Go в поисковом движке используйте слово golang, а не go. Например, чтобы найти, как открыть файл, надо ввести golang open file.
Стоит учесть, что в повседневном разговоре не стоит называть язык golang, потому что это будет раздражать многих педантов. Все будут понимать, о чём вы говорите, но не удивляйтесь, что кто-то рано или поздно сделает замечание.
Для пакетов, имеющих package main:
go build - собирает команду и оставляет результат в текущей рабочей папке.
go install - собирает команду во временной папке, а потом перемещает её в $GOPATH/bin.
Для пакетов:
go build - собирает пакет и удаляет результаты.
go install - выполняет сборку, а затем устанавливает пакет в папку $GOPATH/pkg.
Если вы хотите выполнять кросс-компиляцию, то есть делать сборки для Linux в Windows или наоборот, то можно настроить целевую архитектуру и ОС через переменные окружения. Значения по умолчанию можно просмотреть командой go env, но чтобы изменить их, нужно будет сделать нечто подобное:
GOOS=darwin GOARCH=amd64 go build
GOOS=darwin GOARCH=arm64 go build
GOOS=windows GOARCH=amd64 go build
GOOS=windows GOARCH=arm64 go build
GOOS=linux GOARCH=amd64 go build
GOOS=linux GOARCH=arm64 go build
Двоичные файлы Go по умолчанию «толстые» и больше, чем можно было ожидать. Есть простой способ уменьшить размер:
go build -ldflags="-s -w"
Он вырезает отладочную информацию. Для уменьшения двоичных файлов, если вам не важно время запуска, можно использовать https://upx.github.io/, однако я обнаружил у него проблемы при кросс-компиляции. См. мой пост, который я написал, пользуясь обеими методиками.
Хотя для создания собственных пакетов можно использовать вышеупомянутые GOOS и GOARCH, я крайне рекомендую применять goreleaser. Он существенно упрощает развёртывание, а его руководство обеспечивает правильность тегов.
Хотя для этого можно использовать sonar и множество других инструментов, я предпочитаю то, что можно запускать локально и легко интегрировать в свою систему CI/CD.
Для линтинга и статического анализа: https://github.com/golangci/golangci-lint.
Для проверок безопасности я пользуюсь gitleaks: https://github.com/gitleaks/gitleaks и выполняю его со следующими проверками.
gitleaks detect -v -c gitleaks.toml
gitleaks protect -v -c gitleaks.tom
Имейте в виду, что нужно включить файл toml gitleaks. В качестве основы я использую этот; в нём я включил игнорирование папки vendor, потому что такие вещи, как AWS SDK, сводят gitleaks с ума.
Профилирование в Go имеет первоклассную поддержку. В случае HTTP-сервисов профайлер должен выполняться в течение периода времени, а в случае краткосрочно работающих приложений — при выходе из программы.
В функцию main краткосрочно работающих приложений добавьте следующее:
f, _ := os.Create("profile.pprof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
Благодаря этому профилирование будет запускаться при выполнении команды, а результаты после завершения программы сохранятся в profile.pprof.
Для HTTP подойдёт нечто подобное:
f, _ := os.Create("profile.pprof")
_ = pprof.StartCPUProfile(f)
go func() {
time.Sleep(30 * time.Second)
pprof.StopCPUProfile()
}()
Код начинает собирать информацию профилирования CPU в течение 30 секунд, а потом сохраняет её на диск. Этот код можно поместить или внутрь функции main, или за route, или даже создать фоновую задачу для сбора информации профилирования в течение долгого времени.
Профилирование памяти делает снэпшот кучи. Я обычно использую их в основном для того, чтобы понять, что происходит в длительно работающих HTTP-сервисах.
f, _ := os.Create("memprofile.pprof")
_ = pprof.WriteHeapProfile(f)
Поместив этот код за простой route, мы будем выполнять дамп кучи на диск, чтобы потом анализировать.
В обоих случаях анализ профиля одинаков:
go tool pprof -http=localhost:8090 profile.pprof
Приведённая выше команда откроет http-сервер на порту 8090, который вы сможете исследовать. Обычно так я изучаю результаты профилирования, потому что HTTP-интерфейс кажется понятным и мне очень нравятся flame-графики. Подробности можно прочитать в статье о pprof на go.dev .
Для выполнения всех юнит-тестов своего кода (благодаря кэшированию больше нет причин не запускать их все вместе) необходимо запустить следующее:
go test ./...
Для запуска бенчмарков выполните показанную ниже команду в папке, где находится бенчмарк. Допустим, внутри вашего проекта есть ./processor/ с файлом бенчмарка. Зайдите в эту папку и выполните
go test --bench .
Для запуска встроенных fuzz-тестов, выполните
go test -fuzz .
Чтобы создать файл теста, достаточно лишь создать файл с суффиксом _test в имени. Например, для создания теста файла с именем file.go нужно будет вызвать файл file_test.go.
Если вы захотите выполнить отдельный тест, то это можно сделать так:
go test ./... -run NameOfTest
Эта команда попытается запустить все тесты во всех пакетах, имеющие имя NameOfTest. Помните, что аргумент NameOfTest поддерживает регулярные выражения, поэтому при правильном именовании тестов можно выполнять их по группам. Для запуска всех тестов можно использовать аргумент ., который соответствует всему.
Если вы хотите запускать тесты, игнорируя кэш, то можно сделать следующее:
GOCACHE=off go test ./...
Стандартная практика с тестами Go заключается в том, чтобы помещать их рядом с тестируемым файлом. Однако на самом деле это не требуется. Если вы можете импортировать код (который становится доступным благодаря префиксу в верхнем регистре), то можете и помещать тесты в любое место. Разумеется, это значит, что вы не сможете тестировать приватный код, который некоторые всё равно считают антипаттерном.
Для выполнения fuzz-тестирования рекомендую прочитать это руководство bitfield consulting, в котором подробно рассматривается использование встроенного fuzz-детектора. Учтите, что если вы ищете информацию о том, как выполнять fuzz-тестирование в Go, то, вероятно, найдёте статьи об инструменте https://github.com/dvyukov/go-fuzz, который был лучшим выбором раньше, так что ищите статьи, написанные после середины 2022 года.
В общем случае для мокинга в Go достаточно определить интерфейс над тем, мок чего вы хотите создать. Однако некоторым не нравится делать это вручную и они предпочитают инструменты наподобие testify и mockery.
Если вы раньше работали с Java, то не пытайтесь найти замену Mockito. В Go нет ничего даже близко напоминающего его.
Лично я обычно предпочитаю работать вручную, поэтому не имею каких-то предпочтений в выборе представленных выше инструментов. Однако если вкратце, то придерживайтесь в своём коде методики «принимать интерфейсы, возвращать структуры», и всё будет в порядке. Об этом можно прочитать здесь: https://medium.com/swlh/golangs-interfaces-explained-with-mocks-886f69eca6f0, https://bryanftan.medium.com/accept-interfaces-return-structs-in-go-d4cab29a301b, https://tutorialedge.net/golang/accept-interfaces-return-structs/.
Если вы решите добавить в свой код на Go интеграционные тесты, то обычно их разделяют при помощи тегов. Например, в начале тестового файла вы помещаете следующее:
//go:build integration
package mypackage
После этого вы сможете запускать их так:
go test --tags=integration ./...
При этом тесты без тегов всё равно будут выполняться. Также это можно использовать для разделения тестов на группы. Однако надо быть аккуратным, потому что по умолчанию при выполнении каждой группы они выполняется в собственном контексте, поэтому методы в одной группе тестов не будут доступны другим, что вызовет ошибку компиляции.
По умолчанию результаты тестов кэшируются, что может неидеально подходить для интеграционных тестов. Это поведение можно переопределить при помощи -count=1, добавленного к выполняемой команде, чтобы запускать тест один раз, игнорируя кэшированные результаты. При необходимости можно заметить 1 на более высокое значение.
go test -count=1 --tags=integration ./...
Лучше всего общаться с другими разработчиками на Go либо в сабреддите, либо в Slack. Из них двух Slack мне кажется более приятным.
Аккаунты в Twitter я нахожу полезными, однако некоторые из них могли переехать на fediverse, что можно проверить в профиле.
Стоит также подписаться на следующую рассылку: https://golangweekly.com/. Это отличный способ следить за последними обновлениями.
На следующих сайтах/блогах обычно есть качественный контент по Go, который стоит изучить:
Иногда вам необходимо потенциально иметь множество точек входа в приложение при помощи множества файлов main.go в основном пакете. Один из способов достичь этого является наличие общего кода в одном репозитории и импорт его в другие. Однако это может быть неудобно, если вы хотите использовать импорты vendor.
Распространённый паттерн решения этой проблемы заключается в создании папки внутри корня приложения и в размещении в ней файлов main.go. Например:
SRC
├── cmd
│ ├── commandline
│ │ └── main.go
│ ├── webhttp
│ │ └── main.go
│ ├── convert1.0-2.0
│ │ └── main.go
Благодаря этому каждая точка входа сможет выполнять импорт из корневого пакета, а вы сможете скомпилировать множество точек входа в приложении и исполнять их. Допустим, если ваше приложение находится в http://github.com/name/mycode, то вам понадобится выполнить подобный импорт в каждое приложение:
package main
import (
"github.com/name/mycode"
)
Благодаря этой схеме вы сможете вызывать код, раскрытый пакетом репозитория в корне.
Иногда вам понадобится код, который не компилируется или не запускается в разных операционных системах. Чаще всего эту проблему решают созданием в приложении следующей структуры:
main_darwin.go
main_linux.go
main_windows.go
Если в данном случае всё это — просто изолированные определения разрывов строк в разных операционных системах, например, const LineBreak = "\n\r" или const LineBreak = "\n", то можно выполнить импорт и ссылаться на LineBreak так, как вам нужно. Ту же методику можно применить к функциям и всему остальному, что вам нужно включить.
При помощи вышеприведённых методик можно легко выполнять код внутри Docker, используя множественные точки входа. Пример dockerfile для этого показан ниже, он использует код из нашего гипотетического репозитория https://username@bitbucket.code.company-name.com.au/scm/code/random-code.git.
Приведённые ниже команды выполняют сборку и запуск основного приложения
FROM golang:1.20
COPY ./ /go/src/bitbucket.code.company-name.com.au/scm/code/
WORKDIR /go/src/bitbucket.code.company-name.com.au/scm/code/
RUN go build main.go
CMD ["./main"]
Этот код выполняет сборку и запуск из одной из альтернативных точек входа в приложение:
FROM golang:1.20
COPY ./ /go/src/bitbucket.code.company-name.com.au/scm/code/
WORKDIR /go/src/bitbucket.code.company-name.com.au/scm/code/cmd/webhttp/
RUN go build main.go
CMD ["./main"]
Некоторые из прочитавших этот пост предложили использовать многоэтапные сборки docker (https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds), которые хорошо работают с Docker 17.05 и выше. Подробнее можно прочитать об этом здесь: https://medium.com/travis-on-docker/multi-stage-docker-builds-for-creating-tiny-go-images-e0e1867efe5a.
Пример:
FROM golang:1.20
COPY . /go/src/bitbucket.code.company-name.com.au/scm/code
WORKDIR /go/src/bitbucket.code.company-name.com.au/scm/code/
RUN CGO_ENABLED=0 go build main.go
FROM alpine:3.7
RUN apk add --no-cache ca-certificates
COPY --from=0 /go/src/bitbucket.code.company-name.com.au/scm/code/main .
CMD ["./main"]
В результате вы получите образы гораздо меньшего размера для запуска кода, что всегда приятно.
Ниже приведён краткий список понравившихся мне полезных инструментов для разработки на Go и пакетов, которые я люблю использовать. Обратите внимание, что некоторые из них написаны не на Go.
gow https://github.com/mitranim/gow — команда watch mode. Выполняйте её со своими аргументами, и она будет производить внутреннюю горячую рекомпиляцию. Очень полезно для HTTP-разработки. Например, gow -e=go,html,css run . будет наблюдать за изменениями во всех файлах Go, HTML или CSS, а при их обнаружении повторно выполнять команду go run ., обеспечивая горячую перезагрузку.
hyperfine https://github.com/sharkdp/hyperfine — инструмент бенчмаркинга, работающий из командной строки. Можно считать его заменой многократного запуска time с усреднением результатов.
dcd https://github.com/boyter/dcd — детектор дублирующегося кода. Это мой собственный проект (поэтому я не был уверен, стоит ли его добавлять), но его можно использовать для поиска примеров повторений кода в проекте. Он особенно полезен, если вы хотите заняться рефакторингом.
gotestsum https://github.com/gotestyourself/gotestsum — инструмент для запуска изменяющихся тестов. Выдаёт результаты различных тестов с выбираемыми опциями форматирования. Может создавать формат результатов junit для работы с системами CI/CD.
https://mholt.github.io/json-to-go/ — генератор из JSON в Go. Goland тоже может это делать, но этот инструмент довольно неплохо подходит для вставки JSON и получения структуры, которая может его хранить.
gofumpt https://github.com/mvdan/gofumpt — более строгий форматтер, чем gofmt. Лично я не пользовался им, но мне его рекомендовали.
https://github.com/golangci/golangci-lint — инструмент статической проверки типов и применения lint. Используйте его с самого начала работы над проектом, и он сэкономит вам много времени на очистку. Его рекомендации всегда полезны и он помогает тем, что задаёт стандартные вопросы по аудиту. Для наилучших результатов подсоедините его к своему конвейеру CI/CD в качестве шлюза развёртывания.
gitleaks https://github.com/gitleaks/gitleaks — инструмент SAST для поиска и выявления зачекиненных секретов, паролей и прочего. Тоже хорошо подходит для прохождения вопросов аудита.
BFG Repo-Cleaner https://rtyley.github.io/bfg-repo-cleaner/ — самый простой способ удаления больших двоичных файлов и зачекиненных секретов из репозитория git. Очень полезен для устранения найденных gitleaks проблем.
https://github.com/tidwall/gjson — быстрый способ получения JSON-значений из документа JSON. Вместо десериализации в структуру можно просто получить нужное значение. Особенно полезен для интеграционных тестов при запуске с вашими собственными конечными точками. g := gjson.Get(resp.Body, "response.someValue").
https://github.com/rs/zerolog/ — структурированные JSON-логи. Быстрый способ получить удобные структурированные логи. Я предпочитаю использовать его с уникальным кодом date | md5 | cut -c 1-8, позволяющим находить ошибки в конкретной строке log.Error().Str(common.UniqueCode, "9822f401").Err(err).Msg(""). Добавление контекстной информации для получения подробностей о вызове также обеспечивает определённый уровень наблюдаемости логов.
https://github.com/gorilla/mux — замена стандартного обработчика маршрутов Go, который немного неудобен. К сожалению, теперь этот код архивирован. Тем не менее, он не потерял актуальности и по-прежнему работает. В этом посте есть приличный обзор его потенциальных замен: https://mariocarrion.com/2022/12/19/gorilla-mux-archived-migration-path.html
https://github.com/google/go-cmp — справляется с проверками тождественности лучше, чем reflect.DeepEqual.
https://github.com/google/uuid/ — наверно, стандарт де-факто для создания различных версий UUID.