15 Struktur Penulisan Resolver yang Baik di Go
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:
1type UserResolver interface {
2 GetUser(ctx context.Context, id string) (*User, error)
3}2. Struct Resolver Memiliki Dependency Explicit
Deklarasikan dependency eksternal (service, repositori, dsb) secara eksplisit pada struct:
1type userResolver struct {
2 repo UserRepository
3 log Logger
4}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:
1func (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):
1if strings.TrimSpace(id) == "" {
2 return nil, errors.New("user ID required")
3}5. Error Handling yang Konsisten
Selalu tangani error di setiap langkah utama. Gunakan wrapping error agar stack trace jelas:
1user, err := r.repo.FindByID(ctx, id)
2if err != nil {
3 return nil, fmt.Errorf("repo.FindByID: %w", err)
4}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:
1func mapUserToResponse(u *User) *UserResponse {
2 return &UserResponse{
3 ID: u.ID,
4 Name: u.Name,
5 Email: u.Email,
6 }
7}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:
1if user.Status == "pending" && payment.Status == "success" {
2 // ...logic approval
3}Benar:
1approve, 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:
1log.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:
1// 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.
1func TestGetUser_Success(t *testing.T) {
2 // Arrange: mock repo response
3 // Act: call resolver.GetUser
4 // Assert: response benar, error nil
5}14. Pattern Dependency Injection
Implementasikan dependency injection agar mudah diganti (misalnya untuk test):
1func NewUserResolver(repo UserRepository, log Logger) UserResolver {
2 return &userResolver{repo: repo, log: log}
3}15. Hindari Side Effect yang Tidak Perlu
Resolver harus idempotent. Jangan lakukan aksi yang berubah setiap call, kecuali memang perlu.
Salah:
1func (r *userResolver) GetUser(ctx context.Context, id string) (*User, error) {
2 sendTrackingEvent(ctx, id) // side effect: tracking!
3 // ...
4}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.
1// Interface
2type UserResolver interface {
3 GetUser(ctx context.Context, id string) (*UserResponse, error)
4}
5
6// Struct
7type userResolver struct {
8 repo UserRepository
9 log Logger
10}
11
12// Response Shape
13type UserResponse struct {
14 ID string
15 Name string
16 Email string
17}
18
19// Implementation
20func (r *userResolver) GetUser(ctx context.Context, id string) (*UserResponse, error) {
21 // 1. Validasi awal
22 if strings.TrimSpace(id) == "" {
23 r.log.Errorf("invalid id input")
24 return nil, errors.New("user ID is required")
25 }
26
27 // 2. Ambil data dari repo
28 user, err := r.repo.FindByID(ctx, id)
29 if err != nil {
30 r.log.Errorf("error FindByID: %v", err)
31 return nil, fmt.Errorf("failed to find user: %w", err)
32 }
33
34 // 3. Mapping ke response
35 resp := mapUserToResponse(user)
36 return resp, nil
37}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! 🚀