Как написать элегантный код Golang

Go
Как написать элегантный код Golang

Язык Go — это простой и легкий в освоении язык программирования.Для инженеров с опытом программирования нетрудно выучить язык Go и написать код, который может работать.Для разработчиков, имеющих опыт работы с другими языками, это на самом деле проблема написать любой язык, подобный языку, который вы выучили.Если вы хотите по-настоящему интегрироваться в экологию и писать элегантный код, вы должны потратить некоторое время и энергию, чтобы понять философию дизайна и лучшие практики, лежащие в основе языка.

bottle-of-wate

Если у вас нет предыдущего опыта разработки на языке Go, но вы изучаете и используете язык Go, я считаю, что эта статья поможет вам быстрее написать элегантный код на языке Go; в этой статье мы не будем давать слишком много. Список описывает, как переменные, методы, и структуры должны быть названы.Эти спецификации кода Go можно найти наGo Code Review CommentsОни очень важны, но не в центре внимания этой статьи. Мы расскажем, как писать элегантный код, исходя из нескольких различных аспектов структуры кода, лучших практик и модульного тестирования. Перейти код языка.

написать впереди

Писать хороший код непросто, это требует от нас постоянно размышлять над существующим кодом — как переписать этот код, чтобы сделать его более элегантным. Элегантность звучит как очень перцептивный и трудно поддающийся количественной оценке результат, но это самое интуитивное чувство, которое может вызвать хороший код, и оно может неявно включать следующие характеристики:

  • легко читать и понимать;
  • Легко тестировать, поддерживать и расширять;
  • Наименование понятное, однозначное, аннотации полные и понятные;

Я считаю, что после прочтения этой статьи мы не сможем сразу написать элегантный код на языке Go, но если мы будем следовать нескольким простым в использовании и практичным методам, представленным здесь, это поможет нам сделать первый шаг Автор написал эту статью со следующими целями:

  • Помогите разработчикам Go понять нормы и инструменты экосистемы и написать более элегантный код;
  • Обеспечьте правила, консенсус и лучшие практики, которые широко приняты сообществом для управления кодом и проектами;

спецификация кода

Спецификация кода на самом деле является старомодной проблемой, и мы не можем избежать ее или должны кратко представить соответствующий контент.Язык Go относительно распространен, и официально предоставлена ​​широко используемая спецификация кода.Go Code Review Comments, независимо от того, программируете ли вы на Go в краткосрочной или долгосрочной перспективе, вам следуетПрочитайте это официальное руководство по спецификации кода хотя бы один раз полностью., что является не только правилами, которым мы должны следовать при написании кода, но и спецификациями, на которые нужно обращать внимание при просмотре кода.

Изучение спецификаций кода, связанных с языком Go, очень важно, и это также первый шаг к тому, чтобы наши проекты следовали унифицированным спецификациям.Хотя очень важно читать документы, относящиеся к спецификациям кода, мы не можем полагаться на инженеры сознательно в реальной работе.Соответствие часто рассматривается как форма проверки кода, но требует инструментов, помогающих в его выполнении.

Вспомогательный инструмент

Использование автоматизированных инструментов для обеспечения соответствия проекта некоторым основным спецификациям кода очень просто в эксплуатации и эффективно, в то время как способ проверки кода в человеческом теле более подвержен ошибкам, а также будут некоторые особые случаи нарушения правил. Соблюдение спецификаций кода Лучший способ«Постарайтесь автоматизировать все шаги, которые можно автоматизировать, и позвольте инженерам изучить логику и дизайн, которые действительно имеют значение»..

В этом разделе мы представим два очень эффективных способа, которые помогут нам автоматизировать некоторые проверки спецификации кода и статические проверки в проекте для обеспечения качества проекта.

goimports

goimportsЭто официальный инструмент, предоставляемый языком Go.Он может автоматически форматировать для нас код языка Go и управлять всеми импортированными пакетами, включая автоматическое добавление и удаление ссылок на зависимые пакеты, сортировку и классификацию зависимых пакетов в алфавитном порядке. Я считаю, что IDE, используемые многими людьми, будут использовать другой официально предоставленный инструмент.gofmtформатировать код иgoimportsравноgofmtПлюс управление пакетами зависимостей.

golang-goimports

Рекомендуется для использования всеми разработчиками Go при разработкеgoimports,Несмотря на то чтоgoimportsИногда вводятся неправильные пакеты, но эти случайные баги допустимы по мнению автора по сравнению с преимуществами; конечно, не хочется использоватьgoimportsРазработчики также должны включить автоматическоеgofmt(автоматически форматируется при сохранении).

Включить автоматически в проверках IDE и CIgofmtилиgoimportsПроверка не является и не должна обсуждаться, это вопрос использования и развития языка Go.долженсписок задач.

golint

Другим часто используемым инструментом статической проверки являетсяgolintТеперь, как официальный инструмент, он имеет очень плохую поддержку настройки, мы можем запустить его только следующим образом.golintПосмотрите наши проекты:

$ golint ./pkg/...
pkg/liquidity/liquidity_pool.go:18:2: exported var ErrOrderBookNotFound should have comment or be unexported
pkg/liquidity/liquidity_pool.go:23:6: exported type LiquidityPool should have comment or be unexported
pkg/liquidity/liquidity_pool.go:23:6: type name will be used as liquidity.LiquidityPool by other packages, and that stutters; consider calling this Pool
pkg/liquidity/liquidity_pool.go:31:1: exported function NewLiquidityPool should have comment or be unexported
...

сообщество оgolintИндивидуальныеобсуждать,golintразработчиков привели следующие моменты, чтобы объяснить, почемуgolintПользовательские функции не поддерживаются:

  • lintЦель состоит в том, чтобы поощрять единый и последовательный стиль программирования в языковом сообществе Go.Некоторые разработчики могут не соглашаться с некоторыми спецификациями, но использование единого стиля имеет большое преимущество для языкового сообщества Go и может переключать указанные rules.function приведет кgolintне уметь эффективно выполнять работу;
  • Есть некоторые статически проверенные правила, которые вызывают некоторые ложные предупреждения, которые действительно раздражают, но я бы предпочел поддерживать сохранение или удаление этих правил непосредственно в golint, а не предоставлять возможность добавлять или удалять правила произвольно;
  • в состоянии пройтиmin_confidenceОтфильтруйте некоторые статические правила проверки, но нам нужно выбрать соответствующие значения;

golintТочка зрения автора таковаissueЯ получил много 👎, но трудно сказать правильно или неправильно; очень полезно обеспечить согласованные стандарты программирования в сообществе, но для многих внутренних сервисов или проектов компании это может быть бизнес-сервисы. более сложные ситуации, когда нет очевидных преимуществ от использования такого чрезмерно сильного ограничения.

golang-lint

Более рекомендуемый способ - использовать в базовой библиотеке или фреймворкеgolintВыполните статические проверки (или используйте обаgolintиgolangci-lint), использовать настраиваемый в других проектахgolangci-lintдля статической проверки, потому что наложение строгих ограничений в базовых библиотеках и фреймворках дает больше преимуществ для общего качества кода.

Автор будет использовать его в своем собственном проекте Go.golint + golangci-lintИ включите все проверки, чтобы найти все дефекты в коде, включая документацию, как можно раньше.

