golang

Как я написал пакет для быстрого создания и управления формами в Go

  • суббота, 8 марта 2025 г. в 00:00:14
https://habr.com/ru/articles/888728/

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

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


Как всё начиналось

Идея создания goform появилась во время работы над одним из моих пет-проектов. Я заметил, что постоянно приходится писать однотипный код для создания, рендеринга и валидации форм. Это было достаточно утомительно потому как в каждой форме приходилось руками подставлять значения. Мне захотелось создать инструмент, который бы автоматизировал эти процессы и позволил сосредоточиться на логике приложения, а не на рутинных задачах.

Так и родилась идея goform - пакета, который предоставляет удобный API для работы с HTML-формами в Go. Основная цель - сделать процесс создания и обработки форм максимально простым и гибким.

Какой у goform функционал на данный момент?

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

  1. Упрощение создания форм: Вместо того чтобы вручную прописывать каждое поле и его атрибуты, вы можете описать форму с помощью структуры Go, и GoForm сделает всё остальное.

  2. Гибкость рендеринга: Пакет поддерживает рендеринг как в HTML, так и в JSON, что делает его универсальным для монолитных приложений и разделённых фронтенда и бэкенда.

  3. Валидация данных: Пакет предоставляет встроенные правила валидации и позволяет добавлять кастомные.

  4. Поддержка AJAX: Пакет автоматически определяет AJAX-запросы и возвращает данные в формате JSON.

  5. Интеграция с фреймворками: goform легко интегрируется с фреймворком - Echo (просто потому что в своей практике написания веб-приложений на Go, я применяю его в 99% случаев).

Об остальном функционале сможете прочитать подробнее в документации пакета (ссылки предоставлю в конце статьи).


Основные функции пакета

1. Создание формы

Для создания формы достаточно описать её структуру и вызвать функцию NewForm:

Скрытый текст
// Инициализируем структуру (поля будущей формы).
type RegistrationForm struct {
    Username string `form:"username" validate:"required,min=3"`
    Email    string `form:"email" validate:"required,email"`
    Password string `form:"password" validate:"required,min=6"`
    Method   string `form:"-"`
    FormID   string `form:"-"`
}

func main() {
    http.HandleFunc("/someurl", func(w http.ResponseWriter, r *http.Request) {
        model := &RegistrationForm{
            Method: "SomeMethod",
            FormID: "SomeFormID",
        }
        /*
            model - структура, описывающая поля формы.
            Method - HTTP-метод формы (например, "POST").
            FormID - уникальный идентификатор формы.
        */
        form := core.NewForm(model, model.Method, model.FormID)

        // Остальной код
    })
}

2. Рендеринг формы

GoForm поддерживает рендеринг как в HTML, так и в JSON. Это особенно полезно, если фронтенд и бэкенд разделены:

Скрытый текст
response := form.ToResponse()

if form.RenderHTML {
    // Рендеринг HTML
    _ = tmpl.ExecuteTemplate(w, "имя_шаблона.html", response)
} else {
    // Возврат JSON
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

3. Валидация данных

Пакет предоставляет встроенные правила валидации, такие как requiredminmaxemail, и позволяет добавлять кастомные:

Скрытый текст
form.AddCustomValidation("password", func(value string) error {
    if len(value) < 6 {
        return errors.New("password must be at least 6 characters long")
    }
    return nil
})

4. Обработка AJAX-запросов

Пакет автоматически определяет AJAX-запросы и возвращает данные в формате JSON:

Скрытый текст
if isAjax(r) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(form.ToResponse())
}

5. Поддержка CSRF-токенов

Пакет поддерживает генерацию и проверку CSRF-токенов для защиты от атак:

Скрытый текст
token, err := core.GenerateCSRFToken()
if err != nil {
    http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
}
form.AddCSRFToken(token)


С чем я столкнулся во время разработки?

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

  1. Гибкость рендеринга: Одна из главных задач - сделать пакет универсальным. Я хотел, чтобы он мог работать как с монолитными приложениями, так и с разделёнными фронтендом и бэкендом. Это потребовало реализации поддержки как HTML, так и JSON.

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

  3. Интеграция с фреймворком: Чтобы goform был полезен во-первых мне, а во-вторых максимальному числу разработчиков, я добавил поддержку фреймворка Echo. Это потребовало некоторого переосмысления, но результат того стоил.

