Skip to main content

Excel

The excel package provides Excel import/export capabilities based on the tabular tag system. It uses excelize under the hood and integrates with VEF's validation system.

Overview

OperationEntry PointDescription
Importexcel.NewImporterFor[T]()Parse Excel file into typed struct slices
Exportexcel.NewExporterFor[T]()Write struct slices to Excel files

Both importer and exporter use tabular struct tags to define column mapping, formatting, and parsing rules.

tabular Tag

The tabular struct tag controls how model fields map to Excel columns.

Tag Attributes

AttributeMeaningExample
(default value)Column header nametabular:"Username"
nameExplicit column nametabular:"name:Username"
widthColumn width hinttabular:"width:20"
orderColumn order (0-based)tabular:"order:1"
defaultDefault value for empty cells on importtabular:"default:N/A"
formatFormat template (date/number)tabular:"format:2006-01-02"
formatterCustom formatter name for exporttabular:"formatter:status"
parserCustom parser name for importtabular:"parser:date"
diveRecurse into embedded structtabular:"dive"
-Ignore this fieldtabular:"-"

Model Example

type Employee struct {
orm.FullAuditedModel `tabular:"-"`

Name string `json:"name" bun:"name" tabular:"姓名,width:20"`
Email string `json:"email" bun:"email" tabular:"邮箱,width:30"`
Department string `json:"department" bun:"department" tabular:"部门,width:15"`
JoinDate timex.Date `json:"joinDate" bun:"join_date" tabular:"入职日期,format:2006-01-02,width:15"`
Salary decimal.Decimal `json:"salary" bun:"salary" tabular:"薪资,width:12"`
IsActive bool `json:"isActive" bun:"is_active" tabular:"是否在职,width:10"`
}

Exporting

Basic Export

import "github.com/coldsmirk/vef-framework-go/excel"

// Create a typed exporter
exporter := excel.NewExporterFor[Employee]()

// Export to file
err := exporter.ExportToFile(employees, "employees.xlsx")

// Export to buffer (for HTTP response)
buf, err := exporter.Export(employees)

Export Options

// Custom sheet name (default: "Sheet1")
exporter := excel.NewExporterFor[Employee](
excel.WithSheetName("Employees"),
)

Custom Formatter

Register a formatter to customize how values are rendered in Excel:

exporter := excel.NewExporterFor[Employee]()

// Register a custom formatter for the "status" name
exporter.RegisterFormatter("status", tabular.FormatterFunc(func(value any) (string, error) {
if active, ok := value.(bool); ok && active {
return "Active", nil
}
return "Inactive", nil
}))

Then reference it in the struct tag:

IsActive bool `tabular:"Status,formatter:status"`

Export in HTTP Handler

func (r *EmployeeResource) Export(ctx fiber.Ctx, db orm.DB) error {
var employees []Employee
err := db.NewSelect().Model(&employees).Scan(ctx.Context())
if err != nil {
return err
}

exporter := excel.NewExporterFor[Employee]()
buf, err := exporter.Export(employees)
if err != nil {
return err
}

ctx.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
ctx.Set("Content-Disposition", "attachment; filename=employees.xlsx")
return ctx.Send(buf.Bytes())
}

Importing

Basic Import

// Create a typed importer
importer := excel.NewImporterFor[Employee]()

// Import from file
data, importErrors, err := importer.ImportFromFile("employees.xlsx")
if err != nil {
// Fatal error (file not found, etc.)
return err
}

// Check row-level errors
if len(importErrors) > 0 {
for _, e := range importErrors {
log.Printf("Row %d, Column %s: %v", e.Row, e.Column, e.Err)
}
}

// Type-assert the result
employees := data.([]Employee)

Import from io.Reader

// Import from an uploaded file (io.Reader)
data, importErrors, err := importer.Import(reader)

Import Options

// Specify sheet by name
importer := excel.NewImporterFor[Employee](
excel.WithImportSheetName("Staff"),
)

// Specify sheet by index (default: 0)
importer := excel.NewImporterFor[Employee](
excel.WithImportSheetIndex(1),
)

// Skip leading rows (e.g., title rows before the header)
importer := excel.NewImporterFor[Employee](
excel.WithSkipRows(2),
)

Custom Parser

Register a parser to customize how cell values are parsed:

importer := excel.NewImporterFor[Employee]()

importer.RegisterParser("date", tabular.ValueParserFunc(func(cellValue string, targetType reflect.Type) (any, error) {
return time.Parse("01/02/2006", cellValue)
}))

Then reference it in the struct tag:

JoinDate time.Time `tabular:"Join Date,parser:date"`

Validation

Imported records are automatically validated using validator.Validate(...). If validation fails, the row is added to importErrors and skipped from the result slice.

type Employee struct {
Name string `tabular:"Name" validate:"required"`
Email string `tabular:"Email" validate:"required,email"`
}

Error Handling

Import Errors

ErrorMeaning
ErrSheetIndexOutOfRangeSheet index exceeds available sheets
ErrNoDataRowsFoundFile has no data rows (only header or empty)
ErrDuplicateColumnNameDuplicate column names in header row
ErrFieldNotSettableStruct field cannot be set (unexported)

Row-level errors are returned as []tabular.ImportError (non-fatal):

type ImportError struct {
Row int // 1-based row number (including header)
Column string // Column header name
Field string // Struct field name
Err error // Underlying error
}

Export Errors

type ExportError struct {
Row int // 0-based data row index
Column string
Field string
Err error
}

Column Mapping Rules

  1. The importer matches Excel header names → tabular tag name (or field name if no tag)
  2. Unmatched Excel columns are silently ignored
  3. Missing Excel columns leave the struct field at its zero value (or default if specified)
  4. Empty rows are automatically skipped

Default Type Support

The default parser automatically handles:

Go TypeParsing
stringDirect assignment
int, int8int64Integer parsing
uint, uint8uint64Unsigned integer parsing
float32, float64Float parsing
boolBoolean parsing (true/false, 1/0)
decimal.DecimalDecimal string parsing
timex.Date / timex.DateTimeDate/time parsing using format attribute
*T (pointer types)Nil for empty cells, parsed value otherwise