автоматизация

Будь то для проверки спецификаций и зависимостей кодаgoimportsили инструмент статической проверкиglintилиgolangci-lint, пока мы внедряем эти инструменты в проект, мы должны добавить соответствующие автоматические проверки в процесс CI кода:

  • На GitHub мы можем использоватьTravis CIилиCircleCI;
  • В Gitlab мы можем использоватьGitlab CI;

Мы также должны сделать все возможное, чтобы найти подходящие инструменты на самодельных или других платформах для размещения кода.Современные инструменты для размещения кода должны иметь очень хорошую поддержку CI / CD; нам нужно использовать эти инструменты CI, чтобы изменить автоматическую проверку кода на это. становится предварительным условием для слияния и выпуска PR, уменьшая упущения, которые могут возникнуть при проверке кода инженерами.

Лучшие практики

В предыдущем разделе мы рассказали о некоторых проблемах, которые могут быть обнаружены с помощью автоматизированных инструментов. Передовой опыт, упомянутый в этом разделе, может не гарантироваться автоматическими инструментами. Эти передовые методы больше похожи на внутренний процесс разработки языкового сообщества Go. опыт и консенсус, следование этим передовым методам может помочь нам написать код, который соответствует «аромату» языка Go. В этом разделе мы рассмотрим следующие части:

  • Структура каталогов;
  • раздельный модуль;
  • явный вызов;
  • Интерфейс;

Эти четыре части являются относительно распространенными соглашениями в сообществе.Если мы изучим и будем следовать этим соглашениям и будем практиковать эти части в проектах на языке Go, я считаю, что нам будет полезно разрабатывать проекты на языке Go.

Структура каталогов

Структура каталогов — это, по сути, фасад проекта. Во многих случаях по структуре каталогов видно, достаточно ли у разработчика опыта работы с языком, поэтому здесь лучше всего представить, как использовать языковой проект Go. организовать код в службе.

Официальный не дает рекомендуемого способа разделения каталога.Многие проекты также очень небрежно относятся к разделению структуры каталогов, что на самом деле не является проблемой, но в сообществе все же есть некоторые общепринятые соглашения, такие как:golang-standards/project-layoutВ проекте определена относительно стандартная структура каталогов.

├── LICENSE.md
├── Makefile
├── README.md
├── api
├── assets
├── build
├── cmd
├── configs
├── deployments
├── docs
├── examples
├── githooks
├── init
├── internal
├── pkg
├── scripts
├── test
├── third_party
├── tools
├── vendor
├── web
└── website

Здесь мы просто кратко представляем некоторые из наиболее распространенных и важных каталогов и файлов, чтобы помочь нам быстро понять, как использовать структуру каталогов, показанную выше.Если читатели хотят знать причины использования других каталогов, они могут перейти кgolang-standards/project-layoutДля получения более подробной информации см. README в проекте.

/pkg

/pkgКаталог является очень распространенным каталогом в проектах на языке Go, и мы можем найти его почти во всех известных проектах с открытым исходным кодом (не фреймворк), таких как:

  • prometheusБаза данных временных рядов для отчетности и хранения показателей
  • istioСервисная сетка 2.0
  • kubernetesСистема управления расписанием контейнеров
  • grafanaПанели мониторинга, показывающие мониторинг и метрики

В этом каталоге хранится библиотека кода в проекте, которую могут использовать внешние приложения, а другие проекты могут напрямую передаватьimportимпортируйте код сюда, поэтому, когда мы поместим код вpkgМы должны быть осторожны, но если мы разрабатываем сервисы интерфейса HTTP или RPC или внутренние сервисы компании, поместите как частный, так и публичный код в/pkgВ этом нет ничего плохого, потому что, будучи проектом верхнего уровня, другие приложения редко напрямую используют его. Конечно, очень полезно строго следовать разделению публичного и приватного кода. код правильно разделен.

приватный код

Приватный код рекомендуется размещать в/internalкаталог, реальный код проекта должен быть написан в/internal/appВ то же время базы кода, от которых зависят эти внутренние приложения, должны быть в/internal/pkgподкаталоги и/pkg, на следующем рисунке показано использование/internalСтруктура проекта каталога:

golang-internal-app-and-pkg

Когда мы внедряем включения в другие проектыinternal, язык Go сообщит об ошибке при компиляции:

An import of a path containing the element “internal” is disallowed
if the importing code is outside the tree rooted at the parent of the 
"internal" directory.

Такая ошибка возникает только тогда, когдаinternalПроисходит только в том случае, если пакет не существует в текущем дереве проекта, если проектinternalВ пакете нет этой ошибки.

/src

Структура каталогов, которой не должно быть в проекте на языке Go, на самом деле/srcДа, в некоторых проектах сообщества есть/srcПапка, но перед разработчиками этих проектов у большинства есть опыт программирования Java на Java и других языках, что на самом деле является относительно обычной организацией кода, но как разработчику языка Go мы не должны допускать проект при наличии/srcсодержание.

Самая важная причина заключается в том, что проекты языка Go по умолчанию помещаются в$GOPATH/srcПод каталогом этот каталог хранит весь код проекта, который мы разрабатываем и от которого зависим, если мы используем его в нашем собственном проекте./srcкаталог, проектPATHбудет дваsrc:

$GOPATH/src/github.com/draveness/project/src/code.go

Приведенная выше структура каталогов выглядит очень странно, поэтому мы не рекомендуем использовать ее в языке Go./srcСамая важная причина для каталога.

Конечно, даже если мы используем языковой проект Go/srcКаталог не приведет к сбою компиляции или другим проблемам.Если вы настаиваете на таком подходе, это никак не повлияет на удобство использования проекта.Однако, если вы хотите, чтобы мы "выглядели" более профессионально, мы должны следовать установленным правилам в сообществе и сократить количество других языков Go. Разработчики понимают цену, что хорошо для сообщества.

кафельная плитка

Еще один способ организовать код на языке Go — поместить код проекта в корневой каталог проекта.Этот метод очень распространен во многих фреймворках или библиотеках.Если вы хотите ввести использованиеpkgКогда в рамках структуры каталогов нам часто приходится использоватьgithub.com/draveness/project/pkg/somepkg, когда код разложен в корневом каталоге проекта, нужно использовать толькоgithub.com/draveness/project, что значительно сокращает длину инструкции, относящейся к зависимому пакету.

Таким образом, для языковой среды или библиотеки Go нормально размещать код в корневом каталоге, но использование этого метода организации кода в языковой службе Go может быть неуместным.

/cmd

/cmdВсе исполняемые файлы текущего проекта хранятся в каталоге, и каждый подкаталог в этом каталоге должен содержать исполняемые файлы, которые нам нужны.grpcСервис, это может быть/cmd/server/main.goОн содержит код для запуска сервисного процесса и исполняемый файл, сгенерированный после компиляции.server.

мы не должны быть/cmdПоместите слишком много кода в каталог, мы должны поместить общедоступный код в/pkgв и поместите приватный код в/internalв и в/cmdВнедрение этих пакетов для обеспеченияmainКод в функции максимально прост и минимален.

/api