Как goform может быть полезен?

goform - это не просто инструмент для работы с формами. Это решение, которое может сэкономить вам время и усилия. Вот несколько сценариев, где он может быть полезен:

  1. Монолитные приложения: Если вы разрабатываете монолитное приложение, GoForm упростит создание и обработку форм.

  2. Разделённые фронтенд и бэкенд: Пакет поддерживает рендеринг в JSON, что делает его идеальным для REST API.

  3. Быстрое прототипирование: С goform вы можете быстро создавать и тестировать формы, не тратя время на рутинные задачи.


Пример использования

Вот пример простой формы регистрации:

Скрытый текст
package main

import (
	"net/http"
	"github.com/DBenyukh/goform/core"
    "html/template"
    "path/filepath"
    "log"
)

type RegistrationForm struct {
	Username string `form:"username" validate:"required,min=3"`
	Email    string `form:"email" validate:"required,email"`
	Password string `form:"password" validate:"required,min=6"`
	Method   string `form:"-"`
	FormID   string `form:"-"`
}

var tmpl *template.Template

func init() {
	projectDir, err := filepath.Abs(".")
	if err != nil {
		log.Fatalf("Error getting absolute project directory path: %v", err)
	}

	templateDir := filepath.Join(projectDir, "templates")
	renderer, err := core.NewTemplateRenderer(templateDir, "")
	if err != nil {
		log.Fatalf("Failed to create template renderer: %v", err)
	}

	tmpl = renderer.Templates
}

func main() {
	http.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
		model := &RegistrationForm{
			Method: "POST",
			FormID: "register_form",
		}
		form := core.NewForm(model, model.Method, model.FormID)
		form.RenderHTML = true

		if r.Method == http.MethodGet {
			// Рендеринг формы
			response := form.ToResponse()
			if form.RenderHTML {
				// Рендеринг HTML
				_ = tmpl.ExecuteTemplate(w, "имя_шаблона.html", response)
			} else {
				// Возврат JSON
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(response)
			}
			return
		}

		// Обработка POST-запроса
		if err := form.Bind(r); err != nil {
			http.Error(w, "Invalid form data", http.StatusBadRequest)
			return
		}

		if err := form.Validate(model); err != nil {
			// Возврат ошибок валидации
			response := form.ToResponse()
			if form.RenderHTML {
				_ = tmpl.ExecuteTemplate(w, "имя_шаблона.html", response)
			} else {
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(response)
			}
			return
		}

		// Обработка успешной отправки формы
		w.Write([]byte("User registered successfully!"))
	})

	http.ListenAndServe(":8080", nil)
}

И соответственно html-шаблон который использую:

Скрытый текст
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    <form method="{{ if eq .Method "GET" }}GET{{ else }}POST{{ end }}">
        {{ if and (ne .Method "GET") (ne .Method "POST")  }}
            <input type="hidden" name="_method" value="{{ .Method }}">
        {{ end }}
        <input type="hidden" name="form_id" value="{{ .FormID }}">
        {{ range .Fields }}
            {{ if not .Hidden }}
            <div>
                <label>{{ .Name }}</label>
                <input type="{{ .Type }}" name="{{ $.FormID }}_{{ .Name }}" value="{{ .Value }}">
                {{ if .Error }}
                    <span style="color: red;">{{ .Error }}</span>
                {{ end }}
            </div>
            {{ end }}
        {{ end }}
        <input type="hidden" name="{{ .FormID }}_csrf_token" value="{{ .CSRF }}">
        <button type="submit">Submit</button>
    </form>
</body>
</html>


Заключение

goform - это результат моего желания сделать работу с формами в Go более удобной и эффективной. Пакет уже используется в моём проекте (над которым пока работаю), и я надеюсь, что он будет полезен и другим разработчикам. Если у вас есть идеи или предложения по улучшению, буду рад услышать их в комментариях / личке на хабре / ТГ (в ссылках). А так же буду рад всевозможной критике.

И конечно же ссылочки, куда без них:

Пакет goform

Фреймворк Echo

Мой телеграм (если вдруг захотите просто пообщаться или предложить работу)

Спасибо за внимание! Удачи в ваших проектах!