Category: Golang

  • 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 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.

  • 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.

  • Belajar Golang dari Nol #4: Struct dan Method, Cara Go Menyusun Data

    Belajar Golang dari Nol #4: Struct dan Method, Cara Go Menyusun Data

    Di Belajar Golang dari Nol #3 kita sudah belajar membungkus logika ke dalam function, mengembalikan lebih dari satu nilai, dan menangani error pertama kita. Sekarang saatnya naik satu tingkat. Kalau function adalah cara Go menyusun logika, maka struct adalah cara Go menyusun data. Di bagian ini kita akan kenalan dengan struct, method, dan pola constructor yang dipakai hampir di semua project Go sungguhan.

    Kenapa Kita Butuh Struct

    Bayangkan kamu bikin program kasir sederhana. Satu produk punya nama, harga, dan stok. Kalau cuma pakai variabel biasa, kodenya jadi seperti ini.

    var namaProduk string = "Kopi Arabika"
    var hargaProduk int = 25000
    var stokProduk int = 10

    Untuk satu produk masih aman. Tapi begitu ada produk kedua, kamu harus bikin namaProduk2, hargaProduk2, stokProduk2. Produk ketiga, tambah lagi. Datanya tercerai berai, padahal ketiganya jelas satu paket. Nama, harga, dan stok itu milik produk yang sama.

    Struct menyelesaikan masalah ini. Anggap saja struct itu seperti formulir. Formulir pendaftaran punya kolom nama, alamat, dan nomor telepon. Kolomnya sudah ditentukan dari awal. Setiap orang yang mendaftar mengisi formulir yang sama, tapi isinya beda. Struct persis begitu. Kamu mendefinisikan kolomnya sekali, lalu bisa membuat banyak isian dari cetakan yang sama.

    Deklarasi Struct dan Cara Membuat Instance

    Mendefinisikan struct di Go pakai kata kunci type dan struct. Kolomnya disebut field. Setiap field punya nama dan tipe data.

    package main
    
    import "fmt"
    
    type Produk struct {
    	Nama  string
    	Harga int
    	Stok  int
    }
    
    func main() {
    	kopi := Produk{
    		Nama:  "Kopi Arabika",
    		Harga: 25000,
    		Stok:  10,
    	}
    
    	fmt.Println(kopi.Nama)
    	fmt.Println(kopi.Harga)
    
    	kopi.Stok = 15
    	fmt.Println(kopi.Stok)
    }

    Beberapa hal yang perlu kamu perhatikan dari kode di atas.

    • type Produk struct hanya mendefinisikan cetakannya. Belum ada data apa pun di sini.
    • kopi := Produk{...} membuat isian nyata dari cetakan itu. Ini yang disebut instance.
    • Saat mengisi, kita menulis nama field-nya secara eksplisit, seperti Nama: "Kopi Arabika". Ini cara yang paling aman dan paling mudah dibaca. Biasakan pakai gaya ini.
    • Membaca dan mengubah field pakai tanda titik. kopi.Nama untuk membaca, kopi.Stok = 15 untuk mengubah.

    Satu catatan yang nyambung ke materi bagian 2 soal zero value. Kalau kamu menulis var kosong Produk tanpa mengisi apa pun, setiap field otomatis berisi zero value tipenya, jadi Nama berisi string kosong dan Harga serta Stok berisi nol.

    Struct di Dalam Struct

    Field sebuah struct tidak harus tipe dasar. Dia boleh berupa struct lain. Ini berguna waktu datamu memang bertingkat. Contohnya data pelanggan yang punya alamat.

    type Alamat struct {
    	Jalan string
    	Kota  string
    }
    
    type Pelanggan struct {
    	Nama   string
    	Alamat Alamat
    }
    
    func main() {
    	budi := Pelanggan{
    		Nama: "Budi",
    		Alamat: Alamat{
    			Jalan: "Jl. Merdeka No. 10",
    			Kota:  "Purwokerto",
    		},
    	}
    
    	fmt.Println(budi.Alamat.Kota)
    }

    Cara aksesnya tinggal dirantai pakai titik. budi.Alamat.Kota artinya ambil field Alamat milik budi, lalu ambil field Kota di dalamnya. Sederhana, tapi pola ini yang nanti kamu pakai untuk memodelkan data dunia nyata seperti pesanan yang punya pelanggan, pelanggan yang punya alamat.

    Method: Function yang Menempel di Struct

    Sejauh ini struct kita cuma tempat menyimpan data. Padahal data biasanya punya perilaku. Produk bisa dihitung harga diskonnya. Di bagian 3 kamu sudah bisa menulis function seperti ini.

    func HitungDiskon(p Produk, persen int) int {
    	potongan := p.Harga * persen / 100
    	return p.Harga - potongan
    }

    Kode itu jalan. Tapi Go punya cara yang lebih rapi, namanya method. Method adalah function biasa yang diberi satu tambahan di depan namanya, disebut receiver. Receiver menentukan struct mana yang memiliki method itu.

    func (p Produk) HitungDiskon(persen int) int {
    	potongan := p.Harga * persen / 100
    	return p.Harga - potongan
    }

    Perhatikan bagian (p Produk) sebelum nama function. Itulah receiver. Artinya, method HitungDiskon milik struct Produk, dan di dalam method kita bisa mengakses datanya lewat p.

    Cara memanggilnya juga berubah. Dari HitungDiskon(kopi, 10) menjadi kopi.HitungDiskon(10). Bedanya terasa waktu kode mulai besar. Dengan method, kamu bertanya ke datanya langsung. Ketik kopi. di editor, dan semua kemampuan produk muncul sebagai saran. Data dan perilakunya jadi satu paket, tidak berserakan sebagai function lepas.

    Pointer Receiver vs Value Receiver

    Sekarang bagian yang paling sering bikin pemula bingung. Tenang, kita bahas pelan pakai contoh. Coba tebak output kode ini.

    func (p Produk) TambahStokSalah(jumlah int) {
    	p.Stok = p.Stok + jumlah
    }
    
    func (p *Produk) TambahStokBenar(jumlah int) {
    	p.Stok = p.Stok + jumlah
    }
    
    func main() {
    	kopi := Produk{Nama: "Kopi Arabika", Harga: 25000, Stok: 10}
    
    	kopi.TambahStokSalah(5)
    	fmt.Println(kopi.Stok)
    
    	kopi.TambahStokBenar(5)
    	fmt.Println(kopi.Stok)
    }

    Outputnya:

    10
    15

    Panggilan pertama tidak mengubah apa pun. Kenapa bisa begitu. Receiver (p Produk) disebut value receiver. Saat method dipanggil, Go membuat salinan dari kopi, lalu method bekerja pada salinan itu. Stok salinannya memang bertambah, tapi begitu method selesai, salinan itu dibuang. kopi yang asli tidak tersentuh.

    Receiver (p *Produk) dengan tanda bintang disebut pointer receiver. Method tidak menerima salinan, tapi semacam alamat yang menunjuk ke kopi yang asli. Jadi perubahan stoknya benar-benar nempel.

    Kamu belum perlu paham teori pointer secara mendalam sekarang. Pointer akan kita bahas khusus di bagian mendatang. Untuk saat ini, pegang satu aturan praktis ini saja.

    • Method mau mengubah data di struct: pakai pointer receiver, (p *Produk).
    • Method cuma membaca data, seperti menghitung atau menampilkan: value receiver, (p Produk), sudah cukup.

    Kalau masih ragu, banyak programmer Go memilih pointer receiver untuk semua method di satu struct supaya konsisten. Itu juga pilihan yang wajar.

    Constructor Pattern ala Go

    Ada satu masalah kecil yang belum kita selesaikan. Tidak ada yang melarang orang membuat produk dengan data ngawur, misalnya Produk{Nama: "", Harga: -5000}. Go tidak punya constructor bawaan seperti bahasa lain. Sebagai gantinya, komunitas Go punya kebiasaan: buat function biasa dengan awalan New yang tugasnya membuat struct sekaligus memvalidasi isinya.

    Di sinilah materi error dari bagian 3 kepakai lagi. Constructor mengembalikan dua nilai, struct dan error.

    func NewProduk(nama string, harga int, stok int) (Produk, error) {
    	if nama == "" {
    		return Produk{}, errors.New("nama produk tidak boleh kosong")
    	}
    	if harga <= 0 {
    		return Produk{}, errors.New("harga harus lebih dari nol")
    	}
    	return Produk{Nama: nama, Harga: harga, Stok: stok}, nil
    }

    Kalau data valid, kita kembalikan struct yang sudah terisi dan error nil. Kalau tidak valid, kita kembalikan struct kosong plus error yang menjelaskan masalahnya. Pemanggilnya wajib mengecek error dulu sebelum memakai struct-nya, persis pola if err != nil yang sudah kamu latih di bagian 3. Dengan pola ini, semua produk yang lolos dari NewProduk dijamin datanya masuk akal.

    Latihan: Sistem Stok Mini

    Sekarang kita gabungkan semuanya. Struct, method dengan pointer receiver, constructor dengan validasi, dan error. Kita bikin sistem stok mini untuk satu produk. Ketik ulang program ini, jangan copas, supaya tanganmu ikut hafal.

    package main
    
    import (
    	"errors"
    	"fmt"
    )
    
    type Produk struct {
    	Nama  string
    	Harga int
    	Stok  int
    }
    
    func NewProduk(nama string, harga int, stok int) (Produk, error) {
    	if nama == "" {
    		return Produk{}, errors.New("nama produk tidak boleh kosong")
    	}
    	if harga <= 0 {
    		return Produk{}, errors.New("harga harus lebih dari nol")
    	}
    	if stok < 0 {
    		return Produk{}, errors.New("stok awal tidak boleh negatif")
    	}
    	return Produk{Nama: nama, Harga: harga, Stok: stok}, nil
    }
    
    func (p *Produk) TambahStok(jumlah int) error {
    	if jumlah <= 0 {
    		return errors.New("jumlah tambahan harus lebih dari nol")
    	}
    	p.Stok = p.Stok + jumlah
    	return nil
    }
    
    func (p *Produk) KurangiStok(jumlah int) error {
    	if jumlah <= 0 {
    		return errors.New("jumlah pengurangan harus lebih dari nol")
    	}
    	if jumlah > p.Stok {
    		return fmt.Errorf("stok tidak cukup: sisa %d, diminta %d", p.Stok, jumlah)
    	}
    	p.Stok = p.Stok - jumlah
    	return nil
    }
    
    func (p Produk) Info() string {
    	return fmt.Sprintf("%s | harga %d | stok %d", p.Nama, p.Harga, p.Stok)
    }
    
    func main() {
    	kopi, err := NewProduk("Kopi Arabika", 25000, 10)
    	if err != nil {
    		fmt.Println("gagal membuat produk:", err)
    		return
    	}
    	fmt.Println(kopi.Info())
    
    	err = kopi.TambahStok(5)
    	if err != nil {
    		fmt.Println("gagal tambah stok:", err)
    	}
    	fmt.Println(kopi.Info())
    
    	err = kopi.KurangiStok(8)
    	if err != nil {
    		fmt.Println("gagal kurangi stok:", err)
    	}
    	fmt.Println(kopi.Info())
    
    	err = kopi.KurangiStok(100)
    	if err != nil {
    		fmt.Println("gagal kurangi stok:", err)
    	}
    	fmt.Println(kopi.Info())
    
    	_, err = NewProduk("", 25000, 10)
    	if err != nil {
    		fmt.Println("gagal membuat produk:", err)
    	}
    }

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

    Kopi Arabika | harga 25000 | stok 10
    Kopi Arabika | harga 25000 | stok 15
    Kopi Arabika | harga 25000 | stok 7
    gagal kurangi stok: stok tidak cukup: sisa 7, diminta 100
    Kopi Arabika | harga 25000 | stok 7
    gagal membuat produk: nama produk tidak boleh kosong

    Baca outputnya baris per baris dan cocokkan dengan kodenya.

    • Stok awal 10, tambah 5 jadi 15, kurangi 8 jadi 7. Method TambahStok dan KurangiStok pakai pointer receiver, jadi perubahannya nempel.
    • Waktu kita minta 100 padahal sisa 7, method menolak dan mengembalikan error. Perhatikan baris setelahnya: stok tetap 7, tidak berubah jadi minus.
    • Percobaan membuat produk tanpa nama juga ditolak oleh NewProduk. Data yang tidak valid tidak pernah sempat jadi struct yang dipakai program.

    Sebagai latihan tambahan, coba tambahkan method HitungDiskon dari bab sebelumnya ke program ini. Lalu coba tambahkan field Kategori ke struct Produk dan lihat bagian mana saja yang perlu kamu sesuaikan.

    Rangkuman dan Bagian Selanjutnya

    Hari ini kamu sudah pegang fondasi penting Go.

    • Struct mengelompokkan data yang berhubungan ke satu tempat, seperti formulir dengan kolom yang sudah ditentukan.
    • Field diakses dan diubah pakai tanda titik, dan struct bisa berisi struct lain.
    • Method adalah function dengan receiver, sehingga data dan perilakunya jadi satu paket.
    • Mau mengubah data, pakai pointer receiver. Cuma membaca, value receiver cukup.
    • Constructor NewProduk memastikan struct selalu lahir dengan data yang valid.

    Pola yang barusan kamu tulis ini bukan sekadar latihan. Struct dengan method dan validasi seperti ini juga fondasi yang kami pakai saat membangun sistem aplikasi berbasis Go untuk klien.

    Di bagian 5 kita akan bahas Slice dan Map: Kumpulan Data di Go. Di sana sistem stok mini ini akan naik kelas, dari satu produk menjadi daftar banyak produk. Sampai ketemu di bagian berikutnya.

  • Belajar Golang dari Nol #3: Function, Multiple Return, dan Error

    Belajar Golang dari Nol #3: Function, Multiple Return, dan Error

    Di Belajar Golang dari Nol #2 kamu sudah pegang variabel, tipe data, zero value, dan konversi tipe. Di sana kamu juga sempat ketemu sekilas dengan if err != nil waktu memakai strconv.Atoi. Waktu itu saya minta kamu terima dulu polanya tanpa banyak tanya. Sekarang saatnya membongkar asal-usulnya. Di bagian ini kita belajar function, multiple return, dan cara Go memperlakukan error.

    Function: bungkus logika biar bisa dipakai ulang

    Bayangkan function seperti mesin jus. Kamu masukkan buah, mesin bekerja, keluar jus. Kamu tidak perlu tahu isi mesinnya setiap kali mau bikin jus. Cukup tahu apa yang dimasukkan dan apa yang keluar.

    Di Go, bentuk dasarnya seperti ini.

    func sapa(nama string) string {
        return "Halo, " + nama
    }

    Baca pelan-pelan dari kiri ke kanan. func adalah kata kunci untuk membuat function. sapa adalah namanya. nama string artinya function ini menerima satu parameter bernama nama yang bertipe string. string di paling kanan adalah tipe nilai balikannya. Lalu return mengirim nilai itu keluar ke pemanggil.

    Kalau kamu pernah lihat Java atau C, urutannya terasa terbalik, karena Go memang sengaja menaruh tipe setelah nama supaya deklarasi tetap enak dibaca saat mulai kompleks.

    Begini cara memanggilnya dalam program utuh.

    package main
    
    import "fmt"
    
    func sapa(nama string) string {
        return "Halo, " + nama
    }
    
    func main() {
        pesan := sapa("Sari")
        fmt.Println(pesan)
    }

    Jalankan dengan go run main.go, hasilnya:

    Halo, Sari

    Parameter dan return value

    Function boleh menerima lebih dari satu parameter. Kita coba function hitung sederhana.

    func luasPersegiPanjang(panjang int, lebar int) int {
        return panjang * lebar
    }

    Ada satu trik kecil yang sering kamu temui di kode Go orang lain. Kalau beberapa parameter bertipe sama, tipenya cukup ditulis sekali di belakang.

    func luasPersegiPanjang(panjang, lebar int) int {
        return panjang * lebar
    }

    Keduanya sama persis artinya. Memanggilnya juga biasa saja: luas := luasPersegiPanjang(8, 5) akan mengisi luas dengan 40. Sampai sini belum ada yang aneh. Bagian menariknya baru mulai sekarang.

    Multiple return: ciri khas Go

    Di kebanyakan bahasa, function hanya bisa mengembalikan satu nilai. Go berbeda. Satu function boleh mengembalikan dua nilai atau lebih sekaligus. Ini bukan sekadar fitur gaya-gayaan. Ini fondasi dari cara Go menangani error.

    Contoh paling klasik adalah pembagian. Membagi angka dengan nol itu operasi yang tidak sah. Jadi function pembagi kita sebaiknya menjawab dua hal: berapa hasilnya, dan apakah operasinya berhasil.

    package main
    
    import (
        "errors"
        "fmt"
    )
    
    func bagi(a, b float64) (float64, error) {
        if b == 0 {
            return 0, errors.New("tidak bisa membagi dengan nol")
        }
        return a / b, nil
    }
    
    func main() {
        hasil, err := bagi(10, 2)
        if err != nil {
            fmt.Println("terjadi error:", err)
            return
        }
        fmt.Println("hasil:", hasil)
    }

    Perhatikan bagian (float64, error). Itu artinya function ini mengembalikan dua nilai: hasil pembagian bertipe float64, dan sebuah error. Kalau semuanya lancar, error yang dikembalikan adalah nil, yang artinya kosong, tidak ada masalah. Kalau ada masalah, kita kembalikan nilai error yang berisi pesan.

    Di sisi pemanggil, dua nilai itu ditangkap sekaligus: hasil, err := bagi(10, 2). Ini pola yang akan kamu tulis ratusan kali selama menulis Go, jadi biasakan dari sekarang.

    Konvensi error di Go

    Di Go, error bukan sesuatu yang spesial. Error adalah nilai biasa, sama seperti angka atau string, hanya saja tipenya error. Karena dia nilai biasa, dia bisa dikembalikan dari function, disimpan di variabel, atau dibandingkan.

    Ada beberapa konvensi tidak tertulis yang dipegang hampir semua programmer Go:

    • Kalau function bisa gagal, nilai balikan terakhirnya bertipe error. Bukan yang pertama, bukan di tengah. Selalu yang terakhir.
    • Untuk membuat error dengan pesan tetap, pakai errors.New("pesan errornya") dari package errors.
    • Untuk pesan yang butuh format, pakai fmt.Errorf. Misalnya fmt.Errorf("stok tinggal %d", sisa).

    Satu hal lagi yang cukup kamu tahu namanya dulu. fmt.Errorf punya kata kerja format khusus, yaitu %w, yang dipakai untuk membungkus error lain di dalam error baru. Teknik ini namanya error wrapping, dan detailnya kita bahas di bagian lanjutan seri ini. Sekarang cukup ingat bahwa %w itu ada.

    Pola if err != nil, dijelaskan tuntas

    Sekarang kita jawab pertanyaan yang tertunda dari bagian 2: kenapa kode Go penuh dengan if err != nil?

    Bahasa seperti Java, Python, atau JavaScript memakai try-catch. Kamu tulis kode di blok try, dan kalau ada yang meledak di mana pun di dalamnya, eksekusi melompat ke blok catch. Go mengambil jalan lain. Tidak ada try-catch. Function yang bisa gagal mengembalikan error sebagai nilai, dan kamu memeriksanya langsung setelah pemanggilan.

    Alasannya soal kejujuran kode. Dengan try-catch, kamu bisa memanggil sepuluh function tanpa tahu mana yang bisa gagal. Di Go, kemungkinan gagal tertulis jelas di tanda tangan function. Pembaca kode langsung tahu titik mana saja yang bisa bermasalah, dan compiler akan protes kalau kamu menangkap err lalu tidak menyentuhnya sama sekali.

    Supaya adil, pendekatan ini ada harganya. Kode Go jadi lebih panjang, dan mengetik if err != nil berulang-ulang memang terasa repetitif. Ini kritik paling umum terhadap Go, dan kritik itu valid. Sebagian orang menganggapnya bertele-tele, sebagian lagi justru menyukainya karena alur kegagalan terlihat gamblang tanpa lompatan tersembunyi. Setelah beberapa minggu menulis Go, kebanyakan orang berhenti menganggapnya masalah.

    Pola pendampingnya bernama early return: begitu ketemu error, langsung return dan hentikan function. Kode sukses tidak perlu dibungkus blok else, jadi alur utamanya tetap lurus dari atas ke bawah. Contohnya sudah kamu lihat di function main tadi. Kalau err tidak nil, cetak pesannya lalu return. Baris di bawahnya hanya berjalan saat semuanya aman.

    Named return: singkat saja

    Go mengizinkan nilai balikan diberi nama di tanda tangan function.

    func bagi(a, b float64) (hasil float64, err error) {
        if b == 0 {
            err = errors.New("tidak bisa membagi dengan nol")
            return
        }
        hasil = a / b
        return
    }

    hasil dan err otomatis menjadi variabel di dalam function, dan return polos akan mengembalikan nilai keduanya saat itu. Kelihatan praktis, tapi pada function yang panjang, pembaca harus menebak apa isi return polos itu. Saran saya untuk sekarang: tulis return secara eksplisit seperti contoh-contoh sebelumnya. Named return baru berguna di kasus khusus yang akan kamu temui jauh di depan.

    Variadic function: parameter yang jumlahnya bebas

    Kadang kamu tidak tahu berapa banyak nilai yang akan dikirim ke function. Menjumlahkan isi keranjang belanja, misalnya. Bisa dua barang, bisa sepuluh. Untuk itu Go punya variadic function, ditandai tiga titik sebelum tipe.

    func totalBelanja(harga ...int) int {
        total := 0
        for _, h := range harga {
            total += h
        }
        return total
    }

    harga ...int artinya function ini menerima nol, satu, atau banyak nilai int. Di dalam function, harga berperilaku seperti kumpulan nilai yang bisa kita jelajahi dengan for range. Perulangan sendiri akan kita kupas dalam di bagian lain, untuk sekarang cukup pahami bahwa baris itu mengunjungi tiap harga satu per satu dan menambahkannya ke total.

    Memanggilnya luwes sekali: totalBelanja(15000, 20000) sah, totalBelanja(5000, 7500, 12000, 3000) juga sah. fmt.Println yang kamu pakai sejak bagian 1 juga variadic. Itu sebabnya dia mau menerima berapa pun argumen.

    Latihan: kalkulator diskon

    Sekarang kita rangkai semuanya jadi satu program utuh. Skenarionya sederhana: kasir menjumlahkan keranjang belanja, lalu menerapkan diskon. Diskon di bawah 0 persen atau di atas 100 persen jelas tidak masuk akal, jadi harus ditolak lewat error.

    package main
    
    import "fmt"
    
    func totalKeranjang(harga ...float64) float64 {
        total := 0.0
        for _, h := range harga {
            total += h
        }
        return total
    }
    
    func hitungDiskon(total, persen float64) (float64, error) {
        if persen < 0 {
            return 0, fmt.Errorf("persen diskon tidak boleh negatif, dapat %v", persen)
        }
        if persen > 100 {
            return 0, fmt.Errorf("persen diskon tidak boleh lebih dari 100, dapat %v", persen)
        }
        potongan := total * persen / 100
        return total - potongan, nil
    }
    
    func main() {
        total := totalKeranjang(15000, 42500, 8000)
        fmt.Println("Total keranjang:", total)
    
        bayar, err := hitungDiskon(total, 20)
        if err != nil {
            fmt.Println("Gagal menghitung diskon:", err)
            return
        }
        fmt.Println("Bayar setelah diskon 20 persen:", bayar)
    
        bayar, err = hitungDiskon(total, 150)
        if err != nil {
            fmt.Println("Gagal menghitung diskon:", err)
            return
        }
        fmt.Println("Bayar setelah diskon 150 persen:", bayar)
    }

    Jalankan, dan outputnya seperti ini.

    Total keranjang: 65500
    Bayar setelah diskon 20 persen: 52400
    Gagal menghitung diskon: persen diskon tidak boleh lebih dari 100, dapat 150

    Mari bedah sebentar apa yang terjadi:

    • totalKeranjang adalah variadic function. Dia menerima tiga harga dan menjumlahkannya jadi 65500.
    • hitungDiskon memakai multiple return: hasil bertipe float64 plus error di posisi terakhir, sesuai konvensi.
    • Validasi dilakukan di awal function dengan early return. Persen 20 lolos, jadi errornya nil dan program mencetak 52400.
    • Panggilan kedua mengirim persen 150. Validasi menolaknya, err terisi, dan blok if err != nil mencetak pesan lalu menghentikan main. Baris cetak “Bayar setelah diskon 150 persen” tidak pernah dijalankan.

    Coba ubah sendiri: ganti persen jadi -5, tambah barang di keranjang, atau buat function baru hitungPajak dengan pola yang sama. Semakin sering tanganmu menulis pola ini, semakin cepat dia jadi refleks.

    Rangkuman dan lanjut ke mana

    Hari ini kamu sudah belajar cukup banyak:

    • Function dideklarasikan dengan func, dan tipe ditulis setelah nama.
    • Function di Go bisa mengembalikan lebih dari satu nilai.
    • Konvensi error: nilai balikan terakhir bertipe error, dibuat dengan errors.New atau fmt.Errorf.
    • Go memilih pengecekan error eksplisit lewat if err != nil plus early return, bukan try-catch.
    • Named return ada, tapi untuk sekarang lebih baik return eksplisit.
    • Variadic function menerima jumlah argumen yang bebas.

    Dengan bekal ini, kamu sudah bisa memecah program jadi potongan-potongan kecil yang rapi dan tahan gagal. Di bagian berikutnya kita naik satu tingkat: “Belajar Golang dari Nol #4: Struct dan Method: Cara Go Menyusun Data”. Di sana function yang baru kamu kuasai akan menempel langsung ke data.

    Pola function dan error yang kamu latih hari ini persis pola yang kami pakai sehari-hari saat membangun backend produksi dengan Go untuk klien Arrazy.

  • Belajar Golang dari Nol #2: Variabel, Tipe Data, dan Zero Value

    Belajar Golang dari Nol #2: Variabel, Tipe Data, dan Zero Value

    Di Belajar Golang dari Nol #1 kamu sudah install Go dan menjalankan program pertama lewat go run. Sekarang kita masuk ke bahan bangunan paling dasar dari semua program: variabel, tipe data, dan satu konsep khas Go yang namanya zero value.

    Anggap variabel itu seperti kotak berlabel. Labelnya nama variabel, isinya nilai. Bedanya dengan bahasa lain, di Go setiap kotak juga punya aturan ketat soal barang apa yang boleh masuk. Kotak untuk angka tidak bisa diisi teks. Aturan ini yang bikin program Go jarang error aneh saat sudah jalan.

    Tiga Cara Deklarasi Variabel di Go

    Go punya tiga gaya untuk membuat variabel. Semuanya sering dipakai, jadi kenali satu per satu.

    1. var tanpa nilai awal

    var nama string
    var umur int
    
    fmt.Println(nama) // (string kosong)
    fmt.Println(umur) // 0

    Di sini kamu hanya menyiapkan kotaknya. Isinya belum ada, tapi Go otomatis memberi nilai default. Soal nilai default ini kita bahas lebih dalam di bagian zero value nanti.

    2. var dengan nilai awal

    var nama string = "Rizky"
    var umur = 25 // tipe int ditebak otomatis oleh Go

    Kalau nilai awal sudah ada, kamu boleh menulis tipenya atau membiarkan Go menebak sendiri. Menebak tipe dari nilai ini disebut type inference. Nilai 25 otomatis dianggap int, nilai "Rizky" otomatis dianggap string.

    3. Short declaration dengan :=

    nama := "Rizky"
    umur := 25

    Ini cara paling ringkas dan paling sering kamu lihat di kode Go sehari-hari. Tanda := artinya “buat variabel baru sekaligus isi nilainya”. Tanpa kata var, tanpa menulis tipe.

    Ada satu aturan penting: := hanya boleh dipakai di dalam function. Di luar function, misalnya di level package, kamu wajib pakai var. Kode di bawah ini menunjukkan bedanya.

    package main
    
    import "fmt"
    
    var appName = "Kasir Sederhana" // level package, wajib var
    
    func main() {
    	versi := "1.0" // di dalam function, boleh :=
    	fmt.Println(appName, versi)
    }

    Lalu kapan pakai yang mana? Panduan sederhananya begini:

    • Pakai := untuk variabel biasa di dalam function. Ini pilihan default kamu.
    • Pakai var kalau kamu butuh variabel di level package, atau kalau kamu sengaja ingin variabel kosong dulu dan diisi belakangan.
    • Pakai var dengan tipe eksplisit kalau tipe tebakan Go bukan yang kamu mau. Misalnya var saldo float64 = 10000, karena saldo := 10000 akan jadi int.

    Tipe Data Dasar yang Wajib Kamu Kenal

    Go punya banyak tipe data. Untuk sekarang, empat tipe ini cukup untuk hampir semua latihan awal.

    string

    Teks. Selalu ditulis dengan tanda kutip dua, bukan kutip satu.

    namaToko := "Kopi Senja"
    alamat := "Jl. Merdeka No. 10, Purwokerto"

    int

    Bilangan bulat. Cocok untuk hal yang dihitung satuan: jumlah barang, umur, jumlah user.

    stok := 120
    jumlahKaryawan := 8

    Go juga punya varian seperti int8, int32, dan int64. Angka di belakangnya menunjukkan berapa bit memori yang dipakai, yang menentukan seberapa besar angka yang bisa ditampung. int64 misalnya sering dipakai untuk ID di database yang nilainya bisa sangat besar. Untuk belajar, pakai int biasa saja dulu. Sisanya akan terasa gunanya saat kamu mulai kerja dengan database atau API.

    float64

    Bilangan desimal. Cocok untuk harga, berat, rating, dan apa pun yang punya koma.

    hargaKopi := 25000.0
    beratBiji := 0.25 // kg

    Perhatikan 25000.0. Titik nol di belakang itu penting. Tanpa itu, Go menganggapnya int. Dengan titik desimal, Go tahu kamu mau float64.

    bool

    Nilai benar atau salah. Isinya cuma dua kemungkinan: true atau false.

    sudahBayar := true
    tokoTutup := false

    Tipe ini kelihatan sepele, tapi hampir semua logika program dibangun di atasnya. Setiap if yang kamu tulis nanti ujungnya selalu sebuah bool.

    Zero Value: Nilai Default yang Bikin Tenang

    Sekarang konsep khas Go yang tadi sempat disinggung. Di Go, variabel yang dideklarasikan tanpa nilai awal tidak pernah kosong tanpa arti. Go selalu mengisinya dengan nilai default sesuai tipenya. Nilai default ini disebut zero value.

    • string menjadi "" (string kosong)
    • int dan float64 menjadi 0
    • bool menjadi false
    • tipe seperti pointer, slice, dan map menjadi nil

    Coba jalankan program kecil ini dan lihat hasilnya.

    package main
    
    import "fmt"
    
    func main() {
    	var nama string
    	var stok int
    	var harga float64
    	var aktif bool
    
    	fmt.Printf("nama: %q\n", nama)
    	fmt.Printf("stok: %d\n", stok)
    	fmt.Printf("harga: %f\n", harga)
    	fmt.Printf("aktif: %t\n", aktif)
    }

    Outputnya:

    nama: ""
    stok: 0
    harga: 0.000000
    aktif: false

    Kenapa ini desain yang enak? Karena kamu tidak pernah perlu bertanya “variabel ini sudah diisi atau belum, isinya sampah memori atau bukan”. Setiap variabel selalu punya nilai yang jelas dan bisa diprediksi sejak lahir. Kalau kamu pernah pegang JavaScript, ini beda jauh dengan undefined yang sering muncul tiba-tiba di tempat yang tidak kamu duga.

    Zero value juga sering dimanfaatkan langsung. Counter mulai dari 0, flag mulai dari false, teks mulai dari kosong. Banyak kode Go sengaja didesain agar zero value-nya langsung berguna tanpa perlu setup tambahan.

    Konstanta: Nilai yang Dikunci

    Kadang ada nilai yang tidak boleh berubah selama program berjalan. Rate diskon, nama aplikasi, batas maksimal upload. Untuk itu Go punya const.

    const appName = "Kasir Senja"
    const diskonMember = 0.1
    const maksimalQty = 100

    Bedanya dengan var ada dua. Pertama, nilai const harus sudah diketahui saat program di-compile, jadi kamu tidak bisa mengisinya dari input user atau hasil pemanggilan function biasa. Kedua, nilainya terkunci. Kalau kamu coba mengubahnya, program tidak akan bisa di-compile.

    const diskonMember = 0.1
    diskonMember = 0.2 // error: cannot assign to diskonMember

    Ini bagus. Lebih baik error muncul saat compile daripada diam-diam ada kode yang mengubah rate diskon di tengah jalan. Aturan praktisnya: kalau sebuah nilai memang tidak akan pernah berubah, jadikan const. Sisanya baru var atau :=.

    Konversi Tipe: Go Itu Ketat, dan Itu Bagus

    Go tidak mau menebak-nebak saat dua tipe berbeda bertemu. Operasi int dicampur float64 langsung ditolak compiler. Kamu harus konversi dulu secara eksplisit.

    qty := 3          // int
    harga := 12500.0  // float64
    
    // total := qty * harga        // error: mismatched types
    total := float64(qty) * harga  // benar: 37500

    Polanya sederhana: tulis nama tipe tujuan, lalu bungkus nilainya dengan tanda kurung. float64(qty) artinya “ubah qty menjadi float64”. Berlaku juga sebaliknya, int(harga), dengan catatan angka di belakang koma akan dibuang.

    Angka ke string dan sebaliknya dengan strconv

    Untuk konversi antara angka dan string, tipe castingnya berbeda urusan. string(65) tidak menghasilkan teks “65”, tapi karakter dengan kode 65. Yang benar, pakai package strconv dari standard library.

    package main
    
    import (
    	"fmt"
    	"strconv"
    )
    
    func main() {
    	// int ke string: Itoa (Integer to ASCII)
    	umur := 25
    	teksUmur := strconv.Itoa(umur)
    	fmt.Println("Umur saya " + teksUmur + " tahun")
    
    	// string ke int: Atoi (ASCII to Integer)
    	angka, err := strconv.Atoi("123")
    	if err != nil {
    		fmt.Println("Gagal konversi:", err)
    		return
    	}
    	fmt.Println("Hasil tambah satu:", angka+1)
    }

    Perhatikan baris angka, err := strconv.Atoi("123"). Kenapa hasilnya dua? Karena konversi string ke angka bisa gagal. String "123" jelas bisa jadi angka, tapi bagaimana dengan "abc"? Go menjawab kemungkinan gagal itu dengan mengembalikan dua nilai: hasilnya dan errornya. Kalau konversi sukses, err bernilai nil. Kalau gagal, err berisi keterangan kegagalannya dan kamu wajib menanganinya.

    Pola if err != nil ini akan kamu tulis ratusan kali sepanjang karier Go kamu. Ini cara Go memaksa programmer memikirkan kegagalan sejak awal, bukan menundanya sampai program crash di production. Detail cara kerja multiple return dan error akan kita kupas tuntas di bagian 3. Untuk sekarang, cukup hafalkan bentuknya.

    Kenalan dengan fmt.Printf

    Sebelum masuk latihan, kenalan dulu dengan fmt.Printf. Kalau fmt.Println mencetak apa adanya, Printf mencetak dengan format yang kamu atur lewat kode persen. Yang paling sering dipakai:

    • %s untuk string
    • %d untuk bilangan bulat
    • %f untuk desimal, dan %.2f untuk membatasi dua angka di belakang koma
    • %t untuk bool
    • %v untuk nilai apa pun, berguna saat malas mikir tipenya
    nama := "Rizky"
    qty := 3
    harga := 12500.0
    member := true
    
    fmt.Printf("%s beli %d barang seharga %.2f, member: %t\n", nama, qty, harga, member)
    // Rizky beli 3 barang seharga 12500.00, member: true

    Jangan lupa \n di akhir untuk pindah baris. Printf tidak menambahkannya otomatis seperti Println.

    Latihan: Program Hitung Total Belanja

    Sekarang gabungkan semua materi jadi satu program utuh. Kasusnya sederhana: hitung total belanja satu barang, lalu beri diskon 10 persen kalau pembelinya member. Buat file main.go baru, ketik ulang kodenya sendiri, jangan copy paste. Mengetik ulang itu cara belajar paling cepat.

    package main
    
    import "fmt"
    
    const diskonMember = 0.1 // 10 persen, dikunci dengan const
    
    func main() {
    	// data belanja
    	namaBarang := "Kopi Arabika 200g" // string
    	qty := 3                          // int
    	harga := 45000.0                  // float64
    	isMember := true                  // bool
    
    	// qty harus dikonversi dulu agar bisa dikali float64
    	subtotal := float64(qty) * harga
    
    	// zero value float64 adalah 0, jadi non-member otomatis diskonnya 0
    	var diskon float64
    	if isMember {
    		diskon = subtotal * diskonMember
    	}
    
    	total := subtotal - diskon
    
    	// cetak struk
    	fmt.Println("===== Struk Belanja =====")
    	fmt.Printf("Barang   : %s\n", namaBarang)
    	fmt.Printf("Qty      : %d\n", qty)
    	fmt.Printf("Harga    : Rp%.0f\n", harga)
    	fmt.Printf("Subtotal : Rp%.0f\n", subtotal)
    	fmt.Printf("Member   : %t\n", isMember)
    	fmt.Printf("Diskon   : Rp%.0f\n", diskon)
    	fmt.Printf("Total    : Rp%.0f\n", total)
    }

    Jalankan dengan go run main.go. Outputnya:

    ===== Struk Belanja =====
    Barang   : Kopi Arabika 200g
    Qty      : 3
    Harga    : Rp45000
    Subtotal : Rp135000
    Member   : true
    Diskon   : Rp13500
    Total    : Rp121500

    Lihat bagaimana semua materi hari ini terpakai. Ada const untuk rate diskon, := untuk variabel di dalam function, empat tipe data dasar, konversi float64(qty), zero value yang dimanfaatkan di variabel diskon, dan Printf untuk merapikan output.

    Sebagai latihan tambahan, coba ubah isMember jadi false dan jalankan lagi. Perhatikan diskon otomatis jadi 0 tanpa kamu menulis kode tambahan. Itu zero value yang bekerja untukmu. Lalu coba ganti qty := 3 menjadi qty := 3.0 dan lihat error apa yang muncul. Membaca pesan error compiler sejak dini akan sangat membantu di bagian-bagian berikutnya.

    Lanjut ke Bagian 3

    Hari ini kamu sudah pegang fondasi terpenting: tiga cara deklarasi variabel, empat tipe data dasar, zero value, konstanta, konversi tipe, dan kenalan pertama dengan pola if err != nil. Semua program Go, sesederhana atau serumit apa pun, dibangun dari bahan-bahan ini.

    Di bagian berikutnya kita masuk ke topik yang membuat kode kamu mulai terstruktur: “Belajar Golang dari Nol #3: Function, Multiple Return, dan Error Pertamamu”. Di sana misteri kenapa Atoi mengembalikan dua nilai akan terjawab tuntas.

    Seri ini kami tulis dari pengalaman sehari-hari, karena tim Arrazy memang membangun backend produksi dengan Go untuk berbagai sistem aplikasi klien kami.

  • Belajar Golang dari Nol #1: Kenalan dengan Go dan Cara Installnya

    Belajar Golang dari Nol #1: Kenalan dengan Go dan Cara Installnya

    Go (sering disebut Golang) adalah bahasa pemrograman open source yang dibuat di Google oleh Robert Griesemer, Rob Pike, dan Ken Thompson, lalu dirilis ke publik tahun 2009. Bahasa ini dirancang untuk satu hal: membuat software yang cepat dan mudah dirawat, tanpa ritual yang ribet. Di dunia nyata, Go paling sering dipakai untuk backend API, tool CLI, dan service yang butuh performa tinggi. Google jelas memakainya. Uber memakainya untuk banyak microservice mereka. Di Indonesia, Gojek dan Tokopedia sudah lama menjadikan Go tulang punggung backend mereka.

    Jadi kalau kamu ingin serius di dunia backend, Go adalah salah satu pilihan paling masuk akal saat ini. Seri “Belajar Golang dari Nol” ini akan menemani kamu dari titik paling awal: belum pernah menyentuh Go sama sekali. Mungkin kamu baru bisa sedikit PHP atau JavaScript. Tidak masalah. Kita mulai pelan.

    Kenapa Go Enak untuk Pemula Backend

    Ada beberapa alasan kenapa saya suka merekomendasikan Go sebagai bahasa backend pertama atau kedua.

    • Kompilasinya cepat. Kamu edit kode, jalankan, hasilnya muncul dalam hitungan detik. Siklus coba-salah-perbaiki jadi singkat, dan itu penting banget saat belajar.
    • Hasilnya satu binary. Saat kamu build program Go, hasilnya satu file executable yang bisa langsung dijalankan di server. Tidak perlu install runtime, interpreter, atau setumpuk dependency di server tujuan. Kalau kamu terbiasa dengan PHP yang butuh web server plus interpreter di sisi produksi, ini rasanya seperti pindah dari koper besar ke satu tas selempang.
    • Sintaksnya kecil. Go hanya punya 25 keyword. Bandingkan dengan bahasa lain yang bisa dua kali lipatnya. Artinya, hal yang perlu kamu hafal sedikit. Kode Go milik orang lain juga gampang dibaca karena semua orang menulis dengan gaya yang kurang lebih sama.
    • Ada garbage collector. Kamu tidak perlu mengatur memori secara manual seperti di C. Go yang membereskan memori yang sudah tidak terpakai. Kamu fokus ke logika program saja.

    Biar adil, Go juga punya sisi yang kurang enak. Pertama, kodenya cenderung verbose. Hal yang di JavaScript bisa satu baris, di Go kadang butuh lima baris. Kedua, error handling-nya berulang. Kamu akan sering sekali menulis pola if err != nil sampai hafal di luar kepala. Sebagian orang menganggap ini membosankan. Tapi ada hikmahnya: kamu dipaksa memikirkan setiap kemungkinan error, dan itu kebiasaan bagus untuk programmer backend.

    Cara Install Go

    Semua file instalasi resmi ada di satu tempat: go.dev/dl. Jangan unduh dari tempat lain. Pilih bagian sesuai sistem operasi kamu di bawah ini.

    Install Go di Linux

    Di Linux, cara paling bersih adalah memakai tarball resmi. Hindari install dari apt bawaan distro karena versinya sering tertinggal jauh.

    Pertama, unduh tarball dari go.dev/dl. Cek dulu nomor versi terbaru di halaman itu, lalu sesuaikan nama filenya. Contoh di bawah memakai versi 1.25.1 untuk arsitektur amd64:

    wget https://go.dev/dl/go1.25.1.linux-amd64.tar.gz

    Kedua, hapus instalasi Go lama (kalau ada) dan ekstrak ke /usr/local:

    sudo rm -rf /usr/local/go
    sudo tar -C /usr/local -xzf go1.25.1.linux-amd64.tar.gz

    Perintah tar -C /usr/local artinya ekstrak isi arsip ke folder /usr/local. Hasilnya, Go terpasang di /usr/local/go.

    Ketiga, tambahkan Go ke PATH. PATH itu daftar folder tempat terminal mencari program saat kamu mengetik sebuah perintah. Kalau folder bin milik Go tidak ada di PATH, terminal tidak akan kenal perintah go. Buka file ~/.bashrc (atau ~/.zshrc kalau kamu pakai zsh), lalu tambahkan baris ini di paling bawah:

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

    Simpan, lalu muat ulang konfigurasi shell:

    source ~/.bashrc

    Kalau pakai zsh, ganti dengan source ~/.zshrc.

    Install Go di Windows

    Di Windows jauh lebih sederhana:

    • Buka go.dev/dl lalu unduh file installer berakhiran .msi untuk Windows.
    • Jalankan file itu dengan klik dua kali, lalu ikuti wizard-nya. Klik Next terus sampai selesai. Secara default Go terpasang di C:\Program Files\Go.
    • Installer ini otomatis mengatur PATH untukmu, jadi tidak ada langkah manual tambahan.

    Satu catatan penting: setelah instalasi selesai, tutup semua jendela Command Prompt atau PowerShell yang sedang terbuka, lalu buka yang baru. Terminal yang sudah terbuka sebelum instalasi belum membaca PATH yang baru.

    Install Go di macOS

    Kalau kamu sudah pakai Homebrew, satu perintah ini cukup:

    brew install go

    Homebrew akan mengurus PATH secara otomatis. Kalau belum pakai Homebrew, alternatifnya unduh installer .pkg dari go.dev/dl, klik dua kali, lalu ikuti wizard-nya seperti install aplikasi Mac pada umumnya.

    Verifikasi Instalasi

    Apa pun sistem operasinya, cara mengeceknya sama. Buka terminal baru, lalu ketik:

    go version

    Kalau instalasi berhasil, kamu akan melihat output kurang lebih seperti ini:

    go version go1.25.1 linux/amd64

    Nomor versi dan nama platformnya bisa berbeda sesuai apa yang kamu install. Yang penting perintah itu mengeluarkan versi, bukan pesan “command not found”. Kalau muncul “command not found” di Linux atau macOS, kemungkinan besar masalahnya di langkah PATH tadi. Cek lagi apakah baris export sudah tersimpan dan kamu sudah menjalankan source atau membuka terminal baru.

    Oh ya, kalau kamu sempat baca tutorial Go yang lama, mungkin kamu menemukan pembahasan panjang soal GOPATH dan struktur folder yang wajib diikuti. Abaikan saja. Sejak Go versi modern memakai sistem module, kamu bisa menaruh proyek di folder mana pun yang kamu suka. Tidak perlu setting tambahan apa-apa lagi setelah langkah instalasi di atas.

    Program Go Pertama: Hello World

    Sekarang bagian paling menyenangkan. Buat folder baru untuk latihan, misalnya halo-go, lalu masuk ke dalamnya:

    mkdir halo-go
    cd halo-go

    Buat file bernama main.go dengan teks editor favoritmu (VS Code juga boleh), lalu isi dengan kode ini:

    package main
    
    import "fmt"
    
    func main() {
    	fmt.Println("Halo, Go!")
    }

    Enam baris saja, tapi setiap barisnya punya makna. Mari kita bedah satu per satu.

    package main

    Setiap file Go wajib diawali deklarasi package. Package itu semacam label yang mengelompokkan file-file kode yang saling berhubungan. Nah, package main punya arti khusus: ini penanda bahwa program kamu adalah program yang bisa dijalankan langsung, bukan sekadar pustaka yang dipakai program lain. Anggap saja seperti label “pintu masuk utama” pada sebuah gedung.

    import “fmt”

    Baris ini mengambil package fmt dari pustaka standar Go. Nama fmt singkatan dari format, isinya fungsi-fungsi untuk mencetak dan memformat teks. Tanpa baris import ini, kamu tidak bisa memakai fmt.Println. Go bahkan menolak kompilasi kalau kamu meng-import package yang tidak dipakai. Ketat memang, tapi hasilnya kode kamu selalu bersih dari import sampah.

    func main()

    func adalah kata kunci untuk mendefinisikan fungsi. Fungsi bernama main di dalam package main adalah titik awal eksekusi program. Saat program dijalankan, Go langsung mencari fungsi ini dan mengeksekusi isinya baris per baris. Satu program hanya boleh punya satu func main.

    fmt.Println(“Halo, Go!”)

    Ini isi programnya: memanggil fungsi Println milik package fmt untuk mencetak teks ke layar, lalu pindah ke baris baru. Pola namaPackage.NamaFungsi ini akan sangat sering kamu temui di Go.

    Menjalankan Program: go run vs go build

    Ada dua cara menjalankan program Go, dan keduanya penting kamu pahami. Cara pertama, go run:

    go run main.go

    Outputnya:

    Halo, Go!

    Perintah go run mengompilasi kode ke file sementara, menjalankannya, lalu membuang hasil kompilasinya. Cocok untuk fase belajar dan development karena praktis: satu perintah, langsung lihat hasil.

    Cara kedua, go build:

    go build -o halo main.go

    Perintah ini mengompilasi kode dan menyimpan hasilnya sebagai file executable bernama halo (flag -o menentukan nama outputnya). Di Windows hasilnya halo.exe. Sekarang jalankan binary itu:

    ./halo

    Di Windows cukup ketik halo. Hasilnya sama: teks “Halo, Go!” muncul di layar.

    Bedanya apa? go run itu seperti masak lalu langsung makan di dapur. go build seperti masak lalu mengemas makanannya rapi dalam kotak, siap dibawa dan dimakan di mana saja. File hasil go build bisa kamu salin ke server atau komputer lain yang bahkan tidak punya Go terinstall, dan program tetap jalan. Inilah keunggulan “satu binary” yang saya sebut di awal, dan alasan kenapa deployment aplikasi Go terkenal simpel.

    Persiapan Bagian 2: go mod init

    Satu hal lagi sebelum kita tutup. Proyek Go modern selalu diawali dengan membuat module. Module itu semacam identitas proyek plus catatan dependency-nya, mirip peran package.json di dunia Node.js. Di dalam folder proyekmu, jalankan:

    go mod init halo-go

    Perintah ini membuat file go.mod yang berisi nama module dan versi Go yang dipakai. Untuk latihan lokal, nama module bebas. Nanti kalau proyekmu dipublikasikan, biasanya nama module memakai alamat repositori seperti github.com/username/nama-proyek. Untuk sekarang, cukup tahu bahwa setiap proyek Go yang serius dimulai dengan go mod init. Kita akan memakainya terus di sepanjang seri ini.

    Sampai Ketemu di Bagian 2

    Sampai di sini kamu sudah punya Go yang terpasang rapi, paham struktur dasar sebuah program Go, dan tahu bedanya go run dengan go build. Itu fondasi yang cukup kuat untuk melangkah.

    Di bagian berikutnya, “Belajar Golang dari Nol #2: Variabel, Tipe Data, dan Zero Value di Go”, kita mulai menulis kode yang sesungguhnya: menyimpan data, mengenal tipe-tipe bawaan Go, dan memahami konsep zero value yang membuat Go beda dari kebanyakan bahasa lain. Sambil menunggu, coba ubah-ubah program hello world tadi. Ganti teksnya, tambah baris Println lain, lalu build ulang. Membiasakan jari itu bagian dari belajar.

    Seri ini kami tulis dari pengalaman langsung, karena tim Arrazy memakai Go untuk membangun backend produksi milik klien lewat layanan pengembangan sistem aplikasi.