/apiВ каталоге хранятся различные типы файлов определения интерфейса API, предоставленные текущим проектом, которые могут содержать аналогичные/api/protobuf-spec,/api/thrift-specили/api/http-specЭти каталоги содержат все файлы API, предоставленные и зависящие от текущего проекта:

$ tree ./api
api
└── protobuf-spec
    └── oceanbookpb
        ├── oceanbook.pb.go
        └── oceanbook.proto

Основная функция вторичного каталога состоит в том, чтобы избежать потенциальных конфликтов и сделать организацию структуры проекта более четкой, когда проект предоставляет множество различных методов доступа одновременно.

Makefile

последний, чтобы представитьMakefileФайлы тоже очень заслуживают внимания.В любом проекте будут какие-то скрипты которые надо запускать.Эти скриптовые файлы надо поместить в/scriptsкаталог и поMakefileЗапустите, закрепите эти часто выполняемые команды в сценариях, чтобы уменьшить появление «команд предков».

резюме

В общем, каждый проект должен быть реализован в соответствии с установленным организационным методом.Хотя это соглашение не является обязательным, будь то внутри группы, компании или всего языкового сообщества Go, пока есть соглашение, другие инженеры. помогает быстро разобраться и понять проект.

Метод организации языкового проекта Go, представленный в этом разделе, не является обязательным.Это просто метод организации проекта, который часто встречается в языковом сообществе Go.Большой проект также будет тонко настраивать при использовании этой структуры каталогов, но это тип организации действительно более распространен и разумен.

модуль сплит

Теперь, когда мы представили, как организовать структуру проекта с верхнего уровня, мы углубимся в проект и представим некоторые методы разделения модулей в языке Go.

Некоторые проекты верхнего уровня языка Go в конечном итоге приводят к тому, что он сильно отличается от других языков программирования в разделении модулей.Многие веб-фреймворки на других языках используют архитектурный шаблон MVC, например Rails и Spring MVC, и язык Go разделяет модули.Подход полностью отличается от Ruby и Java.

Разделить по слоям

Будь то Java или Ruby, их самые известные фреймворки глубокоАрхитектурный шаблон MVCВлияние Spring MVC, мы можем понять влияние MVC на него из названия Spring MVC, и фреймворк Rails сообщества Ruby также очень тесно связан с MVC, который является наиболее распространенным архитектурным методом веб-фреймворка. Различные компоненты разделены на три уровня: Модель, Представление и Контроллер.

divide-by-laye

Этот метод разделения модулей на самом деле разделяет по уровням.Код, сгенерированный скаффолдингом Rails по умолчанию, фактически помещает эти три разных исходных файла в соответствующий каталог:models,viewsиcontrollers, мы проходимrails new exampleПосле создания нового проекта Rails вы можете увидеть в нем структуру каталогов:

$ tree -L 2 app
app
├── controllers
│   ├── application_controller.rb
│   └── concerns
├── models
│   ├── application_record.rb
│   └── concerns
└── views
    └── layouts

И многие проекты Spring MVC также будут выглядеть похожими.model,dao,viewЕсть несколько причин для такого дизайна разделения модулей по слоям:

  1. Паттерн архитектуры MVC. Сам MVC делает упор на проектирование обязанностей по уровням, поэтому фреймворк, который следует этому шаблону, естественно, имеет тот же образ мышления;
  2. Плоское пространство имен - будь то весна MVC или рельсы, пространство имен в том же проекте очень плоский. Использование классов или методов, определенных в других папках, в папках, не требует введения новых пакетов, а также не требует классов, определенных в других файлах. Дополнительные Префикс необходимо добавить, а классы, определенные несколькими файлами, являются «объединены» в то же самое пространство имен;
  3. Сценарии монолитных сервисов. Когда Spring MVC и Rails впервые появились, архитектуры SOA и микросервисов не были так распространены, как сегодня, и в большинстве сценариев не требовалось разделения сервисов;

Вышеперечисленные причины в совокупности определяют появление Spring MVC и Rails.models,viewsиcontrollersкаталог и разделить модули иерархически.

Разделение по обязанностям

Язык Go использует совершенно другую идею при разделении модулей.Хотя архитектурный шаблон MVC нельзя избежать, когда мы пишем веб-сервисы, по сравнению с горизонтальным разделением различных уровней, проекты языка Go часто основаны на Обязанности разбиты на модули:

divide-by-responsibility

Для относительно распространенной системы блогов проекты, использующие язык Go, будут вертикально разделены на разные обязанности.post,user,commentТри модуля, каждый модуль обеспечивает соответствующие функции для внешнего мира,postМодуль содержит соответствующие определения модели и представления, а также контроллер (или службу), используемый для обработки запросов API:

$ tree pkg
pkg
├── comment
├── post
│   ├── handler.go
│   └── post.go
└── user

Каждый файловый каталог в проекте языка Go представляет собойотдельное пространство имен, то есть отдельный пакет, когда мы хотим обратиться к каталогу других папок, нам сначала нужно использоватьimportКлючевое слово вводится в соответствующий каталог файлов, а затем черезpkg.xxxВ виде ссылок на структуры, функции или константы, определенные другими каталогами, если мы используем язык Gomodel,viewиcontrollerчтобы разделить иерархию, вы увидите многое в других модуляхmodel.Post,model.Commentиview.PostView.

Этот метод разделения слоев очень избыточен в языке Go, и если управление зависимостями проекта недостаточно тщательно, могут возникнуть циклы ссылок.Основная причина этих проблем на самом деле очень проста:

  1. Язык Go изолирует пространства имен разных каталогов в одном проекте. Классы и методы, определенные во всем проекте, не находятся в одном и том же пространстве имен, что требует от разработчиков поддерживать зависимости между разными пакетами;
  2. Очень легко разделить микросервисы в соответствии с вертикальным разделением обязанностей, когда один сервис сталкивается с узким местом Мы можем напрямую разделить один сервис, отвечающий за независимые функции.packageВыньте его и расширьте емкость этой части точки доступа отдельно;

резюме

Нет абсолютного добра или зла, разделен ли проект на модули в соответствии с иерархией или обязанностями.Дизайн на уровне языка и фреймворка в конечном итоге определяет, как мы должны организовать проект и код.

Такие языки, как Java и Ruby, часто используют горизонтальное разделение для разделения обязанностей на разных уровнях фреймворка, а лучшая практика для языковых проектов Go — разделение модулей по вертикали в соответствии с обязанностями и разделение кода на несколько функциональных групп.package, дело не в том, что в Go нет горизонтального разделения модулей, простоpackageВ качестве наименьшей детализации управления доступом в языке Go мы должны следовать дизайну верхнего уровня, чтобы таким образом создавать высокосвязные модули.

явный и неявный

От изучения и использования языка Go до участия в некоторых проектах Golang с открытым исходным кодом в сообществе автор обнаружил, что языковое сообщество Go очень важно для языкового сообщества Go.Явная инициализация, вызов метода и обработка ошибокОчень уважаемые, такие фреймворки, как Spring Boot и Rails, широко приняли центральную идею «конвенция важнее конфигурации», упрощая рабочую нагрузку разработчиков и инженеров.

Однако, хотя сообщество языка Go достигло большого консенсуса и соглашений, начиная с дизайна языка и использования инструментов, мы можем обнаружить, что явный вызов методов и обработка ошибок приветствуются.

