Binding Metode
Panggil metode Go dari JavaScript dengan keamanan tipe
Binding Go-JavaScript yang Aman secara Tipe
Wails secara otomatis menghasilkan binding JavaScript/TypeScript yang aman secara tipe untuk metode Go Anda. Tulis kode Go, jalankan satu perintah, lalu dapatkan fungsi frontend dengan tipe lengkap tanpa overhead HTTP, tanpa pekerjaan manual, dan tanpa kode boilerplate.
Mulai Cepat
1. Tulis layanan Go:
type GreetService struct{}
func (g *GreetService) Greet(name string) string {
return "Hello, " + name + "!"
}2. Daftarkan layanan:
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&GreetService{}),
},
})3. Hasilkan binding:
wails3 generate bindings
4. Gunakan di JavaScript:
import { Greet } from './bindings/changeme/greetservice'
const message = await Greet("World")
console.log(message) // "Hello, World!"Selesai! Panggilan dari Go ke JavaScript yang aman secara tipe.
Membuat Layanan
Layanan Dasar
package main
import "github.com/wailsapp/wails/v3/pkg/application"
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
}
func (c *CalculatorService) Multiply(a, b int) int {
return a * b
}
func (c *CalculatorService) Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}Daftarkan:
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&CalculatorService{}),
},
})Poin penting:
- Hanya metode yang diekspor (PascalCase) yang dibuatkan binding
- Metode dapat mengembalikan nilai atau
(value, error) - Layanan merupakan singleton (satu instans per aplikasi)
Layanan dengan State
type CounterService struct {
count int
mu sync.Mutex
}
func (c *CounterService) Increment() int {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
return c.count
}
func (c *CounterService) Decrement() 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: Layanan digunakan bersama oleh semua jendela. Gunakan mutex untuk keamanan thread.
Layanan dengan Dependensi
type DatabaseService struct {
db *sql.DB
}
func NewDatabaseService(db *sql.DB) *DatabaseService {
return &DatabaseService{db: db}
}
func (d *DatabaseService) GetUser(id int) (*User, error) {
var user User
err := d.db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
return &user, err
}Daftarkan dengan dependensi:
db, _ := sql.Open("sqlite3", "app.db")
app := application.New(application.Options{
Services: []application.Service{
application.NewService(NewDatabaseService(db)),
},
})Menghasilkan Binding
Pembuatan Dasar
wails3 generate bindings
Output:
INFO 347 Packages, 3 Services, 12 Methods, 0 Enums, 0 Models in 1.98s
INFO Output directory: /myproject/frontend/bindingsStruktur yang dihasilkan:
- frontend/bindings
- myapp
- calculatorservice.js
- counterservice.js
- databaseservice.js
- index.js
- myapp
Pembuatan TypeScript
wails3 generate bindings -ts
Menghasilkan file .ts dengan tipe TypeScript lengkap.
Direktori Output Khusus
wails3 generate bindings -d ./src/bindings
Mode Pemantauan (Pengembangan)
wails3 dev
Secara otomatis menghasilkan ulang binding ketika kode Go berubah.
Menggunakan Binding
JavaScript
Binding yang dihasilkan:
// frontend/bindings/<full-go-import-path>/calculatorservice.js
// (Real generated output — imports $Call from /wails/runtime.js and calls $Call.ByID
// with a numeric method ID. Generate with `wails3 generate bindings -names` to get
// $Call.ByName("<package>.<Struct>.<Method>", ...) instead.)
import { Call as $Call, Create as $Create } from "/wails/runtime.js";
/**
* @param {number} $0
* @param {number} $1
* @returns {Promise<number>}
*/
export function Add($0, $1) {
return $Call.ByID(1234567890, $0, $1); // numeric ID assigned by the generator
}Penggunaan:
import { Add, Subtract, Multiply, Divide } from './bindings/changeme/calculatorservice'
// Simple calls
const sum = await Add(5, 3) // 8
const diff = await Subtract(10, 4) // 6
const product = await Multiply(7, 6) // 42
// Error handling
try {
const result = await Divide(10, 0)
} catch (error) {
console.error("Error:", error) // "division by zero"
}TypeScript
Binding yang dihasilkan:
// frontend/bindings/changeme/calculatorservice.ts
export function Add(a: number, b: number): Promise<number>
export function Subtract(a: number, b: number): Promise<number>
export function Multiply(a: number, b: number): Promise<number>
export function Divide(a: number, b: number): Promise<number>Penggunaan:
import { Add, Divide } from './bindings/changeme/calculatorservice'
const sum: number = await Add(5, 3)
try {
const result = await Divide(10, 0)
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message)
}
}Manfaat:
- Pemeriksaan tipe lengkap
- Pelengkapan otomatis IDE
- Kesalahan pada waktu kompilasi
- Refactoring yang lebih baik
File Indeks
Indeks yang dihasilkan:
// frontend/bindings/changeme/index.js
export * as CalculatorService from './calculatorservice.js'
export * as CounterService from './counterservice.js'
export * as DatabaseService from './databaseservice.js'Impor yang disederhanakan:
import { CalculatorService } from './bindings/myapp'
const sum = await CalculatorService.Add(5, 3)Pemetaan Tipe
Tipe Primitif
| Tipe Go | JavaScript/TypeScript |
|---|---|
string |
string |
bool |
boolean |
int, int8, int16, int32, int64 |
number |
uint, uint8, uint16, uint32, uint64 |
number |
float32, float64 |
number |
byte |
number |
rune |
number |
Tipe Kompleks
| Tipe Go | JavaScript/TypeScript | Catatan |
|---|---|---|
[]T |
T[] |
- |
[N]T |
T[] |
- |
map[string]T |
{ [_: string]: T } |
map dengan kunci string |
map[K]V |
{ [_ in K]?: V } |
K non-string dirender sebagai tipe terpetakan, bukan Map JS |
[]byte |
string |
dikodekan dengan base64 |
struct |
class / interface |
dengan field |
time.Time |
any |
diserialisasi sebagai string RFC3339Nano saat runtime |
*T |
T | null |
pointer berarti dapat bernilai null |
any / interface{} |
any |
- |
error |
any / Exception |
Exception jika digunakan sebagai nilai kembalian; jika tidak, any |
Tipe yang Tidak Didukung
Tipe-tipe ini tidak dapat diteruskan melalui bridge:
chan T(channel)func()(fungsi)- Interface kompleks (kecuali
interface{}) - Field yang tidak diekspor (huruf kecil)
Solusi alternatif: Gunakan ID atau handle:
// ❌ Can't return file handle
func OpenFile(path string) (*os.File, error)
// ✅ Return file ID instead
var files = make(map[string]*os.File)
func OpenFile(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
id := generateID()
files[id] = file
return id, nil
}
func ReadFile(id string) ([]byte, error) {
file := files[id]
return io.ReadAll(file)
}
func CloseFile(id string) error {
file := files[id]
delete(files, id)
return file.Close()
}Penanganan Error
Sisi Go
func (d *DatabaseService) GetUser(id int) (*User, error) {
if id <= 0 {
return nil, errors.New("invalid user ID")
}
var user User
err := d.db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("user %d not found", id)
}
if err != nil {
return nil, fmt.Errorf("database error: %w", err)
}
return &user, nil
}Sisi JavaScript
Ketika metode yang terikat gagal, promise yang dikembalikan ditolak dengan objek Error JavaScript:
import { GetUser } from './bindings/changeme/databaseservice'
try {
const user = await GetUser(123)
console.log("User:", user)
} catch (error) {
console.error(error.message)
// "user 123 not found"
}Runtime melempar tipe error yang berbeda bergantung pada penyebab kegagalan:
| Tipe error | Dilempar ketika |
|---|---|
TypeError |
Pemanggilan memiliki jumlah argumen yang salah, atau suatu argumen tidak dapat dikonversi ke tipe Go |
RuntimeError |
Metode mengembalikan error atau mengalami panic saat dijalankan |
Error |
Kegagalan lainnya, misalnya pemanggilan metode yang tidak ada |
Setiap error menyediakan:
name: tipe error dari tabel di atasmessage: pesan error Gocause: error Go yang diserialisasi sebagai JSON, jika tersedia. Jika metode mengembalikan beberapa error,causeberupa array dengan satu entri untuk setiap error.
Kelas RuntimeError diekspor oleh paket @wailsio/runtime, sehingga Anda dapat membedakan error yang dikembalikan oleh kode Go Anda dari kegagalan lainnya:
import { Call } from '@wailsio/runtime'
import { GetUser } from './bindings/changeme/databaseservice'
try {
const user = await GetUser(123)
} catch (error) {
if (error instanceof Call.RuntimeError) {
// GetUser returned an error
} else {
// The call itself failed
}
}Data Error Terstruktur
Mengembalikan tipe error khusus dari Go membuat bentuk JSON-nya tersedia pada properti cause dari error yang dilempar:
type ValidationError struct {
Field string `json:"field"`
Reason string `json:"reason"`
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Reason)
}
func (s *UserService) UpdateEmail(id int, email string) error {
if !strings.Contains(email, "@") {
return &ValidationError{Field: "email", Reason: "invalid email address"}
}
// ...
return nil
}import { UpdateEmail } from './bindings/changeme/userservice'
try {
await UpdateEmail(1, "not-an-email")
} catch (error) {
console.log(error.message) // "email: invalid email address"
console.log(error.cause) // { field: "email", reason: "invalid email address" }
}Error diserialisasi dengan paket standar encoding/json, sehingga hanya field yang diekspor yang disertakan. Error yang dibuat dengan errors.New atau fmt.Errorf tidak memiliki field yang diekspor dan diserialisasi sebagai objek kosong.
Untuk mengontrol sepenuhnya cara error diserialisasi, sediakan fungsi MarshalError dalam opsi layanan:
app := application.New(application.Options{
Services: []application.Service{
application.NewServiceWithOptions(&UserService{}, application.ServiceOptions{
MarshalError: func(err error) []byte {
var validationErr *ValidationError
if errors.As(err, &validationErr) {
data, _ := json.Marshal(map[string]string{
"type": "validation",
"field": validationErr.Field,
})
return data
}
return nil // fall back to the default serialisation
},
}),
},
})MarshalError harus mengembalikan JSON yang valid, atau nil untuk kembali menggunakan serialisasi default.
Performa
Overhead Pemanggilan
Panggilan umum: <1ms
JavaScript → Bridge → Go → Bridge → JavaScript
↓ ↓ ↓ ↓ ↓
<0.1ms <0.1ms [varies] <0.1ms <0.1msDibandingkan dengan alternatif:
- HTTP/REST: 5-50ms
- IPC: 1-10ms
- Wails: <1ms
Tips Optimasi
✅ Kelompokkan operasi:
// ❌ Slow: N calls
for (const item of items) {
await ProcessItem(item)
}
// ✅ Fast: 1 call
await ProcessItems(items)✅ Simpan hasil dalam cache:
// ❌ Repeated calls
const config1 = await GetConfig()
const config2 = await GetConfig()
// ✅ Cache
const config = await GetConfig()
// Use config multiple times✅ Gunakan event untuk streaming:
func ProcessLargeFile(path string) error {
// Emit progress events
for line := range lines {
app.Event.Emit("progress", line)
}
return nil
}Contoh Lengkap
Go:
package main
import (
"fmt"
"github.com/wailsapp/wails/v3/pkg/application"
)
type TodoService struct {
todos []Todo
}
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
func (t *TodoService) GetAll() []Todo {
return t.todos
}
func (t *TodoService) Add(title string) Todo {
todo := Todo{
ID: len(t.todos) + 1,
Title: title,
Completed: false,
}
t.todos = append(t.todos, todo)
return todo
}
func (t *TodoService) Toggle(id int) error {
for i := range t.todos {
if t.todos[i].ID == id {
t.todos[i].Completed = !t.todos[i].Completed
return nil
}
}
return fmt.Errorf("todo %d not found", id)
}
func (t *TodoService) Delete(id int) error {
for i := range t.todos {
if t.todos[i].ID == id {
t.todos = append(t.todos[:i], t.todos[i+1:]...)
return nil
}
}
return fmt.Errorf("todo %d not found", id)
}
func main() {
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&TodoService{}),
},
})
app.Window.New()
app.Run()
}JavaScript:
import { GetAll, Add, Toggle, Delete } from './bindings/changeme/todoservice'
class TodoApp {
async loadTodos() {
const todos = await GetAll()
this.renderTodos(todos)
}
async addTodo(title) {
try {
const todo = await Add(title)
this.loadTodos()
} catch (error) {
console.error("Failed to add todo:", error)
}
}
async toggleTodo(id) {
try {
await Toggle(id)
this.loadTodos()
} catch (error) {
console.error("Failed to toggle todo:", error)
}
}
async deleteTodo(id) {
try {
await Delete(id)
this.loadTodos()
} catch (error) {
console.error("Failed to delete todo:", error)
}
}
renderTodos(todos) {
const list = document.getElementById('todo-list')
list.innerHTML = todos.map(todo => `
<div class="todo ${todo.Completed ? 'completed' : ''}">
<input type="checkbox"
${todo.Completed ? 'checked' : ''}
onchange="app.toggleTodo(${todo.ID})">
<span>${todo.Title}</span>
<button onclick="app.deleteTodo(${todo.ID})">Delete</button>
</div>
`).join('')
}
}
const app = new TodoApp()
app.loadTodos()Praktik Terbaik
✅ Lakukan
- Buat metode tetap sederhana - Satu tanggung jawab
- Kembalikan error - Jangan memicu panic
- Gunakan state yang aman untuk thread - Gunakan mutex untuk data bersama
- Kelompokkan operasi - Kurangi panggilan melalui bridge
- Simpan dalam cache di sisi Go - Hindari pekerjaan berulang
- Dokumentasikan metode - Komentar menjadi JSDoc
❌ Jangan Lakukan
- Jangan memblokir - Gunakan goroutine untuk operasi yang berlangsung lama
- Jangan kembalikan channel - Gunakan event sebagai gantinya
- Jangan kembalikan fungsi - Tidak didukung
- Jangan abaikan error - Selalu tangani error tersebut
- Jangan gunakan field yang tidak diekspor - Field tersebut tidak akan dibuatkan binding
Langkah Berikutnya
Pelajari sistem layanan secara mendalam.
Buat binding untuk struktur data yang kompleks.
Pahami mekanisme bridge.
Gunakan event untuk komunikasi pub/sub.
Ada pertanyaan? Tanyakan di Discord atau lihat contoh binding.