Итак, наше приложение будет иметь две основные части: клиентская и серверная. (Какой сейчас год?). Серверная часть будет на Go, а клиентская — на JS. Давайте сначала поговорим о серверной части. Серверная часть нашего приложения будет ответственной за первоначальное обслуживание всего необходимого для JavaScript и всего остального, типа статических файлов и данных в формате JSON. Это все, всего две функциональности: (1) статика и (2) JSON. Стоит отметить, что обслуживание статики опционально: статику можно обслуживать в CDN, например. Но важно то, что это не проблема для нашего Go приложения — в отличие от Python/Ruby приложения, оно может работать наравне с Ngnix и Apache, обслуживающими статику. Делегирование раздачи статических файлов какому-то другому приложению для облегчения нагрузки особо не требуется, хотя и имеет смысл в некоторых ситуациях. Для упрощения давайте представим, что мы создаем приложение, которое обслуживает список людей (только имена и фамилии), хранящийся в таблицы базы данных, и все. Код находится здесь — https://github.com/grisha/gowebapp. Как показывает мой опыт, разделение функциональности между пакетами на ранней стадии является хорошей идеей в Go. Даже если не совсем ясно, как будет структурирована финальная версия, по возможности лучше все держать в разложенном виде. Для веб-приложения, на мой взгляд, имеет смысл такой макет: На верхнем уровне у нас расположен пакет main, а его код — в файле main.go. Главный плюс в том, что при таком раскладе go get github.com/user/foo — это единственная команда, требуемая для установки всего приложения в $GOPATH/bin. Пакет main должен быть минимальным, насколько это возможно. Единственный код, который тут находится, — это анализ аргументов команды. Если у приложения был бы конфигурационный файл, я поместил бы парсинг и проверку этого файла в еще один пакет, который, скорее всего, назвал бы config. После этого main должен передать управление пакету daemon. Вот основа main.go: Приведенный код принимает три параметра: -listen, -db-connect и -assets-path, ничего особенного. В строке cfg := &daemon.Config{} мы создаем объект daemon.Config. Его основная цель — представить конфигурацию в структурированном и понятном формате. Каждый из наших пакетов определяет свой собственный тип Config, описывающий необходимые ему параметры, и который может включать в себя настройки других пакетов. Мы видим пример этого в processFlags() выше: flag.StringVar(&cfg.Db.ConnectString, .... Здесь db.Config включен в daemon.Config. На мой взгляд, это очень полезный прием. Использование структур также оставляет возможность сериализации настроек в виде JSON, TOML или еще чего-нибудь. http.Dir(assetsPath) в setupHttpAssets — это подготовка к тому, как мы будем обслуживать статику в пакете ui. Сделано это именно так, чтобы оставить возможность для другой реализации cfg.UI.Assets (который является интерфейсом http.FileSystem), например, отдавать этот контент из оперативной памяти. Я расскажу об этом более детально позже, в отдельном посте. В конце концов, main вызывает daemon.Run(cfg), который фактически запускает наше приложение и блокируется до момента завершения работы. Пакет daemon содержит все, что связано с запуском процесса. Сюда относится, например, какой порт будет прослушиваться, здесь будет определен пользовательский журнал, а также все, что связано с вежливым перезапуском и т.д. Поскольку задачей пакета daemon является инициализация подключения к базе данных, ему нужно импортировать пакет db. Он также отвечает за прослушивание TCP порта и запуск пользовательского интерфейса для этого слушателя, поэтому ему необходимо импортировать пакет ui, а поскольку пакету ui необходимо иметь доступ к данным, который обеспечивается пакетом model, ему также необходимо импортировать пакет model. Скелет модуля daemon выглядит примерно так: Обратите внимание, Config включает db.Config и ui.Config, как я уже упоминал. Все действие происходит в Run(*Config). Мы инициализируем соединение с базой данных, создаем экземпляр model.Model и запускаем ui, передавая ему настройки, указатели на модель и слушателя. Назначение model заключается в отделении того, как данные хранятся в базе данных от ui, а также в обеспечении бизнес-логики, которую может иметь приложение. Это мозг вашего приложения, если угодно. Пакет model должен определить структуру (Model выглядит подходящим названием), а указатель на экземпляр этой структуры должен быть передан всем функциям и методам ui. В нашем приложении должен быть только один такой экземпляр — для дополнительной уверенности вы можете реализовать это программно с помощью синглтона, но я не думаю, что это так уж необходимо. В качестве альтернативы, вы можете обойтись без структуры Model и просто использовать сам пакет model. Мне не нравится такой подход, тем не менее это вариант. Модель также должна определять структуры для сущностей данных, с которыми мы имеем дело. В нашем примере это будет структура Person. Его члены должны быть экспортированы (именованы с заглавной буквы), потому что другие пакеты будут к ним обращаться. Если вы используете sqlx, здесь же необходимо указать тэги, которые привязывают элементы структуры к названиям колонок в БД, например db:"first_name". Наш тип Person: Тут нам не нужны тэги, потому что имена колонок соответствуют именам элементов структуры, а sqlx позаботится о регистре так, что Last соответствует колонке с именем last. Несколько контринтуитивно то, что model не должен импортироватьdb. Но он не должен потому, что пакету db необходимо импортировать model, а цикличные импорты запрещены в Go. Это тот самый случай, когда очень пригождаются интерфейсы. model необходимо задать интерфейс, которому должен удовлетворять db. Пока мы только знаем, что нам нужен список людей, поэтому можем начать с этого определения: Наше приложение делает не очень много, но мы знаем, что в нем перечислены люди, поэтому наша модель, скорее всего, должна иметь метод People() ([]*Person, error): Чтобы все было аккуратно, код лучше размещать в разных файлах, например структура Person должна быть определена в person.go, и т.д. Но для удобочитаемости здесь представлена однофайловая версия нашего пакета model: db — это фактическая реализация взаимодействия с базой данных. Здесь конструируются и исполняются операторы SQL. Этот пакет также импортирует model, п.ч. он должен будет создать эти структуры из данных базы данных. В первую очередь, db должен предоставить функцию InitDB, которая установит соединение с базой данных, а также создаст необходимые таблицы и подготовит SQL запросы. Наше упрощенный пример не поддерживает миграции, но, теоретически, именно здесь их и надо выполнять. Мы используем PostgreSQL, что означает, что нам необходимо импортировать драйвер pq. Мы также будем полагаться на sqlx и нам нужна наша model. Вот начало реализации нашего db: Экспортируемая функция InitDb() создает экземпляр pgDb, который является Postgres-реализацией нашего интерфейса model.db. Он содержит все необходимое для связи с базой данных, включая подготовленные запросы, и реализует необходимые интерфейсу методы. Ниже приведен код для создания таблиц и подготовки запросов. С точки зрения SQL тут все довольно упрощенно и, конечно, есть куда совершенствовать: Наконец, нам нужно предоставить метод, реализующий интерфейс: Здесь мы используем преимущество sqlx для выполнения запроса и построения слайса из результатов, просто вызывая Select() (Обратите внимание: p.sqlSelectPeople имеет тип *sqlx.Stmt). Без sqlx нам надо было бы итерироваться по строкам результата, обрабатывая каждую с помощью Scan, что получилось бы более многословно. Остерегайтесь одного очень тонкого момента. people можно было бы определить как var people []*model.Person и метод работал бы точно так же. Однако, если база данных вернет пустой набор строк, метод вернет nil, а не пустой слайс. Если результат данного метода позднее будет закодирован в JSON, то он станет null, а не . Это может вызвать проблемы, если клиентская сторона не знает, как обращаться с null. Вот и все для db. В конце концов, нам нужно обслуживать все это через HTTP, и это именно то, что делает пакет ui. Вот очень упрощенный вариант: Обратите внимание, что indexHTML почти ничего не содержит. Это почти на 100% весь HTML, который будет использовать наше приложение. Он немного изменится, когда мы приступим к клиентской части приложения, буквально на несколько строк. Также следует отметить как определяется обработчик. Если эта идиома вам не знакома, стоит потратить несколько минут (или день), чтобы усвоить ее полностью, поскольку она очень распространена в Go. indexHandler() — это не сам обработчик, он возвращает функцию-обработчик. Это делается таким образом для того, чтобы мы могли передать *model.Model через замыкание, так как сигнатура функции-обработчика HTTP фиксирована и указатель на модель не является одним из ее параметров. Пока мы в indexHandler() ничего не делаем с указателем на модель, но когда дойдем до фактической реализации списка людей, он нам понадобится. Выше перечислено, по сути, все, что необходимо знать для создания базового веб-приложения на Go, по крайней мере со стороны Go. В следующей статье я займусь клиентской частью и мы завершим код списка людей. Продолжение АСУшник