GoNetNinja: дни 1 и 2


Справочная информация

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

Цель программы

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

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

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

Конкретные цели

Я хочу, чтобы веб-приложение

  1. Позволяло членам клуба планировать сети, составлять список предстоящих и прошедших сетей.
  2. Записывать каждое действие (открытие сети, регистрация, комментарии, изменение контроля сети) для сети.
  3. Возможность просмотра всей активности для одной сети или всей активности для нескольких сетей.
  4. Простая форма отчета по сетке «после действия» для создания и записи активности в сети за один раз.
    1. Запуск и остановка сети
    2. Оператор(ы) управления сетью
    3. Регистрация сети (ранняя, регулярная)
    4. комментарии сети
    5. После отправки формы запланированная сеть будет зарегистрирована, с отдельными активами, рассчитанными на основе полей начала/остановки сети.
  5. Имеется интерфейс для редактирования активности сети после факта. Простая сеть после выполнения действия будет находиться здесь.
  6. Интерфейс для записи сетевой активности в реальном времени, временные метки по умолчанию «сейчас». Возможно, это будет примерно та же форма, что и страница редактирования сети.

Я также обхожу многие из своих обычных процессов. Например, я являюсь бэкенд-разработчиком, ориентированным на 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.

Создание сети с правильным именем и выбором времени

И, наконец, просмотр единственной сети, которую мы только что создали.

Оцените статью
Procodings.ru
Добавить комментарий