Lewati ke konten

Jembatan Go-Frontend

Wails menyediakan jembatan in-memory langsung antara Go dan JavaScript, memungkinkan komunikasi seamless tanpa overhead HTTP, batas proses, atau bottleneck serialisasi.

Diagram

Wawasan kunci: Tanpa HTTP, tanpa IPC, tanpa batas proses. Hanya panggilan fungsi langsung dengan type safety.

Ketika aplikasi Anda start, Wails memindai service Anda:

type GreetService struct {
prefix string
}
func (g *GreetService) Greet(name string) string {
return g.prefix + name + "!"
}
func (g *GreetService) Add(a, b int) int {
return a + b
}
// Register service
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&GreetService{prefix: "Hello, "}),
},
})

Yang dilakukan Wails:

  1. Memindai struct untuk metode yang diekspor
  2. Mengekstrak informasi tipe (parameter, tipe return)
  3. Membangun registry yang memetakan nama metode ke fungsi
  4. Menghasilkan binding TypeScript dengan definisi tipe lengkap

Wails menghasilkan binding TypeScript secara otomatis:

frontend/bindings/GreetService.ts
export function Greet(name: string): Promise<string>
export function Add(a: number, b: number): Promise<number>

Pemetaan tipe:

Tipe GoTipe TypeScript
stringstring
int, int32, int64number
float32, float64number
boolboolean
[]TT[]
map[string]TRecord<string, T>
structinterface
time.TimeDate
errorException (thrown)

Developer memanggil metode Go dari JavaScript:

import { Greet, Add } from './bindings/GreetService'
// Call Go from JavaScript
const greeting = await Greet("World")
console.log(greeting) // "Hello, World!"
const sum = await Add(5, 3)
console.log(sum) // 8

Yang terjadi:

  1. Fungsi binding dipanggil - Greet("World")
  2. Pesan dibuat - { service: "GreetService", method: "Greet", args: ["World"] }
  3. Dikirim ke bridge - Via JavaScript bridge WebView
  4. Promise dikembalikan - Menunggu respons

Bridge menerima pesan dan memprosesnya:

Diagram

Keamanan: Hanya service terdaftar dan metode yang diekspor yang dapat dipanggil.

Metode Go dieksekusi:

func (g *GreetService) Greet(name string) string {
// This runs in Go
return g.prefix + name + "!"
}

Context eksekusi:

  • Berjalan di goroutine (non-blocking)
  • Memiliki akses ke semua fitur Go (file system, jaringan, database)
  • Dapat memanggil kode Go lain dengan bebas
  • Mengembalikan hasil atau error

Hasil dikirim kembali ke JavaScript:

// Promise resolves with result
const greeting = await Greet("World")
// greeting = "Hello, World!"

Penanganan error:

func (g *GreetService) Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
try {
const result = await Divide(10, 0)
} catch (error) {
console.error("Go error:", error) // "division by zero"
}

Overhead panggilan tipikal: <1ms

Frontend Call → Bridge → Go Execution → Bridge → Frontend Response
↓ ↓ ↓ ↓ ↓
&lt;0.1ms &lt;0.1ms [varies] &lt;0.1ms &lt;0.1ms

Dibanding alternatif:

  • HTTP/REST: 5-50ms (network stack, serialisasi)
  • IPC: 1-10ms (batas proses, marshalling)
  • Wails Bridge: <1ms (in-memory, panggilan langsung)

Overhead per-panggilan: ~1KB (buffer pesan)

Optimasi zero-copy: Data besar (>1MB) menggunakan shared memory jika memungkinkan.

Panggilan bersifat concurrent:

  • Setiap panggilan berjalan di goroutine sendiri
  • Beberapa panggilan dapat dieksekusi secara simultan
  • Tidak ada blocking antar panggilan
// These run concurrently
const [result1, result2, result3] = await Promise.all([
SlowOperation1(),
SlowOperation2(),
SlowOperation3(),
])
// Go
func Example(
s string,
i int,
f float64,
b bool,
) (string, int, float64, bool) {
return s, i, f, b
}
// TypeScript (auto-generated)
function Example(
s: string,
i: number,
f: number,
b: boolean,
): Promise<[string, number, number, boolean]>
// Go
func Sum(numbers []int) int {
total := 0
for _, n := range numbers {
total += n
}
return total
}
// TypeScript
function Sum(numbers: number[]): Promise<number>
// Usage
const total = await Sum([1, 2, 3, 4, 5]) // 15
// Go
func GetConfig() map[string]interface{} {
return map[string]interface{}{
"theme": "dark",
"fontSize": 14,
"enabled": true,
}
}
// TypeScript
function GetConfig(): Promise<Record<string, any>>
// Usage
const config = await GetConfig()
console.log(config.theme) // "dark"
// Go
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func GetUser(id int) (*User, error) {
return &User{
ID: id,
Name: "Alice",
}, nil
}
// TypeScript (auto-generated)
interface User {
id: number
name: string
email: string
}
function GetUser(id: number): Promise<User>
// Usage
const user = await GetUser(1)
console.log(user.name) // "Alice"

Tag JSON: Gunakan tag json: untuk mengontrol nama field di TypeScript.

