10 Trik Golang untuk Developer Produktif
Golang Tutorial Best Practices #golang #go #productivity #best-practices

10 Trik Golang untuk Developer Produktif

A
Abd. Asis
3 min read
Bagikan:

Kamu sudah nyaman menulis Go, tapi kadang ada perasaan bahwa workflow-mu bisa lebih cepat — terlalu banyak langkah manual, terlalu banyak boilerplate yang ditulis berulang, atau test yang susah dirawat seiring project berkembang. Go punya banyak fitur bawaan dan konvensi komunitas yang justru sering dilewatkan karena tidak terlalu disorot di tutorial dasar.

Artikel ini membahas 10 trik yang langsung bisa kamu terapkan di project Go berikutnya — dari cara mengelola lifecycle request yang lebih rapi, hingga pola pengujian yang mengurangi duplikasi kode secara dramatis. Sebagian besar tidak membutuhkan library tambahan, cukup memanfaatkan apa yang sudah ada di ekosistem Go.

Bawa context ke Setiap Layer

context.Context bukan sekadar formalitas. Ia adalah mekanisme utama untuk mengirim sinyal pembatalan, deadline, dan tracing ID menembus batas fungsi dan goroutine. Tanpa context, sulit mengontrol berapa lama sebuah operasi boleh berjalan — terutama saat menangani database query atau HTTP call ke service lain.

Bayangkan sebuah handler yang memproses ekspor laporan. Jika user menutup koneksi di tengah jalan, kamu ingin query ke database juga berhenti:

// handler/report.go

func ExportReportHandler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
    defer cancel()

    reportID := r.URL.Query().Get("id")
    data, err := reportService.Generate(ctx, reportID)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, "Timeout: laporan butuh terlalu lama", http.StatusGatewayTimeout)
            return
        }
        http.Error(w, "Gagal generate laporan", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(data)
}

Dengan context.WithTimeout, jika Generate belum selesai dalam 10 detik, semua operasi turunan yang menerima ctx yang sama — termasuk query SQL di dalam reportService — akan menerima sinyal untuk berhenti. Tidak ada resource yang terbuang percuma.

Selalu teruskan ctx sebagai argumen pertama di setiap fungsi yang melakukan I/O. Ini adalah konvensi idiomatik Go yang membuat kode lebih mudah ditelusuri dan diuji.

Gunakan go run untuk Eksperimen Cepat

Setiap kali kamu ingin menguji sebuah fungsi kecil atau ide baru, tidak perlu repot menyiapkan binary. Cukup jalankan langsung dengan:

go run main.go

Untuk project multi-file di direktori yang sama, gunakan:

go run .

Ini menghilangkan satu gesekan kecil yang kalau dibiarkan bisa memperlambat siklus eksperimen kamu secara keseluruhan. Khususnya berguna saat menulis utilitas kecil atau skrip one-off yang belum layak dijadikan binary permanen.

Otomasi Repetisi dengan go generate

go generate adalah perintah yang sering diabaikan, padahal potensinya besar. Ia membaca komentar khusus di source code dan menjalankan perintah eksternal saat kamu memanggil go generate ./....

Contoh penggunaan umum: membuat mock dari interface secara otomatis menggunakan mockgen.

// service/notification.go

//go:generate mockgen -source=notification.go -destination=../mocks/notification_mock.go -package=mocks

type NotificationSender interface {
    Send(ctx context.Context, to string, message string) error
    SendBulk(ctx context.Context, recipients []string, message string) (int, error)
}

Setelah menambahkan komentar //go:generate, jalankan satu perintah:

go generate ./...

Go akan membaca setiap file, menemukan komentar //go:generate, lalu mengeksekusinya. Hasilnya adalah file mock yang selalu sinkron dengan interface aslinya — tanpa perlu update manual setiap kali interface berubah.

Batasi Akses Paket dengan Direktori internal

Go punya fitur akses kontrol bawaan yang elegan: direktori bernama internal. Paket yang berada di dalam internal/ hanya bisa diimpor oleh kode yang berada satu level di atasnya atau di bawahnya dalam hierarki direktori — tidak bisa diimpor dari luar modul.

project/
  cmd/
    api/
      main.go
  internal/
    repository/
      user_repo.go     <- hanya bisa diakses dari dalam project/
    domain/
      user.go
  pkg/
    validator/
      email.go         <- bisa diakses dari luar

