Custom Type Package
The customtype package provides custom types for common data formats including dates, datetimes, and high-precision decimals with proper JSON marshaling/unmarshaling.
Table of Contents
- Overview
- DateTime Type
- Date Type
- Decimal Type
- JSON Marshaling
- Database Integration
- Best Practices
- Examples
Overview
Import Path: github.com/primadi/lokstra/common/customtype
Key Features:
✓ DateTime Type - ISO-like datetime with flexible parsing
✓ Date Type - Date-only type (YYYY-MM-DD)
✓ Decimal Type - High-precision decimal numbers
✓ JSON Integration - Automatic marshaling/unmarshaling
✓ Database Support - SQL driver integration (Decimal)
✓ Flexible Parsing - Multiple input formats accepted
✓ Custom Formatting - FormatYMD for custom date formats
DateTime Type
Basic Usage
import "github.com/primadi/lokstra/common/customtype"
// Create DateTime
dt := customtype.DateTime{Time: time.Now()}
// Standard format: "YYYY-MM-DD HH:mm:SS"
fmt.Println(dt.String()) // "2024-03-15 14:30:45"
// Access underlying time.Time
year := dt.Year()
month := dt.Month()
day := dt.Day()
hour := dt.Hour()
JSON Marshaling
type Event struct {
Name string `json:"name"`
StartTime customtype.DateTime `json:"start_time"`
EndTime customtype.DateTime `json:"end_time"`
}
// Marshal to JSON
event := Event{
Name: "Conference",
StartTime: customtype.DateTime{Time: time.Date(2024, 3, 15, 9, 0, 0, 0, time.UTC)},
EndTime: customtype.DateTime{Time: time.Date(2024, 3, 15, 17, 0, 0, 0, time.UTC)},
}
jsonData, _ := json.Marshal(event)
// {
// "name": "Conference",
// "start_time": "2024-03-15 09:00:00",
// "end_time": "2024-03-15 17:00:00"
// }
JSON Unmarshaling
Accepts flexible input formats:
// Format 1: "YYYY-MM-DD HH:mm:SS"
jsonData := []byte(`{"created_at": "2024-03-15 14:30:45"}`)
// Format 2: "YYYY-MM-DD HH:mm"
jsonData := []byte(`{"created_at": "2024-03-15 14:30"}`)
// Format 3: "YYYY-MM-DD"
jsonData := []byte(`{"created_at": "2024-03-15"}`)
// Format 4: Compact "YYYYMMDDHHmmSS"
jsonData := []byte(`{"created_at": "20240315143045"}`)
type Record struct {
CreatedAt customtype.DateTime `json:"created_at"`
}
var record Record
json.Unmarshal(jsonData, &record)
// All formats parse successfully
Null Handling:
// Null value
jsonData := []byte(`{"created_at": null}`)
var record Record
json.Unmarshal(jsonData, &record)
// record.CreatedAt.IsZero() == true
// Empty string
jsonData := []byte(`{"created_at": ""}`)
json.Unmarshal(jsonData, &record)
// record.CreatedAt.IsZero() == true
Custom Formatting
dt := customtype.DateTime{Time: time.Date(2024, 3, 15, 14, 30, 45, 0, time.UTC)}
// FormatYMD with custom format strings
fmt.Println(dt.FormatYMD("YYYY-MM-DD")) // "2024-03-15"
fmt.Println(dt.FormatYMD("DD/MM/YYYY")) // "15/03/2024"
fmt.Println(dt.FormatYMD("MM/DD/YYYY HH:mm")) // "03/15/2024 14:30"
fmt.Println(dt.FormatYMD("YYYY-MM-DD HH:mm:ss")) // "2024-03-15 14:30:45"
// Month names
fmt.Println(dt.FormatYMD("DD MMMM YYYY")) // "15 March 2024"
fmt.Println(dt.FormatYMD("MMM DD, YYYY")) // "Mar 15, 2024"
// 12-hour format with AM/PM
fmt.Println(dt.FormatYMD("hh:mm AMPM")) // "02:30 PM"
fmt.Println(dt.FormatYMD("hh:mm a")) // "02:30 PM"
Format Tokens:
| Token | Meaning | Example |
|---|---|---|
YYYY |
4-digit year | 2024 |
YY |
2-digit year | 24 |
MMMM |
Full month name | January |
MMM |
Short month name | Jan |
MM |
2-digit month | 03 |
DD |
2-digit day | 15 |
HH |
24-hour format | 14 |
hh |
12-hour format | 02 |
mm |
Minutes | 30 |
ss |
Seconds | 45 |
AMPM |
AM/PM indicator | PM |
Date Type
Basic Usage
// Create Date
date := customtype.Date{Time: time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)}
// Standard format: "YYYY-MM-DD"
fmt.Println(date.String()) // "2024-03-15"
// Access components
year := date.Year()
month := date.Month()
day := date.Day()
JSON Marshaling
type Person struct {
Name string `json:"name"`
BirthDate customtype.Date `json:"birth_date"`
}
person := Person{
Name: "Alice",
BirthDate: customtype.Date{Time: time.Date(1990, 5, 15, 0, 0, 0, 0, time.UTC)},
}
jsonData, _ := json.Marshal(person)
// {
// "name": "Alice",
// "birth_date": "1990-05-15"
// }
JSON Unmarshaling
// Format 1: "YYYY-MM-DD"
jsonData := []byte(`{"birth_date": "1990-05-15"}`)
// Format 2: "YYYY/MM/DD"
jsonData := []byte(`{"birth_date": "1990/05/15"}`)
// Format 3: Compact "YYYYMMDD"
jsonData := []byte(`{"birth_date": "19900515"}`)
type Person struct {
BirthDate customtype.Date `json:"birth_date"`
}
var person Person
json.Unmarshal(jsonData, &person)
// All formats parse successfully
Null Handling:
// Null value
jsonData := []byte(`{"birth_date": null}`)
var person Person
json.Unmarshal(jsonData, &person)
// person.BirthDate.IsZero() == true
Custom Formatting
date := customtype.Date{Time: time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)}
fmt.Println(date.FormatYMD("YYYY-MM-DD")) // "2024-03-15"
fmt.Println(date.FormatYMD("DD/MM/YYYY")) // "15/03/2024"
fmt.Println(date.FormatYMD("MM/DD/YYYY")) // "03/15/2024"
fmt.Println(date.FormatYMD("DD MMMM YYYY")) // "15 March 2024"
fmt.Println(date.FormatYMD("MMMM DD, YYYY")) // "March 15, 2024"
fmt.Println(date.FormatYMD("MMM DD, YY")) // "Mar 15, 24"
Date Operations
date := customtype.Date{Time: time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)}
// Add/subtract days
tomorrow := customtype.Date{Time: date.AddDate(0, 0, 1)}
yesterday := customtype.Date{Time: date.AddDate(0, 0, -1)}
// Compare dates
if date1.Before(date2.Time) {
fmt.Println("date1 is earlier")
}
// Calculate difference
duration := date2.Sub(date1.Time)
days := int(duration.Hours() / 24)
Decimal Type
High-precision decimal numbers using shopspring/decimal library.
Basic Usage
import "github.com/primadi/lokstra/common/customtype"
// From string
price, err := customtype.NewDecimalFromString("19.99")
if err != nil {
// Handle parsing error
}
// From float
amount := customtype.NewDecimalFromFloat(123.456)
// String representation (fixed scale)
fmt.Println(price.StringFixed(2)) // "19.99"
Default Scale
// Default scale is 2 decimal places
customtype.DefaultScale = 2
// Change default scale
customtype.DefaultScale = 4
dec, _ := customtype.NewDecimalFromString("123.456789")
// Rounded to 4 decimal places: 123.4568
JSON Marshaling
type Product struct {
Name string `json:"name"`
Price customtype.Decimal `json:"price"`
}
product := Product{
Name: "Book",
Price: customtype.NewDecimalFromFloat(19.99),
}
jsonData, _ := json.Marshal(product)
// {
// "name": "Book",
// "price": 19.99
// }
JSON Unmarshaling
Accepts both string and number formats:
// Format 1: String
jsonData := []byte(`{"price": "19.99"}`)
// Format 2: Number
jsonData := []byte(`{"price": 19.99}`)
// Format 3: Null
jsonData := []byte(`{"price": null}`)
type Product struct {
Price customtype.Decimal `json:"price"`
}
var product Product
json.Unmarshal(jsonData, &product)
Arithmetic Operations
price := customtype.NewDecimalFromFloat(19.99)
quantity := customtype.NewDecimalFromFloat(3)
// Multiply
total := customtype.Decimal{
Decimal: price.Mul(quantity.Decimal),
Scale: customtype.DefaultScale,
}
// total = 59.97
// Add
discount := customtype.NewDecimalFromFloat(5.00)
finalPrice := customtype.Decimal{
Decimal: total.Sub(discount.Decimal),
Scale: customtype.DefaultScale,
}
// finalPrice = 54.97
// Divide
unitPrice := customtype.Decimal{
Decimal: total.Div(quantity.Decimal),
Scale: customtype.DefaultScale,
}
// unitPrice = 19.99
// Compare
if price.GreaterThan(discount.Decimal) {
fmt.Println("Price is greater than discount")
}
Database Integration
Decimal implements sql.Scanner and driver.Valuer:
type Product struct {
ID int `db:"id"`
Name string `db:"name"`
Price customtype.Decimal `db:"price"`
}
// Query from database
var product Product
err := db.Get(&product, "SELECT id, name, price FROM products WHERE id = ?", 1)
// Insert into database
_, err := db.Exec(
"INSERT INTO products (name, price) VALUES (?, ?)",
product.Name,
product.Price,
)
SQL Value Conversion:
// Value() - Convert to SQL value
price := customtype.NewDecimalFromFloat(19.99)
sqlValue, _ := price.Value() // "19.99" (string)
// Scan() - Read from SQL result
var price customtype.Decimal
price.Scan("19.99") // From string
price.Scan([]byte("19.99")) // From bytes
JSON Marshaling
Complete Example
type Invoice struct {
InvoiceNumber string `json:"invoice_number"`
IssueDate customtype.Date `json:"issue_date"`
DueDate customtype.Date `json:"due_date"`
CreatedAt customtype.DateTime `json:"created_at"`
Items []InvoiceItem `json:"items"`
Total customtype.Decimal `json:"total"`
}
type InvoiceItem struct {
Description string `json:"description"`
Quantity customtype.Decimal `json:"quantity"`
UnitPrice customtype.Decimal `json:"unit_price"`
Amount customtype.Decimal `json:"amount"`
}
// Create invoice
invoice := Invoice{
InvoiceNumber: "INV-2024-001",
IssueDate: customtype.Date{Time: time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)},
DueDate: customtype.Date{Time: time.Date(2024, 4, 15, 0, 0, 0, 0, time.UTC)},
CreatedAt: customtype.DateTime{Time: time.Now()},
Items: []InvoiceItem{
{
Description: "Widget A",
Quantity: customtype.NewDecimalFromFloat(10),
UnitPrice: customtype.NewDecimalFromFloat(19.99),
Amount: customtype.NewDecimalFromFloat(199.90),
},
},
Total: customtype.NewDecimalFromFloat(199.90),
}
// Marshal to JSON
jsonData, _ := json.Marshal(invoice)
Result:
{
"invoice_number": "INV-2024-001",
"issue_date": "2024-03-15",
"due_date": "2024-04-15",
"created_at": "2024-03-15 14:30:45",
"items": [
{
"description": "Widget A",
"quantity": 10.00,
"unit_price": 19.99,
"amount": 199.90
}
],
"total": 199.90
}
Database Integration
Table Definition
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
order_number VARCHAR(50) NOT NULL,
order_date DATE NOT NULL,
created_at TIMESTAMP NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
discount_amount DECIMAL(10, 2) DEFAULT 0,
final_amount DECIMAL(10, 2) NOT NULL
);
Go Struct
type Order struct {
ID int `db:"id"`
OrderNumber string `db:"order_number"`
OrderDate customtype.Date `db:"order_date"`
CreatedAt customtype.DateTime `db:"created_at"`
TotalAmount customtype.Decimal `db:"total_amount"`
DiscountAmount customtype.Decimal `db:"discount_amount"`
FinalAmount customtype.Decimal `db:"final_amount"`
}
CRUD Operations
// Create
order := Order{
OrderNumber: "ORD-2024-001",
OrderDate: customtype.Date{Time: time.Now()},
CreatedAt: customtype.DateTime{Time: time.Now()},
TotalAmount: customtype.NewDecimalFromFloat(199.90),
DiscountAmount: customtype.NewDecimalFromFloat(10.00),
FinalAmount: customtype.NewDecimalFromFloat(189.90),
}
_, err := db.Exec(`
INSERT INTO orders (order_number, order_date, created_at, total_amount, discount_amount, final_amount)
VALUES (?, ?, ?, ?, ?, ?)
`, order.OrderNumber, order.OrderDate, order.CreatedAt, order.TotalAmount, order.DiscountAmount, order.FinalAmount)
// Read
var order Order
err := db.Get(&order, "SELECT * FROM orders WHERE id = ?", 1)
// Update
_, err := db.Exec(`
UPDATE orders
SET total_amount = ?, discount_amount = ?, final_amount = ?
WHERE id = ?
`, order.TotalAmount, order.DiscountAmount, order.FinalAmount, order.ID)
Best Practices
Date/DateTime Usage
✓ DO: Use Date for date-only fields
type Person struct {
BirthDate customtype.Date `json:"birth_date"` // No time component needed
}
✗ DON'T: Use DateTime when you don't need time
type Person struct {
BirthDate customtype.DateTime `json:"birth_date"` // BAD: Unnecessary time component
}
✓ DO: Use DateTime for timestamps
type Record struct {
CreatedAt customtype.DateTime `json:"created_at"` // Full timestamp
UpdatedAt customtype.DateTime `json:"updated_at"`
}
✓ DO: Check for zero values
if record.CreatedAt.IsZero() {
// Handle unset datetime
}
Decimal Usage
✓ DO: Use Decimal for monetary values
type Product struct {
Price customtype.Decimal `json:"price" db:"price"`
}
✗ DON'T: Use float64 for money
type Product struct {
Price float64 `json:"price"` // BAD: Precision issues
}
✓ DO: Set appropriate default scale
func init() {
customtype.DefaultScale = 2 // For currency (2 decimal places)
}
✓ DO: Normalize after arithmetic operations
result := customtype.Decimal{
Decimal: price.Mul(quantity.Decimal),
Scale: customtype.DefaultScale,
}
JSON Parsing
✓ DO: Handle parsing errors
var record Record
if err := json.Unmarshal(jsonData, &record); err != nil {
return fmt.Errorf("failed to parse JSON: %w", err)
}
✓ DO: Check for zero values after unmarshaling
if record.CreatedAt.IsZero() {
record.CreatedAt = customtype.DateTime{Time: time.Now()}
}
✓ DO: Use consistent date formats in API
// Always use ISO 8601 formats
// Date: "YYYY-MM-DD"
// DateTime: "YYYY-MM-DD HH:mm:SS"
Database Integration
✓ DO: Use appropriate SQL types
// PostgreSQL
price DECIMAL(10, 2)
order_date DATE
created_at TIMESTAMP
✓ DO: Handle NULL values
type Product struct {
Price *customtype.Decimal `db:"price"` // Pointer for nullable
}
✓ DO: Set default scale before database operations
customtype.DefaultScale = 2
Examples
E-commerce Product
type Product struct {
ID int `json:"id" db:"id"`
SKU string `json:"sku" db:"sku"`
Name string `json:"name" db:"name"`
Price customtype.Decimal `json:"price" db:"price"`
CostPrice customtype.Decimal `json:"cost_price" db:"cost_price"`
Stock int `json:"stock" db:"stock"`
CreatedAt customtype.DateTime `json:"created_at" db:"created_at"`
UpdatedAt customtype.DateTime `json:"updated_at" db:"updated_at"`
}
func CreateProduct(name string, price, costPrice float64) *Product {
return &Product{
SKU: generateSKU(),
Name: name,
Price: customtype.NewDecimalFromFloat(price),
CostPrice: customtype.NewDecimalFromFloat(costPrice),
Stock: 0,
CreatedAt: customtype.DateTime{Time: time.Now()},
UpdatedAt: customtype.DateTime{Time: time.Now()},
}
}
func (p *Product) CalculateMargin() customtype.Decimal {
margin := p.Price.Sub(p.CostPrice.Decimal)
return customtype.Decimal{
Decimal: margin,
Scale: customtype.DefaultScale,
}
}
Event Scheduling
type Event struct {
ID int `json:"id"`
Title string `json:"title"`
EventDate customtype.Date `json:"event_date"`
StartTime customtype.DateTime `json:"start_time"`
EndTime customtype.DateTime `json:"end_time"`
CreatedAt customtype.DateTime `json:"created_at"`
}
func CreateEvent(title string, eventDate time.Time, startHour, endHour int) *Event {
return &Event{
Title: title,
EventDate: customtype.Date{Time: eventDate},
StartTime: customtype.DateTime{
Time: time.Date(eventDate.Year(), eventDate.Month(), eventDate.Day(),
startHour, 0, 0, 0, time.UTC),
},
EndTime: customtype.DateTime{
Time: time.Date(eventDate.Year(), eventDate.Month(), eventDate.Day(),
endHour, 0, 0, 0, time.UTC),
},
CreatedAt: customtype.DateTime{Time: time.Now()},
}
}
Financial Calculation
type Transaction struct {
ID int `json:"id" db:"id"`
Type string `json:"type" db:"type"` // debit/credit
Amount customtype.Decimal `json:"amount" db:"amount"`
Fee customtype.Decimal `json:"fee" db:"fee"`
NetAmount customtype.Decimal `json:"net_amount" db:"net_amount"`
Date customtype.Date `json:"date" db:"date"`
ProcessedAt customtype.DateTime `json:"processed_at" db:"processed_at"`
}
func CreateTransaction(txType string, amount, feePercent float64) *Transaction {
amountDec := customtype.NewDecimalFromFloat(amount)
feePercentDec := customtype.NewDecimalFromFloat(feePercent / 100)
fee := customtype.Decimal{
Decimal: amountDec.Mul(feePercentDec.Decimal),
Scale: customtype.DefaultScale,
}
netAmount := customtype.Decimal{
Decimal: amountDec.Sub(fee.Decimal),
Scale: customtype.DefaultScale,
}
return &Transaction{
Type: txType,
Amount: amountDec,
Fee: fee,
NetAmount: netAmount,
Date: customtype.Date{Time: time.Now()},
ProcessedAt: customtype.DateTime{Time: time.Now()},
}
}
API Response with Custom Types
type OrderResponse struct {
OrderNumber string `json:"order_number"`
OrderDate customtype.Date `json:"order_date"`
Items []OrderItem `json:"items"`
Subtotal customtype.Decimal `json:"subtotal"`
Tax customtype.Decimal `json:"tax"`
Total customtype.Decimal `json:"total"`
CreatedAt customtype.DateTime `json:"created_at"`
}
type OrderItem struct {
ProductName string `json:"product_name"`
Quantity customtype.Decimal `json:"quantity"`
UnitPrice customtype.Decimal `json:"unit_price"`
Total customtype.Decimal `json:"total"`
}
func GetOrderHandler(w http.ResponseWriter, r *http.Request) {
// Fetch order from database
order := getOrder(orderID)
// Convert to response
response := OrderResponse{
OrderNumber: order.OrderNumber,
OrderDate: order.OrderDate,
Items: convertItems(order.Items),
Subtotal: order.Subtotal,
Tax: order.Tax,
Total: order.Total,
CreatedAt: order.CreatedAt,
}
// Marshal and send
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
Related Documentation
- Helpers Overview - All helper packages
- Cast Package - Type conversion utilities
- Validator Package - Struct validation
Next: JSON Package - JSON utilities