init

Здесь мы начнем с очень распространенной функцииinitНапример, ввести уважение к явному вызову в языковом сообществе Go; я полагаю, что многие люди в той или иной степениpackageЯ прочитал этот код в:

var grpcClient *grpc.Client

func init() {
    var err error
    grpcClient, err = grpc.Dial(...)
    if err != nil {
        panic(err)
    }
}

func GetPost(postID int64) (*Post, error) {
    post, err := grpcClient.FindPost(context.Background(), &pb.FindPostRequest{PostID: postID})
    if err != nil {
        return nil, err
    }
    
    return post, nil
}

Хотя этот код компилируется и работает правильно, код здесьinitФункция на самом деле неявно инициализирует ресурсы соединения grpc, если другойpackageзависит от текущего пакета, инженер, внедривший эту зависимость, может сильно растеряться, столкнувшись с ошибками, потому что вinitИнициализация таких ресурсов в функции требует очень много времени и чревата проблемами.

Более разумный подход на самом деле таков: сначала мы определяем новыйClientstruct и метод для инициализации структурыNewClientфункция, эта функция получает соединение grpc в качестве входного параметра и возвращаетPostклиент ресурса,GetPostстановится методом этой структуры всякий раз, когда мы вызываемclient.GetPostСохраненное в структуре соединение grpc используется, когда:

// pkg/post/client.go
type Client struct {
    grpcClient *grpc.ClientConn    
}

func NewClient(grpcClient *grpcClientConn) Client {
    return &Client{
        grpcClient: grpcClient,
    }
}

func (c *Client) GetPost(postID int64) (*Post, error) {
    post, err := c.grpcClient.FindPost(context.Background(), &pb.FindPostRequest{PostID: postID})
    if err != nil {
        return nil, err
    }
    
    return post, nil
}

Код для инициализации соединения grpc должен быть помещен вmainфункция илиmainвызовы функций выполняются в других функциях, если мы находимся вmainЗависимость явной инициализации в функции очень проста для понимания другими инженерами.Начнем сmainНачало функции может упорядочить весь процесс запуска программы.

// cmd/grpc/main.go
func main() {
    grpcClient, err := grpc.Dial(...)
    if err != nil {
        panic(err)
    }
    
    postClient := post.NewClient(grpcClient)
    // ...
}

Каждый модуль будет формировать древовидную структуру и отношения зависимости.Модуль верхнего уровня будет содержать интерфейс или структуру модуля нижнего уровня, и не будет никаких изолированных и не связанных объектов.

golang-project-and-tree-structure

Два, которые появляются на картинке вышеDatabaseпо фактуmainсоединения с базой данных, инициализированные в функциях, которые могут представлять одно и то же соединение с базой данных в памяти во время выполнения проекта.

когда мы используемgolangci-lintи включиgochecknoinitsиgochecknoglobalsПри статической проверке это на самом деле строго ограничивает нашу способностьinitИспользование функций и глобальных переменных.

Конечно, это не означает, что мы не должны использоватьinitФункции, как возможность, которую язык Go дает разработчикам, потому что он может неявно выполнять некоторый код при импорте пакета, поэтому мы должны использовать их с осторожностью.

Некоторые рамки будут вinitчтобы определить, соблюдены ли предварительные условия для использования, но для многих веб-служб или API-сервисов требуется большое количествоinitЧасто означает снижение качества кода и неразумный дизайн.