Ini cara terbaik untuk menegakkan boundary antar layer tanpa menulis kode boilerplate. Kalau UserRepository tidak seharusnya diakses langsung dari luar modul, letakkan di internal/repository/ — Go compiler sendiri yang akan menolak import yang tidak sah.

Untuk arsitektur yang lebih lengkap, baca panduan Clean Architecture di Go yang membahas cara mengorganisir layer ini secara sistematis.

Struct Tag JSON yang Benar dari Awal

Kesalahan kecil di struct tag JSON bisa menyebabkan API response yang tidak konsisten atau data yang tidak terkirim. Dua hal yang sering dilewatkan: menggunakan omitempty untuk field opsional, dan memastikan nama field mengikuti konvensi snake_case yang umum di API.

// domain/invoice.go

type Invoice struct {
    ID          string    `json:"id"`
    CustomerID  string    `json:"customer_id"`
    TotalAmount float64   `json:"total_amount"`
    PaidAt      *time.Time `json:"paid_at,omitempty"`
    Notes       string    `json:"notes,omitempty"`
    CreatedAt   time.Time `json:"created_at"`
}

Field PaidAt bertipe pointer ke time.Time — jika nil, omitempty akan menghapusnya dari output JSON sepenuhnya, bukan mengirimkan null. Field Notes yang string kosong juga tidak akan muncul di response. Ini menjaga payload API tetap bersih dan tidak membingungkan client.

omitempty pada tipe non-pointer seperti int atau bool akan mengabaikan nilai 0 dan false — yang bisa jadi bug jika nilai tersebut memang bermakna. Gunakan pointer untuk field yang harus bisa bernilai zero secara eksplisit.

Table-Driven Tests untuk Pengujian yang Skalabel

Pola table-driven test adalah idiom Go yang paling banyak direkomendasikan untuk situasi di mana kamu perlu menguji banyak skenario input/output dari satu fungsi. Daripada menulis 10 fungsi test terpisah, kamu definisikan satu tabel kasus uji dan iterasi di atasnya.

// domain/discount_test.go

func TestCalculateDiscount(t *testing.T) {
    cases := []struct {
        name           string
        originalPrice  float64
        memberTier     string
        expectedResult float64
    }{
        {"harga normal tanpa member", 100_000, "none", 100_000},
        {"diskon 10% untuk silver", 100_000, "silver", 90_000},
        {"diskon 20% untuk gold", 100_000, "gold", 80_000},
        {"diskon 30% untuk platinum", 100_000, "platinum", 70_000},
        {"harga nol tetap nol", 0, "gold", 0},
    }

    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            result := CalculateDiscount(tc.originalPrice, tc.memberTier)
            if result != tc.expectedResult {
                t.Errorf("got %.0f, want %.0f", result, tc.expectedResult)
            }
        })
    }
}

Menambah skenario baru cukup dengan menambah satu baris di slice cases. Tidak perlu menyentuh logika test sama sekali. Ini juga membuat test failures lebih mudah dibaca karena nama subtest langsung menggambarkan kasus yang gagal.

Constructor Function untuk Inisialisasi yang Konsisten

Menginisialisasi struct secara langsung (ProjectService{}) di berbagai tempat rentan terhadap inkonsistensi — satu tempat lupa set field tertentu, field baru ditambahkan tapi tidak semua inisialisasi ikut diperbarui. Constructor function menyelesaikan masalah ini.

// service/project.go

type ProjectService struct {
    repo   ProjectRepository
    mailer MailSender
    logger *slog.Logger
}

func NewProjectService(repo ProjectRepository, mailer MailSender, logger *slog.Logger) *ProjectService {
    if repo == nil {
        panic("ProjectService: repo tidak boleh nil")
    }
    return &ProjectService{
        repo:   repo,
        mailer: mailer,
        logger: logger,
    }
}

Dengan NewProjectService, setiap pembuatan ProjectService dijamin melewati satu pintu masuk yang sama. Validasi dependency bisa dilakukan di satu tempat, dan field baru yang wajib diisi bisa dipaksa ada dengan mengubah signature constructor — compiler langsung menginformasikan semua tempat yang perlu diperbarui.

sync.Once untuk Inisialisasi Mahal yang Cukup Sekali

