Progress belajar
Modul 8 dari 73
0% 0/73 modul selesai
Setelah selesai, tandai modul ini agar progres kursus tetap rapi.
Progress disimpan lokal di browser ini.
Slice, Map, dan Koleksi
Bahan Baku Logic API
Koleksi data adalah bahan baku setiap service backend: daftar produk, item cart, order item, dan lookup stok saat checkout. Di modul ini kita kuasai slice dan map cara Go, lengkap dengan backing array, gotcha aliasing, dan stdlib modern yang menggantikan banyak helper buatan tangan.
Koleksi sebagai Bahan Baku Backend
Dari daftar produk dan item cart sampai lookup stok
Di React kamu hampir selalu memakai array untuk me-render list dan object atau Map untuk lookup. Di Go kebutuhannya sama, tetapi mekanisme memorinya jauh lebih eksplisit, dan justru di situ letak kekuatannya untuk backend.
Di modul struct dan method kita sudah memodelkan entitas inti online shop skincare: Product, CartItem, Order, Payment, Inventory, dan User. Hampir semua entitas itu hidup dalam koleksi. Endpoint katalog mengembalikan banyak Product. Cart menampung banyak CartItem. Satu Order berisi banyak item. Checkout butuh lookup stok per produk. Modul ini fokus pada dua tipe koleksi yang paling sering kamu pakai di Go: slice dan map.
Kalau di React kamu menulis products.map(p => <Card .../>) untuk merender kartu produk, di Go kamu memakai for range atas []Product lalu membangun response DTO dengan append. Tidak ada method .map, .filter, atau .reduce bawaan di slice; kamu menulis loop yang eksplisit.
Go menyediakan array, slice, dan map sebagai tipe bawaan. Spesifikasi resmi mendefinisikan slice sebagai descriptor untuk segmen berurutan dari sebuah array, dan map sebagai kumpulan elemen tak berurutan dengan tipe key dan value yang sama. Builtin append, len, cap, make, copy, delete, dan (sejak Go 1.21) clear, min, max menjadi alat utamanya. Acuan resmi: Go specification, Go Blog: Go Slices, Go Blog: Go maps in action, dan builtin package.
Slice
Daftar berurutan yang bisa bertambah, seperti []Product, []CartItem, atau []Order. Inilah koleksi default sehari-hari.
Map
Lookup cepat berbasis key, seperti map[int64]int untuk stok per ProductID atau map[string]Product untuk lookup per SKU.
range
Loop idiomatik untuk membaca slice dan map tanpa callback tersembunyi, dengan error handling dan early return yang terlihat jelas.
Array vs Slice
Array fixed-length value, slice dinamis yang dipakai setiap hari
Array Go punya panjang tetap, dan panjang itu adalah bagian dari tipenya. Slice adalah tampilan fleksibel di atas sebuah array, dan inilah yang hampir selalu kamu pakai di kode backend.
Di JavaScript kamu terbiasa dengan Array yang bisa bertambah panjang kapan saja. Di Go, array seperti [3]Product berukuran tetap, dan [3]Product adalah tipe yang berbeda dari [4]Product. Lebih dari itu, array adalah value type: menyalin array atau melewatkannya ke fungsi akan menyalin seluruh isinya. Karena itu array jarang dipakai sebagai parameter service. Untuk daftar yang bertambah atau hasil query database, gunakan slice seperti []Product.
Arraytunggal yang bisa tumbuh dan menyusut sesuka hati.- Selalu by reference; melewatkannya tidak menyalin isi.
- Tidak ada konsep panjang yang menjadi bagian tipe.
[3]Productadalah array: panjang tetap, bagian dari tipe.- Array adalah value; menyalinnya menyalin seluruh elemen.
[]Productadalah slice: dinamis, ringan disalin, default sehari-hari.
Kita teruskan model Product dari modul sebelumnya. Sesuai konvensi proyek, uang selalu PriceRupiah int64, ID selalu int64, dan status produk bertipe ProductStatus.
internal/product/product.gopackage product type ProductStatus string const ( ProductStatusDraft ProductStatus = "draft" ProductStatusActive ProductStatus = "active" ProductStatusArchived ProductStatus = "archived" ProductStatusOutOfStock ProductStatus = "out_of_stock" ) func (s ProductStatus) IsSellable() bool { return s == ProductStatusActive } type Product struct { ID int64 SKU string Name string Category string PriceRupiah int64 Quantity int Status ProductStatus }
internal/product/collections.gopackage product func collectionsExample() { // Array: panjang 3 adalah bagian dari tipe, semua elemen ber-zero value. var fixed [3]Product fixed[0] = Product{ID: 1, Name: "Gentle Cleanser", Status: ProductStatusActive} // Slice: bentuk yang kita pakai untuk katalog, response, dan hasil query. products := []Product{ {ID: 1, SKU: "SKN-CLN-01", Name: "Gentle Cleanser", PriceRupiah: 89000, Status: ProductStatusActive}, {ID: 2, SKU: "SKN-TON-02", Name: "Hydrating Toner", PriceRupiah: 99000, Status: ProductStatusActive}, {ID: 3, SKU: "SKN-SER-03", Name: "Niacinamide Serum", PriceRupiah: 129000, Status: ProductStatusActive}, } _ = fixed _ = products }
Melewatkan array ke fungsi menyalin semua elemennya, jadi mutasi di dalam fungsi tidak terlihat oleh pemanggil. Melewatkan slice hanya menyalin header-nya (pointer, len, cap), sehingga mutasi elemen lewat index akan terlihat oleh pemanggil. Ini kebalikan dari intuisi sebagian pendatang dari JS.
Saat mendesain API, repository, service, atau response JSON, default-kan ke slice. Pakai array hanya saat ukuran benar-benar bagian dari model, misalnya kode OTP enam digit atau buffer berukuran tetap.
Slice Header, len, dan cap
Konsep kecil yang menentukan seluruh perilaku append
Slice terlihat seperti daftar, tetapi secara internal ia adalah descriptor kecil berisi tiga field: pointer ke backing array, len, dan cap. Memahami header ini membuat setiap kejutan append menjadi masuk akal.
Struktur runtime tiga field yang menjadi nilai sebuah slice: sebuah pointer ke elemen pertama di backing array, len (jumlah elemen yang terlihat), dan cap (jumlah elemen dari posisi awal slice sampai akhir backing array). Saat kamu melewatkan slice ke fungsi, yang disalin adalah header ini, bukan elemen-elemennya.
Array nyata di memori yang menyimpan elemen di balik satu atau lebih slice. Beberapa slice bisa menunjuk ke backing array yang sama, sehingga perubahan elemen lewat satu slice dapat terlihat dari slice lain selama mereka masih berbagi backing array yang sama.
len, dan cap. Dua slice (products dan featured := products[:1]) bisa menunjuk ke backing array yang sama, sehingga keduanya berbagi memori yang persis sama meskipun len mereka berbeda.len adalah jumlah elemen yang sedang terlihat oleh slice. cap adalah jumlah elemen yang bisa ditampung dari posisi awal slice sampai ujung backing array. Saat kamu memanggil append, Go memakai kapasitas yang tersedia jika masih cukup; jika tidak cukup, Go mengalokasikan backing array baru yang lebih besar dan menyalin elemen lama ke sana.
internal/product/len_cap.gopackage product import "fmt" func lenCapExample() { products := make([]Product, 0, 3) // len 0, cap 3 products = append(products, Product{ID: 1, Name: "Cleanser"}) products = append(products, Product{ID: 2, Name: "Toner"}) fmt.Println(len(products)) // 2 elemen terlihat fmt.Println(cap(products)) // 3 ruang tersedia, append berikutnya masih in place }
Di JavaScript, panjang dan kapasitas array tersembunyi sepenuhnya oleh engine. Di Go, cap terlihat dan penting, karena ia menentukan apakah append berikutnya menulis di tempat (in place) pada backing array lama atau memicu realokasi ke backing array baru.
Saat realokasi terjadi, Go memilih kapasitas baru yang lebih besar (umumnya berlipat untuk slice kecil, lalu tumbuh lebih landai untuk slice besar). Angka pastinya adalah detail implementasi yang bisa berubah antar versi, jadi jangan menulis kode yang bergantung pada nilai cap tertentu setelah append. Yang dijamin spesifikasi hanyalah len hasil dan bahwa elemen lama terbawa.
Membuat Slice: nil, empty, dan make
Tiga bentuk yang semuanya sah tetapi bermakna berbeda
Ada beberapa cara membuat slice. Semuanya bisa menerima append, tetapi maknanya berbeda saat kamu bicara nil, encoding JSON, dan alokasi awal.
Tiga bentuk paling sering terlihat adalah var s []Product (nil slice), s := []Product{} (empty slice non-nil), dan make([]Product, 0, n) (empty slice dengan kapasitas awal). Untuk service logic, ketiganya menerima append dengan aman, termasuk nil slice. Perbedaannya muncul saat kamu ingin membedakan “tidak ada data” dari “daftar kosong eksplisit”, atau ingin menyiapkan kapasitas untuk mengurangi realokasi.
internal/product/slice_create.gopackage product func sliceCreation() { var fromZeroValue []Product // nil slice: len 0, cap 0, == nil bernilai true emptyLiteral := []Product{} // empty slice non-nil: len 0, cap 0, != nil withMake := make([]Product, 0) // sama efektifnya dengan empty literal withCapacity := make([]Product, 0, 20) // len 0, cap 20: siap menampung 20 tanpa realokasi // Keempatnya, termasuk nil slice, aman untuk append. fromZeroValue = append(fromZeroValue, Product{ID: 1, Name: "Cleanser"}) emptyLiteral = append(emptyLiteral, Product{ID: 2, Name: "Toner"}) withMake = append(withMake, Product{ID: 3, Name: "Serum"}) withCapacity = append(withCapacity, Product{ID: 4, Name: "Sunscreen"}) _, _, _, _ = fromZeroValue, emptyLiteral, withMake, withCapacity }
| Bentuk | Makna praktis | Kapan dipakai |
|---|---|---|
var s []Product | nil slice, len 0, cap 0, s == nil true | Akumulator yang langsung di-append, default paling ringan |
s := []Product{} | empty slice non-nil | Response yang harus selalu jadi [] saat di-encode JSON |
make([]Product, 0, n) | empty slice dengan kapasitas awal n | Saat kamu tahu estimasi jumlah dan ingin menekan realokasi |
Berbeda dari null JS atau null PHP yang akan meledak saat kamu coba iterasi, nil slice di Go sepenuhnya aman: len(nil) adalah 0, range atas nil slice tidak pernah jalan, dan append ke nil slice bekerja normal. nil slice adalah “daftar kosong yang belum punya backing array”, bukan jebakan.
Bedanya baru terasa saat encode JSON. Dengan package encoding/json, nil slice menjadi null, sedangkan empty slice menjadi []. Untuk API publik, banyak tim memilih selalu mengirim [] agar frontend tidak perlu menangani dua bentuk.
internal/httpapi/dto/product_list.gopackage dto import "github.com/kamu/skincare-backend/internal/product" type ProductListResponse struct { Items []ProductView `json:"items"` Total int `json:"total"` } type ProductView struct { ID int64 `json:"id"` SKU string `json:"sku"` Name string `json:"name"` PriceRupiah int64 `json:"price_rupiah"` } func NewProductListResponse(products []product.Product) ProductListResponse { items := make([]ProductView, 0, len(products)) // selalu non-nil: JSON akan jadi [] bukan null for _, p := range products { items = append(items, ProductView{ ID: p.ID, SKU: p.SKU, Name: p.Name, PriceRupiah: p.PriceRupiah, }) } return ProductListResponse{Items: items, Total: len(items)} }
Mulai akumulator response dengan make([]T, 0, len(src)), bukan var s []T. Selain menjamin JSON keluar sebagai [], kapasitas awal len(src) menghindari realokasi berulang saat append di dalam loop. Ini pola yang akan kamu ulang di hampir setiap mapping DTO.
append, Aliasing, copy, dan Three-Index
append mengembalikan slice baru, dan sub-slice bisa berbagi memori
append tidak memutasi variabel slice secara ajaib. Ia mengembalikan slice hasil, dan kamu wajib menyimpan hasilnya. Inilah aturan paling penting sekaligus sumber bug paling halus di Go.
Pola yang benar selalu products = append(products, p). Karena header slice membawa len dan cap, setelah append panjangnya berubah dan kamu harus memakai header baru itu. Memanggil append(products, p) tanpa menyimpan hasilnya adalah bug diam-diam.
internal/checkout/cart.gopackage checkout import "github.com/kamu/skincare-backend/internal/product" type CartItem struct { Product product.Product Qty int } func (item CartItem) LineTotal() int64 { return item.Product.PriceRupiah * int64(item.Qty) } type Cart struct { Items []CartItem } func (c *Cart) AddItem(item CartItem) { c.Items = append(c.Items, item) // simpan hasil, jangan hanya append(c.Items, item) }
Di JS, arr.push(x) memutasi arr di tempat dan kamu mengabaikan return value-nya. Di Go, append boleh saja merealokasi backing array, sehingga ia mengembalikan header baru. Pikirkan append sebagai fungsi murni yang menghasilkan slice, bukan method yang memutasi.
Gotcha aliasing muncul saat kamu membuat slice dari slice lain. Jika kapasitas sub-slice masih menjangkau backing array asal, append akan menulis ke backing array yang sama, dan perubahan itu bocor ke slice lain yang masih menunjuk ke sana.
internal/product/append_gotcha.gopackage product func featuredGotcha() []Product { products := []Product{ {ID: 1, Name: "Cleanser"}, {ID: 2, Name: "Toner"}, {ID: 3, Name: "Serum"}, } // featured: len 1, tetapi cap 3 (masih menjangkau backing array products). featured := products[:1] // append menulis ke slot index 1 backing array yang SAMA dengan products. featured = append(featured, Product{ID: 99, Name: "Promo Sunscreen"}) // products[1] sekarang menjadi "Promo Sunscreen", bukan "Toner". Bocor. return products }
flowchart TD
A["append products, x"] --> B{"len < cap?"}
B -->|"ya, ruang cukup"| C["tulis x di backing array yang ada"]
C --> D["header baru: len bertambah 1, ptr dan cap sama"]
C --> W["jika ada slice lain berbagi array, isinya ikut berubah"]
B -->|"tidak, cap habis"| E["alokasi backing array baru lebih besar"]
E --> F["salin elemen lama ke array baru"]
F --> G["header baru: ptr baru, cap lebih besar"]
G --> S["slice lama TIDAK terpengaruh, sudah terpisah memori"]Gambar 2. Dua jalur append. Selama len < cap, penulisan terjadi di backing array yang ada, dan slice lain yang berbagi array bisa ikut berubah. Begitu cap habis, Go merealokasi, dan sejak titik itu slice menjadi terpisah memori.
Jika sub-slice masih punya kapasitas (cap lebih besar dari len), append dapat menimpa elemen di backing array yang juga dipakai slice lain. Bug ini sulit terlihat karena tidak ada error, hanya data yang “tiba-tiba berubah”.
Ada dua cara aman. Pertama, salin secara eksplisit saat hasil harus independen. copy(dst, src) menyalin sebanyak min(len(dst), len(src)) elemen dan mengembalikan jumlah yang tersalin.
internal/product/append_safe.gopackage product func safeFeatured(products []Product) []Product { // make + copy: backing array baru yang independen. featured := make([]Product, 1) copy(featured, products[:1]) // Idiom ringkas yang setara: append ke nil dengan spread elemen sumber. // featured := append([]Product(nil), products[:1]...) featured = append(featured, Product{ID: 99, Name: "Promo Sunscreen"}) return featured // products tidak terpengaruh }
Kedua, batasi kapasitas sub-slice dengan three-index slicing a[low:high:max]. Bentuk ini menyetel cap hasil menjadi max - low, sehingga append berikutnya dijamin merealokasi alih-alih menimpa backing array asal.
Bentuk slicing dengan tiga indeks. low dan high menentukan elemen yang terlihat (len = high - low), sedangkan max membatasi kapasitas (cap = max - low). Dengan menyetel high == max, cap hasil menjadi sama dengan len, sehingga append pertama pasti mengalokasikan backing array baru dan tidak akan menimpa slice asal.
internal/product/three_index.gopackage product func featuredCapped(products []Product) []Product { // products[:1:1] => len 1, cap 1. append berikutnya pasti realokasi. featured := products[:1:1] featured = append(featured, Product{ID: 99, Name: "Promo Sunscreen"}) return featured // aman: products tidak ikut berubah }
Untuk koleksi baru yang harus hidup sendiri, pilih salah satu: make plus copy, idiom append([]T(nil), src...), atau three-index src[:n:n]. Untuk memotong slice tanpa khawatir aliasing, three-index adalah cara paling ringkas menyatakan “potongan ini tidak boleh menimpa asalnya”.
range untuk Membaca dan Mengubah Bentuk
Padanan Go untuk map, filter, dan forEach di JS
Go tidak punya method map, filter, atau reduce pada slice. Kamu menulis loop eksplisit dengan for range. Bagi developer React ini terasa lebih verbose, tetapi untuk backend justru lebih jujur.
Loop eksplisit membuat validasi, perhitungan total, error handling, dan early return terlihat di satu tempat tanpa callback bertingkat. Inilah transformasi []Product menjadi []ProductView untuk response, padanan langsung dari products.map(toView) di React.
map/filterringkas untuk transformasi kecil.- Error handling sering masuk callback atau di-throw.
- Early return sulit; biasanya pakai
some/every.
for rangemembuat alur data terlihat lurus.if err != nildanreturnlangsung di dalam loop.continuedanbreakuntuk filter dan early stop.
internal/httpapi/dto/product_view.gopackage dto import "github.com/kamu/skincare-backend/internal/product" func ToProductViews(products []product.Product) []ProductView { views := make([]ProductView, 0, len(products)) for _, p := range products { views = append(views, ProductView{ ID: p.ID, SKU: p.SKU, Name: p.Name, PriceRupiah: p.PriceRupiah, }) } return views }
Padanan mental JS ke GoJS: const views = products.map(toView) Go: views := make([]ProductView, 0, len(products)) for _, p := range products { views = append(views, toView(p)) }
Untuk filter, tetap bangun slice hasil baru. Hindari menghapus elemen dari slice yang sedang kamu iterasi kecuali kamu paham betul konsekuensinya pada index dan backing array.
internal/product/filter.gopackage product // FilterSellable mengembalikan hanya produk yang boleh dijual saat ini. func FilterSellable(products []Product) []Product { result := make([]Product, 0, len(products)) for _, p := range products { if !p.Status.IsSellable() { continue } result = append(result, p) } return result }
for _, p := range products menyalin tiap elemen ke p. Mengubah p.Quantity di dalam loop tidak mengubah slice asal. Untuk memutasi elemen di tempat, indeks langsung: for i := range products { products[i].Quantity = 0 }. Sejak Go 1.22 variabel loop juga di-scope ulang tiap iterasi, sehingga mengambil &p di dalam loop tidak lagi menghasilkan pointer yang sama untuk semua iterasi seperti pada Go lama.
for i, v := range products memberi index dan value. Jika index tidak dipakai, gunakan _. Jika value tidak dipakai, cukup for i := range products. Atas slice, range juga aman pada nil slice: loop tidak pernah jalan.
Map dan Lookup dengan comma-ok
Object atau Map versi Go dengan tipe key dan value yang tegas
Map di Go adalah koleksi pasangan key-value dengan tipe yang tegas. Untuk backend, map paling sering dipakai sebagai lookup table di memory: ubah pencarian O(n) di dalam slice menjadi lookup O(1).
Di JavaScript kamu memakai object biasa untuk lookup sederhana atau Map ketika key lebih fleksibel. Di Go, map selalu punya tipe key dan tipe value yang pasti, misalnya map[int64]int, map[int64]Product, atau map[string]Product. Bangun lookup dari slice dengan sekali loop.
internal/inventory/lookup.gopackage inventory import "github.com/kamu/skincare-backend/internal/product" // BuildStockLookup mengubah daftar inventory menjadi lookup stok per ProductID. func BuildStockLookup(items []product.Inventory) map[int64]int { lookup := make(map[int64]int, len(items)) // beri hint kapasitas untuk efisiensi for _, item := range items { lookup[item.ProductID] = item.Available } return lookup }
Lookup map memakai bentuk comma-ok. Ini penting karena value zero bisa valid: kalau stok sebuah produk adalah 0, kamu tetap harus bisa membedakan “key ada dengan stok 0” dari “key memang tidak ada”.
internal/inventory/check.gopackage inventory func HasEnoughStock(stockByProductID map[int64]int, productID int64, qty int) bool { available, ok := stockByProductID[productID] if !ok { return false // produk tidak ada dalam lookup } return available >= qty }
Di JS, akses key yang tidak ada menghasilkan undefined, dan kamu sulit membedakannya dari nilai yang memang undefined. Di Go, membaca key yang tidak ada mengembalikan zero value dari tipe value (0, "", nil, dst), jadi kamu memakai v, ok := m[k] untuk tahu pasti apakah key benar-benar ada.
flowchart LR
Q["v, ok := m[k]"] --> D{"ok?"}
D -->|"true"| H["key ada, v adalah nilai tersimpan"]
D -->|"false"| M["key tidak ada, v adalah zero value tipe value"]Gambar 3. Bentuk comma-ok memisahkan “key ada” dari “value kebetulan zero”. Tanpa ok, stok 0 dan produk yang tidak terdaftar terlihat identik.
Lookup map[int64]Product sangat berguna saat kamu sudah memuat katalog dan ingin mengambil produk by ID berulang kali tanpa men-scan slice setiap saat.
internal/product/index.gopackage product func IndexByID(products []Product) map[int64]Product { byID := make(map[int64]Product, len(products)) for _, p := range products { byID[p.ID] = p } return byID } func IndexBySKU(products []Product) map[string]Product { bySKU := make(map[string]Product, len(products)) for _, p := range products { bySKU[p.SKU] = p } return bySKU }
nil map bisa dibaca (mengembalikan zero value) tetapi akan panic saat ditulis. Selalu inisialisasi dengan literal map[K]V{} atau make sebelum assignment. Berbeda dari nil slice yang aman di-append, nil map tidak aman ditulisi.
internal/product/map_init.gopackage product func mapInit() { var broken map[int64]Product // nil map _ = broken[1] // OK: baca nil map mengembalikan zero value // broken[1] = Product{} // PANIC: assignment to entry in nil map bySKU := map[string]Product{} // literal, siap ditulis byID := make(map[int64]Product) // make, siap ditulis bySKU["SKN-CLN-01"] = Product{ID: 1, Name: "Gentle Cleanser"} byID[1] = Product{ID: 1, Name: "Gentle Cleanser"} delete(byID, 1) // delete aman bahkan jika key tidak ada }
Iterasi range atas map tidak punya urutan yang dijamin, dan urutannya sengaja diacak antar run. Ini desain Go agar kamu tidak diam-diam bergantung pada urutan tertentu. Kalau butuh output berurutan, kumpulkan key ke slice lalu urutkan, seperti yang kita lihat di bagian stdlib berikutnya.
Grouping Data per Kategori
Dari daftar datar menjadi map kategori ke daftar produk
Grouping adalah pola map paling berguna di service backend. Kamu mengubah daftar datar menjadi struktur yang jauh lebih cepat untuk dihitung dan ditampilkan.
Misalnya endpoint katalog ingin menampilkan produk yang dikelompokkan per kategori. Di JS kamu mungkin memakai reduce untuk membangun object. Di Go, pakai map[string][]Product dan append ke bucket masing-masing kategori.
internal/product/group.gopackage product // GroupByCategory mengelompokkan produk per kategori untuk tampilan katalog. func GroupByCategory(products []Product) map[string][]Product { grouped := make(map[string][]Product) for _, p := range products { grouped[p.Category] = append(grouped[p.Category], p) } return grouped }
Pola ini bekerja mulus berkat dua sifat Go yang sudah kita pelajari. Membaca key map yang belum ada mengembalikan zero value tipe value; untuk []Product, zero value-nya adalah nil slice. Dan nil slice aman untuk append. Jadi grouped[p.Category] = append(grouped[p.Category], p) bekerja untuk kategori baru maupun kategori yang sudah ada, tanpa pengecekan eksistensi.
flowchart LR A["[]Product datar"] --> B["for range tiap produk"] B --> C["baca grouped[p.Category]"] C --> D["append p ke bucket kategori"] D --> E["map kategori ke []Product"] E --> F["map string Skincare Wajah ke 3 produk"] E --> G["map string Sunscreen ke 2 produk"]
Gambar 4. Grouping mengubah slice produk datar menjadi map[string][]Product per kategori. Karena nil slice aman di-append, kategori baru terbentuk otomatis tanpa cek eksistensi terlebih dulu.
Pola yang sama berlaku untuk grouping order item. Untuk audit inventory, kita kelompokkan semua item dari banyak order berdasarkan ProductID, lalu jumlahkan kuantitas terjual per produk dalam satu lookup.
internal/order/aggregate.gopackage order import "github.com/kamu/skincare-backend/internal/checkout" // SoldQtyByProductID menjumlahkan kuantitas terjual per produk dari banyak cart item. func SoldQtyByProductID(items []checkout.CartItem) map[int64]int { sold := make(map[int64]int) for _, item := range items { sold[item.Product.ID] += item.Qty // map[int64]int: zero value 0, += aman } return sold }
Grouping menghindari loop bersarang yang mahal. Untuk laporan dengan ribuan order item, sekali pass membangun lookup map jauh lebih jelas dan cepat daripada mencari produk satu per satu di dalam slice untuk tiap item.
slices, maps, clear, dan min/max
Stdlib modern yang menggantikan banyak helper buatan tangan
Sejak Go 1.21, standard library menambahkan package slices dan maps plus builtin clear, min, dan max. Banyak loop yang dulu kamu tulis sendiri kini punya padanan baku yang teruji dan idiomatik.
Package slices membawa fungsi generik seperti slices.Sort, slices.Contains, slices.Index, dan slices.SortFunc. Untuk mengurutkan katalog atau mencari produk dalam slice, ini lebih ringkas dan jelas daripada loop manual atau sort.Slice versi lama.
internal/product/sort_search.gopackage product import ( "cmp" "slices" ) // SortByPrice mengurutkan produk menaik berdasarkan harga (in place). func SortByPrice(products []Product) { slices.SortFunc(products, func(a, b Product) int { return cmp.Compare(a.PriceRupiah, b.PriceRupiah) // -1, 0, +1 menentukan urutan }) } // ContainsSKU mengecek apakah daftar SKU memuat sku tertentu. func ContainsSKU(skus []string, sku string) bool { return slices.Contains(skus, sku) } // IndexOfID mencari posisi produk dengan id tertentu, -1 jika tidak ada. func IndexOfID(ids []int64, id int64) int { return slices.Index(ids, id) }
Kalau kamu sempat melihat kode Go lama dengan sort.Slice(s, func(i, j int) bool { ... }), versi modern slices.SortFunc(s, func(a, b T) int { ... }) lebih jelas: comparator membandingkan dua elemen langsung dan mengembalikan int bertanda, bukan dua index. Hasilnya lebih dekat ke Array.prototype.sort yang sudah kamu kenal di JS.
Package maps membawa maps.Keys dan maps.Values. Sejak Go 1.23 keduanya mengembalikan iterator (iter.Seq), bukan slice, sehingga bisa langsung dirangkai. Karena urutan iterasi map acak, pola idiomatik untuk key terurut adalah slices.Sorted(maps.Keys(m)): maps.Keys menghasilkan iterator key, lalu slices.Sorted mengumpulkannya ke slice dan mengurutkannya.
internal/product/sorted_keys.gopackage product import ( "maps" "slices" ) // SortedCategoryNames mengembalikan nama kategori secara terurut dan deterministik. func SortedCategoryNames(grouped map[string][]Product) []string { return slices.Sorted(maps.Keys(grouped)) // maps.Keys -> iter.Seq[string], lalu Sorted }
keys := make([]K, 0, len(m))for k := range m { keys = append(keys, k) }slices.Sort(keys)
keys := slices.Sorted(maps.Keys(m))- Satu baris, intent jelas, tanpa akumulator manual.
- Tersedia sejak Go 1.23 lewat iterator
iter.Seq.
Builtin clear (Go 1.21) berperilaku berbeda untuk map dan slice. Pada map, clear menghapus semua entry sehingga len menjadi 0. Pada slice, clear menyetel semua elemen ke zero value tetapi len tetap, karena panjang adalah bagian dari nilai slice.
internal/inventory/reset.gopackage inventory func resetExamples(stock map[int64]int, batch []int) { clear(stock) // map: semua entry dihapus, len(stock) menjadi 0 clear(batch) // slice: tiap elemen jadi 0, len(batch) TIDAK berubah _ = batch }
Builtin min dan max (Go 1.21) bekerja pada tipe ordered (integer, float, string) dengan jumlah argumen tetap. Praktis untuk membatasi kuantitas atau menghitung total tanpa fungsi helper.
internal/checkout/clamp.gopackage checkout // ClampQty membatasi kuantitas pesanan di antara 1 dan stok tersedia. func ClampQty(requested, available int) int { return max(1, min(requested, available)) }
Package slices dan maps serta builtin clear, min, max hadir sejak Go 1.21. Iterator (iter.Seq) dan bentuk maps.Keys/maps.Values yang mengembalikan iterator hadir sejak Go 1.23, dan dipakai bersama slices.Sorted. Proyek ini memakai Go 1.26, jadi semuanya tersedia. Acuan: slices, maps, dan Go 1.21 release notes.
Skenario Checkout Skincare
Menyatukan slice dan map dalam satu alur service nyata
Sekarang kita sambungkan slice dan map ke alur checkout yang akan terus dipakai di roadmap berikutnya. Inilah tempat lookup map dan loop slice bekerja bersama.
Dalam online shop skincare, checkout menerima []CartItem, memvalidasi tiap item terhadap lookup stok map[int64]int, lalu menghitung total. Kombinasi slice untuk daftar dan map untuk lookup adalah tulang punggung service layer pertama kita.
internal/checkout/service.gopackage checkout import ( "errors" "github.com/kamu/skincare-backend/internal/product" ) type OrderSummary struct { Items []CartItem Total int64 } var ErrOutOfStock = errors.New("checkout: insufficient stock") // BuildOrderSummary memvalidasi cart terhadap stok lalu menghitung total. func BuildOrderSummary(items []CartItem, inventory []product.Inventory) (OrderSummary, error) { // 1) Bangun lookup stok per ProductID: O(n) sekali, lalu O(1) per cek. stockByID := make(map[int64]int, len(inventory)) for _, inv := range inventory { stockByID[inv.ProductID] = inv.Available } // 2) Validasi tiap item dengan comma-ok, kumpulkan yang valid, jumlahkan total. accepted := make([]CartItem, 0, len(items)) var total int64 for _, item := range items { available, ok := stockByID[item.Product.ID] if !ok || available < item.Qty { return OrderSummary{}, ErrOutOfStock } accepted = append(accepted, item) total += item.LineTotal() } return OrderSummary{Items: accepted, Total: total}, nil }
sequenceDiagram
participant H as Handler
participant S as checkout.BuildOrderSummary
participant L as Lookup stok map int64 int
H->>S: items CartItem, inventory Inventory
S->>L: bangun lookup stok sekali
loop tiap CartItem
S->>L: available, ok := lookup ProductID
alt ok dan stok cukup
S->>S: append item, total += LineTotal
else stok kurang atau tidak ada
S-->>H: OrderSummary kosong, ErrOutOfStock
end
end
S-->>H: OrderSummary Items Total, nilGambar 5. Alur checkout. Lookup stok dibangun sekali sebagai map, lalu tiap cart item divalidasi dengan comma-ok. Begitu satu item gagal, service mengembalikan error eksplisit, persis pola error return dari modul fungsi.
Katalog
[]Product untuk response daftar, FilterSellable, dan GroupByCategory per kategori.
Cart
[]CartItem menjaga urutan item, tiap item punya LineTotal() sebagai method.
Checkout
map[int64]int untuk validasi stok per ProductID, lalu total dihitung lewat slice.
Laravel punya Collection dengan method chain panjang seperti ->filter()->map()->sum(). Di Go, service logic memakai slice, map, dan fungsi kecil yang eksplisit. Lebih banyak baris, tetapi alur error dan return terbaca lurus tanpa magic, dan setiap langkah mudah dites sendiri.
Hands-on Ringan
Bangun fungsi kecil yang terasa seperti service backend
Latihan ini tidak butuh database. Tujuannya membuat kamu nyaman dengan slice, map, grouping, comma-ok, dan total checkout.
Pakai product.Product, product.Inventory, dan checkout.CartItem dari modul sebelumnya. Tidak perlu mendefinisikan ulang.
Ubah []product.Inventory menjadi map[int64]int agar validasi checkout tidak perlu men-scan slice berulang.
Terima hanya item yang ProductID-nya ada dan stoknya cukup, lalu jumlahkan LineTotal() menjadi total.
Uji kasus stok cukup (sukses) dan stok kurang (ErrOutOfStock), lalu jalankan go test ./....
internal/checkout/service_test.gopackage checkout import ( "errors" "testing" "github.com/kamu/skincare-backend/internal/product" ) func TestBuildOrderSummary(t *testing.T) { items := []CartItem{ {Product: product.Product{ID: 1, Name: "Cleanser", PriceRupiah: 89000}, Qty: 2}, {Product: product.Product{ID: 2, Name: "Toner", PriceRupiah: 99000}, Qty: 1}, } inventory := []product.Inventory{ {ProductID: 1, Available: 10}, {ProductID: 2, Available: 3}, } summary, err := BuildOrderSummary(items, inventory) if err != nil { t.Fatalf("expected no error, got %v", err) } if len(summary.Items) != 2 { t.Fatalf("expected 2 items, got %d", len(summary.Items)) } // 2*89000 + 1*99000 = 277000 if summary.Total != 277000 { t.Fatalf("expected total 277000, got %d", summary.Total) } } func TestBuildOrderSummaryOutOfStock(t *testing.T) { items := []CartItem{ {Product: product.Product{ID: 1, Name: "Cleanser", PriceRupiah: 89000}, Qty: 5}, } inventory := []product.Inventory{ {ProductID: 1, Available: 2}, // stok kurang dari Qty } _, err := BuildOrderSummary(items, inventory) if !errors.Is(err, ErrOutOfStock) { t.Fatalf("expected ErrOutOfStock, got %v", err) } }
Terminalgo test ./...
Jangan langsung membuat generic collection helper. Tulis loop biasa dulu sampai pola slice dan map terasa natural. Setelah nyaman, refactor pencarian ke slices.Contains atau slices.Index saat memang menyederhanakan.
Jebakan Umum dari JS dan PHP
Hampir semua bug koleksi lahir dari asumsi yang terbawa dari JS
Kebanyakan bug slice dan map bukan karena sintaksnya susah, tetapi karena kebiasaan dari JavaScript dan PHP terbawa ke Go tanpa disadari.
Lupa menyimpan hasil append
Tulis items = append(items, x), bukan hanya append(items, x). append mengembalikan header baru; mengabaikannya membuang hasilnya.
Sub-slice berbagi backing array
s[:n] masih membawa kapasitas asal. Untuk hasil independen, pakai copy, append([]T(nil), s...), atau three-index s[:n:n].
Menulis ke nil map
nil map panic saat ditulis. Selalu make atau literal sebelum assignment. nil slice aman di-append, nil map tidak.
Mengandalkan urutan iterasi map
Urutan range map sengaja acak. Untuk output berurutan, slices.Sorted(maps.Keys(m)) lalu iterasi key.
Lupa comma-ok di lookup
v := m[k] tidak membedakan “tidak ada” dari “value zero”. Pakai v, ok := m[k] saat zero adalah nilai yang sah.
Memutasi variabel range
Variabel v pada range adalah salinan. Untuk memutasi elemen di tempat, pakai index: s[i].Field = ....
Saat frontend butuh urutan stabil, jangan kirim map. Kumpulkan key ke slice, urutkan, lalu bangun response sebagai slice. Inilah versi modern menggantikan loop manual plus sort.Slice.
internal/product/ordered_response.gopackage product import ( "maps" "slices" ) // CategoriesInOrder mengembalikan kategori terurut beserta produknya untuk response. func CategoriesInOrder(grouped map[string][]Product) []CategoryGroup { names := slices.Sorted(maps.Keys(grouped)) // deterministik, bukan urutan acak map out := make([]CategoryGroup, 0, len(names)) for _, name := range names { out = append(out, CategoryGroup{Category: name, Products: grouped[name]}) } return out } type CategoryGroup struct { Category string Products []Product }
Map unggul untuk lookup, bukan untuk daftar yang harus tampil berurutan. Kalau frontend menampilkan kategori atau produk dalam urutan tertentu, kirim slice yang sudah diurutkan. Map yang di-encode ke JSON pun key-nya tidak dijamin berurutan.
Ringkasan & Poin Penting
Slice dan map adalah fondasi koleksi data Go yang akan terus muncul dari handler API sampai repository dan service checkout.
Yang Wajib Menempel
- Array Go fixed-length dan value type; slice adalah header (pointer,
len,cap) di atas backing array, dan inilah koleksi default sehari-hari. lenmenghitung elemen terlihat,capadalah ruang sampai ujung backing array;appendmemakaicapjika cukup, atau merealokasi backing array baru bila habis.appendmengembalikan slice baru, jadi selalu simpan hasilnya; sub-slice bisa berbagi backing array, jadi pakaicopy,append([]T(nil), s...), atau three-indexs[:n:n]untuk hasil independen.for rangeadalah pola transformasi utama Go yang menggantikanmap,filter, danreduce; variabel range adalah salinan, gunakan index untuk mutasi di tempat.- Map butuh inisialisasi sebelum ditulis (nil map panic), iterasinya acak, dan comma-ok
v, ok := m[k]memisahkan “tidak ada” dari “value zero”. - Grouping
map[K][]Vmemanfaatkan nil slice yang aman di-append; pola ini mengubah daftar datar menjadi lookup per kategori atau per produk. - Stdlib modern:
slices.Sort/Contains/Index/SortFunc,maps.Keys/Values(iterator sejak Go 1.23),slices.Sorted(maps.Keys(m)), serta builtinclear,min,maxsejak Go 1.21.
Pemetaan ke proyek online shop skincare
Koleksi siap pakai di tiga area
Endpoint katalog memakai []Product dengan filter dan grouping, cart memakai []CartItem dengan LineTotal, dan checkout memakai map[int64]int untuk validasi stok per ProductID.
Lookup menggantikan loop bersarang
Mengubah pencarian O(n) dalam slice menjadi lookup O(1) lewat map adalah pola yang akan kamu ulang di repository, service, dan agregasi laporan.
Modul berikutnya masuk ke pointer dan dasar memori: apa itu pointer, perbedaan pass by value dan pass by reference, nil, serta kapan memakai pointer untuk mutasi dan nilai opsional. Slice header yang menyimpan pointer ke backing array dan pointer receiver MarkPaid dari modul struct adalah pintu masuknya. Setelah pointer, kita lanjut ke interface, package, context, lalu concurrency, sebelum Roadmap 2 membangun Web API dengan chi.
Pastikan kamu bisa menjelaskan beda array dan slice, menggambar slice header di atas backing array, menjelaskan kenapa append atas sub-slice bisa bocor, menulis lookup map dengan comma-ok, melakukan grouping per kategori, dan menyebut padanan stdlib modern untuk sort, search, dan key terurut.
Progress disimpan lokal di browser ini.