func init() {
    if user == "" {
        log.Fatal("$USER not set")
    }
    if home == "" {
        home = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath may be overridden by --gopath flag on command line.
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

Приведенный выше код на самом делеEffective GoПредставляемinitИспользование метода показано в примере кода, что является разумнымinitпример использования функции, мы не должныinitВ процессе выполняется тяжелая логика инициализации, но делаются некоторые простые и легкие суждения о предварительных условиях.

error

Еще одна вещь, которую следует представить, — это механизм обработки ошибок языка Go.Хотя обработка ошибок Golang уже давно подвергается критике со стороны разработчиков, инженеры пишут каждый день.if err != nil { return nil, err }Логика обработки ошибок на самом деле заключается в том, чтобы явно обрабатывать ошибки, обращать внимание на все вызовы методов, которые могут вызвать ошибки, и передавать их в модуль верхнего уровня, когда они не могут быть обработаны.

func ListPosts(...) ([]Post, error) {
    conn, err := gorm.Open(...)
    if err != nil {
        return []Post{}, err
    }
    
    var posts []Post
    if err := conn.Find(&posts).Error; err != nil {
        return []Post{}, err
    }
    
    return posts, nil
}

Приведенный выше код просто показывает общую логику обработки ошибок в языке Go, мыне следуетВ этом методе инициализируйте подключение к базе данных.

Хотя в Голанге есть похожие Java или Rubytry/catchключевое слово, но мало кто будет использовать его в кодеpanicиrecoverреализовать обработку ошибок и исключений, а такжеinitКак и функции, язык Go дляpanicиrecoverтакже используется с большой осторожностью.

Когда мы имеем дело с логикой, связанной с ошибками в языке Go, наиболее важными на самом деле являются следующие вещи:

  1. использоватьerrorРеализовать обработку ошибок — хоть это и выглядит очень неловко;
  2. Выбросить ошибку на верхний уровень для обработки— должен ли метод возвращатьerrorЭто также требует от нас тщательного обдумывания Когда ошибка выбрасывается вверх, мы можем пройтиerrors.WrapИметь некоторую дополнительную информацию для облегчения принятия решений на высшем уровне;
  3. обрабатывать все возможные ошибки— Все, что может вернуть ошибку, в конечном итоге вернет ошибку, и всестороннее рассмотрение может помочь нам построить более надежный проект;

резюме

При использовании языка Go автор может глубоко оценить его поддержку явного вызова методов и обработки ошибок, что не только помогает другим разработчикам проекта быстро понять контекст, но также помогает нам создавать более надежную и отказоустойчивую разработку с лучшим надежность и ремонтопригодность.

ориентированный на интерфейс

Интерфейсно-ориентированное программирование — тема избитая.интерфейсЕго функция состоит в том, чтобы обеспечить определенный средний уровень для модулей на разных уровнях.Верхний поток больше не должен полагаться на конкретную реализацию нисходящего потока, а восходящий и нисходящий потоки полностью разделены.

golang-interface

Этот метод программирования рекомендуется не только в языке Go, но и почти во всех языках программирования, мы будем рекомендовать этот метод программирования, он обеспечивает нашим программам очень сильную гибкость, надежные проекты языка Go, совершенно невозможно обойтись без использования интерфейсов.

Если слегка масштабированный проект не показывает никакихtype ... interfaceопределение , то автор может сделать вывод, что это высокая вероятностьПлохое инженерное качество и малое покрытие юнит-тестамипроект, нам действительно нужно серьезно подумать о том, как провести рефакторинг проекта с использованием интерфейсов.

Модульное тестирование является одним из наиболее эффективных способов обеспечения качества проекта и имеет наивысшую отдачу от инвестиций.Как статический язык Golang, трудно писать модульные тесты с достаточным покрытием (минимальное покрытие основной логики), потому что мы не люблю динамические языкислучайныйИзмените поведение функций и методов, и интерфейс станет нашей спасительной соломинкой. Написание хорошего абстрактного интерфейса и изоляция зависимостей через интерфейс может помочь нам эффективно улучшить качество и тестируемость проекта. Мы подробно представим его в следующий раздел Как писать модульные тесты.

package post

var client *grpc.ClientConn

func init() {
    var err error
    client, err = grpc.Dial(...)
    if err != nil {
        panic(err)
    }
}

func ListPosts() ([]*Post, error) {
    posts, err := client.ListPosts(...)
    if err != nil {
        return []*Post{}, err
    }
    
    return posts, nil
}

Приведенный выше код на самом деле не является хорошо разработанным кодом, он не только вinitВ функции неявно инициализирована глобальная переменная соединения grpc, и нетListPostsВыставлять через интерфейс, который будет делать зависимостиListPostsМодули верхнего уровня трудно тестировать.

Мы можем использовать следующий код, чтобы переписать исходную логику, упростив ту же логику для тестирования и обслуживания:

package post

type Service interface {
    ListPosts() ([]*Post, error)
}

type service struct {
    conn *grpc.ClientConn
}

func NewService(conn *grpc.ClientConn) Service {
    return &service{
        conn: conn,
    }
}

func (s *service) ListPosts() ([]*Post, error) {
    posts, err := s.conn.ListPosts(...)
    if err != nil {
        return []*Post{}, err
    }
    
    return posts, nil
}
  1. через интерфейсServiceнезащищенныйListPostsметод;
  2. использоватьNewServiceинициализация функцииServiceРеализация интерфейса и через тело приватного интерфейсаserviceудерживать соединение grpc;
  3. ListPostsБольше не полагаются на глобальные переменные, а полагаться на тело интерфейсаserviceсоединение установлено;

Когда мы рефакторим код таким образом, мы можемmainЯвно инициализировать подключение и создание grpc в функцииServiceРеализация интерфейса и вызовListPostsметод:

package main

import ...

func main() {
    conn, err = grpc.Dial(...)
    if err != nil {
        panic(err)
    }
    
    svc := post.NewService(conn)
    posts, err := svc.ListPosts()
    if err != nil {
        panic(err)
    }
    
    fmt.Println(posts)
}

Такой способ организации кода с использованием интерфейсов очень распространен в языке Go, и мы должны максимально использовать эту идею и шаблон в нашем коде для предоставления внешних функций:

  1. С заглавной буквыServiceметод внешнего воздействия;
  2. использовать строчные буквыserviceРеализовать методы, определенные в интерфейсе;
  3. пройти черезNewServiceинициализация функцииServiceинтерфейс;

Когда мы используем описанный выше метод для организации кода, мы фактически разделяем зависимости различных модулей, что также следует предложению, часто упоминаемому в разработке программного обеспечения — «полагаться на интерфейс, а не на реализацию», то естьинтерфейсно-ориентированное программирование.

резюме

В этом разделе представлены три «элемента», которые часто имеют дело с языком Go:initфункция,errorИ интерфейс, мы в основном должны передать три разных примера, чтобы каждый мог использовать его как можно больше.явный (явный) способНапишите код Go.

модульный тест

Проект с гарантированным качеством и инженерным качеством кода должен иметь относительно разумное покрытие для тестирования единиц. Проект без модульных тестов должен быть неквалифицирован или неважно. Тесты на единиц должны быть кодом, что все проекты должны иметь, и все тесты на единицу представляют собой возможную ситуацию СМодульное тестирование — это бизнес-логика.

Как инженеры-программисты, рефакторинг существующего проекта должен быть для нас относительно нормальным делом. Если в проекте нет модульного тестирования, нам сложно провести рефакторинг проекта без изменения существующей бизнес-логики. потеряться при рефакторинге, когдаcaseИнженера-разработчика может уже не быть в команде, а проектная документация может исчезнуть в архивах.wiki(Больше проектов могут быть полностью незарегистрированы), единственным вещам, которые мы можем доверять рефакторингу, являются текущая логика кода (вероятно, неправильный) и единичные тесты (вероятно, нет).

Подводя итог, можно сказать, что отсутствие юнит-тестирования будет означать не только более низкое качество проектирования, но и сложность рефакторинга.Проект с юнит-тестированием не может гарантировать одинаковую логику до и после рефакторинга.Проект без юнит-тестирования не может гарантировать то же самое. логика до и после рефакторинга Качество самого проекта, скорее всего, будет иметь значение, не говоря ужеКак провести рефакторинг без потери бизнес-логики.

Тестируемый

Писать код несложно, но написать тестируемый код в проекте непросто, а элегантный код должен быть тестируемым.В этом разделе нам нужно обсудить, что такое код, который можно тестировать.

Если мы хотим выяснить, что можно тестировать, нам сначала нужно узнать, что такое тест? Под тестированием автор понимает контроль переменных.После выделения некоторых зависимостей в тестируемом методе, когда определены входные параметры функции, должно быть получено ожидаемое возвращаемое значение.

golang-unit-test

Как управлять модулями, которые зависят от тестируемого метода, очень важно при написании модульных тестов.MockЧтобы устранить неопределенность, чтобы уменьшить сложность каждого модульного теста, нам необходимо:

  1. Максимально уменьшите зависимости целевого метода, чтобы целевой метод зависел только от необходимых модулей;
  2. Зависимые модули также должны быть очень просты в использовании.Mock;

Выполнение модульных тестов не должно зависеть от каких-либо внешних модулей, будь то вызов внешних HTTP-запросов или данных в базе данных, мы должны максимально постараться смоделировать возможные ситуации, поскольку модульные тесты не являются интеграционными тестами, и их работа не должна зависеть от любая система, кроме кода проекта.

интерфейс

На языке Go, если мы вообще не используем интерфейсы, мы не можем писать код, который легко проверить. Как статический язык Голанга, только с помощью интерфейсов мы можем избавиться от дилеммы полагаться на конкретные реализации. Использование Интерфейсов могут принести нам более четкое абстракция помогает нам подумать о том, как разработать код, а также позволяет нам легче реализовывать зависимостиMock.

Давайте вернемся к общим шаблонам, показанным во введении к интерфейсам в предыдущем разделе:

type Service interface { ... }

type service struct { ... }

func NewService(...) (Service, error) {
    return &service{...}, nil
}

Приведенный выше код очень распространен в языке Go. Если вы не знаете, следует ли использовать интерфейс для предоставления услуг внешнему миру, вам следует использовать приведенный выше режим, чтобы без раздумий выставить метод. Этот режим можно использовать в большинство сценариев работают, по крайней мере, автор пока не видел ни одного неприменимого.

Простая функция

Другое предложение состоит в том, чтобы сохранить каждую функцию максимально простым. Простота здесь не только означает простую и одну функцию, но и означает, что функция легко понять, и именование является самоснабжением.

некоторые языкиlintИнструмент на самом деле проверяет восприятие функции функции, то есть проверяет функции, которые появляются в функции.if/else,switch/caseКоличество веток и вызовов методов сообщит об ошибке при превышении согласованного порога. Rubocop в сообществе Ruby и вышеупомянутый golangci-lint имеют эту функцию.

Rubocop в сообществе Ruby имеет очень строгие ограничения на длину и сложность функций, по умолчанию количество строк функции не может превышать10ОК, мы не можем понять сложность более чем7, Кроме того, Rubocop на самом деле имеет другие ограничения сложности, такие как CyclomaticComplexity.Все эти ограничения сложности предназначены для обеспечения простоты и легкости понимания функции.

Организация

Стоит обсудить и организацию теста: файлы и код юнит-теста в Golang помещаются в ту же директорию, что и исходный кодpackageорганизован,server.goТестовый код, соответствующий файлу, должен быть помещен в тот же каталогserver_test.goв файле.

Если файл не начинается с_test.goконец, когда мы бежимgo test ./pkgЕсли тест кейс в файле не будет найден, код в нем не будет выполнен, что также является соглашением языка Go для метода организации тестирования.

Test

Наиболее распространенная организация модульных тестов по умолчанию — писать их на_test.goВ файле в конце все тестовые методы тоже начинаются сTestзапускается и принимает только одинtesting.TПараметры типа:

func TestAuthor(t *testing.T) {
    author := blog.Author()
    assert.Equal(t, "draveness", author)
}

Если бы мы дали функции имяAddметод написания модульных тестов, то соответствующий метод тестирования обычно будет записан какTestAdd, чтобы протестировать содержимое нескольких веток одновременно, мы можем организовать его следующим образомAddФункциональные тесты:

func TestAdd(t *testing.T) {
    assert.Equal(t, 5, Add(2, 3))
}

func TestAddWithNegativeNumber(t *testing.T) {
    assert.Equal(t, -2, Add(-1, -1))
}

В дополнение к этому распределению функционального теста на несколькоTestметод, мы можем использоватьforЗацикливание для сокращения повторяющегося тестового кода, которое очень полезно в более сложных тестах, может уменьшить много повторяющегося кода, но также требует от нас тщательного проектирования:

func TestAdd(t *testing.T) {
    tests := []struct{
        name     string
        first    int64
        second   int64
        expected int64
    } {
        {
            name:     "HappyPath":
            first:    2,
            second:   3,
            expected: 5,
        },
        {
            name:     "NegativeNumber":
            first:    -1,
            second:   -1,
            expected: -2,
        },
    }
    
    for _, test := range tests {
        t.Run(test.name, func(t *testing.T) {
            assert.Equal(t, test.expected, Add(test.first, test.second))
        })
    }
}

На самом деле, этот метод также может генерировать результаты теста в виде дерева.AddСвязанные тесты сгруппированы в группу, чтобы облегчить наше наблюдение и понимание.Однако этот метод организации тестирования требует от нас обеспечения универсальности тестового кода.Когда есть много функциональных зависимостей, нам часто нужно написать многоif/elseУсловные операторы влияют на наше быстрое понимание теста.

Автор обычно использует первый метод организации, когда код теста относительно прост, и использует второй метод, когда много зависимостей и сложных функций, но это не вывод, нам нужно решить, как спроектировать тест в соответствии с реальным ситуация .

Suite

Второй, более распространенный способ — организация по кластерам, что на самом деле является простой инкапсуляцией стандартного метода тестирования языка Go.stretchr/testifyсерединаsuiteПакеты организуют тесты:

import (
    "testing"
    "github.com/stretchr/testify/suite"
)

type ExampleTestSuite struct {
    suite.Suite
    VariableThatShouldStartAtFive int
}

func (suite *ExampleTestSuite) SetupTest() {
    suite.VariableThatShouldStartAtFive = 5
}

func (suite *ExampleTestSuite) TestExample() {
    suite.Equal(suite.VariableThatShouldStartAtFive, 5)
}

func TestExampleTestSuite(t *testing.T) {
    suite.Run(t, new(ExampleTestSuite))
}

мы можем использоватьsuiteпакет, который организует тестовые кластеры в структуру,suiteкоторый предоставилSetupTest/SetupSuiteиTearDownTest/TearDownSuiteЭто метод ловушки до и после выполнения теста, а также до и после выполнения тестового кластера, в котором мы можем завершить инициализацию некоторых общих ресурсов и сократить код инициализации в тесте.

BDD

Последний способ организовать код — организовать модульные тесты в стиле BDD.ginkgoЭто наиболее распространенная структура BDD в сообществе Golang. Упомянутые здесь разработка на основе поведения (BDD) и разработка на основе тестирования (TDD) являются методологиями для обеспечения качества проекта. Чтобы практиковать этот тип мышления в проекте, все еще нужны некоторые изменения и адаптации в мышлении, то есть сначала написать спецификацию метода контракта модульного теста или теста поведения, а затем реализовать метод, чтобы позволить нашему тесту пройти.Это относительно научный метод, метод, он может вселить в нас большую уверенность.

Хотя нам не обязательно использовать идеи BDD/TDD для разработки проектов, мы можемСтильный способ использования BDDОрганизуйте очень читаемый тестовый код:

var _ = Describe("Book", func() {
    var (
        book Book
        err error
    )

    BeforeEach(func() {
        book, err = NewBookFromJSON(`{
            "title":"Les Miserables",
            "author":"Victor Hugo",
            "pages":1488
        }`)
    })

    Describe("loading from JSON", func() {
        Context("when the JSON fails to parse", func() {
            BeforeEach(func() {
                book, err = NewBookFromJSON(`{
                    "title":"Les Miserables",
                    "author":"Victor Hugo",
                    "pages":1488oops
                }`)
            })

            It("should return the zero-value for the book", func() {
                Expect(book).To(BeZero())
            })

            It("should error", func() {
                Expect(err).To(HaveOccurred())
            })
        })
    })
})

BDD Framework обычно содержитDescribe,Contextа такжеItи т.д. блок кода, гдеDescribeРоль заключается в описании независимого поведения кода.Contextнесколько разных контекстов в рамках одного акта, окончательныйItИспользуемые для описания ожидаемого поведения, эти блоки кода в конечном итоге образуют предложение типа «опишите…, когда…, следует…», чтобы помочь нам быстро понять тестовый код.

Имитационный метод

Юнит-тест в проекте должен быть стабильным и не зависеть ни от какого внешнего проекта, это всего лишь проверка функций и методов в проекте, поэтому нам нужно Mock все сторонние нестабильные зависимости в юнит-тесте, т.е. , имитировать интерфейсы этих сторонних сервисов; кроме того, чтобы упростить контекст модульного теста, мы также имитируем другие модули в том же проекте, чтобы имитировать возвращаемые значения этих зависимых модулей.

Суть модульного тестирования состоит в том, чтобы изолировать зависимости и проверить правильность ввода и вывода.Являясь статическим языком, язык Go предоставляет меньше функций времени выполнения, что также затрудняет имитацию зависимостей в языке Go.

Основная функция Mock — гарантировать, что контекст тестируемого метода фиксирован.В это время, независимо от того, сколько модульных тестов мы запускаем для текущего метода, если бизнес-логика не меняется, он должен возвращать точную тот же результат. Подробно представлены различные методы Mock. Прежде чем мы должны сначала очистить некоторые общие зависимости. Общие зависимости функции или метода могут быть следующими:

  1. интерфейс
  2. база данных
  3. HTTP-запрос
  4. Редис, кеш и т.д.

Эти различные сценарии в основном охватывают ситуации, возникающие при написании модульных тестов.Мы расскажем, как справляться с вышеупомянутыми различными зависимостями, в следующем содержании.

интерфейс

Первое, что нужно представить, это на самом деле самый распространенный и распространенный метод Mock в языке Go, то есть метод, который может Mock интерфейса.golang/mockFramework, который может генерировать реализации Mock на основе интерфейсов, предположим, что у нас есть следующий код:

package blog

type Post struct {}

type Blog interface {
	ListPosts() []Post
}

type jekyll struct {}

func (b *jekyll) ListPosts() []Post {
 	return []Post{}
}

type wordpress struct{}

func (b *wordpress) ListPosts() []Post {
	return []Post{}
}

Наш блог может использоватьjekyllилиwordpressкак двигатель, но они оба обеспечиваютListsPostsМетод используется для возврата списка всех статей, в это время нам нужно определитьPostИнтерфейс, интерфейс, которому необходимо следоватьBlogСтруктура должна реализоватьListPostsметод.

golang-interface-blog-example

когда мы определяемBlogПосле интерфейса верхний слойServiceВам больше не нужно полагаться на конкретную реализацию движка блога, вам нужно полагаться только наBlogИнтерфейс может завершить функцию сбора пакетов статей:

package service

type Service interface {
	ListPosts() ([]Post, error)
}

type service struct {
    blog blog.Blog
}

func NewService(b blog.Blog) *Service {
    return &service{
        blog: b,
    }
}

func (s *service) ListPosts() ([]Post, error) {
    return s.blog.ListPosts(), nil
}

если мы хотимServiceДля тестирования мы можем использоватьmockgenГенерация команд инструментаMockBlogstruct используйте следующую команду:

$ mockgen -package=mblog -source=pkg/blog/blog.go > test/mocks/blog/blog.go

$ cat test/mocks/blog/blog.go
// Code generated by MockGen. DO NOT EDIT.
// Source: blog.go

// Package mblog is a generated GoMock package.
...
// NewMockBlog creates a new mock instance
func NewMockBlog(ctrl *gomock.Controller) *MockBlog {
	mock := &MockBlog{ctrl: ctrl}
	mock.recorder = &MockBlogMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockBlog) EXPECT() *MockBlogMockRecorder {
	return m.recorder
}

// ListPosts mocks base method
func (m *MockBlog) ListPosts() []Post {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "ListPosts")
	ret0, _ := ret[0].([]Post)
	return ret0
}

// ListPosts indicates an expected call of ListPosts
func (mr *MockBlogMockRecorder) ListPosts() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPosts", reflect.TypeOf((*MockBlog)(nil).ListPosts))
}