// Go
func GetTimestamp() time.Time {
return time.Now()
}
// TypeScript
function GetTimestamp(): Promise<Date>
// Usage
const timestamp = await GetTimestamp()
console.log(timestamp.toISOString())
// Go
func Validate(input string) error {
if input == "" {
return errors.New("input cannot be empty")
}
return nil
}
// TypeScript
function Validate(input: string): Promise<void>
// Usage
try {
await Validate("")
} catch (error) {
console.error(error) // "input cannot be empty"
}

Tipe ini tidak dapat diteruskan melintasi bridge:

  • Channel (chan T)
  • Fungsi (func())
  • Interface (kecuali interface{} / any)
  • Pointer (kecuali ke struct)
  • Field tidak diekspor (lowercase)

Solusi: Gunakan ID atau handle:

// ❌ Can't pass file handle
func OpenFile(path string) (*os.File, error) {
return os.Open(path)
}
// ✅ 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()
}

Service dapat mengakses context panggilan:

type UserService struct{}
func (s *UserService) GetCurrentUser(ctx context.Context) (*User, error) {
// Access the calling window via the context value
window, _ := ctx.Value(application.WindowKey).(application.Window)
_ = window
// Access the application
app := application.Get()
_ = app
// Your logic
return getCurrentUser(), nil
}

Context menyediakan:

  • Window yang melakukan panggilan
  • Instance aplikasi
  • Metadata request

Untuk data besar, gunakan event alih-alih nilai return:

func ProcessLargeFile(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
// Emit progress events
app.Event.Emit("file-progress", map[string]interface{}{
"line": lineNum,
"text": scanner.Text(),
})
}
return scanner.Err()
}
import { Events } from '@wailsio/runtime'
import { ProcessLargeFile } from './bindings/FileService'
// Listen for progress
Events.On('file-progress', (data) => {
console.log(`Line ${data.line}: ${data.text}`)
})
// Start processing
await ProcessLargeFile('/path/to/large/file.txt')

Gunakan context untuk operasi yang dapat dibatalkan:

func LongRunningTask(ctx context.Context) error {
for i := 0; i < 1000; i++ {
// Check if cancelled
select {
case <-ctx.Done():
return ctx.Err()
default:
// Continue work
time.Sleep(100 * time.Millisecond)
}
}
return nil
}

Catatan: Pembatalan context saat frontend disconnect otomatis.

Kurangi overhead bridge dengan batching:

// ❌ Inefficient: N bridge calls
for _, item := range items {
await ProcessItem(item)
}
// ✅ Efficient: 1 bridge call
await ProcessItems(items)
func ProcessItems(items []Item) ([]Result, error) {
results := make([]Result, len(items))
for i, item := range items {
results[i] = processItem(item)
}
return results, nil
}
app := application.New(application.Options{
Name: "My App",
LogLevel: slog.LevelDebug, // requires `import "log/slog"`
// `Logger` is an optional *slog.Logger; the default-logger helper is
// application.DefaultLogger(slog.Leveler) if you want to construct one explicitly.
})

Output menampilkan:

  • Panggilan metode
  • Parameter
  • Nilai return
  • Error
  • Informasi timing

Periksa frontend/bindings/ untuk melihat TypeScript yang dihasilkan:

// frontend/bindings/<full-go-import-path>/myservice.js (real generated shape)
import { Call as $Call } from "/wails/runtime.js";
export function MyMethod($0) {
return $Call.ByID(1234567890, $0); // numeric method ID assigned by the generator
}

Uji service Go tanpa frontend:

func TestGreetService(t *testing.T) {
service := &GreetService{prefix: "Hello, "}
result := service.Greet("Test")
if result != "Hello, Test!" {
t.Errorf("Expected 'Hello, Test!', got '%s'", result)
}
}
  • Operasi batch - Kurangi panggilan bridge
  • Gunakan event untuk streaming - Jangan return array besar
  • Jaga metode tetap cepat - <100ms ideal
  • Gunakan goroutine - Untuk operasi panjang
  • Cache di sisi Go - Hindari perhitungan berulang
  • Jangan buat panggilan berlebihan - Batch jika memungkinkan
  • Jangan return data besar - Gunakan pagination atau streaming
  • Jangan block - Gunakan goroutine untuk operasi panjang
  • Jangan kirim tipe kompleks - Jaga tetap sederhana
  • Jangan abaikan error - Selalu tangani error

Bridge aman secara default:

  1. Whitelist saja - Hanya service terdaftar yang dapat dipanggil
  2. Validasi tipe - Argumen dicek terhadap tipe Go
  3. Tanpa eval() - Frontend tidak dapat mengeksekusi kode Go sembarangan
  4. Tanpa penyalahgunaan reflection - Hanya metode yang diekspor yang dapat diakses

Praktik terbaik:

  • Validasi input di Go (jangan percaya frontend)
  • Gunakan context untuk autentikasi/otorisasi
  • Rate limit operasi yang mahal
  • Sanitasi path file dan input pengguna

Sistem Build - Pelajari bagaimana Wails mem-build dan membundel aplikasi Anda
Pelajari Selengkapnya →

Service - Pelajari mendalam sistem service
Pelajari Selengkapnya →

Event - Gunakan event untuk komunikasi pub/sub
Pelajari Selengkapnya →


Pertanyaan tentang bridge? Tanyakan di Discord atau lihat contoh binding.