Beberapa resource seperti koneksi database, konfigurasi yang di-load dari file, atau klien HTTP yang sudah dikonfigurasi — seharusnya dibuat hanya sekali, terlepas dari berapa goroutine yang memintanya secara bersamaan. sync.Once menjamin ini dengan zero boilerplate locking manual.

// infrastructure/db.go

var (
    dbInstance *sql.DB
    dbOnce     sync.Once
)

func GetDB() *sql.DB {
    dbOnce.Do(func() {
        dsn := os.Getenv("DATABASE_URL")
        db, err := sql.Open("postgres", dsn)
        if err != nil {
            log.Fatalf("gagal membuka koneksi database: %v", err)
        }
        db.SetMaxOpenConns(25)
        db.SetMaxIdleConns(5)
        dbInstance = db
    })
    return dbInstance
}

dbOnce.Do memastikan fungsi di dalamnya hanya dieksekusi satu kali — bahkan jika GetDB() dipanggil dari ratusan goroutine secara bersamaan saat startup. Goroutine lain akan menunggu inisialisasi selesai, lalu mendapat instance yang sama.

Interface Kecil untuk Arsitektur yang Mudah Diuji

Di Go, interface terbaik adalah yang kecil. Interface dengan satu atau dua method lebih mudah diimplementasi, lebih mudah di-mock, dan lebih mudah dikomposisikan. Prinsip ini sering disebut interface segregation — dan Go mendorong kamu ke sana secara natural karena interface diimplementasi secara implisit.

// port/storage.go — interface di sisi yang membutuhkan, bukan di sisi yang mengimplementasi

type TaskReader interface {
    FindByID(ctx context.Context, id string) (*domain.Task, error)
    FindByProject(ctx context.Context, projectID string) ([]*domain.Task, error)
}

type TaskWriter interface {
    Save(ctx context.Context, task *domain.Task) error
    Delete(ctx context.Context, id string) error
}

// Gabungkan hanya jika benar-benar butuh keduanya
type TaskRepository interface {
    TaskReader
    TaskWriter
}

Dengan memisahkan TaskReader dan TaskWriter, handler yang hanya membaca data tidak perlu tahu tentang operasi tulis — dan mock yang dibuat untuk test juga lebih kecil dan lebih mudah dipahami. Lihat contoh pola ini di arsitektur lengkap dalam artikel Clean Architecture di Go.

Jalankan go mod tidy Secara Rutin

go mod tidy adalah perintah yang satu ini lebih penting dari yang terlihat. Ia menghapus dependency yang tidak lagi dipakai dari go.mod dan go.sum, sekaligus menambahkan yang masih hilang. Tanpanya, file go.mod mudah membengkak dengan dependency yang sudah tidak relevan.

# Jalankan setelah menambah, menghapus, atau mengubah import
go mod tidy

# Verifikasi hasilnya
go mod verify

Jadikan ini kebiasaan — setidaknya sebelum commit, atau masukkan ke dalam Makefile bersama perintah lain yang sering dipakai:

# Makefile

.PHONY: tidy build test

tidy:
	go mod tidy
	go mod verify

build:
	go build -o bin/app ./cmd/api

test:
	go test ./... -race -count=1

Makefile sederhana seperti ini mengurangi cognitive overhead: tidak perlu mengingat flag apa yang dipakai, cukup make test atau make build.

Kesimpulan

Produktivitas di Go bukan soal menulis kode lebih cepat — tapi soal mengurangi keputusan kecil yang harus dibuat berulang. context yang konsisten, constructor function yang terpercaya, table-driven test yang mudah dikembangkan, dan go mod tidy yang rutin adalah fondasi yang membuat kode Go terasa terkendali bahkan saat project makin besar. Kamu tidak perlu menerapkan semuanya sekaligus — pilih dua atau tiga yang paling relevan dengan project saat ini, lalu lihat hasilnya dalam beberapa sprint ke depan.

Referensi

  1. 1Go Concurrency Patterns: Context — The Go Programming Language
  2. 2Effective Go — The Go Programming Language
  3. 3Prefer Table Driven Tests — Dave Cheney
  4. 4Go Modules Reference — The Go Programming Language

Tentang Penulis

Abd. Asis

Abd. Asis

Software Developer dan Laravel Programmer dari Madura, Indonesia. Passionate tentang PHP, Laravel, dan teknologi web modern.

Komentar

Artikel Terkait

Artikel lain yang mungkin menarik untuk kamu