Справочная информация
Я предпринимал несколько попыток изучить Go, включая некоторые онлайн-курсы. Через некоторое время они прекратились. Как правило, я обнаружил, что для того, чтобы начать изучать новый язык программирования, вам нужна конкретная цель и время, чтобы сосредоточиться на этой цели. Если у вас амбициозная цель, у вас не будет достаточно времени. Такие вещи, как «закончить этот курс», не являются для меня достаточно хорошей целью.
Цель программы
В конце концов я остановился на цели — менеджер по регистрации радиосетей. В радиолюбительской среде мы часто проводим так называемые «направленные сети», которые чем-то похожи на конференц-связь, но один человек назначается «управляющим сетью». Он начинает работу, делая объявление в эфире, запрашивает регистрацию и направляет разговор. Типичная небольшая сеть запрашивает регистрацию, принимает
5 или около того комментариев, запрашивает больше комментариев и продолжает принимать комментарии в 2 или более раундах, прежде чем «закрыть» сеть.
После сети они рассылают отчет о сети с указанием времени начала и окончания сети, а также тех, кто зарегистрировался. Они также могут включить список тех, кто зарегистрировался, а также любые заслуживающие внимания
новости из сети. Такие простые сети обычно легко отследить и написать быстрое письмо с кратким описанием активности сети.
В более длинной и сложной сети операторы могут меняться несколько раз, а постоянные операторы могут по несколько раз входить и выходить из сети.
Конкретные цели
Я хочу, чтобы веб-приложение
- Позволяло членам клуба планировать сети, составлять список предстоящих и прошедших сетей.
- Записывать каждое действие (открытие сети, регистрация, комментарии, изменение контроля сети) для сети.
- Возможность просмотра всей активности для одной сети или всей активности для нескольких сетей.
- Простая форма отчета по сетке «после действия» для создания и записи активности в сети за один раз.
- Запуск и остановка сети
- Оператор(ы) управления сетью
- Регистрация сети (ранняя, регулярная)
- комментарии сети
- После отправки формы запланированная сеть будет зарегистрирована, с отдельными активами, рассчитанными на основе полей начала/остановки сети.
- Имеется интерфейс для редактирования активности сети после факта. Простая сеть после выполнения действия будет находиться здесь.
- Интерфейс для записи сетевой активности в реальном времени, временные метки по умолчанию «сейчас». Возможно, это будет примерно та же форма, что и страница редактирования сети.
Я также обхожу многие из своих обычных процессов. Например, я являюсь бэкенд-разработчиком, ориентированным на API. Обычно я пишу API, генерирую документацию swagger и работаю над фронтендом в последнюю очередь. Либо фронтенд делает другая команда, либо я могу сделать его в качестве демонстрации API. В этом случае я пишу все для фронтенда, практически все HTML-формы. Такие вещи, как аутентификация, тестирование и api-документы, относятся к категории «хорошо, но не нужно для MVP» (я всегда могу реализовать базовую http-авторизацию, если потребуется).
Будущая цель этого проекта — создать локальную базу данных поиска позывных, с возможностью поиска через сервис типа hammcall.dev.
Сроки
У меня есть около 5 дней в отпуске, с понедельника по пятницу. В субботу я еду домой. Да, я провожу часть своего отпуска за написанием кода, но я действительно получаю удовольствие от создания вещей. Это немного авантюра, потому что если у меня не получится сделать большую часть работы, я могу расстроиться.
Процесс
По общему мнению сообщества go, нужно отказаться от тяжелых фреймворков и учиться писать с нуля. На самом деле, как человек, перешедший с Python на Django, Flask и FastApi, я могу с этим согласиться. Однако у меня есть 5 дней, поэтому я перебрал кучу вариантов и остановился на Buffalo. У него есть маршрутизатор, модель и контроллер, которые кажутся мне знакомыми, и некоторые django-подобные генераторы. Я не знаю, лучший ли это фреймворк, слишком ли много фреймворка, не будет ли он мешать мне и т.д. Были некоторые другие фреймворки, которые я хотел проверить, такие как Beego и Gorilla. Beego кажется еще более похожим на django, а Gorilla называет себя набором инструментов (выбирайте сами). Martini очень напомнил мне Flask. В итоге я выбрал Buffalo, потому что он заявляет о горячей перезагрузке кода, фронтенд-конвейере, использует gorilla под капотом для маршрутизации и имеет ORM.
База данных для начала будет sqlite3, так как если вам не нужна HA, sqlite довольно производительна и не требует больших затрат.
Начало работы
Документы по началу работы работают, я смог выполнить установку, сгенерировать свой проект, добавить несколько временных пользовательских страниц. Существует основной app.go для вашего маршрутизатора, и каждый маршрут сопоставлен с обработчиком. У обработчика есть Context, который включает все переменные, которые ему нужны, и вы можете получить доступ к этому контексту в шаблоне plush.
База данных
Мне пришлось пересоздать проект пару раз, чтобы установить тип базы данных sqlite вместо Postgres.
по умолчанию.
buffalo new --db-type sqlite3 gonetninja
Я создал директорию data/
для файлов базы данных, добавил их в .gitignore, отредактировал файл database.yml, чтобы исправить путь, а затем запустил buffalo pop create
для создания базы данных
пустые файлы базы данных. В sqlite, я думаю, вам не нужно делать этот последний шаг, поскольку большинство sqlite-накопителей создают файл автоматически.
модели
Я перешел к созданию моделей баз данных. Я обнаружил, что именно здесь документация впервые подвела меня. Если вы soda generate mode {modelname}
, она создает относительно пустую модель и несколько файлов миграции с базовым набором столбцов (ID, CreatedAt, UpdatedAt). Если я отредактирую эту модель и попытаюсь запустить повторную генерацию файлов миграции, эти файлы получаются пустыми.
В таких инструментах, как django, sqlalchemy и alembic, я ожидаю создать свою модель, затем создать свои миграции, которые создадут файл с изменениями, необходимыми для приведения базы данных в соответствие с требованиями.
скорость.
buffalo pop generate model foo -d # creates model, migration up, migration down
# edit model to add Name column
buffalo pop generate fizz foo2 # creates empty migration files
Я также попробовал сначала сделать таблицу, и pop/soda не проверяет базу данных, чтобы сделать модели или приспособления.
sqlite> create table otherfoo (id uuid NOT NULL, created_at timestamp NOT NULL, updated_at timestamp NOT NULL, name character varying(255));
Далее
buffalo pop generate model otherfoo
Итак, на данный момент, я закончил создание таблицы для сетей и создал таблицы вручную. У меня нет файлов миграции для этой таблицы, мне просто придется вернуться и выяснить, как это должно работать позже.
Мой модифицированный models/net.go:
type Net struct {
ID uuid.UUID `json:"id" db:"id"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
Name string `json:"name" db:"name"`
PlannedStart time.Time `json:"planned_start" db:"planned_start"`
PlannedEnd time.Time `json:"planned_end" db:"planned_end"`
}
Мой sql вручную:
sqlite> CREATE TABLE nets (id uuid NOT NULL, created_at timestamp NOT NULL, updated_at timestamp NOT NULL, name character varying(255), planned_start timestamp, planned_end timestamp);
sqlite> insert into nets values (uuid(), strftime('%Y-%m-%d %H-%M-%S','now'), strftime('%Y-%m-%d %H-%M-%S','now'), "test one", strftime('%Y-%m-%d %H-%M-%S','now'), strftime('%Y-%m-%d %H-%M-%S','now'));
sqlite> insert into nets values (uuid(), strftime('%Y-%m-%d %H-%M-%S','now'), strftime('%Y-%m-%d %H-%M-%S','now'), "test two", strftime('%Y-%m-%d %H-%M-%S','now'), strftime('%Y-%m-%d %H-%M-%S','now'));
sqlite> insert into nets values (uuid(), strftime('%Y-%m-%d %H-%M-%S','now'), strftime('%Y-%m-%d %H-%M-%S','now'), "test three", strftime('%Y-%m-%d %H-%M-%S','now'), strftime('%Y-%m-%d %H-%M-%S','now'));
sqlite> insert into nets values (uuid(), strftime('%Y-%m-%d %H-%M-%S','now'), strftime('%Y-%m-%d %H-%M-%S','now'), "test the fourth", strftime('%Y-%m-%d %H-%M-%S','now'), strftime('%Y-%m-%d %H-%M-%S','now'));
sqlite> .dump nets
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE nets (id uuid NOT NULL, created_at timestamp NOT NULL, updated_at timestamp NOT NULL, name character varying(255), planned_start timestamp, planned_end timestamp);
INSERT INTO nets VALUES('c31911fc-5d3c-4b18-b4b1-1e081aa6effd','2022-06-20 23-47-29','2022-06-20 23-47-29','test one','2022-06-20 23-47-29','2022-06-20 23-47-29');
INSERT INTO nets VALUES('ccad3aad-c9ea-4891-a604-8d02e0968ce8','2022-06-20 23-47-47','2022-06-20 23-47-47','test two','2022-06-20 23-47-47','2022-06-20 23-47-47');
INSERT INTO nets VALUES('66b685a9-ea20-4a14-b766-4d23f362be4b','2022-06-20 23-48-17','2022-06-20 23-48-17','test three','2022-06-20 23-48-17','2022-06-20 23-48-17');
INSERT INTO nets VALUES('2bc9c10a-4056-45bc-bbb6-6a482bbb30a9','2022-06-21 13-20-20','2022-06-21 13-20-20','test the fourth','2022-06-21 13-20-20','2022-06-21 13-20-20');
COMMIT;
sqlite>
Я почти уверен, что на данный момент у меня проблема с временной меткой и часовыми поясами. Будет решена позже ^tm.
Отображение данных
Теперь, когда у меня есть мои сети, пришло время считать их на страницу. Страница запроса дала некоторую базовую идею, но не достаточную, чтобы продвинуться куда-либо. Возможно, это связано с недостаточным знакомством с go. Я помнил из другого обучения, что методы возвращают err, и если вы находите этот набор, вы бросаете исключение. Но их пример запроса для всех строк в основном такой:
// To retrieve records from the database in a specific order, you can use the Order method
users := []User{}
err := models.DB.Order("id desc").All(&users)
Я мог видеть, как он выдает SQL-запросы в отладочных журналах, и даже научился устанавливать «пользователей» (в моем случае net) в контекст. Но в итоге я обнаружил, что мне нужно установить соединение,
обернуть это err внутри if, чтобы получить соответствующее повышение, а затем установить контекст.
func NetListHandler(c buffalo.Context) error {
tx := c.Value("tx").(*pop.Connection)
nets := models.Nets{}
if err := tx.Order("name").All(&nets); err != nil {
return errors.WithStack(err)
}
c.Set("nets", nets)
return c.Render(http.StatusOK, r.HTML("home/netlist.plush.html"))
}
Модель «models.Nets» предопределена. В ряде других примеров нужно было сделать что-то вроде nets := []models.Net{}
, но это уже существовало в сгенерированном models/nets.go.
Шаблон plush будет знать о переменной «nets», которую вы можете итерировать.
<%= for (net) in nets { %>
<tr>
<td class="left">
<a href="/nets/<%= net.ID %>"><%= net.Name %></a>
</td>
<td>
<%= net.PlannedStart %>
</td>
<td>
<%= net.PlannedEnd %>
</td>
</tr>
<% } %>
Некоторые примеры, которые я видел, имели <%= variablename =>
, что давало всякие интересные ошибки, но <%= variablename %>
.
Пути
Я жестко закодировал /nets/{netid} в url, но маршрутизатор bufallo/gorilla позволяет мне называть эти пути. Есть и автоматически генерируемые, но мне нравится называть их явно.
app.GET("/nets", NetListHandler).Name("netlistPath")
//app.GET("/nets/{id}", func(c buffalo.Context) error {
// return c.Render(200, r.String(c.Param("id")))
//}).Name("netViewPath")
app.GET("/nets/{id}", NetHandler).Name("netViewPath")
Затем в моем шаблоне я могу указать net.ID в качестве параметра.
<a href="<%= netViewPath({id: net.ID}) %>"><%= net.Name %></a>
Одиночный вход
Для просмотра одной сети все почти идентично, но я получаю идентификатор от маршрутизатора.
app.GET("/nets/{id}", NetHandler).Name("netViewPath")
Маршрутизатор добавляет Params и Param к контексту, поэтому c.Param("id")
дает мне {id} часть пути.
Кроме того, я узнал, что models.DB заменяет tx pop.Connection!
func NetHandler(c buffalo.Context) error {
//tx := c.Value("tx").(*pop.Connection)
net := models.Net{}
//if err := tx.Find(&net, c.Param("id")); err != nil {
// return errors.WithStack(err)
//}
if err := models.DB.Find(&net, c.Param("id")); err != nil {
return errors.WithStack(err)
}
c.Set("net", net)
return c.Render(http.StatusOK, r.HTML("home/netview.plush.html"))
}
Печаль горячей перезагрузки
Запуск в режиме разработки говорит
Команда dev по умолчанию будет следить за вашими файлами .go и .html, а также за папкой asset. Она автоматически перестроит и перезапустит бинарник, так что вам не придется беспокоиться о таких вещах.
По моему опыту, в реальном времени перезагружались только правки html-файлов. Я не думаю, что он действительно перестраивает бинарный файл, а просто видит новый шаблон. Я не видел никаких релевантных проблем, кроме #602 от 2017 года, связанной с docker и монтированием NFS. Но это может быть новая проблема с MacOS Montery (на M1). Если она продолжит беспокоить меня, я буду копать глубже и, возможно, отправлю запрос. Это будет связано с inotify/fsnotify.
https://github.com/gobuffalo/buffalo/issues/510
https://github.com/fsnotify/fsnotify/issues/152
(Примечание по таймеру: начал отслеживать 6/21 @ 7:03PM. Возможно, прошло 3 часа?)
go: обновлено github.com/fsnotify/fsnotify v1.5.1 => v1.5.4
go: обновлено golang.org/x/sys v0.0.0-20211205182925-97ca703d548d => v0.0.0-20220412211240-33da011f77ad
Создайте сеть
После создания формы с именем, начальным и конечным временем, создание CreateNetHandler
было действительно простым.
models.DB.ValidateAndCreate(net)
вроде как существует по умолчанию. Однако моя база данных изначально не сделала «имя» обязательным полем.
func CreateNetHandler(c buffalo.Context) error {
net := &models.Net{}
if err := c.Bind(net); err != nil {
return err
}
newId, err := uuid.NewV1()
if err != nil {
return err
}
net.ID = newId
// Validate the data from the html form
verrs, err := models.DB.ValidateAndCreate(net)
if err != nil {
return errors.WithStack(err)
}
if verrs.HasAny() {
c.Set("net", net)
// Make the errors available inside the html template
c.Set("errors", verrs)
return c.Render(422, r.HTML("home/netnew.plush.html"))
}
c.Flash().Add("success", "Net was created successfully")
return c.Redirect(302, "/nets/%s", net.ID)
}
Я обнаружил, что даже после обновления таблицы nets, чтобы обеспечить принудительное использование not null для каждого столбца, я все равно могу отправлять формы с пустым именем. Похоже, что мы получаем пустую
строку, которая на самом деле не является null. Я думаю, что это нужно исправить сейчас, а не потом.
Моей первой попыткой было заблокировать это в функции Validate. Если бы у нас было много данных с пустыми именами, я бы сделал это в ValidateCreate. Но на данном этапе разработки я планирую очистить все плохие данные в базе данных.
// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method.
// This method is not required and may be deleted.
func (n *Net) Validate(tx *pop.Connection) (*validate.Errors, error) {
if n.Name == "" {
return validate.NewErrors(), errors.New("Name can not be blank")
}
return validate.NewErrors(), nil
}
Проблема с этой проверкой в том, что она выбрасывает ошибку 500 с трассировкой стека. Я бы предпочел получить меньшую ошибку, которая отображается на форме. К счастью, это задокументировано в использовании валидации buffalo. К сожалению, нет четких примеров использования этого метода в сочетании с validate.NewErrors().
func (n *Net) Validate(tx *pop.Connection) (*validate.Errors, error) {
verrs := validate.NewErrors()
if n.Name == "" {
verrs.Add("name", "Name must not be blank!")
}
//verrs.Add(&validators.StringIsPresent{Field: n.Name, Name: "Name", Message: "Name can not be blank"})
return verrs, nil
}
Существует другой синтаксис &validators.StringIsPresent
, но использование другое, и приведенный пример отличается от предыдущих. Моя лучшая подсказка о том, как реализовать, взята из выпуска 2177, где демонстрируется использование подхода validate.NewErrors().Add
/ verrs.Add
.
Итоги второго дня
Я начал писать приложение вечером в понедельник, а основное время провел во вторник. Только через пару часов я начал делать эти заметки и установил таймер. Я скажу, что у меня есть 2-3
часа, и, согласно моему таймеру, сейчас я нахожусь на отметке 1 час 37 минут.
Гнойная проблема — временные метки. Я хочу хранить время в UTC, но отображать его в местном часовом поясе пользователя. Даты из веб-формы отправляются по местному времени, а в базе данных хранятся как UTC (+0000). В других проектах (Python) я мог использовать javascript для получения часового пояса браузера, и я мог использовать методы datetime tz в бэкенде для сохранения в UTC. Я уверен, что здесь есть похожий подход, так что, надеюсь, я найду время, чтобы реализовать здесь правильное зонирование времени.
Еще одна будущая валидация — не позволять сети заканчиваться раньше, чем она начнется.
Проект опубликован на github ytjohn/gonetninja
Скриншоты
Здесь страница списка сетей
Здесь показано создание новой сети с пустой валидацией имени. Просто щелкаем по имени и нажимаем Enter.
Создание сети с правильным именем и выбором времени
И, наконец, просмотр единственной сети, которую мы только что создали.