15 Struktur Penulisan Resolver yang Baik di Go
Go (atau Golang) menghadirkan keseimbangan antara kecepatan, kemudahan deployment, serta sintaks yang sederhana. Saat membangun sistem terdistribusi, microservices, maupun implementasi GraphQL, kita kerap dihadapkan dengan tugas membangun resolver—bagian yang menjembatani permintaan antara service, database, hingga response yang diterima client.
Namun, membuat resolver hanya “berfungsi” saja tidak cukup. Desain resolver yang buruk akan cepat menumpuk utang teknis, membuat debugging sulit, hingga akhirnya memperlambat laju pengembangan tim.
Di artikel ini, saya merangkum 15 struktur penulisan resolver yang baik di Go—berbasis pengalaman nyata membangun backend skala menengah hingga besar. Kita akan membahas best practice, memberi contoh kode, simulasi, serta diagram sederhana agar lebih mudah dipahami.
1. Definisikan Kontrak Interface Resolver
Seringkali kebutuhan di masa depan menuntut tambahan dependency pada resolver. Agar mudah diubah dan dites, deklarasikan interface:
type UserResolver interface {
GetUser(ctx context.Context, id string) (*User, error)
}
Tips: Dengan interface, testing via mock menjadi lebih mudah.
2. Struct Resolver Memiliki Dependency Explicit
Deklarasikan dependency eksternal (service, repositori, dsb) secara eksplisit pada struct:
type userResolver struct {
repo UserRepository
log Logger
}
Penulisan dependency injection secara eksplisit memudahkan tracing dan refactoring.
3. Gunakan Context Secara Konsisten
Widely adopted convention di Go adalah menaruh context.Context
sebagai parameter pertama―penting untuk trace, log correlation, auth, dsb:
func (r *userResolver) GetUser(ctx context.Context, id string) (*User, error)
Jangan pernah “skip” parameter context.
4. Validasi Input Seawal Mungkin
Sebelum masuk ke layer service, lakukan validation pada input. Gunakan helper validator (atau packages seperti go-playground/validator
):
if strings.TrimSpace(id) == "" {
return nil, errors.New("user ID required")
}
5. Error Handling yang Konsisten
Selalu tangani error di setiap langkah utama. Gunakan wrapping error agar stack trace jelas:
user, err := r.repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("repo.FindByID: %w", err)
}
Dari hasil audit, custom error codes & consistent wrapping mempercepat tracing lebih dari 2x lipat.
6. Mapping antara Layer
Resolver tidak serta-merta expose entity dari repo. Selalu lakukan mapping ke response struct:
func mapUserToResponse(u *User) *UserResponse {
return &UserResponse{
ID: u.ID,
Name: u.Name,
Email: u.Email,
}
}
Tujuannya mengunci perubahan layer bawah tidak “bocor” ke response API.
7. Hindari Logic Bisnis Berat di Layer Resolver
Resolver hanya jembatan, bukan pabrik bisnis logic. Call service/biz layer untuk logic utama.
Salah:
if user.Status == "pending" && payment.Status == "success" {
// ...logic approval
}
Benar:
approve, err := r.userService.CanApprove(ctx, user, payment)
8. Logging di Titik Penting Saja
Logging berlebihan akan mask error penting. Log saat terjadi error, bukan setiap langkah:
log.Errorf("failed to find user: %v", err)
9. Gunakan Naming Fungsi yang Jelas
Ikuti pattern {Verb}{Noun}
untuk resolver method. Contoh: GetUser
, UpdateUser
, DeleteUser
.
10. Simpulkan Response Yang Konsisten
Tentukan response shape di awal dan pastikan konsisten seluruh resolver.
Function | Response | Error Returned |
---|---|---|
GetUser | *UserResponse | error |
ListUsers | []UserResponse | error |
DeleteUser | bool | error |
Membantu client consumer untuk implementasi & automasi test lebih mudah.
11. Perhatikan Queries Berlapis dengan Diagram Alur
Resolver kadang harus memanggil beberapa service. Untuk berpikir lebih jernih, buat diagram alur.
flowchart TD A[Receive GetUser Request] --> B[Validate Input] B -->|valid| C[Get From Repo] C -->|found| D[Map To Response] D --> E[Return Response] B -->|invalid| F[Return Error] C -->|not found| F
12. Document Kode Secara Sederhana
Komentari setiap fungsi resolver, terutama bila memiliki edge-cases atau side-effect:
// GetUser mencari user berdasarkan ID. Akan return error jika user tidak ditemukan.
13. Unit Test Tiap Resolver
Testing bukan opsional. Tulis minimal unit test setiap resolver.
func TestGetUser_Success(t *testing.T) {
// Arrange: mock repo response
// Act: call resolver.GetUser
// Assert: response benar, error nil
}
14. Pattern Dependency Injection
Implementasikan dependency injection agar mudah diganti (misalnya untuk test):
func NewUserResolver(repo UserRepository, log Logger) UserResolver {
return &userResolver{repo: repo, log: log}
}
15. Hindari Side Effect yang Tidak Perlu
Resolver harus idempotent. Jangan lakukan aksi yang berubah setiap call, kecuali memang perlu.
Salah:
func (r *userResolver) GetUser(ctx context.Context, id string) (*User, error) {
sendTrackingEvent(ctx, id) // side effect: tracking!
// ...
}
Benar: Proses tracking semacam ini bisa dipasang di middleware, bukan resolver.
Studi Kasus: Simulasi Sederhana
Mari kita lihat bagaimana good practices di atas diimplementasikan dalam resolver sederhana.
// Interface
type UserResolver interface {
GetUser(ctx context.Context, id string) (*UserResponse, error)
}
// Struct
type userResolver struct {
repo UserRepository
log Logger
}
// Response Shape
type UserResponse struct {
ID string
Name string
Email string
}
// Implementation
func (r *userResolver) GetUser(ctx context.Context, id string) (*UserResponse, error) {
// 1. Validasi awal
if strings.TrimSpace(id) == "" {
r.log.Errorf("invalid id input")
return nil, errors.New("user ID is required")
}
// 2. Ambil data dari repo
user, err := r.repo.FindByID(ctx, id)
if err != nil {
r.log.Errorf("error FindByID: %v", err)
return nil, fmt.Errorf("failed to find user: %w", err)
}
// 3. Mapping ke response
resp := mapUserToResponse(user)
return resp, nil
}
Kesimpulan
Menulis resolver yang baik di Go bukan sekadar “bisa jalan”—tapi soal readable, mudah di-test, minim bug, tidak membebani layer atas/bawah, dengan struktur dependency yang sehat. Dengan mengikuti 15 struktur di atas, kerja tim backend akan lebih sustainable, maintainable, dan scalable.
Resolver hanyalah satu bagian kecil dari desain sistem besar—tapi bila diabaikan, bisa jadi lubang yang mengantarkan pada utang teknis. Fokus pada kaidah-kaidah sederhana di atas, dan timmu akan lebih tenang menyambut Project Deadlines berikutnya.
Referensi:
Tulisan ini berdasarkan pengalaman nyata di beberapa proyek Go, baik sebagai individual contributor maupun lead. Jika ada insight tambahan atau pengalaman unik, drop di kolom diskusi! 🚀
37 Otentikasi di Middleware gRPC
Artikel Terhangat
37 Otentikasi di Middleware gRPC
07 Jul 2025
36 Rate Limiting di Interceptor
07 Jul 2025
13 Membuat Resolver Pertama di graphql-go
07 Jul 2025
35 Validasi Request menggunakan Interceptor
07 Jul 2025
12 Menulis File Skema GraphQL Pertama Anda
07 Jul 2025
34 Logging Interceptor dengan Context
07 Jul 2025

37 Otentikasi di Middleware gRPC

36 Rate Limiting di Interceptor

13 Membuat Resolver Pertama di graphql-go

35 Validasi Request menggunakan Interceptor

12 Menulis File Skema GraphQL Pertama Anda
