Menggunakan Gin untuk Layanan
Panduan untuk mengintegrasikan framework web Gin dengan Layanan Wails v3
Menggunakan Gin untuk Layanan
Framework web Gin merupakan pilihan populer untuk membangun layanan HTTP dalam Go. Dengan Wails v3, Anda dapat dengan mudah mengintegrasikan layanan berbasis Gin ke dalam aplikasi untuk menangani permintaan HTTP, mengimplementasikan RESTful API, dan menyajikan konten web secara andal.
Panduan ini akan memandu Anda membuat layanan berbasis Gin yang dapat dipasang pada rute tertentu dalam aplikasi Wails. Kita akan membuat contoh lengkap yang menunjukkan cara:
- Membuat layanan berbasis Gin
- Mengimplementasikan antarmuka Service Wails
- Menyiapkan rute dan middleware
- Mengintegrasikan layanan dengan sistem peristiwa Wails
- Berinteraksi dengan layanan dari frontend
Prasyarat
Sebelum memulai, pastikan Anda memiliki:
- Wails v3 yang telah terinstal
- Pengetahuan dasar tentang Go dan framework Gin
- Pemahaman tentang konsep HTTP dan RESTful API
Anda perlu menambahkan framework Gin ke proyek:
go get github.com/gin-gonic/gin
Membuat Layanan Berbasis Gin
Mari mulai dengan membuat layanan Gin yang mengimplementasikan antarmuka Service Wails. Layanan ini akan mengelola sekumpulan pengguna dan menyediakan endpoint API untuk mengambil serta membuat data pengguna.
1. Tentukan Model Data Anda
Pertama, tentukan struktur data yang akan digunakan oleh layanan:
package services
import (
"context"
"net/http"
"strconv"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/wailsapp/wails/v3/pkg/application"
)
// User represents a user in the system
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"createdAt"`
}
// EventData represents data sent in events
type EventData struct {
Message string `json:"message"`
Timestamp string `json:"timestamp"`
}2. Buat Struktur Layanan Anda
Selanjutnya, tentukan struktur layanan yang akan menyimpan router Gin dan setiap status yang perlu dipertahankan oleh layanan:
// GinService implements a Wails service that uses Gin for HTTP handling
type GinService struct {
ginEngine *gin.Engine
users []User
nextID int
mu sync.RWMutex
app *application.App
}
// NewGinService creates a new GinService instance
func NewGinService() *GinService {
// Create a new Gin router
ginEngine := gin.New()
// Add middlewares
ginEngine.Use(gin.Recovery())
ginEngine.Use(LoggingMiddleware())
service := &GinService{
ginEngine: ginEngine,
users: []User{
{ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now().Add(-72 * time.Hour)},
{ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now().Add(-48 * time.Hour)},
{ID: 3, Name: "Charlie", Email: "charlie@example.com", CreatedAt: time.Now().Add(-24 * time.Hour)},
},
nextID: 4,
}
// Define routes
service.setupRoutes()
return service
}3. Implementasikan Antarmuka Service
Implementasikan metode yang diwajibkan oleh antarmuka Service Wails:
// ServiceName returns the name of the service
func (s *GinService) ServiceName() string {
return "Gin API Service"
}
// ServiceStartup is called when the service starts
func (s *GinService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
// Store the application instance for later use
s.app = application.Get()
// Register an event handler that can be triggered from the frontend
s.app.Event.On("gin-api-event", func(event *application.CustomEvent) {
// Log the event data
s.app.Logger.Info("Received event from frontend", "data", event.Data)
// Emit an event back to the frontend
s.app.Event.Emit("gin-api-response",
map[string]interface{}{
"message": "Response from Gin API Service",
"time": time.Now().Format(time.RFC3339),
},
)
})
return nil
}
// ServiceShutdown is called when the service shuts down.
// IMPORTANT: the interface is `ServiceShutdown() error` (no ctx); a method that
// takes a context.Context does NOT satisfy the interface and will never be called.
func (s *GinService) ServiceShutdown() error {
// Clean up resources if needed
return nil
}3. Implementasikan Antarmuka http.Handler
Agar layanan dapat dipasang pada rute tertentu, implementasikan antarmuka http.Handler. Metode tunggal ini, ServeHTTP, merupakan gerbang bagi semua permintaan HTTP ke layanan Anda. Metode tersebut mendelegasikan penanganan permintaan kepada router Gin sehingga Anda dapat menggunakan semua fitur canggih Gin untuk perutean dan middleware.
// ServeHTTP implements the http.Handler interface
func (s *GinService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// All requests go to the Gin router
s.ginEngine.ServeHTTP(w, r)
}4. Siapkan Rute Anda
Tentukan rute API dalam metode terpisah agar lebih tertata. Pendekatan ini menjaga kode tetap rapi dan memudahkan pemahaman terhadap struktur API. Router Gin menyediakan API fluent untuk menentukan rute, termasuk dukungan bagi grup rute yang membantu menata endpoint terkait.
// setupRoutes configures the API routes
func (s *GinService) setupRoutes() {
// Basic info endpoint
s.ginEngine.GET("/info", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"service": "Gin API Service",
"version": "1.0.0",
"time": time.Now().Format(time.RFC3339),
})
})
// Users group
users := s.ginEngine.Group("/users")
{
// Get all users
users.GET("", func(c *gin.Context) {
s.mu.RLock()
defer s.mu.RUnlock()
c.JSON(http.StatusOK, s.users)
})
// Get user by ID
users.GET("/:id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
s.mu.RLock()
defer s.mu.RUnlock()
for _, user := range s.users {
if user.ID == id {
c.JSON(http.StatusOK, user)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
})
// Create a new user
users.POST("", func(c *gin.Context) {
var newUser User
if err := c.ShouldBindJSON(&newUser); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
s.mu.Lock()
defer s.mu.Unlock()
// Set the ID and creation time
newUser.ID = s.nextID
newUser.CreatedAt = time.Now()
s.nextID++
// Add to the users slice
s.users = append(s.users, newUser)
c.JSON(http.StatusCreated, newUser)
// Emit an event to notify about the new user
s.app.Event.Emit("user-created", newUser)
})
// Delete a user
users.DELETE("/:id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
s.mu.Lock()
defer s.mu.Unlock()
for i, user := range s.users {
if user.ID == id {
// Remove the user from the slice
s.users = append(s.users[:i], s.users[i+1:]...)
c.JSON(http.StatusOK, gin.H{"message": "User deleted"})
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
})
}
}5. Buat Middleware Khusus
Anda dapat membuat middleware Gin khusus untuk menyempurnakan layanan. Fungsi middleware dalam Gin dijalankan sesuai urutan penambahannya ke router dan dapat menjalankan tugas seperti pencatatan log, autentikasi, dan penanganan kesalahan. Contoh ini menunjukkan middleware pencatatan log sederhana yang mencatat detail permintaan.
// LoggingMiddleware is a Gin middleware that logs request details
func LoggingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Start timer
start := time.Now()
// Process request
c.Next()
// Calculate latency
latency := time.Since(start)
// Log request details
log.Printf("[GIN] %s %s %d %s", c.Request.Method, c.Request.URL.Path, c.Writer.Status(), latency)
}
}Mendaftarkan Layanan Anda
Untuk menggunakan layanan berbasis Gin dalam aplikasi Wails, Anda perlu mendaftarkannya pada aplikasi dan menentukan rute tempat layanan tersebut akan dipasang. Hal ini dilakukan saat membuat instans aplikasi Wails. Rute yang Anda tentukan menjadi jalur dasar bagi semua endpoint yang ditetapkan dalam router Gin.
app := application.New(application.Options{
Name: "Gin Service Demo",
Description: "A demo of using Gin in Wails services",
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
LogLevel: slog.LevelDebug,
Services: []application.Service{
application.NewServiceWithOptions(services.NewGinService(), application.ServiceOptions{
Route: "/api",
}),
},
Assets: application.AssetOptions{
Handler: application.BundledAssetFileServer(assets),
},
})Dalam contoh ini, layanan Gin dipasang pada rute /api. Artinya, jika router Gin memiliki endpoint yang ditetapkan sebagai /info, endpoint tersebut dapat diakses melalui /api/info dalam aplikasi. Pendekatan ini memungkinkan Anda menata endpoint API secara logis dan menghindari konflik dengan bagian lain aplikasi.
Mengintegrasikan Layanan dengan Sistem Peristiwa Wails
Salah satu fitur canggih dari penggunaan Gin dengan Layanan Wails adalah kemampuannya untuk berintegrasi secara mulus dengan sistem peristiwa Wails. Integrasi ini memungkinkan komunikasi waktu nyata antara layanan backend dan frontend.
Dalam metode ServiceStartup milik layanan, Anda dapat mendaftarkan handler peristiwa untuk memproses peristiwa dari frontend:
s.app.Event.On("gin-api-event", func(event *application.CustomEvent) {
// Log the event data
s.app.Logger.Info("Received event from frontend", "data", event.Data)
// Emit an event back to the frontend
s.app.Event.Emit("gin-api-response",
map[string]interface{}{
"message": "Response from Gin API Service",
"time": time.Now().Format(time.RFC3339),
},
)
})Anda juga dapat memancarkan peristiwa ke frontend dari rute Gin atau bagian lain layanan:
// After creating a new user
s.app.Event.Emit("user-created", newUser)Integrasi Frontend
Untuk berinteraksi dengan layanan Gin dari frontend, Anda perlu mengimpor runtime Wails, mengirim permintaan HTTP ke endpoint API, dan menggunakan sistem peristiwa Wails untuk komunikasi waktu nyata.
Untuk penggunaan produksi, sebaiknya gunakan paket @wailsio/runtime alih-alih mengimpor /wails/runtime.js secara langsung. Hal ini memastikan keamanan tipe, dukungan IDE yang lebih baik, pengelolaan versi melalui npm, dan kompatibilitas dengan alat JavaScript modern.
Instal paket:
npm install @wailsio/runtime
Kemudian, gunakan paket tersebut dalam kode Anda:
import * as wails from '@wailsio/runtime';
// Event emission
wails.Events.Emit('gin-api-event', eventData);Berikut contoh cara menyiapkan integrasi frontend:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gin Service Example</title>
<!-- Styles omitted for brevity -->
</head>
<body>
<h1>Gin Service Example</h1>
<div class="card">
<h2>API Endpoints</h2>
<p>Try the Gin API endpoints mounted at /api:</p>
<button id="getInfo">Get Service Info</button>
<button id="getUsers">Get All Users</button>
<button id="getUser">Get User by ID</button>
<button id="createUser">Create User</button>
<button id="deleteUser">Delete User</button>
<div id="apiResult">
<pre id="apiResponse">Results will appear here...</pre>
</div>
</div>
<div class="card">
<h2>Event Communication</h2>
<p>Trigger an event to communicate with the Gin service:</p>
<button id="triggerEvent">Trigger Event</button>
<div id="eventResult">
<pre id="eventResponse">Event responses will appear here...</pre>
</div>
</div>
<div class="card" id="createUserForm" style="display: none; border: 2px solid #0078d7;">
<h2>Create New User</h2>
<div>
<label for="userName">Name:</label>
<input type="text" id="userName" placeholder="Enter name">
</div>
<div>
<label for="userEmail">Email:</label>
<input type="email" id="userEmail" placeholder="Enter email">
</div>
<button id="submitUser">Submit</button>
<button id="cancelCreate">Cancel</button>
</div>
<script type="module">
// Import the Wails runtime
// Note: In production, use '@wailsio/runtime' instead
import * as wails from "/wails/runtime.js";
// Helper function to fetch API endpoints
async function fetchAPI(endpoint, options = {}) {
try {
const response = await fetch(`/api${endpoint}`, options);
const data = await response.json();
document.getElementById('apiResponse').textContent = JSON.stringify(data, null, 2);
return data;
} catch (error) {
document.getElementById('apiResponse').textContent = `Error: ${error.message}`;
console.error('API Error:', error);
}
}
// Event listeners for API buttons
document.getElementById('getInfo').addEventListener('click', () => {
fetchAPI('/info');
});
document.getElementById('getUsers').addEventListener('click', () => {
fetchAPI('/users');
});
document.getElementById('getUser').addEventListener('click', async () => {
const userId = prompt('Enter user ID:');
if (userId) {
await fetchAPI(`/users/${userId}`);
}
});
document.getElementById('createUser').addEventListener('click', () => {
const form = document.getElementById('createUserForm');
form.style.display = 'block';
form.scrollIntoView({ behavior: 'smooth' });
});
document.getElementById('cancelCreate').addEventListener('click', () => {
document.getElementById('createUserForm').style.display = 'none';
});
document.getElementById('submitUser').addEventListener('click', async () => {
const name = document.getElementById('userName').value;
const email = document.getElementById('userEmail').value;
if (!name || !email) {
alert('Please enter both name and email');
return;
}
try {
await fetchAPI('/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name, email })
});
document.getElementById('createUserForm').style.display = 'none';
document.getElementById('userName').value = '';
document.getElementById('userEmail').value = '';
// Automatically fetch the updated user list
await fetchAPI('/users');
// Show a success message
const apiResponse = document.getElementById('apiResponse');
const currentData = JSON.parse(apiResponse.textContent);
apiResponse.textContent = JSON.stringify({
message: "User created successfully!",
users: currentData
}, null, 2);
} catch (error) {
console.error('Error creating user:', error);
}
});
document.getElementById('deleteUser').addEventListener('click', async () => {
const userId = prompt('Enter user ID to delete:');
if (userId) {
try {
await fetchAPI(`/users/${userId}`, {
method: 'DELETE'
});
// Show success message
document.getElementById('apiResponse').textContent = JSON.stringify({
message: `User with ID ${userId} deleted successfully`
}, null, 2);
// Refresh the user list
setTimeout(() => fetchAPI('/users'), 1000);
} catch (error) {
console.error('Error deleting user:', error);
}
}
});
// Using Wails Events API for event communication
document.getElementById('triggerEvent').addEventListener('click', async () => {
// Display the event being sent
document.getElementById('eventResponse').textContent = JSON.stringify({
status: "Sending event to backend...",
data: { timestamp: new Date().toISOString() }
}, null, 2);
// Use the Wails runtime to emit an event
const eventData = {
message: "Hello from the frontend!",
timestamp: new Date().toISOString()
};
wails.Events.Emit('gin-api-event', eventData);
});
// Set up event listener for responses from the backend
window.addEventListener('DOMContentLoaded', () => {
// Register event listener using Wails runtime
wails.Events.On("gin-api-response", (data) => {
document.getElementById('eventResponse').textContent = JSON.stringify(data, null, 2);
});
// Also listen for user-created events
wails.Events.On("user-created", (data) => {
document.getElementById('eventResponse').textContent = JSON.stringify({
event: "user-created",
user: data
}, null, 2);
});
// Initial API call to get service info
fetchAPI('/info');
});
</script>
</body>
</html>Penutup
Mengintegrasikan framework web Gin dengan Layanan Wails v3 menyediakan pendekatan yang andal dan fleksibel untuk membangun aplikasi web yang modular serta mudah dipelihara. Dengan memanfaatkan kemampuan perutean dan middleware Gin bersama sistem peristiwa Wails, Anda dapat membuat aplikasi interaktif yang kaya fitur dengan pemisahan tanggung jawab yang jelas.
Kode contoh lengkap untuk panduan ini tersedia di repositori Wails dalam v3/examples/gin-service.