Services
Arsitektur Service
Section titled “Arsitektur Service”Service Wails menyediakan cara terstruktur untuk mengorganisir logika aplikasi dengan komponen modular dan mandiri. Service aware terhadap lifecycle dengan hook startup dan shutdown, otomatis ter-bind ke frontend, dapat di-inject dependensinya, dan sepenuhnya dapat diuji secara terisolasi.
Memulai Cepat
Section titled “Memulai Cepat”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 + "!"}
// Registerapp := application.New(application.Options{ Services: []application.Service{ application.NewService(NewGreetService("Hello, ")), },})Itu saja! Greet sekarang dapat dipanggil dari JavaScript:
import { Greet } from './bindings/changeme/GreetService';
const message = await Greet("World");console.log(message); // "Hello, World!"Membuat Service
Section titled “Membuat Service”Service Dasar
Section titled “Service Dasar”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}Daftarkan:
app := application.New(application.Options{ Services: []application.Service{ application.NewService(&CalculatorService{}), },})Poin penting:
- Hanya metode yang diekspor (PascalCase) yang ter-bind
- Service adalah singleton (satu instance)
- Metode dapat mengembalikan
(value, error)
Service dengan State
Section titled “Service dengan State”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}Penting: Service dibagikan ke semua window. Selalu gunakan mutex untuk thread safety.
Service dengan Dependensi
Section titled “Service dengan Dependensi”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}Daftarkan dengan dependensi:
db, _ := sql.Open("sqlite3", "app.db")logger := slog.Default()
app := application.New(application.Options{ Services: []application.Service{ application.NewService(NewUserService(db, logger)), },})Lifecycle Service
Section titled “Lifecycle Service”ServiceStartup
Section titled “ServiceStartup”Dipanggil saat aplikasi dimulai:
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}Kasus penggunaan:
- Inisialisasi resource
- Validasi konfigurasi
- Jalankan migrasi
- Mulai task background
- Hubungkan ke service eksternal
Penting:
- Service dimulai dalam urutan registrasi
- Kembalikan error untuk mencegah startup aplikasi
- Gunakan
ctxuntuk pembatalan
ServiceShutdown
Section titled “ServiceShutdown”Dipanggil saat aplikasi dimatikan:
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}Kasus penggunaan:
- Tutup koneksi
- Simpan state
- Bersihkan resource
- Flush buffer
- Batalkan task background
Penting:
- Service dimatikan dalam urutan terbalik
- Context aplikasi sudah dibatalkan
- Kembalikan error untuk log peringatan (tidak mencegah shutdown)
Contoh Lifecycle Lengkap
Section titled “Contoh Lifecycle Lengkap”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() } }}Opsi Service
Section titled “Opsi Service”Nama Kustom
Section titled “Nama Kustom”app := application.New(application.Options{ Services: []application.Service{ application.NewServiceWithOptions(&MyService{}, application.ServiceOptions{ Name: "CustomServiceName", }), },})Kasus penggunaan:
- Beberapa instance dari tipe service yang sama
- Logging yang lebih jelas
- Debugging yang lebih baik
Rute HTTP
Section titled “Rute HTTP”Service dapat menangani request HTTP:
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 routeapp := application.New(application.Options{ Services: []application.Service{ application.NewServiceWithOptions(&FileService{root: "./files"}, application.ServiceOptions{ Route: "/files", }), },})Akses: http://wails.localhost/files/...
Kasus penggunaan:
- Penyajian file
- API kustom
- Endpoint WebSocket
- Streaming media
Pola Service
Section titled “Pola Service”Pola Repository
Section titled “Pola Repository”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}Pola Service Layer
Section titled “Pola Service Layer”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}Pola Factory
Section titled “Pola Factory”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, }}Pola Event-Driven
Section titled “Pola Event-Driven”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}Dependency Injection
Section titled “Dependency Injection”Constructor Injection
Section titled “Constructor Injection”type EmailService struct { smtp *smtp.Client logger *slog.Logger}
func NewEmailService(smtp *smtp.Client, logger *slog.Logger) *EmailService { return &EmailService{ smtp: smtp, logger: logger, }}
// RegistersmtpClient := createSMTPClient()logger := slog.Default()
app := application.New(application.Options{ Services: []application.Service{ application.NewService(NewEmailService(smtpClient, logger)), },})Application Injection
Section titled “Application Injection”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 creationapp := application.New(application.Options{})app.RegisterService(application.NewService(NewNotificationService(app)))Dependensi Service-ke-Service
Section titled “Dependensi Service-ke-Service”type OrderService struct { userService *UserService emailService *EmailService}
func NewOrderService(userService *UserService, emailService *EmailService) *OrderService { return &OrderService{ userService: userService, emailService: emailService, }}
// Register in orderuserService := &UserService{}emailService := &EmailService{}orderService := NewOrderService(userService, emailService)
app := application.New(application.Options{ Services: []application.Service{ application.NewService(userService), application.NewService(emailService), application.NewService(orderService), },})Menguji Service
Section titled “Menguji Service”Unit Testing
Section titled “Unit Testing”func TestCalculatorService_Add(t *testing.T) { calc := &CalculatorService{}
result := calc.Add(2, 3)
if result != 5 { t.Errorf("expected 5, got %d", result) }}Pengujian dengan Dependensi
Section titled “Pengujian dengan Dependensi”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) }}Pengujian Lifecycle
Section titled “Pengujian Lifecycle”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) }}Praktik Terbaik
Section titled “Praktik Terbaik”✅ Lakukan
Section titled “✅ Lakukan”- Tanggung jawab tunggal - Satu service, satu tujuan
- Constructor injection - Teruskan dependensi secara eksplisit
- State thread-safe - Gunakan mutex
- Kembalikan error - Jangan panic
- Log event penting - Gunakan structured logging
- Uji secara terisolasi - Mock dependensi
❌ Jangan
Section titled “❌ Jangan”- Jangan gunakan global state - Teruskan dependensi
- Jangan block startup - Jaga ServiceStartup tetap cepat
- Jangan abaikan shutdown - Selalu bersihkan
- Jangan buat dependensi sirkular - Desain dengan hati-hati
- Jangan ekspos metode internal - Jaga tetap privat
- Jangan lupa thread safety - Service dibagikan
Contoh Lengkap
Section titled “Contoh Lengkap”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()}Langkah Selanjutnya
Section titled “Langkah Selanjutnya”- Binding Metode - Pelajari cara bind metode Go ke JavaScript
- Models - Bind struktur data kompleks
- Events - Gunakan event untuk komunikasi pub/sub
- Praktik Terbaik - Pola desain service dan praktik terbaik
Ada pertanyaan? Tanyakan di Discord atau lihat contoh service.