этот абзацmockgenСгенерированный код очень длинный, поэтому мы показываем только часть этого. Его функция заключается в том, чтобы помочь нам проверить входные параметры любого интерфейса и имитировать возвращаемое значение интерфейса; и в процессе создания реализации MOCK, автор суммирует некоторые, которые могут быть общим опытом:

  1. существуетtest/mocksВсе реализации Mock помещаются в каталог. Подкаталог совпадает с вторичным каталогом файла, в котором расположен интерфейс. Здесь расположение исходного файла находится вpkg/blog/blog.go, его вторичный каталогblog/, поэтому соответствующая имплементация Mock будет сгенерирована дляtest/mocks/blog/в справочнике;
  2. уточнитьpackageзаmxxx,дефолтmock_xxxкажется очень избыточным, вышеblogПакет Mock, соответствующий пакету,mblog;
  3. mockgenпоместите команду вMakefileсерединаmockПри унифицированном управлении снижение появления предков;

     mock:
         rm -rf test/mocks
            
         mkdir -p test/mocks/blog
         mockgen -package=mblog -source=pkg/blog/blog.go > test/mocks/blog/blog.go
    

После того, как мы сгенерировали приведенный выше код реализации Mock, мы можем использовать следующие методы дляServiceНапишите модульный тест, этот код проходитNewMockBlogгенерироватьBlogСмоделируйте реализацию интерфейса, затем передайтеEXPECTМетод, управляющий реализацией, будет вызван, когдаListPostsвозвращает пустойPostМножество:


