gui/docs/ref/wails-v3/features/bindings/services.mdx
Snider 4bdbb68f46
Some checks failed
Security Scan / security (push) Failing after 9s
Test / test (push) Failing after 1m21s
refactor: update import path from go-config to core/config
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-14 10:26:36 +00:00

797 lines
17 KiB
Text

---
title: Services
description: Build modular, reusable application components with services
sidebar:
order: 2
---
## Service Architecture
Wails **services** provide a structured way to organise application logic with modular, self-contained components. Services are lifecycle-aware with startup and shutdown hooks, automatically bound to the frontend, dependency-injectable, and fully testable in isolation.
## Quick Start
```go
type GreetService struct {
prefix string
}
func NewGreetService(prefix string) *GreetService {
return &GreetService{prefix: prefix}
}
func (g *GreetService) Greet(name string) string {
return g.prefix + name + "!"
}
// Register
app := application.New(application.Options{
Services: []application.Service{
application.NewService(NewGreetService("Hello, ")),
},
})
```
**That's it!** `Greet` is now callable from JavaScript:
```js
import { Greet } from './bindings/main/GreetService';
const message = await Greet("World");
console.log(message); // "Hello, World!"
```
## Creating Services
### Basic Service
```go
type CalculatorService struct{}
func (c *CalculatorService) Add(a, b int) int {
return a + b
}
func (c *CalculatorService) Subtract(a, b int) int {
return a - b
}
```
**Register:**
```go
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&CalculatorService{}),
},
})
```
**Key points:**
- Only **exported methods** (PascalCase) are bound
- Services are **singletons** (one instance)
- Methods can return `(value, error)`
### Service with State
```go
type CounterService struct {
count int
mu sync.RWMutex
}
func (c *CounterService) Increment() int {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
return c.count
}
func (c *CounterService) GetCount() int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.count
}
func (c *CounterService) Reset() {
c.mu.Lock()
defer c.mu.Unlock()
c.count = 0
}
```
**Important:** Services are shared across all windows. Always use mutexes for thread safety.
### Service with Dependencies
```go
type UserService struct {
db *sql.DB
logger *slog.Logger
}
func NewUserService(db *sql.DB, logger *slog.Logger) *UserService {
return &UserService{
db: db,
logger: logger,
}
}
func (u *UserService) GetUser(id int) (*User, error) {
u.logger.Info("Getting user", "id", id)
var user User
err := u.db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
return &user, nil
}
```
**Register with dependencies:**
```go
db, _ := sql.Open("sqlite3", "app.db")
logger := slog.Default()
app := application.New(application.Options{
Services: []application.Service{
application.NewService(NewUserService(db, logger)),
},
})
```
## Service Lifecycle
### ServiceStartup
Called when application starts:
```go
func (u *UserService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
u.logger.Info("UserService starting up")
// Initialise resources
if err := u.db.Ping(); err != nil {
return fmt.Errorf("database not available: %w", err)
}
// Run migrations
if err := u.runMigrations(); err != nil {
return fmt.Errorf("migrations failed: %w", err)
}
// Start background tasks
go u.backgroundSync(ctx)
return nil
}
```
**Use cases:**
- Initialise resources
- Validate configuration
- Run migrations
- Start background tasks
- Connect to external services
**Important:**
- Services start in **registration order**
- Return error to **prevent app startup**
- Use `ctx` for cancellation
### ServiceShutdown
Called when application shuts down:
```go
func (u *UserService) ServiceShutdown() error {
u.logger.Info("UserService shutting down")
// Close database
if err := u.db.Close(); err != nil {
return fmt.Errorf("failed to close database: %w", err)
}
// Cleanup resources
u.cleanup()
return nil
}
```
**Use cases:**
- Close connections
- Save state
- Cleanup resources
- Flush buffers
- Cancel background tasks
**Important:**
- Services shutdown in **reverse order**
- Application context already cancelled
- Return error to **log warning** (doesn't prevent shutdown)
### Complete Lifecycle Example
```go
type DatabaseService struct {
db *sql.DB
logger *slog.Logger
cancel context.CancelFunc
}
func NewDatabaseService(logger *slog.Logger) *DatabaseService {
return &DatabaseService{logger: logger}
}
func (d *DatabaseService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
d.logger.Info("Starting database service")
// Open database
db, err := sql.Open("sqlite3", "app.db")
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
d.db = db
// Test connection
if err := db.Ping(); err != nil {
return fmt.Errorf("database not available: %w", err)
}
// Start background cleanup
ctx, cancel := context.WithCancel(ctx)
d.cancel = cancel
go d.periodicCleanup(ctx)
return nil
}
func (d *DatabaseService) ServiceShutdown() error {
d.logger.Info("Shutting down database service")
// Cancel background tasks
if d.cancel != nil {
d.cancel()
}
// Close database
if d.db != nil {
if err := d.db.Close(); err != nil {
return fmt.Errorf("failed to close database: %w", err)
}
}
return nil
}
func (d *DatabaseService) periodicCleanup(ctx context.Context) {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
d.cleanup()
}
}
}
```
## Service Options
### Custom Name
```go
app := application.New(application.Options{
Services: []application.Service{
application.NewServiceWithOptions(&MyService{}, application.ServiceOptions{
Name: "CustomServiceName",
}),
},
})
```
**Use cases:**
- Multiple instances of same service type
- Clearer logging
- Better debugging
### HTTP Routes
Services can handle HTTP requests:
```go
type FileService struct {
root string
}
func (f *FileService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Serve files from root directory
http.FileServer(http.Dir(f.root)).ServeHTTP(w, r)
}
// Register with route
app := application.New(application.Options{
Services: []application.Service{
application.NewServiceWithOptions(&FileService{root: "./files"}, application.ServiceOptions{
Route: "/files",
}),
},
})
```
**Access:** `http://wails.localhost/files/...`
**Use cases:**
- File serving
- Custom APIs
- WebSocket endpoints
- Media streaming
## Service Patterns
### Repository Pattern
```go
type UserRepository struct {
db *sql.DB
}
func (r *UserRepository) GetByID(id int) (*User, error) {
// Database query
}
func (r *UserRepository) Create(user *User) error {
// Insert user
}
func (r *UserRepository) Update(user *User) error {
// Update user
}
func (r *UserRepository) Delete(id int) error {
// Delete user
}
```
### Service Layer Pattern
```go
type UserService struct {
repo *UserRepository
logger *slog.Logger
}
func (s *UserService) RegisterUser(email, password string) (*User, error) {
// Validate
if !isValidEmail(email) {
return nil, errors.New("invalid email")
}
// Hash password
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// Create user
user := &User{
Email: email,
PasswordHash: string(hash),
CreatedAt: time.Now(),
}
if err := s.repo.Create(user); err != nil {
return nil, err
}
s.logger.Info("User registered", "email", email)
return user, nil
}
```
### Factory Pattern
```go
type ServiceFactory struct {
db *sql.DB
logger *slog.Logger
}
func (f *ServiceFactory) CreateUserService() *UserService {
return &UserService{
repo: &UserRepository{db: f.db},
logger: f.logger,
}
}
func (f *ServiceFactory) CreateOrderService() *OrderService {
return &OrderService{
repo: &OrderRepository{db: f.db},
logger: f.logger,
}
}
```
### Event-Driven Pattern
```go
type OrderService struct {
app *application.Application
}
func (o *OrderService) CreateOrder(items []Item) (*Order, error) {
order := &Order{
Items: items,
CreatedAt: time.Now(),
}
// Save order
if err := o.saveOrder(order); err != nil {
return nil, err
}
// Emit event
o.app.Event.Emit("order-created", order)
return order, nil
}
```
## Dependency Injection
### Constructor Injection
```go
type EmailService struct {
smtp *smtp.Client
logger *slog.Logger
}
func NewEmailService(smtp *smtp.Client, logger *slog.Logger) *EmailService {
return &EmailService{
smtp: smtp,
logger: logger,
}
}
// Register
smtpClient := createSMTPClient()
logger := slog.Default()
app := application.New(application.Options{
Services: []application.Service{
application.NewService(NewEmailService(smtpClient, logger)),
},
})
```
### Application Injection
```go
type NotificationService struct {
app *application.Application
}
func NewNotificationService(app *application.Application) *NotificationService {
return &NotificationService{app: app}
}
func (n *NotificationService) Notify(message string) {
// Use application to emit events
n.app.Event.Emit("notification", message)
// Or show system notification
n.app.ShowNotification(message)
}
// Register after app creation
app := application.New(application.Options{})
app.RegisterService(application.NewService(NewNotificationService(app)))
```
### Service-to-Service Dependencies
```go
type OrderService struct {
userService *UserService
emailService *EmailService
}
func NewOrderService(userService *UserService, emailService *EmailService) *OrderService {
return &OrderService{
userService: userService,
emailService: emailService,
}
}
// Register in order
userService := &UserService{}
emailService := &EmailService{}
orderService := NewOrderService(userService, emailService)
app := application.New(application.Options{
Services: []application.Service{
application.NewService(userService),
application.NewService(emailService),
application.NewService(orderService),
},
})
```
## Testing Services
### Unit Testing
```go
func TestCalculatorService_Add(t *testing.T) {
calc := &CalculatorService{}
result := calc.Add(2, 3)
if result != 5 {
t.Errorf("expected 5, got %d", result)
}
}
```
### Testing with Dependencies
```go
func TestUserService_GetUser(t *testing.T) {
// Create mock database
db, mock, _ := sqlmock.New()
defer db.Close()
// Set expectations
rows := sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "Alice")
mock.ExpectQuery("SELECT").WillReturnRows(rows)
// Create service
service := NewUserService(db, slog.Default())
// Test
user, err := service.GetUser(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "Alice" {
t.Errorf("expected Alice, got %s", user.Name)
}
}
```
### Testing Lifecycle
```go
func TestDatabaseService_Lifecycle(t *testing.T) {
service := NewDatabaseService(slog.Default())
// Test startup
ctx := context.Background()
err := service.ServiceStartup(ctx, application.ServiceOptions{})
if err != nil {
t.Fatalf("startup failed: %v", err)
}
// Test functionality
// ...
// Test shutdown
err = service.ServiceShutdown()
if err != nil {
t.Fatalf("shutdown failed: %v", err)
}
}
```
## Best Practices
### ✅ Do
- **Single responsibility** - One service, one purpose
- **Constructor injection** - Pass dependencies explicitly
- **Thread-safe state** - Use mutexes
- **Return errors** - Don't panic
- **Log important events** - Use structured logging
- **Test in isolation** - Mock dependencies
### ❌ Don't
- **Don't use global state** - Pass dependencies
- **Don't block startup** - Keep ServiceStartup fast
- **Don't ignore shutdown** - Always cleanup
- **Don't create circular dependencies** - Design carefully
- **Don't expose internal methods** - Keep them private
- **Don't forget thread safety** - Services are shared
## Complete Example
```go
package main
import (
"context"
"database/sql"
"fmt"
"log/slog"
"sync"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
_ "github.com/mattn/go-sqlite3"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"createdAt"`
}
type UserService struct {
db *sql.DB
logger *slog.Logger
cache map[int]*User
mu sync.RWMutex
}
func NewUserService(logger *slog.Logger) *UserService {
return &UserService{
logger: logger,
cache: make(map[int]*User),
}
}
func (u *UserService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
u.logger.Info("Starting UserService")
// Open database
db, err := sql.Open("sqlite3", "users.db")
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
u.db = db
// Create table
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
// Preload cache
if err := u.loadCache(); err != nil {
return fmt.Errorf("failed to load cache: %w", err)
}
return nil
}
func (u *UserService) ServiceShutdown() error {
u.logger.Info("Shutting down UserService")
if u.db != nil {
return u.db.Close()
}
return nil
}
func (u *UserService) GetUser(id int) (*User, error) {
// Check cache first
u.mu.RLock()
if user, ok := u.cache[id]; ok {
u.mu.RUnlock()
return user, nil
}
u.mu.RUnlock()
// Query database
var user User
err := u.db.QueryRow(
"SELECT id, name, email, created_at FROM users WHERE id = ?",
id,
).Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("user %d not found", id)
}
if err != nil {
return nil, fmt.Errorf("database error: %w", err)
}
// Update cache
u.mu.Lock()
u.cache[id] = &user
u.mu.Unlock()
return &user, nil
}
func (u *UserService) CreateUser(name, email string) (*User, error) {
result, err := u.db.Exec(
"INSERT INTO users (name, email) VALUES (?, ?)",
name, email,
)
if err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
id, _ := result.LastInsertId()
user := &User{
ID: int(id),
Name: name,
Email: email,
CreatedAt: time.Now(),
}
// Update cache
u.mu.Lock()
u.cache[int(id)] = user
u.mu.Unlock()
u.logger.Info("User created", "id", id, "email", email)
return user, nil
}
func (u *UserService) loadCache() error {
rows, err := u.db.Query("SELECT id, name, email, created_at FROM users")
if err != nil {
return err
}
defer rows.Close()
u.mu.Lock()
defer u.mu.Unlock()
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt); err != nil {
return err
}
u.cache[user.ID] = &user
}
return rows.Err()
}
func main() {
app := application.New(application.Options{
Name: "User Management",
Services: []application.Service{
application.NewService(NewUserService(slog.Default())),
},
})
app.Window.New()
app.Run()
}
```
## Next Steps
- [Method Bindings](/features/bindings/methods) - Learn how to bind Go methods to JavaScript
- [Models](/features/bindings/models) - Bind complex data structures
- [Events](/features/events/system) - Use events for pub/sub communication
- [Best Practices](/features/bindings/best-practices) - Service design patterns and best practices
---
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [service examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples).