Praktik Terbaik Binding
Praktik Terbaik Binding
Section titled “Praktik Terbaik Binding”Ikuti pola yang terbukti untuk desain binding guna membuat binding yang bersih, performan, dan aman. Panduan ini mencakup prinsip desain API, optimasi performa, pola keamanan, penanganan error, dan strategi pengujian untuk aplikasi yang mudah dirawat.
Prinsip Desain API
Section titled “Prinsip Desain API”1. Tanggung Jawab Tunggal
Section titled “1. Tanggung Jawab Tunggal”Setiap service harus memiliki satu tujuan yang jelas:
// ❌ Buruk: God objecttype AppService struct { // Does everything}
func (a *AppService) SaveFile(path string, data []byte) errorfunc (a *AppService) GetUser(id int) (*User, error)func (a *AppService) SendEmail(to, subject, body string) errorfunc (a *AppService) ProcessPayment(amount float64) error
// ✅ Baik: Service terfokustype 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) error2. Nama Metode yang Jelas
Section titled “2. Nama Metode yang Jelas”Gunakan nama yang deskriptif dan berorientasi aksi:
// ❌ Buruk: Nama tidak jelasfunc (s *Service) Do(x string) errorfunc (s *Service) Handle(data interface{}) interface{}func (s *Service) Process(input map[string]interface{}) bool
// ✅ Baik: Nama jelasfunc (s *FileService) SaveDocument(path string, content string) errorfunc (s *UserService) AuthenticateUser(email, password string) (*User, error)func (s *OrderService) CreateOrder(items []Item) (*Order, error)3. Tipe Return yang Konsisten
Section titled “3. Tipe Return yang Konsisten”Selalu kembalikan error secara eksplisit:
// ❌ Buruk: Penanganan error tidak konsistenfunc (s *Service) GetData() interface{} // How to handle errors?func (s *Service) SaveData(data string) // Silent failures?
// ✅ Baik: Error eksplisitfunc (s *Service) GetData() (Data, error)func (s *Service) SaveData(data string) error4. Validasi Input
Section titled “4. Validasi Input”Validasi semua input di sisi Go:
// ❌ Buruk: Tanpa validasifunc (s *UserService) CreateUser(email, password string) (*User, error) { user := &User{Email: email, Password: password} return s.db.Create(user)}
// ✅ Baik: Validasi terlebih dahulufunc (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)}Pola Performa
Section titled “Pola Performa”1. Operasi Batch
Section titled “1. Operasi Batch”Kurangi panggilan bridge dengan batching:
// ❌ Buruk: N panggilan// JavaScriptfor (const item of items) { await ProcessItem(item) // N bridge calls}
// ✅ Baik: 1 panggilan// Gofunc (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}
// JavaScriptconst results = await ProcessItems(items) // 1 bridge call2. Paginasi
Section titled “2. Paginasi”Jangan kembalikan dataset besar:
// ❌ Buruk: Mengembalikan semuanyafunc (s *Service) GetAllUsers() ([]User, error) { return s.db.FindAll() // Could be millions}
// ✅ Baik: Dipaginasitype 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
Section titled “3. Caching”Cache operasi yang mahal:
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 dengan Event
Section titled “4. Streaming dengan Event”Gunakan event untuk streaming data:
// ❌ Buruk: Pollingfunc (s *Service) GetProgress() int { return s.progress}
// JavaScript pollssetInterval(async () => { const progress = await GetProgress() updateUI(progress)}, 100)
// ✅ Baik: Event// Service must hold a reference to *application.App to emit events / log://// type Service struct {// app *application.App// }//// func NewService(app *application.App) *Service {// return &Service{app: app}// }//// app := application.New(application.Options{})// app.RegisterService(application.NewService(NewService(app)))
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 listensimport { Events } from '@wailsio/runtime'
Events.On("progress", (event) => { updateProgress(event.data.percent)})Pola Keamanan
Section titled “Pola Keamanan”1. Sanitasi Input
Section titled “1. Sanitasi Input”Selalu sanitasi input pengguna:
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. Autentikasi
Section titled “2. Autentikasi”Lindungi operasi sensitif:
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 methodfunc (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
Section titled “3. Rate Limiting”Cegah penyalahgunaan:
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}
// Usagefunc (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)}Pola Penanganan Error
Section titled “Pola Penanganan Error”1. Error Deskriptif
Section titled “1. Error Deskriptif”Berikan konteks dalam error:
// ❌ Buruk: Error generikfunc (s *Service) LoadFile(path string) ([]byte, error) { return os.ReadFile(path) // "no such file or directory"}
// ✅ Baik: Error kontekstualfunc (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. Tipe Error
Section titled “2. Tipe Error”Gunakan error bertipe untuk penanganan spesifik:
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)}
// Usagefunc (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. Pemulihan Error
Section titled “3. Pemulihan Error”Tangani error dengan anggun:
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)}Pola Pengujian
Section titled “Pola Pengujian”1. Unit Testing
Section titled “1. Unit Testing”Uji service secara terisolasi:
func TestUserService_CreateUser(t *testing.T) { // Setup db := &MockDB{} service := &UserService{db: db}
// Test valid input if err != nil { t.Fatalf("unexpected error: %v", err) } }
// Test invalid email _, err = service.CreateUser("invalid", "password123") if err == nil { t.Error("expected error for invalid email") }
// Test short password if err == nil { t.Error("expected error for short password") }}2. Integration Testing
Section titled “2. Integration Testing”Uji dengan dependensi nyata:
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}
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 Service
Section titled “3. Mock Service”Buat interface yang dapat diuji:
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 testingtype MockUserRepository struct { users map[string]*User}
func (m *MockUserRepository) Create(user *User) error { m.users[user.Email] = user return nil}
// Test with mockfunc TestUserService_WithMock(t *testing.T) { mock := &MockUserRepository{ users: make(map[string]*User), }
service := &UserService{repo: mock}
// Test if err != nil { t.Fatal(err) }
// Verify mock was called if len(mock.users) != 1 { t.Error("expected 1 user in mock") }}Ringkasan Praktik Terbaik
Section titled “Ringkasan Praktik Terbaik”✅ Lakukan
Section titled “✅ Lakukan”- Tanggung jawab tunggal - Satu service, satu tujuan
- Penamaan jelas - Nama metode deskriptif
- Validasi input - Selalu di sisi Go
- Kembalikan error - Penanganan error eksplisit
- Operasi batch - Kurangi panggilan bridge
- Gunakan event - Untuk streaming data
- Sanitasi input - Cegah injection
- Uji secara menyeluruh - Unit dan integration test
- Dokumentasikan metode - Komentar menjadi JSDoc
- Versioning API - Rencanakan perubahan
❌ Jangan
Section titled “❌ Jangan”- Jangan buat god object - Jaga service tetap terfokus
- Jangan percaya frontend - Validasi semuanya
- Jangan kembalikan dataset besar - Gunakan paginasi
- Jangan block - Gunakan goroutine untuk operasi panjang
- Jangan abaikan error - Tangani semua kasus error
- Jangan lewatkan pengujian - Uji sejak awal dan sering
- Jangan hardcode - Gunakan konfigurasi
- Jangan ekspos internal - Jaga implementasi tetap privat
Langkah Selanjutnya
Section titled “Langkah Selanjutnya”- Metode - Pelajari dasar binding metode
- Services - Pahami arsitektur service
- Models - Bind struktur data kompleks
- Events - Gunakan event untuk pub/sub
Ada pertanyaan? Tanyakan di Discord atau lihat contoh binding.