688 lines
15 KiB
Text
688 lines
15 KiB
Text
---
|
|
title: Bindings Best Practices
|
|
description: Design patterns and best practices for Go-JavaScript bindings
|
|
sidebar:
|
|
order: 4
|
|
---
|
|
|
|
## Bindings Best Practices
|
|
|
|
Follow **proven patterns** for binding design to create clean, performant, and secure bindings. This guide covers API design principles, performance optimisation, security patterns, error handling, and testing strategies for maintainable applications.
|
|
|
|
## API Design Principles
|
|
|
|
### 1. Single Responsibility
|
|
|
|
Each service should have one clear purpose:
|
|
|
|
```go
|
|
// ❌ Bad: God object
|
|
type AppService struct {
|
|
// Does everything
|
|
}
|
|
|
|
func (a *AppService) SaveFile(path string, data []byte) error
|
|
func (a *AppService) GetUser(id int) (*User, error)
|
|
func (a *AppService) SendEmail(to, subject, body string) error
|
|
func (a *AppService) ProcessPayment(amount float64) error
|
|
|
|
// ✅ Good: Focused services
|
|
type FileService struct{}
|
|
func (f *FileService) Save(path string, data []byte) error
|
|
|
|
type UserService struct{}
|
|
func (u *UserService) GetByID(id int) (*User, error)
|
|
|
|
type EmailService struct{}
|
|
func (e *EmailService) Send(to, subject, body string) error
|
|
|
|
type PaymentService struct{}
|
|
func (p *PaymentService) Process(amount float64) error
|
|
```
|
|
|
|
### 2. Clear Method Names
|
|
|
|
Use descriptive, action-oriented names:
|
|
|
|
```go
|
|
// ❌ Bad: Unclear names
|
|
func (s *Service) Do(x string) error
|
|
func (s *Service) Handle(data interface{}) interface{}
|
|
func (s *Service) Process(input map[string]interface{}) bool
|
|
|
|
// ✅ Good: Clear names
|
|
func (s *FileService) SaveDocument(path string, content string) error
|
|
func (s *UserService) AuthenticateUser(email, password string) (*User, error)
|
|
func (s *OrderService) CreateOrder(items []Item) (*Order, error)
|
|
```
|
|
|
|
### 3. Consistent Return Types
|
|
|
|
Always return errors explicitly:
|
|
|
|
```go
|
|
// ❌ Bad: Inconsistent error handling
|
|
func (s *Service) GetData() interface{} // How to handle errors?
|
|
func (s *Service) SaveData(data string) // Silent failures?
|
|
|
|
// ✅ Good: Explicit errors
|
|
func (s *Service) GetData() (Data, error)
|
|
func (s *Service) SaveData(data string) error
|
|
```
|
|
|
|
### 4. Input Validation
|
|
|
|
Validate all input on the Go side:
|
|
|
|
```go
|
|
// ❌ Bad: No validation
|
|
func (s *UserService) CreateUser(email, password string) (*User, error) {
|
|
user := &User{Email: email, Password: password}
|
|
return s.db.Create(user)
|
|
}
|
|
|
|
// ✅ Good: Validate first
|
|
func (s *UserService) CreateUser(email, password string) (*User, error) {
|
|
// Validate email
|
|
if !isValidEmail(email) {
|
|
return nil, errors.New("invalid email address")
|
|
}
|
|
|
|
// Validate password
|
|
if len(password) < 8 {
|
|
return nil, errors.New("password must be at least 8 characters")
|
|
}
|
|
|
|
// Hash password
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user := &User{
|
|
Email: email,
|
|
PasswordHash: string(hash),
|
|
}
|
|
|
|
return s.db.Create(user)
|
|
}
|
|
```
|
|
|
|
## Performance Patterns
|
|
|
|
### 1. Batch Operations
|
|
|
|
Reduce bridge calls by batching:
|
|
|
|
```go
|
|
// ❌ Bad: N calls
|
|
// JavaScript
|
|
for (const item of items) {
|
|
await ProcessItem(item) // N bridge calls
|
|
}
|
|
|
|
// ✅ Good: 1 call
|
|
// Go
|
|
func (s *Service) ProcessItems(items []Item) ([]Result, error) {
|
|
results := make([]Result, len(items))
|
|
for i, item := range items {
|
|
results[i] = s.processItem(item)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// JavaScript
|
|
const results = await ProcessItems(items) // 1 bridge call
|
|
```
|
|
|
|
### 2. Pagination
|
|
|
|
Don't return huge datasets:
|
|
|
|
```go
|
|
// ❌ Bad: Returns everything
|
|
func (s *Service) GetAllUsers() ([]User, error) {
|
|
return s.db.FindAll() // Could be millions
|
|
}
|
|
|
|
// ✅ Good: Paginated
|
|
type PageRequest struct {
|
|
Page int `json:"page"`
|
|
PageSize int `json:"pageSize"`
|
|
}
|
|
|
|
type PageResponse struct {
|
|
Items []User `json:"items"`
|
|
TotalItems int `json:"totalItems"`
|
|
TotalPages int `json:"totalPages"`
|
|
Page int `json:"page"`
|
|
}
|
|
|
|
func (s *Service) GetUsers(req PageRequest) (*PageResponse, error) {
|
|
// Validate
|
|
if req.Page < 1 {
|
|
req.Page = 1
|
|
}
|
|
if req.PageSize < 1 || req.PageSize > 100 {
|
|
req.PageSize = 20
|
|
}
|
|
|
|
// Get total
|
|
total, err := s.db.Count()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get page
|
|
offset := (req.Page - 1) * req.PageSize
|
|
users, err := s.db.Find(offset, req.PageSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &PageResponse{
|
|
Items: users,
|
|
TotalItems: total,
|
|
TotalPages: (total + req.PageSize - 1) / req.PageSize,
|
|
Page: req.Page,
|
|
}, nil
|
|
}
|
|
```
|
|
|
|
### 3. Caching
|
|
|
|
Cache expensive operations:
|
|
|
|
```go
|
|
type CachedService struct {
|
|
cache map[string]interface{}
|
|
mu sync.RWMutex
|
|
ttl time.Duration
|
|
}
|
|
|
|
func (s *CachedService) GetData(key string) (interface{}, error) {
|
|
// Check cache
|
|
s.mu.RLock()
|
|
if data, ok := s.cache[key]; ok {
|
|
s.mu.RUnlock()
|
|
return data, nil
|
|
}
|
|
s.mu.RUnlock()
|
|
|
|
// Fetch data
|
|
data, err := s.fetchData(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Cache it
|
|
s.mu.Lock()
|
|
s.cache[key] = data
|
|
s.mu.Unlock()
|
|
|
|
// Schedule expiry
|
|
go func() {
|
|
time.Sleep(s.ttl)
|
|
s.mu.Lock()
|
|
delete(s.cache, key)
|
|
s.mu.Unlock()
|
|
}()
|
|
|
|
return data, nil
|
|
}
|
|
```
|
|
|
|
### 4. Streaming with Events
|
|
|
|
Use events for streaming data:
|
|
|
|
```go
|
|
// ❌ Bad: Polling
|
|
func (s *Service) GetProgress() int {
|
|
return s.progress
|
|
}
|
|
|
|
// JavaScript polls
|
|
setInterval(async () => {
|
|
const progress = await GetProgress()
|
|
updateUI(progress)
|
|
}, 100)
|
|
|
|
// ✅ Good: Events
|
|
func (s *Service) ProcessLargeFile(path string) error {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
total := 0
|
|
processed := 0
|
|
|
|
// Count lines
|
|
for scanner.Scan() {
|
|
total++
|
|
}
|
|
|
|
// Process
|
|
file.Seek(0, 0)
|
|
scanner = bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
s.processLine(scanner.Text())
|
|
processed++
|
|
|
|
// Emit progress
|
|
s.app.Event.Emit("progress", map[string]interface{}{
|
|
"processed": processed,
|
|
"total": total,
|
|
"percent": int(float64(processed) / float64(total) * 100),
|
|
})
|
|
}
|
|
|
|
return scanner.Err()
|
|
}
|
|
|
|
// JavaScript listens
|
|
OnEvent("progress", (data) => {
|
|
updateProgress(data.percent)
|
|
})
|
|
```
|
|
|
|
## Security Patterns
|
|
|
|
### 1. Input Sanitisation
|
|
|
|
Always sanitise user input:
|
|
|
|
```go
|
|
import (
|
|
"html"
|
|
"strings"
|
|
)
|
|
|
|
func (s *Service) SaveComment(text string) error {
|
|
// Sanitise
|
|
text = strings.TrimSpace(text)
|
|
text = html.EscapeString(text)
|
|
|
|
// Validate length
|
|
if len(text) == 0 {
|
|
return errors.New("comment cannot be empty")
|
|
}
|
|
if len(text) > 1000 {
|
|
return errors.New("comment too long")
|
|
}
|
|
|
|
return s.db.SaveComment(text)
|
|
}
|
|
```
|
|
|
|
### 2. Authentication
|
|
|
|
Protect sensitive operations:
|
|
|
|
```go
|
|
type AuthService struct {
|
|
sessions map[string]*Session
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func (a *AuthService) Login(email, password string) (string, error) {
|
|
user, err := a.db.FindByEmail(email)
|
|
if err != nil {
|
|
return "", errors.New("invalid credentials")
|
|
}
|
|
|
|
if !a.verifyPassword(user.PasswordHash, password) {
|
|
return "", errors.New("invalid credentials")
|
|
}
|
|
|
|
// Create session
|
|
token := generateToken()
|
|
a.mu.Lock()
|
|
a.sessions[token] = &Session{
|
|
UserID: user.ID,
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
}
|
|
a.mu.Unlock()
|
|
|
|
return token, nil
|
|
}
|
|
|
|
func (a *AuthService) requireAuth(token string) (*Session, error) {
|
|
a.mu.RLock()
|
|
session, ok := a.sessions[token]
|
|
a.mu.RUnlock()
|
|
|
|
if !ok {
|
|
return nil, errors.New("not authenticated")
|
|
}
|
|
|
|
if time.Now().After(session.ExpiresAt) {
|
|
return nil, errors.New("session expired")
|
|
}
|
|
|
|
return session, nil
|
|
}
|
|
|
|
// Protected method
|
|
func (a *AuthService) DeleteAccount(token string) error {
|
|
session, err := a.requireAuth(token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return a.db.DeleteUser(session.UserID)
|
|
}
|
|
```
|
|
|
|
### 3. Rate Limiting
|
|
|
|
Prevent abuse:
|
|
|
|
```go
|
|
type RateLimiter struct {
|
|
requests map[string][]time.Time
|
|
mu sync.Mutex
|
|
limit int
|
|
window time.Duration
|
|
}
|
|
|
|
func (r *RateLimiter) Allow(key string) bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
|
|
// Clean old requests
|
|
if requests, ok := r.requests[key]; ok {
|
|
var recent []time.Time
|
|
for _, t := range requests {
|
|
if now.Sub(t) < r.window {
|
|
recent = append(recent, t)
|
|
}
|
|
}
|
|
r.requests[key] = recent
|
|
}
|
|
|
|
// Check limit
|
|
if len(r.requests[key]) >= r.limit {
|
|
return false
|
|
}
|
|
|
|
// Add request
|
|
r.requests[key] = append(r.requests[key], now)
|
|
return true
|
|
}
|
|
|
|
// Usage
|
|
func (s *Service) SendEmail(to, subject, body string) error {
|
|
if !s.rateLimiter.Allow(to) {
|
|
return errors.New("rate limit exceeded")
|
|
}
|
|
|
|
return s.emailer.Send(to, subject, body)
|
|
}
|
|
```
|
|
|
|
## Error Handling Patterns
|
|
|
|
### 1. Descriptive Errors
|
|
|
|
Provide context in errors:
|
|
|
|
```go
|
|
// ❌ Bad: Generic errors
|
|
func (s *Service) LoadFile(path string) ([]byte, error) {
|
|
return os.ReadFile(path) // "no such file or directory"
|
|
}
|
|
|
|
// ✅ Good: Contextual errors
|
|
func (s *Service) LoadFile(path string) ([]byte, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load file %s: %w", path, err)
|
|
}
|
|
return data, nil
|
|
}
|
|
```
|
|
|
|
### 2. Error Types
|
|
|
|
Use typed errors for specific handling:
|
|
|
|
```go
|
|
type ValidationError struct {
|
|
Field string
|
|
Message string
|
|
}
|
|
|
|
func (e *ValidationError) Error() string {
|
|
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
|
}
|
|
|
|
type NotFoundError struct {
|
|
Resource string
|
|
ID interface{}
|
|
}
|
|
|
|
func (e *NotFoundError) Error() string {
|
|
return fmt.Sprintf("%s not found: %v", e.Resource, e.ID)
|
|
}
|
|
|
|
// Usage
|
|
func (s *UserService) GetUser(id int) (*User, error) {
|
|
if id <= 0 {
|
|
return nil, &ValidationError{
|
|
Field: "id",
|
|
Message: "must be positive",
|
|
}
|
|
}
|
|
|
|
user, err := s.db.Find(id)
|
|
if err == sql.ErrNoRows {
|
|
return nil, &NotFoundError{
|
|
Resource: "User",
|
|
ID: id,
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("database error: %w", err)
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
```
|
|
|
|
### 3. Error Recovery
|
|
|
|
Handle errors gracefully:
|
|
|
|
```go
|
|
func (s *Service) ProcessWithRetry(data string) error {
|
|
maxRetries := 3
|
|
|
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
|
err := s.process(data)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
// Log attempt
|
|
s.app.Logger.Warn("Process failed",
|
|
"attempt", attempt,
|
|
"error", err)
|
|
|
|
// Don't retry on validation errors
|
|
if _, ok := err.(*ValidationError); ok {
|
|
return err
|
|
}
|
|
|
|
// Wait before retry
|
|
if attempt < maxRetries {
|
|
time.Sleep(time.Duration(attempt) * time.Second)
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("failed after %d attempts", maxRetries)
|
|
}
|
|
```
|
|
|
|
## Testing Patterns
|
|
|
|
### 1. Unit Testing
|
|
|
|
Test services in isolation:
|
|
|
|
```go
|
|
func TestUserService_CreateUser(t *testing.T) {
|
|
// Setup
|
|
db := &MockDB{}
|
|
service := &UserService{db: db}
|
|
|
|
// Test valid input
|
|
user, err := service.CreateUser("test@example.com", "password123")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if user.Email != "test@example.com" {
|
|
t.Errorf("expected email test@example.com, got %s", user.Email)
|
|
}
|
|
|
|
// Test invalid email
|
|
_, err = service.CreateUser("invalid", "password123")
|
|
if err == nil {
|
|
t.Error("expected error for invalid email")
|
|
}
|
|
|
|
// Test short password
|
|
_, err = service.CreateUser("test@example.com", "short")
|
|
if err == nil {
|
|
t.Error("expected error for short password")
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Integration Testing
|
|
|
|
Test with real dependencies:
|
|
|
|
```go
|
|
func TestUserService_Integration(t *testing.T) {
|
|
// Setup real database
|
|
db, err := sql.Open("sqlite3", ":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Create schema
|
|
_, err = db.Exec(`CREATE TABLE users (...)`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test service
|
|
service := &UserService{db: db}
|
|
|
|
user, err := service.CreateUser("test@example.com", "password123")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify in database
|
|
var count int
|
|
db.QueryRow("SELECT COUNT(*) FROM users WHERE email = ?",
|
|
user.Email).Scan(&count)
|
|
|
|
if count != 1 {
|
|
t.Errorf("expected 1 user, got %d", count)
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Mock Services
|
|
|
|
Create testable interfaces:
|
|
|
|
```go
|
|
type UserRepository interface {
|
|
Create(user *User) error
|
|
FindByEmail(email string) (*User, error)
|
|
Update(user *User) error
|
|
Delete(id int) error
|
|
}
|
|
|
|
type UserService struct {
|
|
repo UserRepository
|
|
}
|
|
|
|
// Mock for testing
|
|
type MockUserRepository struct {
|
|
users map[string]*User
|
|
}
|
|
|
|
func (m *MockUserRepository) Create(user *User) error {
|
|
m.users[user.Email] = user
|
|
return nil
|
|
}
|
|
|
|
// Test with mock
|
|
func TestUserService_WithMock(t *testing.T) {
|
|
mock := &MockUserRepository{
|
|
users: make(map[string]*User),
|
|
}
|
|
|
|
service := &UserService{repo: mock}
|
|
|
|
// Test
|
|
user, err := service.CreateUser("test@example.com", "password123")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify mock was called
|
|
if len(mock.users) != 1 {
|
|
t.Error("expected 1 user in mock")
|
|
}
|
|
}
|
|
```
|
|
|
|
## Best Practices Summary
|
|
|
|
### ✅ Do
|
|
|
|
- **Single responsibility** - One service, one purpose
|
|
- **Clear naming** - Descriptive method names
|
|
- **Validate input** - Always on Go side
|
|
- **Return errors** - Explicit error handling
|
|
- **Batch operations** - Reduce bridge calls
|
|
- **Use events** - For streaming data
|
|
- **Sanitise input** - Prevent injection
|
|
- **Test thoroughly** - Unit and integration tests
|
|
- **Document methods** - Comments become JSDoc
|
|
- **Version your API** - Plan for changes
|
|
|
|
### ❌ Don't
|
|
|
|
- **Don't create god objects** - Keep services focused
|
|
- **Don't trust frontend** - Validate everything
|
|
- **Don't return huge datasets** - Use pagination
|
|
- **Don't block** - Use goroutines for long operations
|
|
- **Don't ignore errors** - Handle all error cases
|
|
- **Don't skip testing** - Test early and often
|
|
- **Don't hardcode** - Use configuration
|
|
- **Don't expose internals** - Keep implementation private
|
|
|
|
## Next Steps
|
|
|
|
- [Methods](/features/bindings/methods) - Learn method binding basics
|
|
- [Services](/features/bindings/services) - Understand service architecture
|
|
- [Models](/features/bindings/models) - Bind complex data structures
|
|
- [Events](/features/events/system) - Use events for pub/sub
|
|
|
|
---
|
|
|
|
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [binding examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/binding).
|