tutorial

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:

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.

FunctionResponseError Returned
GetUser*UserResponseerror
ListUsers[]UserResponseerror
DeleteUserboolerror

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! 🚀

comments powered by Disqus

Topik Terhangat

programming
207
tutorial
74
tips-and-trick
43
jaringan
28
hardware
11
linux
4
kubernetes
1