Ответ 1
Для этого есть две высокоуровневые части: выяснение того, какой код входит в какой пакет, и настройка ваших API, чтобы уменьшить потребность пакетов в принятии как можно большего количества зависимостей.
О разработке API-интерфейсов, позволяющих избежать необходимости импорта:
Напишите функции конфигурации для подключения пакетов друг к другу во время выполнения, а не во время компиляции. Вместо
routes
, импортирующего все пакеты, которые определяют маршруты, он может экспортироватьroutes.Register
, который может вызыватьmain
(или код в каждом приложении). В общем, информация о конфигурации, вероятно, проходит черезmain
или выделенный пакет; слишком большое разбрасывание может затруднить управление.Передайте основные типы и значения
interface
. Если вы полагаетесь на пакет только для имени типа, возможно, вы можете избежать этого. Возможно, какой-то код, обрабатывающий[]Page
, может вместо этого использовать вместо него[]string
имен файлов или[]int
идентификаторов или какой-то более общий интерфейс (sql.Rows
).Подумайте о том, чтобы иметь пакеты 'схемы' с просто чистыми типами данных и интерфейсами, поэтому
User
отделен от кода, который может загружать пользователей из базы данных. Он не должен сильно зависеть (может быть, от чего-либо), поэтому вы можете включить его из любого места. Бен Джонсон выступил с молниеносной речью на GopherCon 2016, предложив это и упорядочив пакеты по зависимостям.
Об организации кода в пакеты:
Как правило, разбить пакет, когда каждый кусок может быть полезным сам по себе. Если две части функциональности действительно тесно связаны, вам вообще не нужно разбивать их на пакеты; вы можете организовать несколько файлов или типов вместо этого. Большие пакеты могут быть в порядке; Go
net/http
, например, один.Разбейте пакеты для пакетов (
utils
,tools
) по темам или зависимостям. В противном случае вы можете импортировать огромный пакетutils
(и взять на себя все его зависимости) для одной или двух частей функциональности (у которых не было бы так много зависимостей, если бы они были выделены).Подумайте о том, чтобы вставить повторно используемый код "вниз" в пакеты более низкого уровня, не связанные с вашим конкретным вариантом использования. Если у вас есть
package page
, содержащий как логику для вашей системы управления контентом, так и универсальный код HTML-манипуляции, рассмотрите возможность перемещения HTML-содержимого "вниз" вpackage html
, чтобы вы могли использовать его без импорта несвязанных элементов управления контентом.
Здесь я бы изменил порядок вещей, чтобы маршрутизатор не включал маршруты: вместо этого каждый пакет приложения вызывает метод router.Register()
. Это то, что делает пакет веб-инструментария Gorilla mux
. Ваши пакеты routes
, database
и constants
звучат как фрагменты низкого уровня, которые должны импортироваться кодом вашего приложения, а не импортировать его.
Как правило, попробуйте построить ваше приложение в слоях. Ваш высокоуровневый код приложения, специфичный для конкретного случая использования, должен импортировать более фундаментальные инструменты более низкого уровня, а не наоборот. Вот еще несколько мыслей:
Пакеты хороши для ветки независимо используемых битов функциональности с точки зрения вызывающей стороны. Для внутренней организации кода вы можете легко перетасовывать код между исходными файлами в пакете. Исходное пространство имен для символов, которые вы определяете в
x/foo.go
илиx/bar.go
, представляет собой просто пакетx
, и его не так сложно разделить/объединить файлы по мере необходимости, особенно с помощью такой утилиты, какgoimports
.Стандартная библиотека
net/http
составляет около 7 тыс. Строк (считая комментарии/пробелы, но не тесты). Внутренне, это разделено на много меньших файлов и типов. Но это один пакет, я думаю, потому что у пользователей не было причин, скажем, просто обрабатывать файлы cookie самостоятельно. С другой стороны,net
иnet/url
разделены, потому что они используют вне HTTP.Было бы хорошо, если вы можете протолкнуть "служебные" утилиты в библиотеки, которые являются независимыми и выглядят как собственные отшлифованные продукты, или аккуратно наложить собственное приложение (например, пользовательский интерфейс располагается поверх API, поверх некоторых основных библиотек и моделей данных). Точно так же "горизонтальное" разделение может помочь вам держать приложение в голове (например, слой пользовательского интерфейса разбивается на управление учетными записями пользователей, ядро приложения и инструменты администрирования, или что-то более мелкое, чем это). Но суть в том, что вы можете делиться или нет, как работает для вас.
Настройте API для настройки поведения во время выполнения, чтобы вам не приходилось импортировать его во время компиляции. Так, например, ваш URL-маршрутизатор может предоставить метод
Register
вместо импортаappA
,appB
и т.д. и читаяvar Routes
от каждого. Вы можете сделать пакетmyapp/routes
, который импортируетrouter
и все ваши просмотры и вызовыrouter.Register
. Основная идея заключается в том, что маршрутизатор представляет собой универсальный код, который не должен импортировать представления вашего приложения.Несколько способов собрать API конфигурации:
Передайте поведение приложения через
interface
илиfunc
s:http
можно передать пользовательские реализацииHandler
(конечно), но такжеCookieJar
илиFile
.text/template
иhtml/template
могут принимать функции, которые будут доступны из шаблонов (вFuncMap
).При необходимости экспортируйте функции быстрого доступа из вашего пакета: в
http
вызывающие абоненты могут либо создавать и отдельно настраивать некоторые объектыhttp.Server
, либо вызыватьhttp.ListenAndServe(...)
, который использует глобальныйServer
. Это дает вам хороший дизайн - все в объекте и вызывающих объектах может создать несколькоServer
в процессе и тому подобное - но также предлагает ленивый способ настройки в простом случае с одним сервером.Если вам нужно, просто приклейте это на пленку: вам не нужно ограничивать себя сверх элегантными конфигурационными системами, если вы не можете приспособить их к своему приложению: возможно, для некоторых вещей
package "myapp/conf"
с глобальнымvar Conf map[string]interface{}
является полезным. Но следует помнить о недостатках глобальной конф. Если вы хотите написать многократно используемые библиотеки, они не могут импортироватьmyapp/conf
; им нужно принимать всю информацию, которая им нужна в конструкторах и т.д. Глобальные переменные также рискуют получить жесткую привязку в предположении, что что-то всегда будет иметь одно значение для всего приложения, когда это в конечном итоге не будет; может быть, сегодня у вас есть одна конфигурация базы данных или конфигурация HTTP-сервера или что-то подобное, но когда-нибудь вы этого не сделаете.
Некоторые более конкретные способы перемещения кода или изменения определений для уменьшения проблем с зависимостями:
Отделите фундаментальные задачи от зависящих от приложения. В одном приложении, над которым я работаю на другом языке, есть модуль "utils", смешивающий общие задачи (например, форматирование даты-времени или работу с HTML) со специфическими для приложения материалами (которые зависят от пользователя). схема и т.д.). Но пользовательский пакет импортирует утилиты, создавая цикл. Если бы я портировал на Go, я бы переместил зависимые от пользователя утилиты "вверх" из модуля утилит, возможно, чтобы жить с кодом пользователя или даже над ним.
Подумайте о том, чтобы разбить пакеты "grab-bag". Немного расширив последний пункт: если две части функциональности независимы (то есть все работает, даже если вы перемещаете некоторый код в другой пакет) и не связаны с точки зрения пользователя, они ' Ре кандидаты должны быть разделены на два пакета. Иногда связывание безвредно, но в других случаях оно приводит к дополнительным зависимостям, или менее универсальное имя пакета просто сделает более понятный код. Таким образом, мой
utils
выше может быть разбит по темам или зависимостям (например,strutil
,dbutil
и т.д.). Если вы получите множество пакетов таким образом, у нас естьgoimports
, чтобы помочь управлять ими.Замените типы объектов, требующие импорта, в API на базовые типы и
interface
. Скажем, два объекта в вашем приложении имеют отношения многие ко многим, такие какUser
иGroup
s. Если они живут в разных пакетах (большое "если"), вы не можете заставитьu.Groups()
возвращать[]group.Group
иg.Users()
, возвращающие[]user.User
, потому что для этого требуется, чтобы пакеты импортировали друг друга.Однако вы можете изменить один или оба из них, скажем,
[]uint
идентификаторов, илиsql.Rows
, или какой-нибудь другойinterface
, к которому вы можете добраться, безimport
определенного типа объекта. В зависимости от вашего варианта использования, такие типы, какUser
иGroup
могут быть настолько тесно связаны, что лучше просто поместить их в одну упаковку, но если вы решите, что они должны быть разными, это один из способов.
Спасибо за подробный вопрос и продолжение.