服务
使用服务构建模块化、可复用的应用程序组件
服务架构
Wails 服务提供了一种结构化方式,使用模块化、自包含的组件来组织应用程序逻辑。服务支持生命周期,可使用启动和关闭钩子;服务会自动绑定到前端,支持依赖注入,并且完全可以单独测试。
快速入门
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, ")),
},
})就是这样!现在可以从 JavaScript 调用Greet:
js
import { Greet } from './bindings/changeme/GreetService';
const message = await Greet("World");
console.log(message); // "Hello, World!"创建服务
基本服务
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
}注册:
go
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&CalculatorService{}),
},
})要点:
- 仅绑定导出的方法(PascalCase)
- 服务是单例(只有一个实例)
- 方法可以返回
(value, error)
有状态服务
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
}重要:所有窗口共享服务。请始终使用互斥锁来确保线程安全。
具有依赖项的服务
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
}注册具有依赖项的服务:
go
db, _ := sql.Open("sqlite3", "app.db")
logger := slog.Default()
app := application.New(application.Options{
Services: []application.Service{
application.NewService(NewUserService(db, logger)),
},
})服务生命周期
ServiceStartup
在应用程序启动时调用:
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
}使用场景:
- 初始化资源
- 验证配置
- 运行迁移
- 启动后台任务
- 连接外部服务
重要:
- 服务按照注册顺序启动
- 返回错误可阻止应用程序启动
- 使用
ctx执行取消操作
ServiceShutdown
在应用程序关闭时调用:
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
}使用场景:
- 关闭连接
- 保存状态
- 清理资源
- 刷新缓冲区
- 取消后台任务
重要:
- 服务按照相反顺序关闭
- 应用程序上下文此时已取消
- 返回错误可记录警告(不会阻止关闭)
完整的生命周期示例
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()
}
}
}服务选项
自定义名称
go
app := application.New(application.Options{
Services: []application.Service{
application.NewServiceWithOptions(&MyService{}, application.ServiceOptions{
Name: "CustomServiceName",
}),
},
})使用场景:
- 同一服务类型的多个实例
- 使日志更加清晰
- 更便于调试
HTTP 路由
服务可以处理 HTTP 请求:
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",
}),
},
})访问:http://wails.localhost/files/...
使用场景:
- 提供文件
- 自定义 API
- WebSocket 端点
- 媒体流传输
服务模式
仓储模式
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
}服务层模式
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
}工厂模式
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,
}
}事件驱动模式
go
type OrderService struct {
app *application.App
}
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
}依赖注入
构造函数注入
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)),
},
})应用程序注入
go
type NotificationService struct {
app *application.App
}
func NewNotificationService(app *application.App) *NotificationService {
return &NotificationService{app: app}
}
func (n *NotificationService) Notify(message string) {
// Use application to emit events
n.app.Event.Emit("notification", message)
// For native OS notifications, register the notifications service from
// pkg/services/notifications and call notifier.SendNotification(...).
}
// Register after app creation
app := application.New(application.Options{})
app.RegisterService(application.NewService(NewNotificationService(app)))服务间依赖关系
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),
},
})测试服务
单元测试
go
func TestCalculatorService_Add(t *testing.T) {
calc := &CalculatorService{}
result := calc.Add(2, 3)
if result != 5 {
t.Errorf("expected 5, got %d", result)
}
}使用依赖项进行测试
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)
}
}测试生命周期
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)
}
}最佳实践
✅ 应该做
- 单一职责——一个服务只负责一个用途
- 构造函数注入——显式传递依赖项
- 线程安全的状态——使用互斥锁
- 返回错误——不要触发 panic
- 记录重要事件——使用结构化日志
- 独立测试——模拟依赖项
❌ 不应该做
- 不要使用全局状态——传递依赖项
- 不要阻塞启动过程——确保 ServiceStartup 快速完成
- 不要忽视关闭过程——始终执行清理
- 不要创建循环依赖——谨慎设计
- 不要公开内部方法——将其保持为私有方法
- 不要忘记线程安全——服务会被共享
完整示例
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()
}