func TestListPosts(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

 	mockBlog := mblog.NewMockBlog(ctrl)
 	mockBlog.EXPECT().ListPosts().Return([]Post{})
  
 	service := NewService(mockBlog)
  
 	assert.Equal(t, []Post{}, service.ListPosts())
}

Из-за текущегоServiceтолько зависит толькоBlogРеализация , поэтому на данный момент мы можем утверждать, что текущий метод вернет[]Post{}, то возвращаемое значение нашего метода связано только с переданными параметрами (хотяListPostsМетод не имеет параметров), мы можем уменьшить контекст одного внимания и обеспечить стабильность и достоверность теста.

Это самый стандартный метод написания юнит-тестов на языке Go, все зависит отpackageПричина, по которой внутри, так и снаружи проекта следует обрабатывать таким образом (в случае интерфейса), если нет языка интерфейса Go, тесты единиц будут очень сложными для записи, именно поэтому интерфейс сможет судить, являются качество проекта от проекта a.

SQL

Другая общая зависимость в проектах на самом деле является базой данных. Когда мы столкнулись с зависимостями базы данных, мы обычно используемsqlmockЧтобы имитировать подключение к базе данных, когда мы используем sqlmock, мы напишем модульный тест, подобный следующему:

func (s *suiteServerTester) TestRemovePost() {
	entry := pb.Post{
		Id: 1,
	}

	rows := sqlmock.NewRows([]string{"id", "author"}).AddRow(1, "draveness")

	s.Mock.ExpectQuery(`SELECT (.+) FROM "posts"`).WillReturnRows(rows)
	s.Mock.ExpectExec(`DELETE FROM "posts"`).
		WithArgs(1).
		WillReturnResult(sqlmock.NewResult(1, 1))

	response, err := s.server.RemovePost(context.Background(), &entry)

	s.NoError(err)
	s.EqualValues(response, &entry)
	s.NoError(s.Mock.ExpectationsWereMet())
}

Наиболее распространенными методами являютсяExpectQueryиExpectExec, первый в основном используется для моделирования операторов SQL-запросов, а второй используется для имитации добавления и удаления SQL.Из приведенных выше примеров мы можем видеть, как используются эти два метода.Рекомендуется прочитать соответствующиеДокументацияПопробуйте использовать снова.

HTTP

HTTP-запрос также зависит, мы часто встречаемся в проекте,httpmockЭто пакет для всех HTTP-зависимостей Mock.Он использует сопоставление с образцом для сопоставления URL-адресов HTTP-запросов и возвращает предварительно заданный ответ при совпадении определенного запроса.

func TestFetchArticles(t *testing.T) {
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()

	httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles",
		httpmock.NewStringResponder(200, `[{"id": 1, "name": "My Great Article"}]`))

	httpmock.RegisterResponder("GET", `=~^https://api\.mybiz\.com/articles/id/\d+\z`,
		httpmock.NewStringResponder(200, `{"id": 1, "name": "My Great Article"}`))

	...
}

Если вы столкнулись с зависимостями HTTP-запроса, вы можете использовать приведенный вышеhttpmockПакет имитирует зависимые HTTP-запросы.

