A Go library for rendering HTML forms from Go structs using struct tags and Go templates. Supports multiple template styles (Plain, Bootstrap 5, Tailwind CSS) and a wide range of HTML input types.
- Define forms as Go structs with struct tags for field type, label, placeholder, and more
- Supports many HTML input types: text, password, email, tel, number, date, color, range, datetime-local, time, week, month, hidden
- Checkbox, radio, dropdown, and textarea fields
- Grouping and nested struct support for form sections
- Built-in template sets: Plain, Bootstrap 5, Tailwind CSS
- Integrates with
html/templatevia a FuncMap - CSRF Protection
- SortedSelect and SortedMultiSelect for type-safe, mapped dropdowns and multi-selects (see examples)
go get github.com/donseba/go-form/v2import (
"html/template"
"github.com/donseba/go-form/v2"
)
type ExampleForm struct {
Username string `form:"input,text" label:"Username" placeholder:"Enter your username" required:"true"`
Password string `form:"input,password" label:"Password" placeholder:"Enter your password" required:"true"`
Email string `form:"input,email" label:"Email" placeholder:"Enter your email" required:"true"`
Age int `form:"input,number" label:"Age" placeholder:"Enter your age" step:"1"`
}
f := form.NewForm()
funcMap := f.FuncMap()
_ = template.Must(template.New("form").Funcs(funcMap).Parse(`{{ form_render .Form nil }}`))| Template Name | Description |
|---|---|
templates.Plain |
Plain HTML, minimal styles |
templates.BootstrapV5 |
Bootstrap 5 form styles |
templates.TailwindV3 |
Tailwind CSS v3 styles |
| Field Type / Tag Example | Description | Options (Struct Tags) |
|---|---|---|
form:"input,text" |
Text input | label, placeholder, required, maxlength |
form:"input,password" |
Password input | label, placeholder, required |
form:"input,email" |
Email input | label, placeholder, required |
form:"input,number" |
Number input | label, placeholder, required, min, max, step |
form:"input,date" |
Date input | label, placeholder, required |
form:"input,datetime-local" |
DateTime input | label, placeholder, required |
form:"input,time" |
Time input | label, placeholder, required |
form:"input,week" |
Week input | label, placeholder, required |
form:"input,month" |
Month input | label, placeholder, required |
form:"input,color" |
Color input | label, placeholder, required |
form:"input,range" |
Range input | label, min, max, step |
form:"input,hidden" |
Hidden input | value |
form:"input,search" |
Search input | label, placeholder |
form:"input,url" |
URL input | label, placeholder |
form:"input,tel" |
Telephone input | label, placeholder |
form:"input,image" |
Image input | label, src, alt |
form:"checkbox" |
Checkbox | label, required |
form:"radios" |
Radio group | label, values (e.g. a:A;b:B), required |
form:"dropdown" |
Dropdown/select | label, values (e.g. a:A;b:B), required |
form:"multicheckbox" |
Multi-checkbox group | label, values (e.g. a:A;b:B), required |
Other supported tags:
legend— For grouping/nested structs (section title)description— Field description/help textmaxLength— Maximum length for textarea or string inputclass— Custom CSS class for the fielddata— Custom data attributes (e.g.,data="custom:value,foo:bar,baz:qux")translate— Enable translation for enum values (e.g.,translate:"true"for Enumerator fields)
- required: Ensures the field is not empty.
- min, max, step: For numeric fields, enforces minimum, maximum, and step values.
- minLength, maxLength: For string/textarea fields, enforces minimum and maximum character count (Unicode-aware).
- values: For radios/dropdowns, ensures the value is one of the allowed options.
- Email format: Checks for a valid email address format (basic @ check).
- Enumerator, Mapper, SortedMapper: If a field implements one of these interfaces, the value must be present in the allowed set returned by Enum(), Mapper(), or SortedMapper().
For enum values, implement Enumerator:
type Status string
func (s Status) Enum() []any { return []any{"active", "inactive"} }
type MyForm struct {
Status Status `form:"dropdown" label:"Status"`
}For key-value pairs, use Mapper (unordered) or SortedMapper (ordered):
type ColorMap string
func (c ColorMap) Mapper() map[string]string {
return map[string]string{"red": "Red", "blue": "Blue"}
}
// For ordered pairs, implement SortedMapper with []SortedMapYou can add your own validation logic using the validate struct tag and by registering a custom validation function:
// 1. Define your validation function (must return form.FieldErrors)
func isHexColor(val any, field reflect.StructField) form.FieldErrors { /* ... */ }
// 2. Register it with your Form instance
f.RegisterValidationMethod("isHexColor", isHexColor)
// 3. Use it in your struct
type MyForm struct {
Color string `form:"input,text" label:"Color" validate:"isHexColor"`
}
// 4. Call f.ValidateForm(&myForm) to run both built-in and custom validationsCustom validators can be chained with commas in the validate tag. All errors are collected and can be rendered in your template.
go-form supports translation of form labels, error messages, and other UI text. You can provide your own translation function and a Localizer implementation to render forms in different languages or customize the wording for your application.
- Create a translation function: This function receives a Localizer, a key, and optional arguments, and returns the translated string.
- Implement a Localizer: This determines the current locale (e.g., from the user session or request).
- Create the form with translation support: Use
form.NewTranslatedForm(template, translateFunc). - Pass your Localizer when rendering or validating: The form will use your translation function and Localizer to fetch translations.
// Example translation function and Localizer
var translations = map[string]map[string]string{
"en": {"Name": "Name", "form.validation.required": "is required"},
"it": {"Name": "Nome", "form.validation.required": "è obbligatorio"},
}
type MyLocalizer struct { Locale string }
func (l MyLocalizer) GetLocale() string { return l.Locale }
func myTranslate(loc form.Localizer, key string, args ...any) string {
locale := "en"
if l, ok := loc.(MyLocalizer); ok {
locale = l.Locale
}
msg := key
if m, ok := translations[locale]; ok {
if t, ok := m[key]; ok {
msg = t
}
}
if len(args) > 0 {
return fmt.Sprintf(msg, args...)
}
return msg
}
f := form.NewTranslatedForm(templates.Plain, myTranslate)
// When rendering or validating, pass your Localizer:
loc := MyLocalizer{Locale: "it"}
// ...See the example in example/translation/main.go for a complete usage demonstration.
By default, enum values display as-is. To enable translation, add translate:"true":
type Status string
func (s Status) Enum() []any { return []any{"active", "inactive"} }
type MyForm struct {
Status Status `form:"dropdown" label:"Status" translate:"true"`
}To enable translation for all enums by default, set the global variable:
import "github.com/donseba/go-form/v2"
func init() {
form.DefaultEnumTranslation = true // All enums will be translatable by default
}Individual fields can still opt-out using translate:"false". Translation keys follow the format enum||{TypeName}.{value}:
var translations = map[string]map[string]string{
"en": {"enum||Status.active": "Active", "enum||Status.inactive": "Inactive"},
"it": {"enum||Status.active": "Attivo", "enum||Status.inactive": "Inattivo"},
}You can set custom HTML attributes on forms (e.g., hx-post, data-*, etc.) using the Attributes field in your form struct:
form := &FormField{
// ...other fields...
Attributes: map[string]string{
"hx-post": "/some-url",
"data-custom": "value",
},
}You can prepend or append content to input fields using the group tag. This is supported in all template sets (Plain, Bootstrap 5, Tailwind CSS):
type ExampleForm struct {
Username string `form:"input,text" label:"Username" group:"@,.com"`
}This will render an input with @ before and .com after the field, styled according to the selected template.
go-form includes built-in CSRF (Cross-Site Request Forgery) protection for your forms. This prevents attackers from tricking users into submitting unauthorized requests.
-
Create a form renderer which adds a default CSRF protection by default:
formRenderer := form.NewForm(templates.BootstrapV5)
-
Apply the CSRF middleware to your handlers:
// With standard http.ServeMux: protectedHandler := formRenderer.CSRFMiddleware()(yourHandler) // <-- wrap your handler mux.Handle("/", protectedHandler) // With Chi router: import "github.com/go-chi/chi/v5" router := chi.NewRouter() router.Use(formRenderer.CSRFMiddleware()) // <-- load the middleware
-
Associate the form metadata with the model before rendering. This also injects the request's CSRF token:
loginForm := LoginForm{Email: "name@example.com"}
renderModel := form.WithRequestInfo(r, loginForm, form.Info{
Target: "/login",
Method: "post",
SubmitText: "Log In",
})WithInfo works when CSRF is not needed. WithContextInfo is useful in a
rendering service that receives a context.Context rather than an HTTP
request. These wrappers let application- or provider-owned structs use full
form metadata without embedding form.Info or changing their field names.
Models that already embed form.Info remain supported. For those models,
InjectCSRFToken can still populate the embedded metadata directly.
The middleware automatically:
- Generates a secure random token for each form
- Validates the token on submission
- Refreshes tokens after each submission
- Rejects requests with missing or invalid tokens
By default, CSRF validation failures return HTTP error responses. For a better user experience, you can provide custom error handling:
options := form.CSRFOptions{
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
switch {
case errors.Is(err, csrf.ErrTokenMismatch):
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
case errors.Is(err, csrf.ErrTokenExpired):
http.Error(w, "CSRF token expired", http.StatusForbidden)
case errors.Is(err, csrf.ErrKeyOrTokenEmpty):
http.Error(w, "CSRF token or session ID is empty", http.StatusBadRequest)
case errors.Is(err, csrf.ErrTokenNotFound):
http.Error(w, "CSRF token not found", http.StatusBadRequest)
default:
http.Error(w, "CSRF validation error: "+err.Error(), http.StatusBadRequest)
}
},
}
// Use the custom options
protectedHandler := formRenderer.CSRFMiddlewareWithOptions(options)(yourHandler)The default in-memory CSRF store is suitable for single-server applications. For production or distributed environments, you can implement a custom CSRFStore that uses Redis, a database, or another shared storage mechanism:
// Example Redis CSRF Store implementation
type RedisCSRFStore struct {
client *redis.Client
prefix string
ttl time.Duration
}
func (s *RedisCSRFStore) Store(key, token string) error {
return s.client.Set(ctx, s.prefix+key, token, s.ttl).Err()
}
func (s *RedisCSRFStore) Get(key string) (string, error) {
val, err := s.client.Get(ctx, s.prefix+key).Result()
if err == redis.Nil {
return "", csrf.ErrTokenNotFound
}
return val, err
}
// ... implement other required methods ...
// Then use it with your form:
store := &RedisCSRFStore{
client: redisClient,
prefix: "csrf:",
ttl: 30 * time.Minute,
}
formRenderer.SetCSRFStore(store)See the example in example/csrf/main.go for a complete usage demonstration.
SortedSelect and SortedMultiSelect are generic types for type-safe, mapped dropdowns and multi-selects with custom key types. They support form mapping, validation, database integration, and JSON serialization.
- SortedSelect is for single-value dropdowns (e.g.,
DepartmentID form.SortedSelect[int64]). - SortedMultiSelect is for multi-value selections (e.g.,
DepartmentsMulti form.SortedMultiSelect[int64]). - Both support any comparable Go type as the key:
int,int64,string,float64,uuid.UUID,time.Time, etc. - They work seamlessly with form rendering, validation, and database/sql or JSON marshalling.
Minimal usage example:
import "github.com/donseba/go-form/v2"
// Single select
DepartmentID form.SortedSelect[int64] `form:"dropdown" label:"Department"`
// Multi select
DepartmentsMulti form.SortedMultiSelect[int64] `form:"multicheckbox" label:"Departments"`
// Initialize with a source map
form.NewSortedSelect(map[int64]string{1: "HR", 2: "IT"})
form.NewSortedMultiSelect(map[int64]string{1: "HR", 2: "IT"})For advanced usage, see example/sortedselect/main.go — covers single and multi-select fields, supported key types, pre-filled and user-submitted values, validation, error handling, JSON and DB integration.
This project is licensed under the MIT License - see the LICENSE file for details.