Category: Tutorial

  • Perbedaan Docker Image dan Container + Praktik Pertama

    Perbedaan Docker Image dan Container + Praktik Pertama

    Perbedaan Docker image dan container sebenarnya sederhana. Image adalah paket read-only berisi filesystem, aplikasi, dan konfigurasi yang dibutuhkan untuk menjalankan sebuah program. Container adalah proses yang berjalan dari image itu. Image sifatnya statis, seperti file yang diam di disk. Container sifatnya hidup, punya proses, punya memori, dan bisa dihentikan atau dihapus tanpa menyentuh image aslinya.

    Artikel ini bagian kedua dari seri Belajar Docker dari Nol. Setelah paham konsepnya, kita langsung praktik: menjalankan Nginx di container, melihat prosesnya, membukanya di browser, lalu membedah kenapa ada container yang langsung mati begitu dijalankan.

    Prasyarat: Docker Sudah Terpasang

    Kamu butuh Docker Engine yang sudah jalan di komputermu. Kalau belum, ikuti dulu bagian pertama seri ini: Apa Itu Docker dan Cara Install Docker di Ubuntu & Windows. Semua contoh di sini saya jalankan di Ubuntu 24.04 dengan Docker Engine 27, tapi perintahnya sama persis di Windows dengan Docker Desktop.

    Cek dulu instalasimu:

    docker --version

    Output yang diharapkan kurang lebih seperti ini (angka minor boleh beda):

    Docker version 27.5.1, build 9f9e405

    Analogi Paling Gampang: Image Itu Cetakan, Container Itu Hasil Cetakannya

    Bayangkan cetakan kue. Cetakan itu cuma satu, bentuknya tetap, dan tidak berubah sedikit pun setiap kali dipakai. Dari satu cetakan yang sama, kamu bisa membuat sepuluh kue. Tiap kue berdiri sendiri. Kue pertama boleh kamu makan, kue kedua boleh gosong, cetakannya tetap utuh.

    Docker persis seperti itu. Image adalah cetakannya. Container adalah kuenya. Dari satu image nginx, kamu bisa menjalankan lima container Nginx sekaligus, masing-masing dengan nama, port, dan isi memori sendiri. Menghapus salah satu container tidak berpengaruh apa pun ke image, dan tidak berpengaruh ke container lain.

    Kalau dirangkum dalam tabel:

    Aspek Image Container
    Sifat Read-only, statis Hidup, punya proses yang berjalan
    Analogi Cetakan kue Kue hasil cetakan
    Jumlah Satu image Bisa jadi banyak container
    Disimpan di Disk lokal, hasil pull dari registry Dibuat saat docker run, di atas image
    Kalau dihapus Container baru tidak bisa dibuat darinya Image tetap aman, container lain tetap jalan

    Satu detail teknis yang penting: saat container dibuat, Docker tidak menyalin seluruh isi image. Docker hanya menambahkan satu lapisan tulis (writable layer) tipis di atas image. Semua perubahan file di dalam container terjadi di lapisan ini. Itulah kenapa membuat container itu cepat sekali, hitungan detik, bukan menit seperti menyalakan virtual machine.

    Arsitektur Docker: Client, Daemon, dan Registry

    Sebelum praktik, kenali dulu tiga aktor utama supaya kamu paham apa yang sebenarnya terjadi saat mengetik perintah.

    • Docker client, yaitu perintah docker yang kamu ketik di terminal. Dia tidak menjalankan container sendiri, dia cuma mengirim permintaan.
    • Docker daemon (dockerd), proses yang jalan di background. Dialah yang benar-benar bekerja: membangun image, membuat container, mengatur network dan storage.
    • Registry, gudang image di internet atau server internal. Yang paling populer adalah Docker Hub. Dari sinilah image nginx, mysql, dan lainnya diunduh.

    Saat kamu mengetik docker run nginx, urutan kejadiannya seperti ini:

    1. Client mengirim permintaan ke daemon lewat socket.
    2. Daemon mengecek apakah image nginx sudah ada di disk lokal.
    3. Kalau belum ada, daemon melakukan pull dari registry (Docker Hub) dan menyimpannya di lokal.
    4. Daemon membuat container baru dari image itu, menambahkan writable layer di atasnya.
    5. Daemon menjalankan proses utama container, dalam kasus Nginx yaitu web servernya.

    Pemahaman alur ini kepakai terus sampai level produksi. Di tim Arrazy, alur yang sama persis kami pakai saat men-deploy backend Go dan Laravel milik klien ke server, hanya saja registry-nya kadang privat, bukan Docker Hub.

    Praktik: Menjalankan Nginx di Container Pertamamu

    Cukup teori. Jalankan perintah ini:

    docker run -d -p 8080:80 --name webku nginx:1.27

    Penjelasan singkat flag-nya. -d membuat container jalan di background. -p 8080:80 menyambungkan port 8080 di komputermu ke port 80 di dalam container. --name webku memberi nama supaya gampang dirujuk. Detail lengkap soal port mapping dan mode detach dibahas khusus di bagian 4 seri ini, sekarang cukup pakai dulu.

    Karena ini pertama kali, Docker akan pull image dulu. Outputnya kurang lebih:

    Unable to find image 'nginx:1.27' locally
    1.27: Pulling from library/nginx
    a480a496ba95: Pull complete
    f3ace1b8ce45: Pull complete
    11d6fdd0e8a7: Pull complete
    Digest: sha256:...
    Status: Downloaded newer image for nginx:1.27
    3f9c1d2e8b7a...

    Baris terakhir yang berupa deretan huruf dan angka itu adalah ID container barumu. Sekarang lihat container yang sedang jalan:

    docker ps

    Output yang diharapkan:

    CONTAINER ID   IMAGE        COMMAND                  CREATED          STATUS          PORTS                  NAMES
    3f9c1d2e8b7a   nginx:1.27   "/docker-entrypoint.…"   10 seconds ago   Up 9 seconds    0.0.0.0:8080->80/tcp   webku

    Perhatikan kolom STATUS bertuliskan Up. Artinya container hidup dan proses Nginx sedang berjalan di dalamnya. Sekarang buka browser dan akses http://localhost:8080. Kamu akan melihat halaman default dengan tulisan Welcome to nginx! di bagian atasnya. Kalau lebih suka lewat terminal:

    curl http://localhost:8080

    Responsnya HTML yang diawali <title>Welcome to nginx!</title>. Web server pertamamu di Docker sudah jalan.

    Membuktikan Satu Image Bisa Jadi Banyak Container

    Sekarang buktikan analogi cetakan tadi. Jalankan container kedua dari image yang sama, dengan nama dan port berbeda:

    docker run -d -p 8081:80 --name webku2 nginx:1.27

    Kali ini tidak ada proses pull karena image sudah ada di lokal, jadi container langsung jalan dalam waktu kurang dari satu detik. Cek dengan docker ps, sekarang ada dua container, webku dan webku2, dua-duanya dari image nginx:1.27 yang sama. Buka http://localhost:8081, hasilnya halaman Nginx juga. Satu cetakan, dua kue.

    Siklus Hidup Container: Created, Running, Exited

    Container punya beberapa status utama yang perlu kamu hafal:

    • Created: container sudah dibuat dari image tapi prosesnya belum dijalankan. Terjadi kalau kamu pakai docker create tanpa docker start.
    • Running: proses utama di dalam container sedang berjalan. Inilah yang muncul di docker ps.
    • Exited: proses utamanya sudah berhenti, entah karena selesai, error, atau dihentikan manual.

    Coba hentikan container kedua:

    docker stop webku2

    Lalu jalankan docker ps lagi. Container webku2 hilang dari daftar. Nah, di sinilah banyak pemula bingung: containernya ke mana?

    Jawabannya, container itu tidak hilang. Dia masih ada di disk dengan status Exited, hanya saja docker ps secara default cuma menampilkan container yang running. Untuk melihat semuanya, tambahkan flag -a:

    docker ps -a

    Output:

    CONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS                     PORTS                  NAMES
    7b2e4a1c9d03   nginx:1.27   "/docker-entrypoint.…"   5 minutes ago   Exited (0) 30 seconds ago                         webku2
    3f9c1d2e8b7a   nginx:1.27   "/docker-entrypoint.…"   8 minutes ago   Up 8 minutes               0.0.0.0:8080->80/tcp   webku

    Container Exited bisa dihidupkan lagi dengan docker start webku2, lengkap dengan isi writable layer-nya yang masih utuh. Kalau memang sudah tidak dibutuhkan, hapus permanen dengan:

    docker stop webku2
    docker rm webku2

    Ingat, docker rm hanya menghapus container. Image nginx:1.27 tetap ada di disk dan bisa dipakai membuat container baru kapan saja. Menghapus image adalah perintah lain, yaitu docker rmi, dan hanya bisa dilakukan kalau tidak ada container yang masih memakainya.

    Kenapa Container Langsung Exit? Kasus docker run ubuntu

    Ini kebingungan paling klasik. Coba jalankan:

    docker run -d --name coba-ubuntu ubuntu:24.04

    Lalu cek docker ps. Kosong, tidak ada coba-ubuntu. Cek docker ps -a, ternyata statusnya Exited (0) padahal baru saja dijalankan. Kenapa Nginx bertahan hidup tapi Ubuntu langsung mati?

    Aturannya satu: container hanya hidup selama proses utamanya hidup. Image nginx proses utamanya adalah web server yang memang dirancang berjalan terus menerus di foreground, menunggu request. Image ubuntu proses utamanya adalah bash. Karena container jalan di background tanpa terminal interaktif, bash tidak punya input apa pun, jadi dia langsung selesai. Bash selesai, container ikut exit. Ini bukan error, ini perilaku yang benar.

    Kalau mau masuk ke dalam Ubuntu dan main-main di shell-nya, jalankan dengan mode interaktif:

    docker run -it --name ubuntu-interaktif ubuntu:24.04 bash

    Flag -it memberi container terminal interaktif, jadi bash tetap hidup dan kamu langsung berada di dalam container. Prompt-mu berubah jadi seperti root@a1b2c3d4e5f6:/#. Ketik exit untuk keluar, dan begitu bash berhenti, container pun berstatus Exited. Konsisten dengan aturannya.

    Troubleshooting: Error yang Sering Dialami Pemula

    Error: port is already allocated

    docker: Error response from daemon: ... bind: address already in use

    Penyebab: port 8080 di komputermu sudah dipakai, entah oleh container lain atau aplikasi lain. Solusi: pakai port host lain, misalnya -p 8082:80, atau cari siapa yang memakai port itu dengan docker ps lalu stop container yang bentrok.

    Error: the container name is already in use

    docker: Error response from daemon: Conflict. The container name "/webku" is already in use

    Penyebab: nama container harus unik, dan container lama bernama sama masih ada meskipun statusnya Exited. Solusi: hapus dulu container lama dengan docker rm webku, atau pakai nama lain.

    Error: Cannot connect to the Docker daemon

    Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

    Penyebab: dockerd tidak jalan, atau user-mu belum punya izin mengakses socket Docker. Solusi di Ubuntu: jalankan sudo systemctl start docker. Kalau masalahnya izin, pastikan user sudah masuk grup docker seperti dibahas di bagian 1, lalu logout dan login lagi. Di Windows, pastikan aplikasi Docker Desktop sedang berjalan.

    Container Nginx jalan tapi browser tidak bisa akses

    Penyebab paling umum: lupa flag -p saat docker run, jadi port container tidak tersambung ke host. Cek kolom PORTS di docker ps. Kalau kosong, hapus container itu dan jalankan ulang dengan -p 8080:80. Port mapping tidak bisa ditambahkan ke container yang sudah terlanjur dibuat.

    Rangkuman dan Lanjut ke Bagian 3

    Hari ini kamu sudah memegang konsep paling fundamental di Docker. Image itu cetakan yang read-only, container itu instance hidup yang dibuat dari cetakan tersebut, dan satu image bisa melahirkan banyak container yang saling independen. Kamu juga sudah tahu alur kerja client, daemon, dan registry, plus aturan emas bahwa container hanya hidup selama proses utamanya hidup.

    Pola kerja ini sama persis dengan yang dipakai di dunia kerja. Saat tim Arrazy membangun sistem aplikasi untuk klien, satu image backend yang sama bisa dijalankan sebagai container di laptop developer, di server staging, dan di server produksi tanpa ada cerita “di laptop saya jalan kok”.

    Bagian berikutnya, “Perintah Dasar Docker yang Wajib Dikuasai Pemula”, akan membedah perintah harian seperti docker ps, docker images, docker rm, dan kawan-kawannya secara sistematis. Artikelnya terbit menyusul, pantau daftarnya di halaman Belajar Docker dari Nol.

    Referensi

  • Belajar Golang dari Nol #9: Interface, Kontrak yang Bikin Kode Fleksibel

    Belajar Golang dari Nol #9: Interface, Kontrak yang Bikin Kode Fleksibel

    Di Belajar Golang dari Nol #8 kita sudah merapikan kode ke dalam package dan module. Struktur project kamu sekarang sudah bersih. Tapi ada satu masalah yang belum kita sentuh: bagaimana kalau satu tugas bisa dikerjakan dengan banyak cara berbeda? Di sinilah interface masuk. Ini salah satu fitur paling penting di Go, dan kabar baiknya, konsepnya sederhana.

    Masalah: satu tugas, banyak cara

    Bayangkan kamu membangun aplikasi toko online. Setiap kali ada pesanan baru, sistem harus mengirim notifikasi ke pembeli. Ada yang mau lewat WhatsApp, ada yang lewat email, ada yang lewat SMS.

    Dengan bekal sampai bagian 8, kamu mungkin akan menulis begini:

    func KirimNotifikasi(via string, pesan string) error {
    	if via == "wa" {
    		// logika kirim WhatsApp
    	} else if via == "email" {
    		// logika kirim email
    	} else if via == "sms" {
    		// logika kirim SMS
    	}
    	return nil
    }

    Kode ini jalan. Tapi coba pikirkan nasibnya tiga bulan lagi. Perusahaan mau tambah notifikasi lewat Telegram. Kamu harus buka function ini lagi, tambah satu cabang else if lagi. Tambah push notification, tambah cabang lagi. Function-nya makin gemuk, dan setiap perubahan berisiko merusak cabang yang sudah jalan.

    Masalah sebenarnya bukan di if else. Masalahnya, function ini harus tahu detail semua cara pengiriman. Padahal yang dia butuhkan cuma satu hal: sesuatu yang bisa mengirim pesan. Titik.

    Interface itu kontrak

    Interface di Go adalah cara kita menulis kebutuhan itu secara eksplisit. Bentuknya begini:

    type Notifier interface {
    	Kirim(pesan string) error
    }

    Baca deklarasi ini seperti kontrak kerja. Perusahaan pasang lowongan: “kami butuh kurir, syaratnya bisa mengantar paket”. Perusahaan tidak peduli kurirnya naik motor, mobil, atau sepeda. Selama bisa mengantar paket, kontrak terpenuhi, dia bisa dipekerjakan.

    Notifier adalah kontrak versi kode. Isinya satu syarat: siapa pun yang mau disebut Notifier harus punya method Kirim yang menerima string dan mengembalikan error. Interface tidak berisi implementasi sama sekali. Dia cuma daftar syarat.

    Implementasi implisit, ciri khas Go

    Di bahasa lain seperti Java atau PHP, kamu harus menulis implements Notifier secara eksplisit di deklarasi class. Go tidak begitu. Di Go, sebuah type otomatis memenuhi interface kalau dia punya semua method yang disyaratkan. Tidak ada kata kunci implements. Tidak ada pendaftaran apa pun.

    Kembali ke analogi kontrak kerja: kamu tidak perlu sertifikat bertuliskan “saya kurir”. Kalau kamu terbukti bisa mengantar paket, kamu memenuhi syarat. Kemampuanmu yang bicara, bukan labelmu.

    Kenapa ini enak? Dua alasan:

    • Kamu bisa membuat interface untuk type yang bukan milikmu. Misalnya type dari library orang lain sudah punya method yang cocok, dia otomatis memenuhi interface buatanmu tanpa mengubah kode library itu.
    • Kode implementasi tidak perlu tahu interface apa saja yang dia penuhi. Struct WhatsAppSender cukup fokus mengirim WhatsApp. Urusan dia dipakai sebagai Notifier atau bukan, itu urusan pemakainya.

    Contoh utuh: sistem notifikasi

    Sekarang kita rakit semuanya. Dua struct, satu interface, satu function yang menerima interface:

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    type Notifier interface {
    	Kirim(pesan string) error
    }
    
    type WhatsAppSender struct {
    	Nomor string
    }
    
    func (w WhatsAppSender) Kirim(pesan string) error {
    	if w.Nomor == "" {
    		return errors.New("nomor WhatsApp kosong")
    	}
    	fmt.Println("[WA ke", w.Nomor+"]", pesan)
    	return nil
    }
    
    type EmailSender struct {
    	Alamat string
    }
    
    func (e EmailSender) Kirim(pesan string) error {
    	if e.Alamat == "" {
    		return errors.New("alamat email kosong")
    	}
    	fmt.Println("[Email ke", e.Alamat+"]", pesan)
    	return nil
    }
    
    func ProsesNotifikasi(n Notifier, pesan string) {
    	err := n.Kirim(pesan)
    	if err != nil {
    		fmt.Println("Gagal kirim:", err)
    	}
    }
    
    func main() {
    	wa := WhatsAppSender{Nomor: "0812xxxx1234"}
    	email := EmailSender{Alamat: "budi@tokokopi.id"}
    
    	ProsesNotifikasi(wa, "Pesanan kamu sudah dikirim")
    	ProsesNotifikasi(email, "Pesanan kamu sudah dikirim")
    }

    Perhatikan ProsesNotifikasi. Parameternya bertipe Notifier, bukan WhatsAppSender atau EmailSender. Function ini tidak tahu dan tidak peduli pesan dikirim lewat apa. Dia cuma pegang kontrak: apa pun yang masuk, pasti bisa dipanggil method Kirim-nya.

    Output program di atas:

    [WA ke 0812xxxx1234] Pesanan kamu sudah dikirim
    [Email ke budi@tokokopi.id] Pesanan kamu sudah dikirim

    Karena Notifier adalah type biasa, kamu juga bisa membuat slice berisi campuran implementasi. Ini pola broadcast yang sering dipakai:

    	daftar := []Notifier{
    		WhatsAppSender{Nomor: "0812xxxx1234"},
    		EmailSender{Alamat: "budi@tokokopi.id"},
    		EmailSender{},
    	}
    
    	for _, n := range daftar {
    		ProsesNotifikasi(n, "Promo akhir bulan dimulai")
    	}

    Elemen ketiga sengaja dibuat kosong supaya kelihatan jalur error-nya:

    [WA ke 0812xxxx1234] Promo akhir bulan dimulai
    [Email ke budi@tokokopi.id] Promo akhir bulan dimulai
    Gagal kirim: alamat email kosong

    Sekarang bandingkan dengan versi if else di awal. Mau tambah Telegram? Buat struct TelegramSender dengan method Kirim, selesai. ProsesNotifikasi tidak disentuh sama sekali. Kode lama tetap aman, fitur baru tinggal ditambahkan. Ini yang dimaksud kode fleksibel.

    Interface kecil itu idiomatik

    Perhatikan Notifier cuma punya satu method. Ini bukan kebetulan. Di Go, interface yang bagus justru yang kecil. Makin sedikit syarat di kontrak, makin banyak type yang bisa memenuhinya, makin fleksibel kodemu.

    Standard library Go penuh contoh interface satu method. Dua yang paling sering kamu temui:

    error ternyata interface

    Masih ingat bagian 3 waktu kita belajar error handling? Waktu itu saya bilang error akan kita bahas lebih dalam nanti. Ini dia bayarannya. error yang selama ini kamu pakai sebenarnya interface bawaan:

    type error interface {
    	Error() string
    }

    Artinya, type apa pun yang punya method Error() string bisa dipakai sebagai error. Itulah kenapa kamu bisa mengembalikan hasil errors.New, hasil fmt.Errorf, atau error buatan library mana pun dari function yang sama. Semuanya cuma memenuhi satu kontrak kecil.

    fmt.Stringer

    Interface kecil lain yang berguna adalah fmt.Stringer, kontraknya satu method String() string. Kalau struct kamu memenuhinya, fmt.Println otomatis memakai method itu saat mencetak:

    package main
    
    import "fmt"
    
    type Produk struct {
    	Nama  string
    	Harga int
    }
    
    func (p Produk) String() string {
    	return fmt.Sprintf("%s (Rp%d)", p.Nama, p.Harga)
    }
    
    func main() {
    	p := Produk{Nama: "Kopi Gayo 250g", Harga: 85000}
    	fmt.Println(p)
    }

    Tanpa method String, outputnya format default: {Kopi Gayo 250g 85000}. Dengan method itu, outputnya jadi Kopi Gayo 250g (Rp85000). Kamu tidak pernah mendaftarkan apa pun ke package fmt. Dia cuma mengecek: type ini memenuhi kontrak Stringer atau tidak. Implementasi implisit bekerja diam-diam di sini.

    any, interface kosong, dan type assertion

    Ada satu interface ekstrem: interface tanpa syarat sama sekali, ditulis interface{} atau alias modernnya, any. Karena kontraknya kosong, semua type otomatis memenuhinya. Variabel bertipe any bisa diisi apa saja.

    Kedengarannya praktis, tapi hati-hati. Begitu data masuk ke any, compiler tidak bisa lagi menjaga kamu. Kamu kehilangan informasi type, dan itu mahal. Untuk mengambil kembali type aslinya, kamu butuh type assertion:

    var data any = "halo"
    
    s, ok := data.(string)
    if ok {
    	fmt.Println("panjang teks:", len(s))
    } else {
    	fmt.Println("data bukan string")
    }

    Bentuk data.(string) artinya “saya yakin isi data adalah string, tolong keluarkan”. Variabel ok bernilai true kalau tebakanmu benar. Selalu pakai bentuk dua nilai ini. Bentuk satu nilai tanpa ok akan membuat program panic kalau tebakannya salah.

    Aturan praktisnya: pakai any hanya kalau memang tidak ada pilihan, misalnya saat menangani JSON yang strukturnya tidak tentu. Untuk kode sehari-hari, interface dengan kontrak jelas seperti Notifier hampir selalu lebih baik.

    Kapan tidak perlu interface

    Setelah paham interface, ada godaan untuk membuatnya di mana-mana. Tahan dulu. Interface itu abstraksi, dan abstraksi ada biayanya: kode jadi lebih sulit dilacak karena pembaca harus mencari implementasi aslinya.

    Patokan sederhananya:

    • Kalau cuma ada satu implementasi dan belum ada rencana nyata menambah yang lain, pakai struct langsung. Jangan bikin UserServiceInterface hanya karena terlihat rapi.
    • Buat interface saat kebutuhannya muncul: ada implementasi kedua, atau kamu perlu memisahkan kode dari dependensi eksternal.
    • Definisikan interface di sisi pemakai, bukan di sisi implementasi. Ini kebiasaan idiomatik di Go, beda dengan kebiasaan di Java.

    Ingat prinsipnya: interface menjawab masalah “banyak cara untuk satu tugas”. Kalau caranya memang cuma satu, tidak ada masalah yang perlu dijawab.

    Latihan: sistem pembayaran mini

    Sekarang giliran kamu. Kita buat sistem checkout yang menerima beberapa metode pembayaran. Kontraknya satu: bisa membayar sejumlah uang. Aturan validasinya beda per metode. Transfer bank punya minimal nominal, e-wallet dibatasi saldo.

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    type MetodePembayaran interface {
    	Bayar(jumlah float64) error
    }
    
    type TransferBank struct {
    	NamaBank   string
    	NoRekening string
    }
    
    func (t TransferBank) Bayar(jumlah float64) error {
    	if t.NoRekening == "" {
    		return errors.New("nomor rekening belum diisi")
    	}
    	if jumlah < 10000 {
    		return errors.New("minimal transfer bank Rp10.000")
    	}
    	fmt.Printf("Transfer Rp%.0f via %s berhasil\n", jumlah, t.NamaBank)
    	return nil
    }
    
    type EWallet struct {
    	Provider string
    	Saldo    float64
    }
    
    func (e EWallet) Bayar(jumlah float64) error {
    	if jumlah > e.Saldo {
    		return errors.New("saldo " + e.Provider + " tidak cukup")
    	}
    	fmt.Printf("Bayar Rp%.0f pakai %s berhasil\n", jumlah, e.Provider)
    	return nil
    }
    
    func Checkout(m MetodePembayaran, total float64) {
    	fmt.Printf("Memproses pembayaran Rp%.0f...\n", total)
    	err := m.Bayar(total)
    	if err != nil {
    		fmt.Println("Pembayaran gagal:", err)
    		return
    	}
    	fmt.Println("Pesanan dikonfirmasi")
    }
    
    func main() {
    	bca := TransferBank{NamaBank: "BCA", NoRekening: "1234567890"}
    	dana := EWallet{Provider: "Dana", Saldo: 50000}
    
    	Checkout(bca, 150000)
    	Checkout(dana, 35000)
    	Checkout(dana, 100000)
    	Checkout(TransferBank{NamaBank: "BRI"}, 150000)
    }

    Jalankan dengan go run main.go. Outputnya:

    Memproses pembayaran Rp150000...
    Transfer Rp150000 via BCA berhasil
    Pesanan dikonfirmasi
    Memproses pembayaran Rp35000...
    Bayar Rp35000 pakai Dana berhasil
    Pesanan dikonfirmasi
    Memproses pembayaran Rp100000...
    Pembayaran gagal: saldo Dana tidak cukup
    Memproses pembayaran Rp150000...
    Pembayaran gagal: nomor rekening belum diisi

    Amati dua hal. Pertama, Checkout memperlakukan semua metode sama rata lewat kontrak MetodePembayaran, padahal aturan validasi di baliknya berbeda. Kedua, dua kasus gagal ditangani rapi tanpa satu pun if else soal jenis metode di dalam Checkout.

    Untuk latihan mandiri, coba tambah KartuKredit dengan validasi limit, lalu masukkan ke pemanggilan Checkout. Kalau kamu tidak perlu mengubah function Checkout sama sekali, berarti kamu sudah paham inti bab ini.

    Rangkuman dan bagian selanjutnya

    Hari ini kamu belajar bahwa interface adalah kontrak: daftar method yang harus dipenuhi, tanpa implementasi. Go memakai implementasi implisit, jadi type cukup punya method yang cocok. Interface kecil seperti error dan fmt.Stringer adalah gaya idiomatik Go. any ada untuk kasus khusus, dan type assertion adalah pintu keluarnya. Terakhir, jangan bikin interface untuk satu implementasi tanpa alasan.

    Di bagian 10 kita masuk ke materi yang membuat Go terkenal: “Goroutine dan Channel: Kenalan dengan Concurrency”. Sampai ketemu di sana.

    Kalau bisnismu butuh aplikasi yang dibangun dengan fondasi rapi seperti ini, tim Arrazy menyediakan jasa pengembangan sistem aplikasi dari perancangan sampai perawatan.

  • Belajar Linux dari Nol #2: Cara Menggunakan Terminal Linux

    Belajar Linux dari Nol #2: Cara Menggunakan Terminal Linux

    Cara menggunakan terminal Linux sebenarnya sederhana: buka aplikasi Terminal (di Ubuntu 24.04 tekan Ctrl+Alt+T), ketik perintah, lalu tekan Enter. Komputer menjalankan perintah itu dan menampilkan hasilnya sebagai teks. Itu saja intinya. Yang membuat pemula sering mundur bukan karena terminalnya susah, tapi karena layar hitam berisi teks terasa asing dibanding klik-klik di tampilan grafis.

    Artikel ini bagian kedua dari seri Belajar Linux dari Nol. Di sini kita bongkar dulu konsepnya: apa itu terminal, shell, dan bash, lalu langsung praktik perintah pertama seperti whoami, date, dan echo. Di akhir artikel kamu juga belajar membaca manual perintah dan mengenali error yang paling sering bikin pemula panik.

    Prasyarat: Ubuntu Sudah Terpasang

    Semua praktik di seri ini memakai Ubuntu 24.04 LTS. Kalau kamu belum menginstalnya, ikuti dulu panduan Belajar Linux dari Nol #1: Cara Install Ubuntu 24.04. Boleh instal langsung di laptop, dual boot, atau lewat virtual machine. Yang penting kamu punya akses ke desktop Ubuntu yang berjalan normal.

    Apa Itu Terminal, Shell, dan Bash

    Tiga istilah ini sering dipakai bergantian, padahal artinya berbeda. Memahami bedanya akan memudahkan kamu membaca dokumentasi dan tutorial lain.

    • Terminal adalah aplikasi jendelanya. Tempat kamu mengetik dan tempat hasil perintah ditampilkan. Di Ubuntu 24.04 aplikasi bawaannya bernama GNOME Terminal (di edisi terbaru namanya GNOME Console di beberapa varian). Istilah lengkapnya terminal emulator, karena dia meniru terminal fisik zaman dulu.
    • Shell adalah program yang membaca ketikan kamu, menerjemahkannya, lalu meminta sistem operasi menjalankannya. Terminal cuma wadah, shell yang benar-benar bekerja.
    • Bash adalah salah satu jenis shell, kependekan dari Bourne Again Shell. Bash adalah shell default di Ubuntu 24.04. Ada shell lain seperti zsh atau fish, tapi sepanjang seri ini kita pakai bash.

    Analogi sederhananya: terminal itu ruang obrolan, shell itu penerjemahnya, dan bash itu bahasa yang dipakai si penerjemah.

    Beda GUI vs CLI dan Kapan CLI Menang

    GUI (Graphical User Interface) adalah antarmuka grafis yang kamu pakai sehari-hari: ikon, jendela, tombol, klik mouse. CLI (Command Line Interface) adalah antarmuka berbasis teks lewat terminal.

    GUI unggul untuk tugas visual seperti mengedit foto atau menonton video. Tapi ada banyak situasi di mana CLI jelas menang:

    • Tugas berulang. Mengganti nama 500 file lewat GUI butuh 500 kali klik. Lewat CLI cukup satu baris perintah.
    • Server tanpa layar. Hampir semua server di dunia berjalan tanpa GUI untuk menghemat resource. Satu-satunya cara mengelolanya ya lewat terminal, biasanya melalui SSH.
    • Otomatisasi. Perintah CLI bisa dirangkai jadi script yang berjalan sendiri, misalnya backup otomatis tiap malam.
    • Presisi. Perintah teks bisa dicatat, diulang persis, dan dibagikan ke orang lain tanpa perlu screenshot langkah demi langkah.

    Di pekerjaan nyata ini bukan teori. Tim Arrazy mengelola server klien untuk berbagai sistem aplikasi yang kami bangun, dan hampir semuanya dikerjakan lewat terminal: deploy aplikasi, cek log, sampai restart service. Skill terminal adalah fondasi yang dipakai setiap hari.

    Anatomi Prompt Terminal Linux

    Buka terminal dengan Ctrl+Alt+T. Kamu akan melihat satu baris teks seperti ini:

    budi@laptop-kerja:~$

    Baris ini disebut prompt. Artinya shell siap menerima perintah. Mari kita bedah bagian-bagiannya:

    Bagian Contoh Arti
    Username budi Nama user yang sedang login
    Pemisah @ Dibaca “di” (budi di laptop-kerja)
    Hostname laptop-kerja Nama komputer
    Direktori aktif ~ Lokasi kamu sekarang. Tanda ~ artinya home directory
    Penanda akhir $ User biasa. Kalau # artinya kamu login sebagai root

    Perhatikan tanda $ dan #. Ini penting saat membaca tutorial di internet. Perintah yang diawali # biasanya dimaksudkan untuk dijalankan sebagai root (administrator), sedangkan $ untuk user biasa. Tanda itu sendiri tidak ikut diketik.

    Anatomi Perintah: Command, Option, Argument

    Hampir semua perintah Linux mengikuti pola yang sama:

    command [option] [argument]

    Contoh nyata:

    ls -l /home
    • Command: ls, program yang dijalankan. Tugasnya menampilkan isi direktori.
    • Option: -l, pengubah perilaku command. Di sini artinya tampilkan dalam format panjang (detail). Option pendek diawali satu strip, option panjang diawali dua strip seperti --help.
    • Argument: /home, target yang dikenai perintah. Di sini direktori yang ingin dilihat isinya.

    Sekali kamu paham pola ini, perintah apa pun jadi mudah dibaca. apt install nginx? Command apt, argumen install dan nginx. Polanya selalu mirip.

    Praktik: Perintah Pertama di Terminal

    Sekarang giliran tangan yang bekerja. Ketik tiap perintah berikut lalu tekan Enter. Jangan cuma dibaca, otot jari juga perlu hafal.

    whoami: Cek User yang Sedang Login

    whoami

    Output yang diharapkan (sesuai username kamu):

    budi

    Terlihat sepele sekarang, tapi perintah ini penting saat nanti kamu berpindah-pindah user atau masuk ke server orang lain dan lupa sedang login sebagai siapa.

    hostname: Cek Nama Komputer

    hostname

    Output:

    laptop-kerja

    Berguna saat kamu membuka banyak terminal ke banyak server sekaligus dan perlu memastikan sedang berada di mesin yang mana.

    date: Tampilkan Tanggal dan Waktu

    date

    Output kira-kira seperti ini:

    Mon Jul 27 09:15:42 AM WIB 2026

    echo: Cetak Teks ke Layar

    echo "Halo, saya sedang belajar Linux"

    Output:

    Halo, saya sedang belajar Linux

    echo kelihatan remeh, tapi nanti jadi andalan saat menulis shell script dan mengecek isi variabel.

    clear: Bersihkan Layar

    clear

    Layar terminal jadi bersih lagi. Riwayat perintah tidak hilang, cuma tampilannya yang digulung ke atas. Alternatifnya tekan Ctrl+L, hasilnya sama.

    Tab Completion: Biarkan Terminal Melengkapi Ketikanmu

    Ini fitur yang wajib jadi kebiasaan sejak hari pertama. Ketik sebagian nama perintah atau nama file, lalu tekan tombol Tab. Shell akan melengkapinya otomatis.

    Coba: ketik who lalu tekan Tab dua kali. Terminal menampilkan semua perintah yang diawali “who”:

    whoami  who  whoopsie-id

    Ketik whoa lalu tekan Tab sekali, langsung jadi whoami. Selain menghemat ketikan, tab completion juga mencegah typo. Kalau kamu menekan Tab dan tidak terjadi apa-apa, kemungkinan besar ejaan awalnya sudah salah. Itu sinyal untuk berhenti dan mengecek.

    Panah Atas: Panggil Ulang Perintah Sebelumnya

    Tekan tombol panah atas, perintah terakhir muncul kembali. Tekan lagi untuk perintah sebelumnya, dan seterusnya. Panah bawah untuk maju lagi. Kamu juga bisa melihat seluruh riwayat dengan perintah history. Kombinasi panah atas plus Enter jauh lebih cepat daripada mengetik ulang perintah panjang.

    Cara Membaca Manual Perintah Linux

    Tidak ada orang yang hafal semua perintah beserta option-nya. Yang membedakan pemula dan orang berpengalaman adalah kebiasaan membuka manual.

    man: Manual Lengkap

    man ls

    Perintah ini membuka halaman manual untuk ls. Isinya lengkap: deskripsi, semua option, sampai contoh. Navigasinya:

    • Panah atas/bawah atau PgUp/PgDn untuk menggulung
    • /kata lalu Enter untuk mencari kata tertentu, n untuk hasil berikutnya
    • q untuk keluar

    Tombol q ini serius perlu diingat. Banyak pemula mengira terminalnya rusak karena tidak bisa mengetik perintah lagi, padahal mereka masih berada di dalam halaman manual.

    –help: Ringkasan Cepat

    Kalau butuh ringkasan singkat tanpa membuka halaman penuh, hampir semua perintah punya option --help:

    ls --help

    Outputnya daftar option beserta penjelasan satu barisnya, langsung tercetak di terminal. Cocok untuk sekadar mengingatkan “option untuk sort by time itu apa ya”. Untuk pemahaman mendalam, tetap buka man.

    Troubleshooting: Error yang Sering Dialami Pemula

    bash: whoyami: command not found

    Ini error paling umum. Contoh:

    budi@laptop-kerja:~$ whoyami
    whoyami: command not found

    Penyebab: hampir selalu typo. Shell mencari program bernama persis seperti yang kamu ketik, dan Linux membedakan huruf besar kecil. Whoami dengan W besar juga dianggap perintah berbeda dan akan gagal.

    Solusi: cek ejaan, lalu biasakan pakai tab completion supaya typo tidak terjadi. Kalau ejaan sudah benar tapi tetap error, kemungkinan programnya memang belum terinstal. Cara instal aplikasi kita bahas di bagian #11 seri ini.

    Terminal Terlihat Hang, Tidak Merespons Apa pun

    Penyebab: sering kali terminal tidak hang, tapi sedang menunggu input dari kamu. Contoh klasik: kamu mengetik echo "halo dengan tanda kutip yang tidak ditutup, lalu Enter. Prompt berubah jadi > dan terminal seolah diam. Sebenarnya bash menunggu kamu menutup tanda kutip itu.

    Solusi: ketik tanda kutip penutup lalu Enter, atau batalkan sekalian dengan Ctrl+C. Kasus lain: kamu tidak sengaja menjalankan program interaktif yang memang menunggu ketikan. Perhatikan bentuk prompt, kalau bukan nama@host:~$ berarti kamu sedang berada di dalam program lain.

    Perintah Berjalan Terus dan Tidak Berhenti

    Penyebab: beberapa perintah memang berjalan terus sampai dihentikan manual, misalnya ping google.com yang mengirim paket tanpa henti.

    Solusi: tekan Ctrl+C untuk mengirim sinyal berhenti. Ini kombinasi penyelamat nomor satu di terminal. Perintah apa pun yang terasa kebablasan, Ctrl+C dulu. Catatan penting: Ctrl+C di terminal bukan untuk copy. Copy paste di GNOME Terminal memakai Ctrl+Shift+C dan Ctrl+Shift+V.

    Terkunci di Halaman man, Tidak Bisa Mengetik Perintah

    Penyebab: kamu masih di dalam pager (tampilan halaman manual). Semua ketikan diperlakukan sebagai navigasi, bukan perintah shell.

    Solusi: tekan q. Kamu langsung kembali ke prompt dan bisa mengetik perintah lagi.

    Rangkuman dan Lanjut ke Bagian Berikutnya

    Hari ini kamu sudah paham beda terminal, shell, dan bash, bisa membaca anatomi prompt dan struktur perintah, menjalankan whoami, hostname, date, echo, dan clear, memanfaatkan tab completion dan history, membuka manual dengan man dan --help, serta tahu cara keluar dari situasi macet dengan Ctrl+C dan q.

    Latihan kecil sebelum lanjut: buka terminal, jalankan kelima perintah tadi tanpa melihat artikel, lalu buka man date dan cari cara menampilkan tanggal dalam format tertentu. Sepuluh menit latihan mandiri lebih melekat daripada membaca ulang.

    Bagian berikutnya, “Belajar Linux dari Nol #3: Perintah Dasar Linux Navigasi”, membahas cara berpindah direktori dengan cd, melihat isi folder dengan ls, dan mengetahui posisi kamu dengan pwd. Artikelnya terbit menyusul, pantau daftar lengkapnya di halaman hub Belajar Linux.

    Referensi

  • Belajar Golang dari Nol #8: Package dan Go Module

    Belajar Golang dari Nol #8: Package dan Go Module

    Di Belajar Golang dari Nol #7 kita sudah membereskan pointer. Materi paling menakutkan di seri ini sudah lewat. Sekarang saatnya bicara soal kerapian. Selama tujuh bagian, semua latihan kita tulis di satu file bernama main.go. Untuk belajar, itu wajar. Untuk proyek sungguhan, itu resep pusing. Di bagian ini kita belajar memecah kode ke banyak file dan banyak folder. Kuncinya dua hal: package dan Go module.

    Masalahnya: main.go yang mulai gemuk

    Coba lihat file latihan Anda dari bagian 4 sampai 7. Isinya kira-kira begini. Ada struct Produk. Ada beberapa method. Ada function pembantu untuk format harga. Ada function main. Semua tumpuk di satu file.

    // main.go, semua campur di sini
    package main
    
    import "fmt"
    
    type Produk struct { ... }
    
    func (p *Produk) TambahStok(jumlah int) { ... }
    func (p *Produk) KurangiStok(jumlah int) error { ... }
    
    func formatRupiah(angka int) string { ... }
    func cetakLaporan(daftar []Produk) { ... }
    
    func main() { ... }
    

    Untuk 100 baris, ini masih aman. Tapi bayangkan aplikasi kasir sederhana. Ada produk, pelanggan, transaksi, dan laporan. Kalau semua ditulis di main.go, file itu bisa tembus ribuan baris. Anda akan scroll ke sana kemari cuma untuk cari satu function. Nama function juga mulai bentrok. Simpan() untuk produk atau untuk pelanggan? Terpaksa dinamai SimpanProduk dan SimpanPelanggan, lalu file makin ramai.

    Bahasa lain menyelesaikan ini dengan class dan namespace. Go menyelesaikannya dengan package. Satu package berisi kode yang topiknya sama. Kode soal produk masuk package produk. Alur utama program tetap di package main.

    Go module: KTP proyek Anda

    Sebelum bikin package, kita lunasi dulu satu utang dari bagian 1. Waktu itu saya minta Anda menjalankan go mod init tanpa penjelasan panjang. Sekarang waktunya dijelaskan tuntas.

    Module adalah unit proyek di Go. Satu module berisi satu proyek utuh: semua file, semua folder, semua package di dalamnya. Module juga mencatat proyek Anda bergantung ke library apa saja. Anggap saja module itu KTP proyek. Dia menyimpan nama proyek dan daftar kenalannya.

    Membuatnya cukup satu perintah di folder proyek.

    go mod init github.com/budi/tokokita
    

    Perintah ini menghasilkan file go.mod. Isinya pendek.

    module github.com/budi/tokokita
    
    go 1.22
    

    Baris pertama adalah nama module. Baris kedua adalah versi Go minimum yang dipakai proyek ini. Sudah, itu saja untuk sekarang.

    Soal penamaan, ada kebiasaan yang hampir semua programmer Go ikuti: nama module memakai path repository tempat kode itu nanti disimpan. Kalau username GitHub Anda budi dan proyeknya bernama tokokita, nama module-nya github.com/budi/tokokita. Kenapa begitu? Karena kalau suatu hari orang lain mau memakai kode Anda, Go bisa langsung tahu harus download dari mana. Nama module sekaligus jadi alamatnya. Untuk latihan lokal, Anda bebas menamai apa saja, misalnya tokokita saja. Tapi biasakan format path repo sejak awal supaya tidak perlu ganti nanti.

    Membuat package sendiri

    Sekarang kita pecah proyeknya. Aturannya sederhana: satu folder berisi satu package. Semua file di folder yang sama harus memakai nama package yang sama. Kita buat folder produk di dalam proyek, lalu isi dengan file produk.go.

    tokokita/
    ├── go.mod
    ├── main.go
    └── produk/
        └── produk.go
    

    Baris pertama di produk/produk.go bukan lagi package main, tapi package produk. Nama package sebaiknya sama dengan nama foldernya. Go tidak memaksa, tapi kalau berbeda, orang yang baca kode Anda akan bingung.

    Satu hal penting: hanya package main yang punya function main() dan bisa dijalankan langsung. Package lain sifatnya seperti kotak perkakas. Isinya dipakai oleh package lain, tidak jalan sendiri.

    Huruf besar dan huruf kecil: aturan akses Go

    Di banyak bahasa, ada kata kunci public dan private untuk mengatur apa yang boleh diakses dari luar. Go tidak punya kata kunci itu. Aturannya lebih sederhana dan agak unik: dilihat dari huruf pertama namanya.

    • Nama diawali huruf besar, misalnya Produk atau TambahStok: exported. Bisa diakses dari package lain.
    • Nama diawali huruf kecil, misalnya hitungDiskon: unexported. Hanya bisa dipakai di dalam package itu sendiri. Privat.

    Aturan ini berlaku untuk semuanya: function, struct, method, field struct, sampai variabel level package. Field struct yang huruf kecil pun tidak bisa dibaca dari luar package-nya.

    Masih ingat di bagian 1 saya bilang penulisan fmt.Println dengan P besar itu bukan selera, tapi keharusan? Ini jawabannya. Println ada di package fmt milik tim Go. Supaya bisa kita panggil dari luar, dia harus exported. Makanya hurufnya besar. Kalau tim Go menamainya println, kita tidak akan bisa memakainya sama sekali.

    Meng-import package sendiri

    Lalu bagaimana main.go memakai package produk? Lewat import, sama seperti kita meng-import fmt selama ini. Bedanya, path-nya adalah nama module ditambah nama folder.

    import "github.com/budi/tokokita/produk"
    

    Go membaca path itu begini: cari module github.com/budi/tokokita. Itu proyek kita sendiri, sesuai go.mod. Lalu masuk ke folder produk. Setelah di-import, semua isi yang exported bisa dipanggil dengan awalan nama package-nya: produk.Baru(...), mirip fmt.Println(...).

    Praktik: memecah sistem stok jadi dua file

    Teori cukup. Sekarang kita rapikan latihan sistem stok dari bagian 4. Semua yang berhubungan dengan produk pindah ke package produk: struct, constructor, dan method-nya. File main.go hanya berisi alur program. Ini isi lengkap produk/produk.go.

    package produk
    
    import "fmt"
    
    // Produk menyimpan data satu barang di gudang.
    type Produk struct {
    	Nama  string
    	Stok  int
    	Harga int
    }
    
    // Baru membuat Produk baru dan mengembalikan pointer-nya.
    func Baru(nama string, stok int, harga int) *Produk {
    	return &Produk{Nama: nama, Stok: stok, Harga: harga}
    }
    
    // TambahStok menambah jumlah stok.
    func (p *Produk) TambahStok(jumlah int) {
    	p.Stok += jumlah
    }
    
    // KurangiStok mengurangi stok, gagal kalau stok tidak cukup.
    func (p *Produk) KurangiStok(jumlah int) error {
    	if jumlah > p.Stok {
    		return fmt.Errorf("stok %s tinggal %d, tidak cukup untuk keluar %d",
    			p.Nama, p.Stok, jumlah)
    	}
    	p.Stok -= jumlah
    	return nil
    }
    
    // Info mengembalikan ringkasan produk dalam satu baris.
    func (p *Produk) Info() string {
    	return fmt.Sprintf("%s | stok %d | Rp%d", p.Nama, p.Stok, p.Harga)
    }
    

    Perhatikan function Baru. Ini pola yang disebut constructor. Dari luar, pemanggilannya jadi produk.Baru(...), enak dibaca seperti kalimat: buat produk baru. Method-nya memakai pointer receiver *Produk supaya perubahan stok benar-benar tersimpan. Itu materi bagian 7 yang langsung terpakai di sini.

    Sekarang main.go. Pendek dan bersih.

    package main
    
    import (
    	"fmt"
    
    	"github.com/budi/tokokita/produk"
    )
    
    func main() {
    	kopi := produk.Baru("Kopi Arabika", 10, 25000)
    	kopi.TambahStok(5)
    
    	err := kopi.KurangiStok(20)
    	if err != nil {
    		fmt.Println("Gagal:", err)
    	}
    
    	err = kopi.KurangiStok(8)
    	if err != nil {
    		fmt.Println("Gagal:", err)
    	}
    
    	fmt.Println(kopi.Info())
    }
    

    Jalankan dengan go run . dari folder proyek. Titik di akhir artinya jalankan package di folder ini. Outputnya:

    Gagal: stok Kopi Arabika tinggal 15, tidak cukup untuk keluar 20
    Kopi Arabika | stok 7 | Rp25000
    

    Logikanya sama persis dengan versi satu file. Yang berubah cuma letaknya. Tapi rasakan bedanya. Kalau besok Anda mau menambah fitur diskon, Anda tahu persis harus buka produk/produk.go. Kalau mau mengubah alur program, buka main.go. Kode soal pelanggan nanti tinggal dibuatkan folder pelanggan sendiri.

    Memakai package buatan orang lain

    Kekuatan module tidak berhenti di kode sendiri. Dengan go get, Anda bisa memakai ribuan package buatan komunitas. Kita coba satu yang populer dan stabil: github.com/google/uuid, pembuat ID unik. Jalankan dari folder proyek.

    go get github.com/google/uuid
    

    Go akan download package itu, lalu mencatatnya di go.mod. File-nya sekarang punya baris baru.

    module github.com/budi/tokokita
    
    go 1.22
    
    require github.com/google/uuid v1.6.0
    

    Artinya proyek ini butuh package uuid versi 1.6.0. Selain itu muncul file baru bernama go.sum. Isinya kode hash dari package yang di-download. Fungsinya seperti segel: kalau nanti Anda atau teman satu tim download ulang package itu dan isinya berbeda dari yang tercatat, Go langsung menolak. Dua file ini yang menjamin proyek Anda jalan sama persis di komputer siapa pun. Keduanya wajib ikut di-commit ke Git.

    Memakainya sama seperti package lain. Tambahkan di import, panggil function-nya.

    import "github.com/google/uuid"
    
    // di dalam function:
    id := uuid.NewString()
    fmt.Println("ID transaksi:", id)
    

    Setiap dipanggil, uuid.NewString() menghasilkan string acak seperti 3f1c9d2a-8b7e-4f0a-9c2d-1e5b6a7f8d90. Cocok untuk ID transaksi atau nomor pesanan yang tidak boleh kembar.

    Kebiasaan penamaan package yang baik

    Terakhir, soal nama. Komunitas Go punya selera yang cukup seragam di sini.

    • Pendek dan satu kata: produk, laporan, kasir.
    • Semua huruf kecil. Tanpa underscore, tanpa camelCase. Bukan dataProduk atau data_produk.
    • Nama menggambarkan isi. Orang harus bisa menebak isi package dari namanya saja.
    • Hindari pengulangan nama. Function produk.Baru lebih enak daripada produk.ProdukBaru. Nama package sudah memberi konteks, tidak perlu diulang.

    Anti-pattern yang paling sering muncul di pemula: package util atau helper. Isinya gado-gado. Format tanggal, hitung pajak, validasi email, semua dilempar ke sana. Enam bulan kemudian package itu jadi tempat sampah yang tidak ada yang berani menyentuh. Kalau ada function yang bingung mau ditaruh di mana, itu tanda Anda belum menemukan nama topik yang tepat, bukan tanda butuh package util. Format tanggal bisa masuk package tanggal. Hitung pajak masuk pajak. Spesifik selalu menang.

    Rangkuman dan bagian berikutnya

    Hari ini kode Anda naik kelas. Dari satu file main.go menjadi proyek yang tertata. Yang perlu diingat:

    • Module adalah KTP proyek. Dibuat dengan go mod init, dicatat di go.mod, dinamai dengan path repo seperti github.com/username/tokokita.
    • Satu folder satu package. Kode dikelompokkan per topik, bukan ditumpuk di satu file.
    • Huruf besar berarti exported, huruf kecil berarti privat. Itu alasan Println ber-P besar.
    • Import package sendiri memakai nama module ditambah nama folder.
    • go get menarik package pihak ketiga, tercatat di go.mod dan disegel di go.sum.
    • Nama package pendek, huruf kecil, spesifik. Jauhi package util campur aduk.

    Coba latihan kecil sebelum lanjut: pecah latihan Anda sendiri dari bagian 5 atau 6 menjadi minimal satu package terpisah, lalu pastikan go run . tetap jalan. Di bagian 9 kita masuk ke salah satu fitur paling khas Go: interface. Judulnya “Interface: Kontrak yang Bikin Kode Fleksibel”. Struktur proyek seperti ini juga yang kami pakai sehari-hari saat membangun sistem aplikasi untuk bisnis dengan Go di Arrazy.

  • Belajar Kubernetes #1: Kubernetes Adalah + Install Minikube

    Belajar Kubernetes #1: Kubernetes Adalah + Install Minikube

    Kubernetes adalah platform open source untuk mengelola aplikasi berbasis container secara otomatis. Tugasnya mencakup menjalankan container di banyak server sekaligus, menghidupkan ulang container yang mati, membagi beban trafik, dan menaikkan jumlah container saat trafik ramai. Singkatnya, kalau Docker menjalankan satu container, Kubernetes mengatur ratusan container di banyak mesin supaya kamu tidak perlu mengurusnya satu per satu.

    Artikel ini bagian pertama dari seri Belajar Kubernetes dari Nol, total 26 bagian sampai kamu bisa deploy aplikasi web plus database di VPS sendiri. Di bagian ini kita bahas konsep dasarnya, lalu langsung praktik: install kubectl dan Minikube, jalankan cluster pertama, dan deploy nginx sebagai bukti environment kamu hidup. Prasyaratnya cuma satu: paham Docker dasar. Kalau belum, selesaikan dulu seri Belajar Docker dari Nol minimal sampai paham image, container, dan port mapping.

    Kubernetes Adalah Solusi untuk Masalah Banyak Container

    Bayangkan kamu punya aplikasi toko online yang sudah di-container-kan dengan Docker. Di laptop, satu perintah docker run sudah cukup. Masalah muncul waktu aplikasi itu masuk production dan trafiknya naik. Satu server tidak kuat, jadi kamu sewa tiga server. Sekarang pertanyaannya:

    • Container mana jalan di server mana?
    • Kalau satu container crash jam 3 pagi, siapa yang menghidupkan ulang?
    • Kalau satu server mati total, siapa yang memindahkan container ke server lain?
    • Kalau ada update versi aplikasi, bagaimana caranya ganti 20 container tanpa downtime?
    • Trafik naik saat flash sale, siapa yang menambah container lalu menguranginya lagi setelah sepi?

    Semua itu bisa dikerjakan manual dengan SSH ke tiap server dan menjalankan perintah Docker satu per satu. Untuk dua tiga container masih masuk akal. Untuk puluhan container di beberapa server, itu resep begadang. Kubernetes lahir untuk mengambil alih pekerjaan ini. Istilah kerennya container orchestration, orkestrasi container.

    Analogi paling gampang: Kubernetes itu seperti mandor proyek bangunan. Kamu sebagai pemilik proyek cukup bilang “saya mau 5 tukang ngecat tembok”. Mandor yang mengatur tukang mana ditempatkan di sisi mana, mencarikan pengganti kalau ada tukang yang sakit, dan menambah tukang kalau target dipercepat. Kamu tidak perlu mengawasi tukang satu per satu. Di Kubernetes, kamu menulis keinginan itu dalam file konfigurasi, misalnya “jalankan 5 replika aplikasi saya”, lalu Kubernetes yang menjaga kondisi itu terus terpenuhi. Konsep ini disebut desired state, dan ini yang membedakan Kubernetes dari sekadar menjalankan container manual.

    Tim kami di Arrazy memakai pola yang sama saat mengelola backend Go dan Laravel milik klien. Saat aplikasi masih kecil, Docker Compose di satu VPS cukup. Begitu kebutuhan uptime dan skala naik, orkestrasi seperti Kubernetes atau k3s jadi jauh lebih masuk akal daripada mengelola container manual lintas server.

    Alat yang Dibutuhkan: kubectl dan Minikube

    Untuk belajar, kamu tidak perlu sewa tiga server. Ada dua alat yang kita pakai sepanjang seri ini:

    • kubectl: command line untuk berkomunikasi dengan cluster Kubernetes. Semua perintah ke cluster lewat sini.
    • Minikube: alat untuk menjalankan cluster Kubernetes mini di laptop, cukup satu mesin. Cocok untuk belajar dan development.

    Spesifikasi minimum untuk Minikube: 2 CPU, 2 GB RAM bebas, 20 GB disk, dan Docker sudah terinstall (versi 20.10 ke atas, cek dengan docker version). Kita pakai Docker sebagai driver Minikube karena paling ringan dan tidak butuh VirtualBox.

    Install kubectl dan Minikube di Linux

    Contoh untuk Ubuntu 22.04 atau 24.04, arsitektur x86_64. Install kubectl versi stabil terbaru:

    curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
    sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
    kubectl version --client

    Output yang diharapkan kurang lebih seperti ini (versi minor bisa berbeda):

    Client Version: v1.31.0
    Kustomize Version: v5.4.2

    Lanjut install Minikube:

    curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
    sudo install minikube-linux-amd64 /usr/local/bin/minikube
    minikube version

    Output: minikube version: v1.34.0 atau lebih baru.

    Install kubectl dan Minikube di macOS

    Paling cepat lewat Homebrew:

    brew install kubectl minikube
    kubectl version --client
    minikube version

    Berlaku untuk Mac Intel maupun Apple Silicon. Pastikan Docker Desktop sudah jalan sebelum lanjut ke langkah berikutnya.

    Install kubectl dan Minikube di Windows

    Pakai winget dari PowerShell (jalankan sebagai Administrator):

    winget install -e --id Kubernetes.kubectl
    winget install -e --id Kubernetes.minikube

    Tutup dan buka lagi PowerShell supaya PATH terbaca, lalu verifikasi dengan kubectl version --client dan minikube version. Untuk Windows, saya sarankan Docker Desktop dengan backend WSL 2. Kalau kamu sudah terbiasa kerja di WSL 2 Ubuntu, boleh juga ikuti langkah Linux di atas langsung dari dalam WSL.

    Menjalankan Cluster Kubernetes Pertama

    Sekarang bagian serunya. Jalankan cluster dengan Kubernetes versi 1.31 supaya seragam sepanjang seri:

    minikube start --driver=docker --kubernetes-version=v1.31.0

    Proses pertama butuh beberapa menit karena Minikube mengunduh image Kubernetes sekitar 1 GB. Kalau sukses, baris terakhirnya seperti ini:

    Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

    Cek kondisi cluster dengan tiga perintah ini:

    minikube status

    Output yang diharapkan:

    minikube
    type: Control Plane
    host: Running
    kubelet: Running
    apiserver: Running
    kubeconfig: Configured

    Lalu cek node, yaitu mesin yang jadi anggota cluster. Di Minikube cuma ada satu:

    kubectl get nodes
    NAME       STATUS   ROLES           AGE   VERSION
    minikube   Ready    control-plane   1m    v1.31.0

    Terakhir, cek alamat komponen inti cluster:

    kubectl cluster-info
    Kubernetes control plane is running at https://192.168.49.2:8443
    CoreDNS is running at https://192.168.49.2:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

    Kalau ketiga perintah itu keluar tanpa error, selamat, kamu resmi punya cluster Kubernetes di laptop. Istilah seperti control plane dan node akan kita bedah di bagian 3 tentang arsitektur. Untuk sekarang cukup pahami: node adalah mesin pekerja, control plane adalah otaknya.

    Deploy Percobaan: Membuktikan Cluster Hidup

    Kita belum akan belajar deployment secara serius, itu jatah bagian 7. Tapi tidak afdol rasanya punya cluster tanpa menjalankan apa pun. Deploy nginx dengan satu perintah:

    kubectl create deployment nginx --image=nginx:1.27

    Output: deployment.apps/nginx created. Lalu lihat hasilnya:

    kubectl get pods
    NAME                     READY   STATUS    RESTARTS   AGE
    nginx-676b6c5bbc-8k2vw   1/1     Running   0          30s

    Kalau STATUS masih ContainerCreating, tunggu beberapa detik lalu jalankan lagi. Begitu Running, artinya Kubernetes berhasil menarik image nginx dan menjalankannya sebagai pod. Apa itu pod? Sabar, itu materi bagian 5. Sekarang hapus dulu percobaan ini supaya cluster bersih:

    kubectl delete deployment nginx

    Kalau mau istirahat, matikan cluster dengan minikube stop. Data dan konfigurasi tetap tersimpan, tinggal minikube start lagi kapan pun.

    Error Umum Saat Install Minikube dan Solusinya

    Virtualisasi belum aktif di BIOS

    Gejala: Minikube atau Docker Desktop gagal start dengan pesan seperti This computer doesn't have VT-X/AMD-v enabled. Penyebabnya fitur virtualisasi (Intel VT-x atau AMD-V) belum dinyalakan di BIOS/UEFI. Solusi: restart komputer, masuk BIOS (biasanya tombol F2, F10, atau Del), cari opsi bernama Intel Virtualization Technology, VT-x, AMD-V, atau SVM Mode, lalu set ke Enabled. Di Linux kamu bisa cek dulu dengan egrep -c '(vmx|svm)' /proc/cpuinfo. Kalau hasilnya lebih dari 0, virtualisasi didukung CPU kamu.

    Bingung driver docker vs virtualbox

    Minikube bisa jalan di atas beberapa driver. Dua yang paling sering dipakai pemula: docker dan virtualbox. Rekomendasi saya tegas: pakai --driver=docker. Lebih ringan, start lebih cepat, dan tidak perlu install aplikasi tambahan kalau Docker sudah ada. VirtualBox baru relevan kalau kamu memang tidak bisa memasang Docker. Kalau Minikube kamu telanjur terbuat dengan driver lain dan mau ganti, hapus dulu clusternya dengan minikube delete, lalu start ulang dengan flag driver yang benar. Ganti driver tidak bisa dilakukan pada cluster yang sudah jadi.

    minikube start gagal karena RAM kurang

    Gejala: muncul pesan seperti Requested memory allocation is less than the recommended minimum atau proses start macet lalu gagal. Minikube default meminta 2 GB ke atas, dan kalau laptop kamu sisa RAM-nya tipis karena browser membuka 30 tab, start bisa gagal. Solusi: tutup aplikasi berat, lalu jalankan dengan alokasi eksplisit, misalnya minikube start --driver=docker --memory=2048 --cpus=2 --kubernetes-version=v1.31.0. Kalau total RAM laptop 4 GB, pengalaman belajarnya akan berat. Pertimbangkan pakai VPS murah untuk lab, nanti di bagian 24 kita bahas install k3s di VPS yang jauh lebih hemat resource.

    Docker daemon tidak jalan atau permission denied

    Gejala: Cannot connect to the Docker daemon atau permission denied while trying to connect to the Docker daemon socket. Yang pertama artinya service Docker belum hidup, jalankan sudo systemctl start docker di Linux atau buka Docker Desktop di Windows/macOS. Yang kedua artinya user kamu belum masuk grup docker, perbaiki dengan sudo usermod -aG docker $USER lalu logout dan login lagi. Satu catatan penting: jangan menjalankan minikube start sebagai root, driver docker memang menolak dijalankan dengan sudo.

    Peta Lengkap Seri Belajar Kubernetes dari Nol

    Supaya kamu tahu arah perjalanan, ini peta 26 bagian yang akan kita lalui. Semuanya praktik di Minikube dulu, lalu pindah ke VPS sungguhan menjelang akhir.

    Bagian Topik Yang kamu kuasai
    1 sampai 4 Pengenalan, beda Docker vs Kubernetes, arsitektur cluster, perintah dasar kubectl Fondasi konsep dan alat kerja
    5 sampai 8 Pod, YAML manifest, Deployment dan ReplicaSet, rolling update dan rollback Menjalankan dan meng-update aplikasi tanpa downtime
    9 sampai 10 Service (ClusterIP, NodePort) dan Ingress Membuka akses aplikasi dari luar cluster
    11 sampai 14 ConfigMap, Secret, liveness dan readiness probe, resource request dan limit Konfigurasi dan kesehatan aplikasi ala production
    15 sampai 17 Volume, Persistent Volume dan PVC, StatefulSet Menyimpan data dan menjalankan database
    18 sampai 22 Namespace, RBAC, HPA autoscaling, Helm, logging dan monitoring Mengelola cluster secara rapi dan aman
    23 sampai 26 Deploy web plus database lengkap, install k3s di VPS, HTTPS dengan cert-manager, proyek akhir Aplikasi nyata jalan di VPS dengan domain dan HTTPS

    Di akhir seri, kamu akan punya aplikasi web lengkap dengan database yang berjalan di cluster milikmu sendiri di VPS, bisa diakses lewat domain dengan HTTPS. Itu portofolio yang layak ditunjukkan saat melamar kerja sebagai backend atau DevOps engineer.

    Rangkuman dan Lanjut ke Bagian 2

    Hari ini kamu sudah paham bahwa Kubernetes adalah orkestrator container, si mandor yang menjaga aplikasi tetap jalan sesuai keinginanmu di banyak mesin. Kamu juga sudah install kubectl dan Minikube, menjalankan cluster Kubernetes 1.31 pertama, membuktikannya hidup lewat kubectl get nodes, dan sempat deploy nginx sebagai pemanasan.

    Pertanyaan yang paling sering muncul setelah ini: kalau sudah bisa Docker, kenapa repot belajar Kubernetes, dan kapan Docker Compose saja sudah cukup? Itu tema bagian berikutnya, “Belajar Kubernetes #2: Perbedaan Docker dan Kubernetes”, yang terbit menyusul di halaman hub Belajar Kubernetes dari Nol. Sambil menunggu, biarkan Minikube terpasang dan coba ulangi perintah hari ini sampai lancar di luar kepala.

    Referensi

  • Belajar Golang dari Nol #7: Pointer, Alamat Memori Tanpa Pusing

    Belajar Golang dari Nol #7: Pointer, Alamat Memori Tanpa Pusing

    Di Belajar Golang dari Nol #6 kita sudah belajar mengendalikan alur program dengan for dan percabangan. Sekarang kita masuk ke materi yang paling sering bikin pemula gentar sebelum mencoba: pointer. Padahal konsepnya sederhana. Anda hanya perlu satu analogi yang pas, dan sisanya akan terasa masuk akal.

    Masih ingat di bagian 4 kita menyinggung pointer receiver saat membahas method, lalu saya janji akan membahasnya tuntas? Ini bagian yang menepati janji itu.

    Analogi: Fotokopi Dokumen vs Alamat Rumah

    Bayangkan Anda punya dokumen penting di rumah, misalnya sertifikat tanah. Teman Anda butuh dokumen itu. Ada dua cara Anda memberikannya.

    Cara pertama, Anda fotokopi dokumennya lalu kirim fotokopiannya. Teman Anda boleh mencoret-coret fotokopian itu sesuka hati. Dokumen asli di rumah Anda tetap aman, tidak berubah sedikit pun.

    Cara kedua, Anda kasih alamat rumah Anda. Teman Anda datang ke alamat itu, membuka lemari, dan mengubah dokumen aslinya langsung. Apa pun yang dia tulis di sana, itu terjadi pada dokumen asli.

    Di Go, cara pertama adalah perilaku default: setiap nilai yang dioper ke function akan difotokopi. Cara kedua adalah pointer: Anda tidak mengoper nilainya, tapi mengoper alamat tempat nilai itu disimpan di memori. Pegang analogi ini terus. Semua materi di bawah hanyalah variasi dari dua cara itu.

    Go Selalu Mengoper Salinan

    Mari buktikan dulu perilaku defaultnya. Coba jalankan program ini.

    package main
    
    import "fmt"
    
    func tambahBonus(gaji int) {
    	gaji = gaji + 500000
    	fmt.Println("Di dalam function:", gaji)
    }
    
    func main() {
    	gaji := 5000000
    	tambahBonus(gaji)
    	fmt.Println("Di main:", gaji)
    }

    Outputnya:

    Di dalam function: 5500000
    Di main: 5000000

    Perhatikan baik-baik. Di dalam tambahBonus, nilai gaji memang bertambah jadi 5500000. Tapi begitu kembali ke main, nilainya tetap 5000000. Kenapa? Karena yang diterima function adalah fotokopian. Function mencoret-coret fotokopian itu, lalu fotokopiannya dibuang saat function selesai. Dokumen asli di main tidak pernah tersentuh.

    Ini bukan bug. Ini desain Go yang disengaja, namanya pass by value. Tapi kadang kita memang ingin function lain mengubah data asli. Di situlah pointer masuk.

    Dua Operator Kunci: & dan *

    Untuk bermain dengan alamat memori, Go memberi kita dua operator.

    • & artinya “ambil alamat dari variabel ini”. Seperti bertanya, rumah si Budi di mana ya.
    • * artinya “buka isi dari alamat ini”. Seperti datang ke alamat itu dan melihat apa yang ada di dalamnya.

    Contoh kecilnya begini.

    package main
    
    import "fmt"
    
    func main() {
    	umur := 25
    	alamat := &umur
    
    	fmt.Println("Nilai umur:", umur)
    	fmt.Println("Alamat umur:", alamat)
    	fmt.Println("Isi dari alamat:", *alamat)
    }

    Outputnya kira-kira seperti ini:

    Nilai umur: 25
    Alamat umur: 0xc000012028
    Isi dari alamat: 25

    Angka 0xc000012028 itu alamat memori. Di komputer Anda angkanya hampir pasti beda, dan tiap kali program dijalankan ulang angkanya juga bisa berubah. Itu normal. Sistem operasi bebas menaruh data di slot memori mana pun yang kosong, sama seperti mobil yang tidak selalu parkir di slot yang sama tiap ke mal.

    Variabel alamat di atas bertipe *int, dibaca “pointer ke int”. Artinya dia tidak menyimpan angka 25, dia menyimpan alamat tempat angka 25 berada.

    Function yang Benar-Benar Mengubah Nilai

    Sekarang gabungkan keduanya. Kalau parameter function bertipe pointer, function itu menerima alamat, bukan fotokopian. Dia bisa datang ke alamat itu dan mengubah dokumen aslinya.

    package main
    
    import "fmt"
    
    func ubahNilai(x *int) {
    	*x = 100
    }
    
    func ubahNilaiBiasa(x int) {
    	x = 100
    }
    
    func main() {
    	angka := 5
    
    	ubahNilaiBiasa(angka)
    	fmt.Println("Setelah versi biasa:", angka)
    
    	ubahNilai(&angka)
    	fmt.Println("Setelah versi pointer:", angka)
    }

    Outputnya:

    Setelah versi biasa: 5
    Setelah versi pointer: 100

    Baca ubahNilai pelan-pelan. Parameter x *int artinya x adalah alamat. Baris *x = 100 artinya datang ke alamat itu, lalu tulis 100 di sana. Karena yang diubah adalah isi alamat asli, perubahan itu terlihat dari main. Dan perhatikan cara memanggilnya: ubahNilai(&angka). Kita tidak mengirim angkanya, kita mengirim alamatnya.

    Pointer dan Struct: Rahasia di Balik Pointer Receiver

    Sekarang kita bayar utang dari bagian 4. Waktu itu Anda sudah lihat method dengan receiver (r *Rekening) dan saya bilang pakai saja dulu, nanti dijelaskan. Ini penjelasannya.

    package main
    
    import "fmt"
    
    type Pengguna struct {
    	Nama string
    }
    
    func (p Pengguna) GantiNamaValue(baru string) {
    	p.Nama = baru
    }
    
    func (p *Pengguna) GantiNamaPointer(baru string) {
    	p.Nama = baru
    }
    
    func main() {
    	u := Pengguna{Nama: "Budi"}
    
    	u.GantiNamaValue("Andi")
    	fmt.Println("Setelah value receiver:", u.Nama)
    
    	u.GantiNamaPointer("Andi")
    	fmt.Println("Setelah pointer receiver:", u.Nama)
    }

    Outputnya:

    Setelah value receiver: Budi
    Setelah pointer receiver: Andi

    Sekarang mekanismenya jelas. Value receiver (p Pengguna) menerima fotokopian struct. Method mengganti nama di fotokopian, fotokopiannya dibuang, struct asli tetap Budi. Persis seperti kasus tambahBonus tadi. Pointer receiver (p *Pengguna) menerima alamat struct asli, jadi perubahannya nyata.

    Ada satu kemudahan dari Go yang perlu Anda tahu. Secara teori, untuk mengakses field lewat pointer Anda harus menulis (*p).Nama: buka dulu isi alamatnya, baru ambil fieldnya. Tapi Go tahu itu merepotkan, jadi p.Nama saja sudah cukup. Go otomatis mengerti maksud Anda. Itulah kenapa di dalam GantiNamaPointer kita bisa menulis p.Nama = baru tanpa tanda bintang. Hal yang sama berlaku saat memanggil: u.GantiNamaPointer("Andi") otomatis diterjemahkan Go menjadi (&u).GantiNamaPointer("Andi").

    Hati-Hati dengan Nil Pointer

    Masih ingat konsep zero value dari bagian 2? Setiap tipe punya nilai awal. Zero value untuk pointer adalah nil, artinya pointer itu belum menunjuk ke alamat mana pun. Seperti secarik kertas alamat yang masih kosong.

    Masalah muncul kalau Anda mencoba membuka isi dari alamat kosong.

    package main
    
    import "fmt"
    
    func main() {
    	var p *int
    	fmt.Println(*p)
    }

    Program ini langsung crash dengan pesan:

    panic: runtime error: invalid memory address or nil pointer dereference

    Wajar saja. Anda menyuruh Go datang ke sebuah alamat, tapi kertas alamatnya kosong. Mau ke mana? Ini salah satu error paling umum di dunia Go, jadi biasakan mengecek dulu sebelum membuka isi pointer.

    package main
    
    import "fmt"
    
    func main() {
    	var p *int
    
    	if p != nil {
    		fmt.Println("Isi p:", *p)
    	} else {
    		fmt.Println("p masih nil, belum menunjuk ke mana-mana")
    	}
    }

    Outputnya:

    p masih nil, belum menunjuk ke mana-mana

    Pola if p != nil ini akan sangat sering Anda temui di kode Go sungguhan, terutama saat menerima pointer dari function lain.

    Kapan Pakai Pointer, Kapan Tidak

    Supaya tidak bingung di lapangan, pegang aturan praktis ini.

    Pakai pointer kalau:

    • Anda ingin function atau method mengubah data aslinya, seperti semua contoh di atas.
    • Struct Anda besar dan sering dioper ke banyak function. Mengoper alamat jauh lebih ringan daripada memfotokopi seluruh isi struct berulang kali.
    • Menjaga konsistensi method. Kalau satu method di sebuah struct sudah pakai pointer receiver, sebaiknya semua method di struct itu pakai pointer receiver juga, biar tidak membingungkan pembaca kode.

    Tidak perlu pointer kalau:

    • Tipenya kecil seperti int, bool, atau string, dan Anda tidak berniat mengubahnya. Fotokopi tipe kecil itu murah.
    • Datanya slice atau map. Ingat pelajaran bagian 5: slice dan map secara internal sudah menunjuk ke data yang sama, jadi perubahan isi di dalam function memang terlihat dari luar tanpa pointer.

    Satu hal lagi yang layak disebut singkat: function bawaan new(). Menulis p := new(int) artinya Go membuatkan sebuah int dengan zero value, lalu langsung memberi Anda pointernya. Praktiknya, gaya p := &variabel jauh lebih sering dipakai, jadi cukup kenali saja bahwa new() ada.

    Latihan: Sistem Saldo Rekening

    Sekarang rangkai semuanya jadi satu program utuh. Kita buat sistem saldo sederhana: struct Rekening, method Setor dan Tarik dengan pointer receiver, plus function TransferDana yang menerima dua pointer sekaligus. Method Tarik mengembalikan error kalau saldo kurang, persis pola error yang Anda pelajari di bagian 3.

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    type Rekening struct {
    	Pemilik string
    	Saldo   int
    }
    
    func (r *Rekening) Setor(jumlah int) {
    	r.Saldo = r.Saldo + jumlah
    }
    
    func (r *Rekening) Tarik(jumlah int) error {
    	if jumlah > r.Saldo {
    		return errors.New("saldo tidak cukup")
    	}
    	r.Saldo = r.Saldo - jumlah
    	return nil
    }
    
    func TransferDana(dari *Rekening, ke *Rekening, jumlah int) error {
    	err := dari.Tarik(jumlah)
    	if err != nil {
    		return err
    	}
    	ke.Setor(jumlah)
    	return nil
    }
    
    func main() {
    	rekeningBudi := Rekening{Pemilik: "Budi", Saldo: 500000}
    	rekeningSari := Rekening{Pemilik: "Sari", Saldo: 200000}
    
    	rekeningBudi.Setor(100000)
    	fmt.Println("Saldo Budi setelah setor:", rekeningBudi.Saldo)
    
    	err := TransferDana(&rekeningBudi, &rekeningSari, 250000)
    	if err != nil {
    		fmt.Println("Transfer gagal:", err)
    	} else {
    		fmt.Println("Transfer 250000 dari Budi ke Sari berhasil")
    	}
    	fmt.Println("Saldo Budi:", rekeningBudi.Saldo)
    	fmt.Println("Saldo Sari:", rekeningSari.Saldo)
    
    	err = TransferDana(&rekeningBudi, &rekeningSari, 1000000)
    	if err != nil {
    		fmt.Println("Transfer gagal:", err)
    	}
    	fmt.Println("Saldo Budi:", rekeningBudi.Saldo)
    	fmt.Println("Saldo Sari:", rekeningSari.Saldo)
    }

    Outputnya:

    Saldo Budi setelah setor: 600000
    Transfer 250000 dari Budi ke Sari berhasil
    Saldo Budi: 350000
    Saldo Sari: 450000
    Transfer gagal: saldo tidak cukup
    Saldo Budi: 350000
    Saldo Sari: 450000

    Telusuri alurnya. Setor menambah saldo Budi jadi 600000, dan perubahannya nyata karena receivernya pointer. TransferDana menerima alamat dua rekening sekaligus, jadi dia bisa mengurangi saldo Budi dan menambah saldo Sari dalam satu function. Transfer kedua gagal karena Budi hanya punya 350000, dan lihat saldonya: tidak ada satu rupiah pun yang berubah saat transfer gagal. Method Tarik menolak duluan sebelum sempat menyentuh apa pun.

    Coba modifikasi sendiri sebagai latihan. Misalnya, tambahkan validasi agar jumlah setor tidak boleh negatif, atau buat method Info yang mencetak pemilik dan saldo dengan rapi. Kalau kode Anda berperilaku sesuai dugaan, berarti pointer sudah masuk ke kepala.

    Rangkuman dan Lanjut ke Bagian 8

    Hari ini Anda belajar bahwa Go mengoper nilai secara fotokopi, pointer adalah cara mengoper alamat aslinya, & mengambil alamat, * membuka isinya, pointer receiver bekerja karena menerima alamat struct asli, dan nil pointer wajib dicek sebelum dibuka. Materi yang katanya menakutkan ternyata cuma soal fotokopi dan alamat rumah.

    Sejauh ini semua kode kita masih menumpuk di satu file main.go. Untuk program kecil tidak masalah, tapi program sungguhan butuh struktur. Di bagian 8 kita akan membahas Package dan Go Module: Merapikan Kode ke Banyak File. Sampai ketemu di sana.

    Kalau bisnis Anda butuh aplikasi yang dibangun dengan fondasi backend yang rapi seperti ini, tim Arrazy siap bantu lewat layanan pengembangan sistem aplikasi.

  • Belajar Struktur Data dari Nol: Pengenalan dan Persiapan Go

    Belajar Struktur Data dari Nol: Pengenalan dan Persiapan Go

    Struktur data adalah cara menyimpan dan mengatur data di memori supaya cepat diakses dan diubah. Algoritma adalah urutan langkah untuk memproses data itu. Dua hal ini yang membedakan aplikasi yang tetap responsif saat datanya jutaan baris dengan aplikasi yang mulai lemot padahal datanya baru ribuan. Artikel ini bagian pertama dari seri Belajar Struktur Data dari Nol. Di bagian ini kita bereskan fondasinya dulu: paham apa itu struktur data, kenapa serinya pakai Go, lalu siapkan Go 1.24 di komputer sampai program pertama benar-benar jalan.

    Prasyaratnya ringan. Kamu cukup paham sintaks dasar Go: variabel, fungsi, if, dan for. Kalau belum, selesaikan dulu bagian-bagian awal seri Belajar Golang dari Nol, karena dasar bahasanya sengaja tidak diulang di sini. Yang penting kamu bisa membaca kode Go sederhana tanpa bingung.

    Apa Itu Struktur Data dan Algoritma

    Bayangkan dua gudang dengan isi barang yang sama persis. Gudang pertama pakai rak berlabel: lorong A untuk sparepart, lorong B untuk kemasan, tiap rak ada nomornya. Gudang kedua tidak pakai rak sama sekali, semua barang ditumpuk begitu saja di lantai. Saat ada pesanan masuk, petugas gudang pertama langsung jalan ke lorong yang benar dan ambil barangnya dalam hitungan detik. Petugas gudang kedua harus membongkar tumpukan satu per satu, dan makin banyak barangnya makin lama carinya.

    Struktur data adalah sistem raknya, yaitu cara data disusun. Algoritma adalah cara kerja petugasnya, yaitu langkah-langkah mencari, menambah, atau mengeluarkan barang. Keduanya tidak bisa dipisah. Rak serapi apa pun percuma kalau petugasnya tetap memeriksa semua lorong satu per satu. Sebaliknya, petugas secerdas apa pun tidak bisa berbuat banyak kalau barangnya cuma ditumpuk. Itulah kenapa di kampus keduanya diajarkan sebagai satu mata kuliah: pilihan struktur data menentukan algoritma apa saja yang bisa jalan efisien di atasnya.

    Contoh nyatanya ada di HP kamu. Daftar kontak tersimpan terurut berdasarkan nama, jadi pencarian nama bisa langsung melompat ke huruf yang benar. Kalau kontak disimpan acak sesuai urutan input, satu-satunya cara mencari adalah memeriksa semua kontak dari awal. Data sama, struktur beda, kecepatan beda jauh.

    Kenapa Seri Ini Pakai Go 1.24

    Semua contoh kode di 26 bagian seri ini ditulis dengan Go 1.24. Alasannya praktis, bukan sekadar selera:

    • Sintaksnya kecil. Go tidak punya banyak fitur ajaib yang menyembunyikan apa yang terjadi di memori. Saat belajar linked list atau hash table, kamu ingin melihat pointer dan alokasi secara eksplisit, dan Go menampilkannya apa adanya.
    • Tooling bawaan lengkap. Ada go run untuk eksekusi cepat, gofmt untuk format otomatis, dan package testing bawaan. Tidak perlu instal build tool tambahan.
    • Generics sudah matang. Sejak Go 1.18 kita bisa menulis struktur data generik seperti Stack[T], dan di Go 1.24 dukungannya sudah stabil termasuk untuk type alias generik. Ini penting karena kita akan membuat struktur data yang bisa dipakai ulang.
    • Dipakai di industri. Go umum dipakai untuk backend dan tooling infrastruktur. Tim Arrazy sendiri memakai Go di backend sistem aplikasi yang kami bangun untuk klien, jadi materi seri ini diambil dari pola yang memang kami pakai sehari-hari.

    Kamu tidak wajib memakai persis versi 1.24. Versi 1.22 ke atas masih bisa mengikuti hampir semua materi. Tapi supaya output di artikel sama dengan di layar kamu, sebaiknya samakan versinya.

    Install Go 1.24 dan Verifikasi dengan go version

    Cara paling aman adalah unduh langsung dari situs resmi Go, bukan dari package manager sistem. Di Linux (Ubuntu, Debian, dan turunannya) jalankan perintah berikut di terminal:

    wget https://go.dev/dl/go1.24.5.linux-amd64.tar.gz
    sudo rm -rf /usr/local/go
    sudo tar -C /usr/local -xzf go1.24.5.linux-amd64.tar.gz

    Lalu tambahkan Go ke PATH. Buka ~/.bashrc (atau ~/.zshrc kalau pakai zsh), tambahkan baris ini di paling bawah, lalu buka terminal baru:

    export PATH=$PATH:/usr/local/go/bin

    Untuk Windows, unduh installer .msi dari halaman go.dev/dl, klik dua kali, dan ikuti wizard-nya. PATH diatur otomatis. Untuk macOS, unduh installer .pkg dari halaman yang sama.

    Apa pun sistem operasinya, verifikasi dengan perintah ini:

    go version

    Output yang diharapkan (arsitektur bisa berbeda sesuai mesin kamu):

    go version go1.24.5 linux/amd64

    Kalau yang muncul justru error atau versi lama, jangan lanjut dulu. Lihat bagian error umum di bawah, dua masalah paling sering terjadi persis di langkah ini.

    Proyek Pertama: go mod init dan go run

    Semua kode seri ini akan hidup dalam satu proyek bernama struktur-data. Buat foldernya dan inisialisasi Go module:

    mkdir struktur-data
    cd struktur-data
    go mod init struktur-data

    Output yang diharapkan:

    go: creating new go.mod: module struktur-data

    Perintah itu membuat file go.mod, penanda bahwa folder ini adalah sebuah Go module. Tanpa file ini, banyak perintah Go modern akan menolak jalan. Sekarang buat file main.go berisi kode berikut:

    package main
    
    import "fmt"
    
    func main() {
    	angka := []int{7, 3, 9}
    	fmt.Println("Isi slice:", angka)
    	fmt.Println("Jumlah elemen:", len(angka))
    }

    Jalankan:

    go run main.go

    Output yang diharapkan:

    Isi slice: [7 3 9]
    Jumlah elemen: 3

    Kalau output kamu sama, environment sudah siap. Slice yang barusan kamu cetak itu sebenarnya struktur data pertama di seri ini, dan cara kerjanya di dalam memori akan kita bedah di bagian 4 dan 5.

    Setup VS Code dan Struktur Folder untuk Mengikuti Seri

    Editor yang dipakai di seri ini adalah VS Code karena dukungan Go-nya paling mulus. Langkah setupnya:

    1. Buka VS Code, masuk ke tab Extensions, cari Go yang dipublikasikan oleh Go Team at Google (id: golang.go), lalu install.
    2. Buka folder struktur-data lewat File, Open Folder.
    3. Tekan Ctrl+Shift+P, ketik Go: Install/Update Tools, centang semua tool (termasuk gopls dan dlv), lalu OK. Tool ini yang membuat autocomplete dan deteksi error jalan.

    Supaya kode otomatis dirapikan gofmt setiap kali disimpan, buka Settings JSON (Ctrl+Shift+P, lalu Preferences: Open User Settings (JSON)) dan tambahkan:

    {
      "[go]": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "golang.go"
      }
    }

    Terakhir, sepakati struktur folder. Setiap bagian seri dapat satu folder sendiri di dalam module yang sama, jadi kode lama tidak tertimpa kode baru:

    struktur-data/
    ├── go.mod
    ├── bab-01-pengenalan/
    │   └── main.go
    ├── bab-02-kompleksitas/
    │   └── main.go
    └── bab-03-big-o/
        └── main.go

    Pindahkan main.go yang tadi ke bab-01-pengenalan/. Untuk menjalankan kode bab tertentu dari root proyek, cukup sebut foldernya:

    go run ./bab-01-pengenalan

    Error Umum Saat Setup Go dan Cara Mengatasinya

    go: command not found

    Muncul saat mengetik go version padahal Go sudah diekstrak ke /usr/local/go. Penyebabnya hampir selalu PATH: shell tidak tahu harus mencari binary go di mana. Pastikan baris export PATH=$PATH:/usr/local/go/bin benar-benar tersimpan di ~/.bashrc atau ~/.zshrc, lalu buka terminal baru atau jalankan source ~/.bashrc. Cek juga file mana yang dibaca shell kamu dengan echo $SHELL, karena user zsh sering salah menaruh export di .bashrc.

    Versi Go lama padahal baru install

    go version menampilkan misalnya go1.18 padahal kamu baru mengekstrak 1.24. Biasanya karena dulu pernah install Go lewat apt, dan binary lama di /usr/bin/go ditemukan lebih dulu daripada yang baru. Cek dengan which go. Kalau hasilnya /usr/bin/go, hapus versi lama dengan sudo apt remove golang-go, lalu buka terminal baru. Repository apt memang sering tertinggal beberapa versi di belakang rilis resmi, jadi untuk Go selalu unduh dari go.dev/dl.

    go.mod file not found in current directory

    Error lengkapnya biasanya go: cannot find main module atau go.mod file not found. Ini muncul saat menjalankan perintah Go di folder yang belum diinisialisasi sebagai module. Zaman dulu Go mewajibkan semua kode ada di dalam satu folder global bernama GOPATH. Sistem itu sudah ditinggalkan dan diganti Go modules, tapi banyak tutorial lama di internet masih mengajarkan GOPATH sehingga pemula tercampur. Aturannya sekarang sederhana: satu proyek satu folder, jalankan go mod init nama-proyek sekali di root folder itu, selesai. Kamu tidak perlu menyentuh variabel GOPATH sama sekali.

    Autocomplete VS Code tidak jalan

    Kode bisa di-run tapi tidak ada saran kode dan error tidak digarisbawahi. Biasanya gopls belum terinstal atau VS Code dibuka bukan di root proyek. Jalankan lagi Go: Install/Update Tools, pastikan folder yang dibuka adalah folder yang berisi go.mod, lalu restart VS Code.

    Peta Lengkap 26 Bagian Seri Belajar Struktur Data

    Supaya kamu tahu arah perjalanannya, ini peta seri dari awal sampai akhir:

    Bagian Topik Yang kamu kuasai
    1-3 Fondasi: setup, kompleksitas, Big O Mengukur cepat lambatnya algoritma secara objektif
    4-5 Array dan slice Paham perilaku slice Go sampai level memori dan kapasitas
    6-9 Linked list, stack, queue Membangun struktur linear sendiri dengan pointer
    10-11 Hash table dan map Tahu cara kerja map Go dari dalam, termasuk hashing dan collision
    12-15 Rekursi, binary tree, BST, heap Struktur hierarkis dan priority queue
    16-18 Graph, BFS, DFS Memodelkan relasi dan menelusurinya, termasuk deteksi siklus
    19-22 Sorting dan binary search Bubble, insertion, merge, quick sort, plus pencarian cepat di data terurut
    23-24 Greedy dan dynamic programming Strategi memecahkan soal optimasi
    25-26 Latihan interview dan proyek LRU cache Menggabungkan semua materi jadi solusi soal nyata

    Di akhir seri kamu bukan cuma hafal nama-nama struktur data. Kamu sudah pernah mengimplementasikan semuanya sendiri di Go, tahu kapan memakai yang mana, dan siap menghadapi soal coding interview yang menanyakan hal-hal ini.

    Langkah Berikutnya

    Environment sudah siap, program pertama sudah jalan, dan kamu sudah pegang peta serinya. Pertanyaan berikutnya yang wajar muncul: kalau dua program sama-sama benar, bagaimana cara membuktikan yang satu lebih cepat dari yang lain tanpa menebak-nebak. Itu materi bagian kedua, “Kompleksitas Algoritma: Kenapa Struktur Data Menentukan Performa”, yang terbit menyusul dan bisa kamu pantau di halaman hub seri Belajar Struktur Data dari Nol. Sampai artikelnya terbit, pastikan setup kamu beres: go version menampilkan 1.24 dan go run ./bab-01-pengenalan mencetak output tanpa error.

    Referensi

  • Belajar Golang dari Nol #6: Perulangan for dan Percabangan

    Belajar Golang dari Nol #6: Perulangan for dan Percabangan

    Di Belajar Golang dari Nol #5 kita sudah kenalan dengan slice dan map. Waktu itu kita sempat memakai for range sekilas untuk membaca isinya, tapi belum dibedah dalam. Sekarang giliran materi itu dituntaskan. Di bagian keenam ini kita bahas perulangan dan percabangan di Go: for dengan semua bentuknya, for range sampai tuntas, break dan continue, if else, switch, sampai label untuk loop bersarang.

    Dua materi ini adalah otot utama program. Hampir semua logika, dari menghitung total belanja sampai menyaring data kotor, dibangun dari perulangan dan percabangan. Jadi kita jalan pelan seperti biasa, satu konsep, satu contoh, langsung praktik.

    for, Satu-satunya Perulangan di Go

    Kalau kamu pernah menyentuh bahasa lain, mungkin kamu terbiasa dengan while, do while, dan for sebagai tiga hal berbeda. Go memangkas semuanya jadi satu kata kunci saja: for. Tidak ada while di Go. Satu kata kunci, tiga cara pakai.

    Bentuk klasik: init, kondisi, post

    Ini bentuk yang paling sering kamu temui. Ada tiga bagian yang dipisah titik koma: inisialisasi, kondisi, dan apa yang dilakukan setiap akhir putaran.

    package main
    
    import "fmt"
    
    func main() {
    	for i := 1; i <= 5; i++ {
    		fmt.Println("Putaran ke", i)
    	}
    }

    Bacanya seperti resep: mulai dari i bernilai 1, ulangi selama i masih 5 atau kurang, dan setiap selesai satu putaran tambahkan i sebanyak satu. Program ini mencetak lima baris, dari putaran 1 sampai 5.

    Bentuk while: kondisi saja

    Buang bagian init dan post, sisakan kondisinya. Hasilnya persis while di bahasa lain.

    saldo := 100000
    for saldo > 0 {
    	fmt.Println("Sisa saldo:", saldo)
    	saldo -= 30000
    }

    Selama saldo masih di atas nol, loop terus jalan. Begitu saldo habis, loop berhenti sendiri. Anggap saja seperti main game dengan koin: selama koin masih ada, kamu boleh main terus.

    Bentuk tanpa kondisi: loop tak berujung

    Tulis for polos tanpa apa pun, maka loop akan berputar selamanya. Ini bukan bug, ini fitur. Server web misalnya, memang harus terus hidup menunggu request. Tapi untuk program biasa, kamu wajib menyediakan pintu keluar lewat break.

    hitung := 0
    for {
    	hitung++
    	fmt.Println("Masih jalan, hitung:", hitung)
    	if hitung == 3 {
    		break
    	}
    }

    Tanpa break di situ, program ini tidak akan pernah selesai dan terminal kamu penuh angka. Kalau itu terjadi, tekan Ctrl+C untuk menghentikannya.

    for range: Cara Nyaman Menyusuri Slice dan Map

    Di bagian 5 kita sudah pakai for range sekilas. Sekarang kita bongkar aturannya satu per satu, karena bentuk inilah yang paling sering kamu tulis sehari-hari.

    Range pada slice

    Saat dipakai pada slice, range memberi dua nilai setiap putaran: index dan isinya.

    buah := []string{"mangga", "jeruk", "salak"}
    
    for i, nama := range buah {
    	fmt.Println(i, nama)
    }

    Output:

    0 mangga
    1 jeruk
    2 salak

    Sering kali kamu hanya butuh isinya, tidak peduli index-nya. Di sinilah underscore berperan. Tanda _ artinya kamu bilang ke Go: nilai ini sengaja saya abaikan. Ini penting karena Go menolak compile kalau ada variabel yang dideklarasikan tapi tidak dipakai.

    for _, nama := range buah {
    	fmt.Println(nama)
    }

    Sebaliknya, kalau kamu cuma butuh index, tulis satu variabel saja: for i := range buah. Nilainya otomatis hanya index.

    Range pada map

    Pada map, dua nilai yang diberikan adalah key dan value.

    stok := map[string]int{
    	"mangga": 10,
    	"jeruk":  5,
    	"salak":  8,
    }
    
    for nama, jumlah := range stok {
    	fmt.Println(nama, "=", jumlah)
    }

    Nah, ini catatan yang sering bikin pemula bingung. Urutan hasil range pada map itu acak. Jalankan program yang sama dua kali, urutannya bisa beda. Ini disengaja oleh pembuat Go supaya programmer tidak menggantungkan logika pada urutan map. Jadi kalau output map kamu terlihat berantakan urutannya, program kamu tidak rusak. Memang begitu sifatnya. Kalau butuh urutan yang pasti, kumpulkan dulu key-nya ke slice, urutkan, lalu loop slice itu.

    break dan continue

    Dua kata ini adalah rem dan gas di dalam loop. break menghentikan loop sepenuhnya. continue melompati sisa putaran saat ini dan langsung lanjut ke putaran berikutnya.

    Contoh nyata untuk continue: menyaring data yang tidak valid. Misal kamu punya daftar nilai ujian, tapi ada beberapa angka aneh yang harus dilewati.

    nilai := []int{80, -5, 90, 200, 75}
    
    for _, n := range nilai {
    	if n < 0 || n > 100 {
    		continue
    	}
    	fmt.Println("Nilai valid:", n)
    }

    Angka -5 dan 200 dilompati, sisanya dicetak. Sedangkan break cocok saat kamu mencari sesuatu dan ingin berhenti begitu ketemu, supaya tidak buang tenaga memeriksa sisa data.

    daftar := []string{"Andi", "Budi", "Citra", "Dewi"}
    dicari := "Citra"
    
    for i, nama := range daftar {
    	if nama == dicari {
    		fmt.Println("Ketemu di index", i)
    		break
    	}
    }

    if, else if, dan else

    Percabangan di Go bentuknya sederhana. Kondisi tidak perlu tanda kurung, tapi kurung kurawal wajib ada.

    skor := 78
    
    if skor >= 85 {
    	fmt.Println("Grade A")
    } else if skor >= 70 {
    	fmt.Println("Grade B")
    } else {
    	fmt.Println("Belum lulus")
    }

    Go mengecek dari atas ke bawah dan berhenti di kondisi pertama yang cocok. Skor 78 masuk Grade B, karena syarat Grade A gagal lebih dulu.

    if dengan statement pendek

    Ini gaya khas Go yang akan sering kamu lihat di kode orang lain. Kamu boleh menaruh satu statement pendek sebelum kondisi, dipisah titik koma. Masih ingat pola comma ok saat mengecek isi map di bagian 5? Biasanya pola itu ditulis begini:

    stok := map[string]int{"mangga": 10}
    
    if jumlah, ok := stok["mangga"]; ok {
    	fmt.Println("Stok mangga:", jumlah)
    } else {
    	fmt.Println("Mangga tidak ada di data")
    }

    Variabel jumlah dan ok lahir di dalam if itu dan hanya hidup di situ. Setelah blok if selesai, keduanya lenyap. Ini bagus, karena variabel bantu tidak berserakan mengotori bagian lain fungsi.

    switch: if else Berantai yang Lebih Rapi

    Kalau cabangnya banyak, if else berantai mulai capek dibaca. switch hadir untuk itu.

    hari := "sabtu"
    
    switch hari {
    case "sabtu", "minggu":
    	fmt.Println("Akhir pekan")
    case "jumat":
    	fmt.Println("Sebentar lagi libur")
    default:
    	fmt.Println("Hari kerja")
    }

    Perhatikan satu case boleh menampung beberapa nilai sekaligus, dipisah koma. default jalan kalau tidak ada case yang cocok.

    Ada satu perbedaan besar dengan bahasa seperti C atau JavaScript: switch di Go tidak butuh break. Begitu satu case cocok dan selesai dijalankan, Go otomatis keluar dari switch. Kamu tidak akan kena bug klasik lupa break yang bikin eksekusi bocor ke case berikutnya. Kalau kamu memang sengaja ingin lanjut ke case di bawahnya, ada kata kunci fallthrough. Cukup tahu saja dulu, praktiknya jarang sekali dipakai.

    switch tanpa kondisi

    Bentuk ini favorit banyak programmer Go. Tulis switch tanpa apa pun, lalu tiap case berisi kondisi boolean. Hasilnya pengganti if else berantai yang jauh lebih enak dibaca.

    skor := 78
    
    switch {
    case skor >= 85:
    	fmt.Println("Grade A")
    case skor >= 70:
    	fmt.Println("Grade B")
    default:
    	fmt.Println("Belum lulus")
    }

    Sama seperti if else, pengecekan berjalan dari atas ke bawah dan berhenti di yang pertama cocok.

    Label: Keluar dari Loop Bersarang

    Satu materi kecil sebelum latihan. break biasa hanya menghentikan loop terdalam. Kalau kamu punya loop di dalam loop dan ingin keluar dari semuanya sekaligus, pakai label.

    luar:
    	for i := 0; i < 3; i++ {
    		for j := 0; j < 3; j++ {
    			if i*j >= 4 {
    				break luar
    			}
    			fmt.Println(i, j)
    		}
    	}

    break luar langsung melompat keluar dari kedua loop. Jujur saja, label jarang dipakai di kode sehari-hari. Cukup tahu bahwa fitur ini ada, supaya tidak bingung saat menemukannya di kode orang lain.

    Latihan: Rekap Penjualan Mingguan

    Sekarang kita gabungkan semua materi hari ini plus struct dari bagian 4 dan slice dari bagian 5. Skenarionya: kamu punya catatan transaksi toko selama seminggu. Sebagian transaksi batal dan harus dilewati. Sisanya dihitung totalnya, dirata-rata, dan diberi kategori nominal pakai switch.

    package main
    
    import "fmt"
    
    type Transaksi struct {
    	ID     int
    	Produk string
    	Nilai  int
    	Batal  bool
    }
    
    func main() {
    	transaksi := []Transaksi{
    		{ID: 1, Produk: "Kaos Polos", Nilai: 150000, Batal: false},
    		{ID: 2, Produk: "Jaket Hoodie", Nilai: 450000, Batal: false},
    		{ID: 3, Produk: "Topi Baseball", Nilai: 75000, Batal: true},
    		{ID: 4, Produk: "Celana Chino", Nilai: 320000, Batal: false},
    		{ID: 5, Produk: "Kemeja Flanel", Nilai: 260000, Batal: false},
    		{ID: 6, Produk: "Sandal Jepit", Nilai: 35000, Batal: true},
    		{ID: 7, Produk: "Sepatu Lari", Nilai: 780000, Batal: false},
    	}
    
    	total := 0
    	jumlahValid := 0
    
    	fmt.Println("=== Rekap Penjualan Mingguan ===")
    
    	for _, t := range transaksi {
    		if t.Batal {
    			fmt.Printf("Transaksi #%d (%s) batal, dilewati\n", t.ID, t.Produk)
    			continue
    		}
    
    		var kategori string
    		switch {
    		case t.Nilai >= 500000:
    			kategori = "besar"
    		case t.Nilai >= 200000:
    			kategori = "sedang"
    		default:
    			kategori = "kecil"
    		}
    
    		fmt.Printf("#%d %-15s Rp%-8d kategori %s\n", t.ID, t.Produk, t.Nilai, kategori)
    
    		total += t.Nilai
    		jumlahValid++
    	}
    
    	fmt.Println("--------------------------------")
    	fmt.Println("Transaksi valid :", jumlahValid)
    	fmt.Println("Total penjualan : Rp", total)
    
    	if jumlahValid > 0 {
    		rata := total / jumlahValid
    		fmt.Println("Rata-rata       : Rp", rata)
    	}
    }

    Jalankan dengan go run main.go. Hasilnya:

    === Rekap Penjualan Mingguan ===
    #1 Kaos Polos      Rp150000   kategori kecil
    #2 Jaket Hoodie    Rp450000   kategori sedang
    Transaksi #3 (Topi Baseball) batal, dilewati
    #4 Celana Chino    Rp320000   kategori sedang
    #5 Kemeja Flanel   Rp260000   kategori sedang
    Transaksi #6 (Sandal Jepit) batal, dilewati
    #7 Sepatu Lari     Rp780000   kategori besar
    --------------------------------
    Transaksi valid : 5
    Total penjualan : Rp 1960000
    Rata-rata       : Rp 392000

    Coba telusuri alurnya pelan-pelan. Loop for range menyusuri slice of struct. continue melewati dua transaksi yang batal, jadi keduanya tidak ikut total. Switch tanpa kondisi memberi label kategori berdasarkan nominal. Terakhir, if di bawah memastikan pembagian rata-rata hanya jalan kalau ada transaksi valid, supaya program tidak membagi dengan nol.

    Sebagai latihan mandiri, coba ubah programnya sedikit. Tambahkan variabel penghitung untuk tiap kategori, lalu cetak berapa transaksi besar, sedang, dan kecil di bagian rekap. Semua bahannya sudah kamu pegang.

    Rangkuman dan Bagian Selanjutnya

    Hari ini kamu sudah menguasai alat kontrol alur di Go:

    • for adalah satu-satunya perulangan, dengan tiga bentuk: klasik, gaya while, dan tanpa kondisi
    • for range menyusuri slice dan map, dengan catatan urutan map selalu acak
    • break menghentikan loop, continue melompati satu putaran
    • if bisa membawa statement pendek, cocok untuk pola comma ok
    • switch di Go otomatis berhenti per case, tanpa perlu break, dan bentuk tanpa kondisinya menggantikan if else berantai
    • label dipakai untuk keluar dari loop bersarang, meski jarang dibutuhkan

    Di bagian 7 kita masuk ke topik yang sering dianggap seram padahal sebenarnya ramah: Pointer di Go: Memahami Alamat Memori Tanpa Pusing. Sampai ketemu di sana.

    Seri ini ditulis dari pengalaman tim Arrazy membangun berbagai sistem aplikasi untuk bisnis di Indonesia.

  • Apa itu Jaringan Komputer + Setup Lab Praktik di Linux

    Apa itu Jaringan Komputer + Setup Lab Praktik di Linux

    Jaringan komputer adalah kumpulan dua perangkat atau lebih yang saling terhubung supaya bisa bertukar data. Terhubungnya bisa lewat kabel, bisa lewat WiFi. Laptop kamu yang tersambung ke router rumah itu sudah jaringan komputer. HP dan smart TV yang nonton YouTube lewat WiFi yang sama juga bagian dari jaringan itu. Internet sendiri pada dasarnya jaringan raksasa yang menghubungkan jutaan jaringan kecil di seluruh dunia.

    Artikel ini bagian pertama dari seri Belajar Jaringan Komputer dari Nol. Di bagian ini kita bahas apa itu jaringan komputer dengan contoh yang dekat dengan keseharian, lalu langsung praktik: menyiapkan lab di Linux dan menjalankan perintah jaringan pertama kamu. Tidak ada teori panjang dulu. Konsep seperti IP address, DNS, dan routing akan dikupas satu per satu di bagian-bagian berikutnya.

    Apa Itu Jaringan Komputer, dari LAN Rumah sampai Internet

    Biar tidak abstrak, kita lihat tiga contoh nyata dengan skala yang makin besar.

    Contoh 1: LAN di rumah

    Di rumah kamu ada satu router WiFi. Laptop, HP, printer, dan smart TV terhubung ke router itu. Semua perangkat ini membentuk satu LAN (Local Area Network), jaringan lokal dengan cakupan kecil. Karena satu jaringan, laptop bisa mengirim dokumen ke printer tanpa lewat internet sama sekali. Router bertugas jadi penghubung antar perangkat sekaligus gerbang keluar menuju internet.

    Contoh 2: Warnet atau lab kampus

    Warnet dan lab komputer kampus juga LAN, hanya skalanya lebih besar. Puluhan PC terhubung lewat kabel ke satu perangkat bernama switch, lalu switch tersambung ke router. Kalau kamu pernah main game multiplayer antar PC di warnet tanpa internet, itu bukti bahwa komunikasi antar komputer dalam LAN tidak butuh internet. Datanya cukup berputar di dalam jaringan lokal.

    Contoh 3: Internet

    Sekarang bayangkan LAN rumah kamu, LAN warnet, jaringan kantor, dan jaringan data center saling dihubungkan lewat jaringan milik ISP (penyedia internet), lalu ISP di seluruh dunia saling terhubung juga. Itulah internet: jaringan dari banyak jaringan. Saat kamu membuka sebuah website, data berjalan dari server di suatu data center, melompat lewat banyak router, sampai akhirnya tiba di laptop kamu. Perjalanan paket ini akan kita bedah tuntas di bagian 2 seri ini.

    Kenapa developer dan pengelola server wajib paham jaringan

    Kalau kamu cuma pakai komputer untuk browsing, pengetahuan jaringan memang jarang terasa perlu. Tapi begitu kamu menulis kode atau memegang server, ceritanya beda. Beberapa situasi yang pasti kamu temui:

    • API yang kamu panggil timeout. Masalahnya di kode, di DNS, di firewall, atau servernya memang mati? Tanpa dasar jaringan, kamu cuma bisa menebak.
    • Aplikasi jalan di laptop tapi tidak bisa diakses dari HP di WiFi yang sama. Ini soal IP dan port, bukan soal kode.
    • Website klien tiba-tiba tidak bisa dibuka. Kamu perlu tahu cara memastikan apakah masalahnya di DNS, di server, atau di jaringan pengunjung.
    • Deploy aplikasi dengan Docker, lalu container tidak bisa saling komunikasi. Docker punya lapisan jaringan sendiri yang perlu dipahami.

    Tim kami di Arrazy merasakan ini langsung. Saat membangun dan memelihara sistem aplikasi untuk klien, sebagian besar masalah produksi yang kami tangani ujungnya bukan bug di kode, tapi urusan jaringan: DNS yang belum propagasi, port yang tertutup firewall, atau konfigurasi reverse proxy. Developer yang paham dasar jaringan menyelesaikan masalah seperti ini dalam hitungan menit, bukan jam.

    Persiapan Lab: Linux dan Terminal

    Seri ini praktik di Linux karena hampir semua server di dunia berjalan di Linux, dan tool jaringan terbaik tersedia natif di sana. Yang kamu butuhkan:

    • Ubuntu 24.04 LTS atau turunannya seperti Linux Mint. Ubuntu 22.04 juga aman, perintahnya sama.
    • Pengguna Windows bisa pakai WSL 2 (Windows Subsystem for Linux) dengan distro Ubuntu. Sebagian besar praktik seri ini jalan di WSL, dengan beberapa catatan yang saya tulis di bagian troubleshooting.
    • Akses sudo untuk install paket.

    Kamu tidak perlu jago terminal, tapi minimal harus nyaman berpindah direktori, menjalankan perintah, dan membaca output. Kalau perintah seperti cd, ls, dan sudo masih terasa asing, selesaikan dulu beberapa bagian awal seri Belajar Linux dari Nol. Seri jaringan ini menganggap kamu sudah bisa hal-hal dasar itu.

    Install Toolkit Jaringan di Linux via apt

    Sekarang kita pasang semua tool yang dipakai sepanjang 26 bagian seri ini. Sekali install, beres untuk seterusnya. Buka terminal, lalu jalankan:

    sudo apt update
    sudo apt install -y iputils-ping traceroute dnsutils curl iproute2 tcpdump wireshark

    Saat instalasi Wireshark, akan muncul dialog biru bertanya “Should non-superusers be able to capture packets?”. Pilih Yes supaya nanti kamu bisa capture paket tanpa harus jadi root. Kalau terlanjur pilih No, ada cara memperbaikinya di bagian troubleshooting.

    Ini fungsi tiap paket, biar kamu tahu apa yang barusan dipasang:

    Paket Perintah utama Kegunaan
    iputils-ping ping Mengecek apakah sebuah host bisa dijangkau
    traceroute traceroute Melihat rute yang dilewati paket menuju tujuan
    dnsutils dig, nslookup Query DNS, menerjemahkan nama domain ke IP
    curl curl Mengirim request HTTP dari terminal
    iproute2 ip, ss Melihat IP address, tabel routing, dan port terbuka
    tcpdump tcpdump Menangkap paket jaringan lewat terminal
    wireshark wireshark Analisis paket dengan tampilan grafis

    Di Ubuntu 24.04, iproute2 dan curl biasanya sudah terpasang bawaan, tapi tidak masalah ikut ditulis di perintah install. apt akan melewatinya kalau sudah ada.

    Verifikasi instalasi dengan mengecek versi beberapa tool:

    ping -V
    dig -v
    tcpdump --version

    Output yang diharapkan kurang lebih seperti ini (angka versi bisa sedikit beda tergantung distro):

    ping from iputils 20240117
    DiG 9.18.30-0ubuntu0.24.04.2-Ubuntu
    tcpdump version 4.99.4
    libpcap version 1.10.4 (with TPACKET_V3)

    Kalau semua perintah di atas mengeluarkan versi dan bukan error, lab kamu siap.

    Praktik Pertama: Kenali Jaringan Kamu Sendiri

    Dua perintah ini adalah perintah jaringan yang paling sering kamu pakai seumur hidup sebagai developer. Kita jalankan sekarang, penjelasan mendalamnya menyusul di bagian-bagian berikutnya.

    Melihat IP address dengan ip addr

    ip addr

    Outputnya kira-kira seperti ini (punya kamu pasti beda di detailnya):

    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN ...
        inet 127.0.0.1/8 scope host lo
    2: wlp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP ...
        link/ether a4:6b:b6:3e:12:9f brd ff:ff:ff:ff:ff:ff
        inet 192.168.1.7/24 brd 192.168.1.255 scope global dynamic wlp3s0

    Tiga hal yang cukup kamu kenali dulu:

    • lo adalah loopback, alamat internal komputer kamu sendiri. IP-nya selalu 127.0.0.1.
    • wlp3s0 (atau eth0, enp2s0, tergantung perangkat) adalah interface jaringan yang aktif. Baris inet 192.168.1.7/24 artinya komputer kamu punya IP 192.168.1.7 di jaringan lokal.
    • Baris link/ether berisi MAC address, alamat fisik perangkat jaringan kamu.

    Apa arti /24, kenapa IP-nya mulai dengan 192.168, dan apa bedanya dengan IP publik, semua dibahas di bagian 6 dan 7 seri ini. Sekarang cukup tahu bahwa komputer kamu punya alamat di jaringan lokal.

    Mengecek koneksi dengan ping

    ping -c 4 google.com

    Opsi -c 4 artinya kirim 4 kali lalu berhenti. Tanpa opsi ini, ping di Linux jalan terus sampai kamu tekan Ctrl+C. Output yang diharapkan:

    PING google.com (142.251.10.101) 56(84) bytes of data.
    64 bytes from 142.251.10.101: icmp_seq=1 ttl=115 time=24.3 ms
    64 bytes from 142.251.10.101: icmp_seq=2 ttl=115 time=23.8 ms
    64 bytes from 142.251.10.101: icmp_seq=3 ttl=115 time=25.1 ms
    64 bytes from 142.251.10.101: icmp_seq=4 ttl=115 time=24.0 ms
    
    --- google.com ping statistics ---
    4 packets transmitted, 4 received, 0% packet loss, time 3005ms
    rtt min/avg/max/mdev = 23.812/24.300/25.104/0.492 ms

    Baca hasilnya begini: komputer kamu mengirim paket kecil ke server Google, dan server membalas dalam waktu sekitar 24 milidetik. 0% packet loss artinya semua paket sampai dan dibalas, koneksi kamu sehat. Perhatikan juga baris pertama: kamu mengetik google.com, tapi sistem menerjemahkannya jadi IP 142.251.10.101. Penerjemah itu namanya DNS, dan dia dapat bagian khusus di seri ini.

    Kalau dua perintah ini jalan, selamat, lab kamu berfungsi dan kamu baru saja melakukan diagnosis jaringan pertama.

    Error Umum Saat Setup Lab dan Solusinya

    Command not found saat menjalankan ping, dig, atau traceroute

    Contoh pesannya: Command 'traceroute' not found, but can be installed with: sudo apt install traceroute. Penyebabnya jelas, paketnya belum terinstall. Image minimal seperti container Docker atau VPS baru sering tidak menyertakan tool dasar sekalipun. Solusinya jalankan ulang perintah install di atas. Kalau apt-nya sendiri yang error, jalankan sudo apt update dulu untuk menyegarkan daftar paket.

    Wireshark tidak bisa capture: You don’t have permission to capture on that device

    Ini muncul kalau saat instalasi kamu menjawab No pada dialog non-superuser, atau user kamu belum masuk group wireshark. Perbaiki dengan:

    sudo dpkg-reconfigure wireshark-common
    sudo usermod -aG wireshark $USER

    Pada dialog yang muncul, pilih Yes. Setelah itu logout lalu login lagi (atau restart) supaya keanggotaan group aktif. Cek dengan perintah groups, pastikan kata wireshark muncul di daftarnya.

    ping: Temporary failure in name resolution

    Artinya komputer kamu gagal menerjemahkan google.com jadi IP. Coba ping -c 4 8.8.8.8. Kalau ping ke angka ini berhasil tapi ke google.com gagal, masalahnya di DNS, bukan koneksi. Di WSL, kasus ini sering beres dengan restart WSL: tutup terminal, jalankan wsl --shutdown di PowerShell, lalu buka lagi. Kalau ping ke 8.8.8.8 juga gagal, cek dulu apakah WiFi atau kabel kamu memang tersambung.

    Catatan khusus pengguna WSL

    WSL 2 menjalankan Linux di dalam mesin virtual, jadi ada beberapa perbedaan yang wajar kamu temui:

    • ip addr menampilkan interface eth0 dengan IP virtual (misalnya 172.x.x.x), bukan IP WiFi laptop kamu. Ini normal, WSL punya jaringan virtual sendiri di belakang Windows.
    • Wireshark versi GUI butuh WSLg (bawaan Windows 11). Alternatifnya, capture pakai tcpdump di WSL lalu buka file hasilnya di Wireshark versi Windows.
    • Beberapa praktik lanjutan seperti sniffing interface WiFi langsung tidak bisa dilakukan dari dalam WSL. Untuk 80 persen materi seri ini, WSL tetap cukup.

    Lanjut ke Bagian 2: Perjalanan Paket di Internet

    Hari ini kamu sudah paham apa itu jaringan komputer, dari LAN rumah sampai internet, dan lab praktik kamu sudah siap dengan toolkit lengkap. Dua perintah pertama juga sudah jalan: ip addr untuk melihat alamat sendiri dan ping untuk mengecek koneksi.

    Bagian berikutnya berjudul “Cara Kerja Internet: Perjalanan Paket Saat Membuka Website”. Di sana kita ikuti langkah demi langkah apa yang terjadi sejak kamu menekan Enter di address bar sampai halaman muncul, lengkap dengan praktik traceroute untuk melihat rute paketnya. Artikelnya terbit menyusul, pantau daftar lengkapnya di halaman hub seri Belajar Jaringan Komputer.

    Referensi

  • Belajar Golang dari Nol #5: Slice dan Map, Kumpulan Data di Go

    Belajar Golang dari Nol #5: Slice dan Map, Kumpulan Data di Go

    Di Belajar Golang dari Nol #4 kita sudah belajar membungkus data ke dalam struct dan menempelkan method padanya. Satu struct mewakili satu barang, satu user, atau satu produk. Tapi program nyata jarang mengurus satu data saja. Toko punya banyak produk. Sekolah punya banyak siswa. Kita butuh wadah untuk menampung kumpulan data. Di Go, dua wadah yang paling sering dipakai adalah slice dan map. Keduanya jadi menu utama kita hari ini.

    Sekilas tentang array

    Sebelum masuk ke slice, kenalan dulu dengan fondasinya, yaitu array. Array adalah deretan data dengan ukuran tetap. Sekali dibuat berisi 3 slot, selamanya 3 slot.

    var nilai [3]int
    nilai[0] = 80
    nilai[1] = 75
    nilai[2] = 90

    Bayangkan rak dengan jumlah laci yang sudah dipatok dari pabrik. Mau menambah laci keempat, tidak bisa. Karena kaku seperti itu, array jarang dipakai langsung dalam kode Go sehari-hari. Yang hampir selalu dipakai adalah slice, yang di belakang layar sebenarnya menumpang pada array. Cukup tahu itu dulu. Fokus kita sekarang ke slice.

    Slice, daftar yang bisa memanjang

    Slice mirip daftar belanja di kertas. Awalnya berisi tiga barang. Ingat sesuatu, tulis lagi di bawahnya. Daftarnya memanjang sendiri tanpa perlu ganti kertas.

    Membuat slice hampir sama dengan array, hanya tanpa angka di dalam kurung siku.

    package main
    
    import "fmt"
    
    func main() {
    	buah := []string{"apel", "mangga", "jeruk"}
    
    	fmt.Println(buah)      // [apel mangga jeruk]
    	fmt.Println(len(buah)) // 3
    	fmt.Println(buah[0])   // apel
    	fmt.Println(buah[2])   // jeruk
    }

    Tiga hal yang perlu dicatat dari kode di atas.

    • []string{...} disebut slice literal. Kita langsung mengisi datanya saat membuat.
    • len(buah) menghitung jumlah isi slice.
    • buah[0] mengakses isi berdasarkan posisi. Posisi dihitung dari 0, bukan dari 1.

    Untuk menambah isi, pakai append.

    buah = append(buah, "salak")
    fmt.Println(buah)      // [apel mangga jeruk salak]
    fmt.Println(len(buah)) // 4

    Perhatikan polanya. Hasil append harus disimpan kembali ke variabelnya. Menulis append(buah, "salak") saja tanpa menampung hasilnya tidak akan mengubah apa pun, dan Go bahkan menolaknya saat compile. Anggap append seperti fotokopi daftar lama plus satu baris baru. Daftar barunya harus kita pegang.

    Slice juga bisa dipotong. Sintaksnya a[awal:akhir]. Posisi awal ikut diambil, posisi akhir tidak.

    potongan := buah[1:3]
    fmt.Println(potongan) // [mangga jeruk]

    buah[1:3] artinya ambil mulai posisi 1 sampai sebelum posisi 3. Jadi posisi 1 dan 2 saja. Awalnya sering bikin bingung, tapi lama-lama terasa natural.

    Slice berisi struct

    Di bagian 4 kita sudah bisa membuat struct Produk. Sekarang gabungkan. Slice tidak hanya bisa menampung string atau angka. Ia bisa menampung struct. Inilah cara Go menyimpan daftar produk, daftar siswa, atau daftar transaksi.

    package main
    
    import "fmt"
    
    type Produk struct {
    	Nama  string
    	Harga int
    }
    
    func main() {
    	daftar := []Produk{
    		{Nama: "Kopi Bubuk", Harga: 25000},
    		{Nama: "Gula Pasir", Harga: 15000},
    		{Nama: "Teh Celup", Harga: 10000},
    	}
    
    	for _, p := range daftar {
    		fmt.Println(p.Nama, "harganya Rp", p.Harga)
    	}
    }

    Baris for _, p := range daftar artinya kunjungi setiap isi slice satu per satu, lalu simpan isinya ke variabel p. Tanda _ dipakai karena kita tidak butuh nomor urutnya. Detail lengkap soal for dan range kita bahas tuntas di bagian 6. Sekarang cukup pahami bahwa pola ini dipakai untuk menelusuri isi slice.

    Map, mencari data lewat kunci

    Slice mencari data lewat posisi. Map mencari data lewat kunci. Bayangkan loker di kolam renang. Anda tidak menghafal loker keberapa dari kiri. Anda cukup pegang kuncinya, lalu langsung buka loker yang cocok. Kunci pada map bisa berupa string, misalnya nama barang, dan nilainya bisa apa saja.

    Bentuk penulisannya map[tipeKunci]tipeNilai. Contoh map untuk mencatat stok barang.

    package main
    
    import "fmt"
    
    func main() {
    	stok := map[string]int{
    		"kopi": 10,
    		"gula": 5,
    	}
    
    	// menambah data baru
    	stok["teh"] = 8
    
    	// mengubah data yang sudah ada
    	stok["kopi"] = 12
    
    	// menghapus data
    	delete(stok, "gula")
    
    	fmt.Println(stok)        // map[kopi:12 teh:8]
    	fmt.Println(stok["teh"]) // 8
    }

    Menambah dan mengubah pakai sintaks yang sama, yaitu stok["kunci"] = nilai. Kalau kuncinya belum ada, data baru dibuat. Kalau sudah ada, nilainya ditimpa. Menghapus pakai fungsi bawaan delete.

    Comma ok, cara aman mengecek isi map

    Sekarang bagian penting. Apa yang terjadi kalau kita membaca kunci yang tidak ada?

    fmt.Println(stok["beras"]) // 0

    Go tidak error. Ia mengembalikan zero value dari tipe nilainya. Masih ingat materi zero value di bagian 2? Untuk int, zero value adalah 0. Di sinilah masalahnya. Angka 0 itu ambigu. Apakah beras memang tidak terdaftar, atau beras terdaftar tapi stoknya sedang kosong? Dua kondisi yang sangat berbeda, hasilnya sama-sama 0.

    Go menyediakan cara untuk membedakannya. Namanya comma ok idiom.

    jumlah, ok := stok["beras"]
    if ok {
    	fmt.Println("beras terdaftar, stok:", jumlah)
    } else {
    	fmt.Println("beras tidak terdaftar di toko")
    }

    Saat membaca map dengan dua variabel penampung, variabel kedua berisi true kalau kuncinya memang ada, dan false kalau tidak. Dengan begitu kita tidak lagi tertipu oleh zero value. Biasakan pakai pola ini setiap kali keberadaan kunci itu penting untuk logika program. Di program latihan nanti, pola ini yang membedakan barang habis dengan barang yang memang tidak dijual.

    Map dengan value struct

    Sama seperti slice, nilai pada map juga boleh berupa struct. Cocok untuk katalog, di mana nama barang jadi kunci dan detailnya jadi nilai.

    katalog := map[string]Produk{
    	"kopi": {Nama: "Kopi Bubuk", Harga: 25000},
    	"gula": {Nama: "Gula Pasir", Harga: 15000},
    }
    
    fmt.Println(katalog["kopi"].Harga) // 25000

    Sekali akses, kita langsung dapat struct utuh lengkap dengan semua field-nya.

    Kapan pakai slice, kapan pakai map

    Aturan praktisnya sederhana.

    • Pakai slice kalau urutan itu penting atau data memang berbentuk antrean. Daftar transaksi, riwayat chat, isi keranjang belanja. Data tersimpan berurutan dan boleh ada isi yang kembar.
    • Pakai map kalau Anda sering mencari data berdasarkan kunci tertentu. Stok per nama barang, user per email, konfigurasi per nama setting. Aksesnya langsung ketemu tanpa menelusuri satu per satu, tapi urutannya tidak dijamin.

    Satu catatan soal urutan. Saat map ditelusuri dengan range, Go sengaja mengacak urutannya. Jadi jangan pernah mengandalkan urutan pada map. Kalau butuh urutan, pakailah slice. Keduanya juga sering dipakai bersamaan dalam satu program, seperti yang akan kita lakukan di latihan.

    Dua jebakan yang sering menjerat pemula

    Nil map tidak bisa diisi

    Kalau map hanya dideklarasikan tanpa diinisialisasi, nilainya nil. Membaca nil map masih aman, hasilnya zero value. Tapi menulis ke nil map membuat program langsung panic alias berhenti mendadak.

    var umur map[string]int
    umur["budi"] = 20 // panic: assignment to entry in nil map

    Solusinya, siapkan map-nya dulu dengan make atau dengan literal kosong.

    umur := make(map[string]int)
    umur["budi"] = 20 // aman

    Anggap make seperti memasang papan loker sebelum kunci pertama dibagikan. Slice lebih pemaaf di sini, karena append pada slice nil tetap aman. Map tidak. Ini salah satu penyebab panic paling umum pada program Go pemula.

    Slice yang berbagi data

    Jebakan kedua lebih halus. Hasil pemotongan slice tidak menyalin data. Ia menunjuk ke deretan data yang sama dengan slice asalnya. Mengubah satu, yang lain ikut berubah.

    angka := []int{1, 2, 3, 4}
    potongan := angka[0:2]
    potongan[0] = 99
    
    fmt.Println(potongan) // [99 2]
    fmt.Println(angka)    // [99 2 3 4]

    Perhatikan, angka ikut berubah padahal kita hanya mengubah potongan. Keduanya memandang lemari data yang sama dari jendela yang berbeda. Ini bukan bug, ini memang desain Go supaya pemotongan slice murah dan cepat. Tapi kalau tidak sadar, efek samping seperti ini bisa bikin bingung berjam-jam. Kalau memang butuh salinan yang benar-benar terpisah, Go punya fungsi copy yang bisa Anda eksplorasi sendiri.

    Latihan: keranjang belanja mini

    Waktunya merangkai semua materi hari ini jadi satu program utuh. Kita buat keranjang belanja mini dengan aturan begini. Stok barang tersimpan di map. Katalog produk tersimpan di map dengan value struct. Isi keranjang tersimpan di slice. Fungsi penambah barang wajib mengecek dulu lewat comma ok, supaya barang yang tidak dijual dan barang yang stoknya habis diperlakukan beda.

    package main
    
    import "fmt"
    
    type Produk struct {
    	Nama  string
    	Harga int
    }
    
    func tambahKeKeranjang(keranjang []Produk, stok map[string]int, katalog map[string]Produk, nama string) []Produk {
    	sisa, ok := stok[nama]
    	if !ok {
    		fmt.Println(nama, ": barang tidak ada di toko")
    		return keranjang
    	}
    	if sisa == 0 {
    		fmt.Println(nama, ": stok sedang habis")
    		return keranjang
    	}
    
    	stok[nama] = sisa - 1
    	keranjang = append(keranjang, katalog[nama])
    	fmt.Println(nama, ": masuk keranjang")
    	return keranjang
    }
    
    func main() {
    	stok := map[string]int{
    		"kopi": 2,
    		"gula": 0,
    		"teh":  5,
    	}
    
    	katalog := map[string]Produk{
    		"kopi": {Nama: "Kopi Bubuk", Harga: 25000},
    		"gula": {Nama: "Gula Pasir", Harga: 15000},
    		"teh":  {Nama: "Teh Celup", Harga: 10000},
    	}
    
    	var keranjang []Produk
    	keranjang = tambahKeKeranjang(keranjang, stok, katalog, "kopi")
    	keranjang = tambahKeKeranjang(keranjang, stok, katalog, "teh")
    	keranjang = tambahKeKeranjang(keranjang, stok, katalog, "gula")
    	keranjang = tambahKeKeranjang(keranjang, stok, katalog, "beras")
    
    	fmt.Println()
    	fmt.Println("Isi keranjang:")
    	total := 0
    	for _, p := range keranjang {
    		fmt.Println("-", p.Nama, "Rp", p.Harga)
    		total = total + p.Harga
    	}
    	fmt.Println("Total belanja: Rp", total)
    }

    Jalankan dengan go run main.go. Hasilnya seperti ini.

    kopi : masuk keranjang
    teh : masuk keranjang
    gula : stok sedang habis
    beras : barang tidak ada di toko
    
    Isi keranjang:
    - Kopi Bubuk Rp 25000
    - Teh Celup Rp 10000
    Total belanja: Rp 35000

    Coba telusuri alurnya pelan-pelan. Kopi dan teh masuk keranjang karena kuncinya ada dan stoknya lebih dari 0. Gula ditolak dengan pesan stok habis, karena comma ok mengembalikan true tapi sisanya 0. Beras ditolak dengan pesan berbeda, karena comma ok mengembalikan false. Tanpa idiom itu, gula dan beras akan terlihat sama persis di mata program. Perhatikan juga bahwa tambahKeKeranjang mengembalikan slice baru dan hasilnya selalu kita tampung lagi, konsisten dengan pola append yang tadi kita bahas.

    Sebagai latihan tambahan, coba dua hal ini. Pertama, tambahkan barang baru ke katalog dan stok, lalu beli beberapa kali sampai stoknya habis. Kedua, ubah program supaya satu barang bisa dibeli dengan jumlah tertentu sekaligus, bukan satu-satu.

    Selanjutnya di seri ini

    Anda sekarang pegang dua wadah data paling penting di Go. Slice untuk daftar yang berurutan, map untuk pencarian cepat lewat kunci, plus comma ok untuk membaca map dengan aman. Sepanjang artikel ini kita sudah beberapa kali memakai for range dan if tanpa membedahnya dalam-dalam. Itu sengaja. Di bagian 6, Perulangan for dan Percabangan: Mengendalikan Alur Program, keduanya kita kupas tuntas, mulai dari berbagai bentuk for sampai switch. Sampai jumpa di bagian berikutnya.

    Kalau bisnis Anda butuh aplikasi dengan pengelolaan data seperti ini, mulai dari katalog produk sampai sistem stok, tim kami siap membantu lewat layanan pengembangan sistem aplikasi.