патч обезьяны

Последний патч с обезьянами, который мы представили, на самом деле является большим убийцей.bouk/monkeyМожно изменить, заменив указатели функцийпроизвольная функцияПоэтому, если ни один из вышеперечисленных методов не может удовлетворить наши потребности, мы можем полагаться только на более хакерский метод исправления обезьян, Mock:

func main() {
	monkey.Patch(fmt.Println, func(a ...interface{}) (n int, err error) {
		s := make([]interface{}, len(a))
		for i, v := range a {
			s[i] = strings.Replace(fmt.Sprint(v), "hell", "*bleep*", -1)
		}
		return fmt.Fprintln(os.Stdout, s...)
	})
	fmt.Println("what the hell?") // what the *bleep*?
}

Однако использование этого метода имеет некоторые ограничения, поскольку он заменяет указатель функции во время выполнения, поэтому, если вы столкнетесь с некоторыми простыми функциями, такими какrand.Int63nиtime.Now, компилятор может напрямую встроить эту функцию в код, где на самом деле происходит вызов, и не будет вызывать исходный метод, поэтому использование этого метода часто требует от нас указывать дополнительно при тестировании-gcflags=-lОтключает встроенную оптимизацию компилятора.

$ go test -gcflags=-l ./...

bouk/monkeyREADME дает некоторые предостережения относительно его использования, кроме встроенной компиляции, на что нам нужно обратить внимание:Не используйте исправление обезьян вне модульных тестов, мы должны использовать этот метод только при необходимости, например, в зависимой сторонней библиотеке, которая не предоставляетinterfaceили изменитьtime.Nowа такжеrand.Int63nи т. д., когда возвращаемое значение встроенной функции используется для тестирования.

Теоретически с помощью Monkey Patch мы можем запускать все функции на языке Mock Go во время выполнения, что также дает нам окончательное решение для модульного тестирования, зависящего от MOCK.

утверждение

В конце мы просто вводим вспомогательное модульное тестированиеassertpackage, который предоставляет множество методов утверждений, помогающих нам быстро проверить ожидаемое возвращаемое значение и снизить нашу рабочую нагрузку:

func TestSomething(t *testing.T) {
  assert.Equal(t, 123, 123, "they should be equal")

  assert.NotEqual(t, 123, 456, "they should not be equal")

  assert.Nil(t, object)

  if assert.NotNil(t, object) {
    assert.Equal(t, "Something", object.Value)
  }
}

Здесь мы также кратко покажемassertДля получения более подробной информации вы можете прочитать соответствующую документацию, поэтому я не буду показывать ее здесь.

резюме

Если вы вообще не писали модульные тесты или раньше не писали модульные тесты на языке Go, я считаю, что эта статья дала достаточно контекста, чтобы помочь нам начать это делать.Что нам нужно знать, так это то, что модульное тестирование нам не мешает. y прогресс в разработке, он дает уверенность в том, что мы приступим к работе, и является методом обеспечения качества с самой высокой окупаемостью инвестиций.

Обучение написанию хорошего модульного теста определенно потребует некоторого обучения и дискомфорта и даже повлияет на нашу эффективность разработки в краткосрочной перспективе, но после знакомства с этим набором процессов и интерфейсов модульный тест будет нам очень полезен. Модульные тесты представляют собой бизнес-логику.Выполнение модульных тестов при каждой отправке может помочь нам определить, что новый код не повлияет на существующую бизнес-логику с высокой вероятностью, что может значительно снизить риск рефакторинга и количество онлайн-аварий.

Суммировать

В этой статье мы познакомим вас с тем, как писать элегантный код на языке Go с трех аспектов.Автор дает максимально простой и эффективный метод:

  • Спецификация кода: Используйте вспомогательные инструменты, чтобы помочь нам автоматически проверять код каждый раз, когда мы отправляем PR, уменьшая нагрузку на ручную проверку инженеров;
  • Лучшие практики
    • Структура каталогов: следуйте общепринятому языковому сообществу Go.Структура каталогов, снизить коммуникационную стоимость проекта;
    • Разделение модулей: разделение различных модулей в соответствии с их обязанностями не должно появляться в проектах языка Go.model,controllerТакое имя пакета нарушает идею дизайна языка верхнего уровня;
    • Явное против неявного: устраните как можно большеinitфункция, которая гарантирует явный вызов метода и обработку ошибок;
    • Ориентированность на интерфейс: Ориентированность на интерфейс — это метод разработки, поддерживаемый языком Go, и он также может предоставить нам удобство для написания модульных тестов.Мы должны следовать фиксированному шаблону для предоставления внешних функций;
      1. использовать верхний регистрServiceметод внешнего воздействия;
      2. использовать строчные буквыserviceРеализовать методы, определенные в интерфейсе;
      3. пройти черезfunc NewService(...) (Service, error)инициализация функцииServiceинтерфейс;
  • Модульное тестирование: наиболее эффективный способ обеспечить качество разработки проекта;
    • Тестируемый: это означает интерфейсно-ориентированное программирование и сокращение логики, содержащейся в одной функции, с использованием «маленьких методов»;
    • Организация: используйте язык тестовой среды по умолчанию, с открытым исходным кодом.suiteИли рационально организовать юнит-тесты в стиле BDD;
    • Мок-методы: четыре различных фиктивных метода модульного тестирования;
      • gomock: Самый стандартный и самый рекомендуемый способ;
      • sqlmock: обрабатывать зависимую базу данных;
      • httpmock: обрабатывать зависимые HTTP-запросы;
      • monkey: универсальный метод, но используется только в крайнем случае, подобный код очень многословен и неинтуитивен;
    • Утверждение: использовать сообществоtestifyВозвращаемое значение метода быстрой проверки;

Само по себе написание элегантного кода — непростая задача, она требует от нас постоянного обновления и оптимизации нашей системы знаний, переворачивания предыдущего опыта и продолжения улучшения и рефакторинга проекта. постоянно подвергаться рефакторингу). Поведение случайного стека кода не поощряется и не должно происходить. Каждая строка кода должна быть спроектирована и разработана в соответствии с самыми высокими стандартами. Только так мы можем гарантировать качество проектирования.

Автор также усердно работал над тем, чтобы научиться писать более элегантный код. Написание хорошего кода — действительно непростая задача. Автор также надеется помочь инженерам, использующим язык Go, написать больше проектов в стиле Golang с помощью этой статьи.

Reference

О картинках и репринтах

知识共享许可协议
В этой работе используетсяМеждународная лицензия Creative Commons Attribution 4.0Лицензия. При перепечатке просьба указывать ссылку на оригинал.При использовании рисунка просьба сохранять все содержание на рисунке.Его можно соответствующим образом увеличить и ссылку на статью,где находится рисунок,прикрепить к ссылке.Картинка нарисована с помощью Скетча.

Публичный аккаунт WeChat

wechat-account-qrcode

О комментариях и сообщениях

Если эта статьяКак написать элегантный код GolangЕсли у вас есть какие-либо вопросы по содержанию, пожалуйста, оставьте сообщение в системе комментариев ниже, спасибо.

Оригинальная ссылка:Как написать элегантный код на golang · Программирование

Follow: Draveness · GitHub