Progress belajar
Modul 6 dari 73
0% 0/73 modul selesai
Setelah selesai, tandai modul ini agar progres kursus tetap rapi.
Progress disimpan lokal di browser ini.
Struct dan Method
Model Domain Backend
Di modul ini kita mulai memodelkan entitas online shop skincare dengan cara Go: data eksplisit lewat struct, perilaku lewat method, dan komposisi sebagai pengganti inheritance.
Dari Class ke Struct dan Method
Go tidak punya class, tetapi tetap bisa memodelkan domain dengan rapi.
Kalau kamu datang dari TypeScript atau PHP, anggap struct sebagai wadah data yang eksplisit, lalu method sebagai perilaku yang ditempelkan ke tipe itu.
Di TypeScript kamu mungkin membuat interface Product untuk bentuk data dan class ProductService untuk perilaku. Di Laravel kamu sering bertemu Model Eloquent yang sekaligus membawa data, query, relasi, dan sebagian behavior. Go memisahkan hal ini lebih tegas: struct adalah bentuk data, method adalah perilaku pada tipe, sedangkan akses database nanti kita taruh di repository.
Go tidak punya class, constructor magic, inheritance, atau decorator. Go mendorong data yang jelas lewat struct, behavior eksplisit lewat method, dan reuse lewat embedded struct (composition), bukan pohon pewarisan.
struct adalah tipe komposit di Go yang mengelompokkan beberapa field bernama ke dalam satu nilai. Ia adalah tipe runtime yang nyata, bukan sekadar anotasi compile-time seperti interface TypeScript.
method adalah fungsi yang punya receiver, yaitu parameter khusus sebelum nama method, sehingga fungsi itu menjadi perilaku milik suatu tipe.
Modul ini melanjutkan langsung dari modul fungsi dan error. Di sana Order baru kita sketsa seminimal Order{ID, Total} karena fokusnya error return. Sekarang kita modelkan entitas inti dengan benar: User, Product, CartItem, Order, Payment, dan Inventory. Inilah bahasa dasar yang dipakai sepanjang roadmap, sebelum masuk ke Web API, PostgreSQL, clean architecture, dan transaksi checkout.
Acuan resmi yang relevan: Effective Go, Go Specification tentang Struct types dan Method sets, serta paket encoding/json.
Struct sebagai Model Data
Mirip interface TypeScript dari sisi bentuk, tetapi ia hidup sebagai tipe runtime Go.
Struct menjawab satu pertanyaan sederhana: field apa saja yang membentuk satu konsep domain?
Bandingkan cara kamu biasanya mendeskripsikan produk di TypeScript dengan cara Go.
interfacemendeskripsikan shape hanya untuk type checking.- Hilang saat runtime karena TypeScript dikompilasi ke JavaScript.
- Method biasanya ditaruh di function atau class terpisah.
structadalah tipe Go yang benar-benar ada saat program berjalan.- Setiap field punya tipe konkret dan dicek saat compile.
- Method bisa ditempelkan langsung ke tipe domain.
product.tsexport interface Product { id: number; sku: string; name: string; priceRupiah: number; quantity: number; status: "draft" | "active" | "archived" | "out_of_stock"; }
Versi Go meneruskan konvensi proyek dari modul tipe dan control flow: uang sebagai PriceRupiah int64 (bukan float, bukan tipe uang khusus), Quantity sebagai stok tersedia, dan Status 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 // stok tersedia Status ProductStatus }
Perhatikan huruf besar di awal nama field. Di Go, identifier yang diawali huruf besar diekspor dari package, sedangkan yang huruf kecil tidak. Karena Product nanti dibaca handler, service, dan repository di package berbeda, field-nya dibuat exported. Aturan ekspor ini kita perdalam di modul packages nanti.
Mulai dari struct kecil yang jelas. Tambahkan method ketika ada aturan domain yang nyata, bukan sekadar getter dan setter otomatis seperti pola class tradisional.
Tanpa constructor, var p Product sudah valid: semua field terisi zero value (0, "", nil). Inilah kenapa di Go kita sering mengembalikan Product{} sebagai hasil kosong yang aman saat terjadi error, seperti yang kamu lihat di modul sebelumnya.
Method dan Receiver
Receiver membuat fungsi terasa seperti perilaku milik tipe, tanpa class.
Sintaks method Go adalah func (p Product) NamaMethod() tipe. Bagian p Product sebelum nama method itulah receiver-nya.
Method berikut menjawab pertanyaan domain: apakah produk boleh tampil di katalog, apakah produk bisa dibeli, dan bagaimana harga ditampilkan.
internal/product/behavior.gopackage product import "fmt" func (p Product) CanBeListed() bool { return p.Status == ProductStatusActive } func (p Product) CanBePurchased(qty int) bool { return p.Status == ProductStatusActive && p.Quantity >= qty } func (p Product) DisplayPrice() string { return fmt.Sprintf("Rp%d", p.PriceRupiah) }
Dari sisi pemanggilan, method terasa mirip object method di JavaScript atau PHP: product.CanBePurchased(2).
internal/product/behavior_usage.gopackage product func examplePurchase() bool { p := Product{ ID: 1, SKU: "SKN-SERUM-NIA-30", Name: "Niacinamide Serum 30ml", PriceRupiah: 129000, Quantity: 25, Status: ProductStatusActive, } return p.CanBePurchased(2) }
Receiver adalah parameter di antara func dan nama method yang menyatakan tipe pemilik method. func (p Product) ... berarti method itu milik tipe Product, dan di dalamnya p adalah nilai yang method dipanggil padanya.
Method tidak terbatas pada struct. Method bisa didefinisikan pada tipe bernama apa pun yang dideklarasikan di package-mu. Kamu sebenarnya sudah memakai ini sejak modul control flow: IsSellable adalah method pada ProductStatus, sebuah tipe berbasis string, bukan struct.
internal/product/product.go// ProductStatus adalah tipe string, tetapi tetap bisa punya method. func (s ProductStatus) IsSellable() bool { return s == ProductStatusActive }
classDiagram
class ProductStatus {
<<string>>
+IsSellable() bool
}
class Product {
+int64 ID
+string Name
+int64 PriceRupiah
+int Quantity
+ProductStatus Status
+CanBePurchased(qty) bool
+DisplayPrice() string
}
class CartItem {
+Product Product
+int Qty
+LineTotal() int64
}
Product --> ProductStatus : Status
CartItem --> Product : memuatGambar 1. Struct membawa data, method menempel sebagai perilaku milik tipe. ProductStatus membuktikan method tidak harus pada struct: tipe berbasis string pun boleh punya IsSellable.
Method bukan tempat akses database. product.CanBePurchased(2) boleh memeriksa state yang sudah ada di struct, tetapi query stok terbaru dari PostgreSQL nanti menjadi tanggung jawab repository atau service.
Pointer Receiver vs Value Receiver
Pilihan receiver menentukan apakah method bekerja pada salinan atau nilai asli.
Value receiver menerima salinan dari nilai, pointer receiver menerima alamat nilai asli. Perbedaan ini menentukan apakah perubahan di dalam method ikut terlihat oleh pemanggil.
- Pakai ketika method hanya membaca data.
- Cocok untuk struct kecil dan perilaku yang terasa immutable.
- Perubahan pada receiver tidak terlihat oleh pemanggil.
- Pakai ketika method perlu mengubah field.
- Cocok untuk struct besar agar tidak menyalin banyak data.
- Perubahan pada receiver terlihat oleh pemanggil.
Contoh value receiver pada CartItem, karena method hanya menghitung dari data yang sudah ada. Di modul fungsi, LineTotal masih berupa fungsi bebas LineTotal(item). Sekarang kita jadikan ia method milik CartItem, sehingga terbaca sebagai item.LineTotal().
internal/checkout/cart.gopackage checkout import "github.com/kamu/skincare-backend/internal/product" type CartItem struct { Product product.Product Qty int } // LineTotal kini menjadi method milik CartItem, bukan fungsi bebas. // Value receiver sudah cukup karena ia hanya membaca. func (item CartItem) LineTotal() int64 { return item.Product.PriceRupiah * int64(item.Qty) }
Contoh pointer receiver pada Order, karena method mengubah status, payment id, dan waktu pembayaran. Inilah Order versi lengkap yang menggantikan sketsa minimal dari modul fungsi.
internal/order/order.gopackage order import ( "errors" "time" "github.com/kamu/skincare-backend/internal/checkout" ) type OrderStatus string const ( OrderStatusPending OrderStatus = "pending" OrderStatusPaid OrderStatus = "paid" OrderStatusCancelled OrderStatus = "cancelled" ) type Order struct { ID int64 UserID int64 Items []checkout.CartItem Total int64 Status OrderStatus PaymentID int64 PaidAt *time.Time CreatedAt time.Time } func (o *Order) MarkPaid(paymentID int64, paidAt time.Time) error { if o.Status != OrderStatusPending { return errors.New("order must be pending before it can be paid") } o.Status = OrderStatusPaid o.PaymentID = paymentID o.PaidAt = &paidAt return nil }
Kalau MarkPaid memakai value receiver, perubahan status hanya terjadi pada salinan Order, lalu hilang begitu method selesai. Pemanggil tidak akan pernah melihat order menjadi paid.
internal/order/order_bug.gopackage order import "time" // MarkPaidWrong adalah versi salah: value receiver membuat semua mutasi // hanya berlaku pada salinan, lalu dibuang saat method selesai. func (o Order) MarkPaidWrong(paymentID int64, paidAt time.Time) { o.Status = OrderStatusPaid o.PaymentID = paymentID o.PaidAt = &paidAt }
flowchart TD O["order asli, Status pending"] -->|"value receiver, struct disalin"| V["salinan o, Status jadi paid"] V -.->|"method selesai, salinan dibuang"| X["order asli TETAP pending"] O -->|"pointer receiver, kirim alamat"| P["o menunjuk order asli"] P -->|"o.Status = paid"| Y["order asli JADI paid"]
Gambar 2. Inti perbedaan receiver. Value receiver bekerja pada salinan yang dibuang, sedangkan pointer receiver bekerja pada alamat order asli sehingga mutasi benar-benar menempel.
Method yang harus mengubah Status, Stock, Reserved, atau field lain wajib pakai pointer receiver. Dengan value receiver, compiler tidak protes, kode tetap jalan, tetapi mutasinya diam-diam hilang. Bug ini sulit dilihat karena tidak ada error.
Di JavaScript, object selalu dilewatkan sebagai referensi, jadi method selalu memutasi yang asli. Di PHP, object juga dilewatkan lewat handle. Di Go, struct adalah value type yang disalin secara default; kamu yang memutuskan kapan memakai pointer. Kuasa itu sekaligus tanggung jawab.
Aturan praktisnya sederhana. Jika satu tipe punya satu saja method yang butuh pointer receiver, buat method lain pada tipe itu juga memakai pointer receiver agar method set-nya konsisten. Konsistensi ini penting saat tipe bertemu interface di modul nanti, karena method dengan pointer receiver hanya masuk method set *Order, bukan Order.
Embedded Struct dan Composition
Terasa seperti extends, tetapi sebenarnya composition.
Go tidak punya inheritance. Untuk memakai ulang field atau behavior, Go memakai composition lewat embedded struct.
Di Laravel atau OOP PHP, kamu mungkin membuat BaseModel lalu model lain mewarisi field seperti timestamp. Di Go, pola idiomatiknya adalah menyusun (embed) tipe kecil ke dalam tipe lain.
internal/product/timestamps.gopackage product import "time" type Timestamps struct { CreatedAt time.Time UpdatedAt time.Time } func (t Timestamps) WasUpdated() bool { return t.UpdatedAt.After(t.CreatedAt) }
Sekarang kita perbarui product.go agar Product meng-embed Timestamps. Embedded field ditulis tanpa nama field, hanya tipenya.
internal/product/product.gopackage product type Product struct { Timestamps // embedded: field dan method-nya dipromosikan ID int64 SKU string Name string Category string PriceRupiah int64 Quantity int Status ProductStatus }
Karena Timestamps di-embed, field CreatedAt dan UpdatedAt serta method WasUpdated dipromosikan ke Product. Kamu bisa menulis product.CreatedAt dan product.WasUpdated() langsung, seolah keduanya milik Product.
internal/product/audit_usage.gopackage product func recentlyChanged(p Product) bool { // CreatedAt dan WasUpdated berasal dari Timestamps, tetapi dipanggil // langsung pada Product berkat field dan method promotion. return p.WasUpdated() && !p.CreatedAt.IsZero() }
classDiagram
class Timestamps {
+time.Time CreatedAt
+time.Time UpdatedAt
+WasUpdated() bool
}
class Product {
+int64 ID
+string Name
+int64 PriceRupiah
+ProductStatus Status
}
Product *-- Timestamps : embedGambar 3. Embedded struct adalah komposisi, bukan pewarisan. Product tersusun dari Timestamps, lalu field dan method Timestamps dipromosikan agar nyaman diakses lewat Product.
Embedded Timestamps terasa seperti trait timestamp atau BaseModel di Laravel. Bedanya, ini bukan hierarki parent-child. Go hanya menyusun satu tipe dari tipe lain, lalu mempromosikan anggotanya. Tidak ada parent::, tidak ada override, tidak ada is-a relationship.
Gunakan embedded struct untuk concern kecil yang benar-benar berulang, seperti audit field. Jangan membuat BaseEntity besar yang menampung semua hal, karena itu menyeret kembali masalah inheritance yang justru dihindari Go.
DTO vs Domain Model
DTO adalah kontrak keluar masuk API, domain model adalah pusat aturan bisnis.
Kesalahan umum backend pemula adalah memakai satu struct yang sama untuk request JSON, response JSON, database row, dan domain entity sekaligus.
- Berada di boundary HTTP, yaitu request dan response.
- Punya JSON tag dan biasanya berisi tipe sederhana.
- Boleh mengikuti kebutuhan dan bentuk yang dipakai frontend.
- Berada di inti aplikasi, dekat aturan bisnis.
- Punya method domain seperti
MarkPaiddanReserve. - Tidak harus tunduk pada bentuk JSON frontend.
DTO request menampung apa yang dikirim frontend. Bentuknya rata dan sederhana, hanya cukup untuk divalidasi lalu dipetakan ke domain.
internal/httpapi/dto/create_order.gopackage dto type CreateOrderRequest struct { UserID int64 `json:"user_id"` Items []CreateOrderItem `json:"items"` } type CreateOrderItem struct { ProductID int64 `json:"product_id"` Qty int `json:"qty"` }
Domain model adalah order.Order dari bagian sebelumnya: ia membawa method dan aturan bisnis, bukan JSON tag. Yang menyatukan keduanya adalah fungsi mapping eksplisit dari domain ke DTO response.
internal/httpapi/dto/mapping.gopackage dto import "github.com/kamu/skincare-backend/internal/order" func NewOrderResponse(o order.Order) OrderResponse { resp := OrderResponse{ ID: o.ID, UserID: o.UserID, Total: o.Total, Status: string(o.Status), CreatedAt: o.CreatedAt, } if o.PaymentID != 0 { paymentID := o.PaymentID resp.PaymentID = &paymentID } if o.PaidAt != nil { resp.PaidAt = *o.PaidAt } return resp }
Mapping ini terlihat seperti kerja ekstra, tetapi justru di sinilah nilainya. Domain bebas berubah tanpa langsung merusak kontrak API, dan API bisa menyesuaikan bentuk untuk frontend tanpa mengotori aturan bisnis. OrderStatus yang bertipe string di domain pun dengan sengaja dipetakan menjadi string biasa di DTO.
flowchart LR FE["Frontend React"] -->|"JSON masuk"| REQ["CreateOrderRequest DTO"] REQ -->|"validasi dan mapping"| DOM["Order domain model"] DOM -->|"method: NewOrder, MarkPaid"| RULE["aturan bisnis"] DOM -->|"NewOrderResponse"| RES["OrderResponse DTO"] RES -->|"JSON keluar"| FE
Gambar 4. DTO menjaga kontrak HTTP di kedua ujung, domain model menjaga aturan bisnis di tengah. Dua fungsi mapping adalah pintu masuk dan pintu keluarnya.
CreateOrderRequest mirip Form Request dari sisi bentuk input, OrderResponse mirip API Resource dari sisi bentuk output, sedangkan order.Order bukan Eloquent model. Bedanya, di Go mapping antar lapisan ditulis sebagai kode yang terlihat, bukan konvensi framework yang tersembunyi.
Memodelkan Entitas Online Shop Skincare
Menyusun tipe inti yang dipakai sepanjang sisa roadmap.
Sekarang kita satukan semuanya menjadi enam entitas inti yang disebut Student Outcome roadmap: User, Product, CartItem, Order, Payment, dan Inventory.
Setiap entitas tinggal di package fiturnya sendiri, melanjutkan struktur yang sudah kita bentuk di modul fungsi dan error.
- cmd/
- api/
- main.go entry point API
- internal/
- user/
- user.go akun pelanggan
- product/
- product.go Product, ProductStatus, Timestamps
- behavior.go CanBePurchased, DisplayPrice
- inventory.go Inventory: available vs reserved
- checkout/
- cart.go CartItem + LineTotal (method)
- order/
- order.go Order, OrderStatus, MarkPaid, NewOrder
- payment/
- payment.go Payment dari provider
- httpapi/
- dto/
- order_response.go OrderResponse + tag JSON
- create_order.go CreateOrderRequest
- mapping.go NewOrderResponse
- go.mod module github.com/kamu/skincare-backend
User memakai value receiver karena DisplayName hanya membaca.
internal/user/user.gopackage user import "time" type User struct { ID int64 Email string FullName string CreatedAt time.Time } func (u User) DisplayName() string { if u.FullName != "" { return u.FullName } return u.Email }
Inventory memisahkan stok Available dari stok Reserved, dan memakai pointer receiver pada Reserve karena ia memutasi state.
internal/product/inventory.gopackage product import "errors" type Inventory struct { ProductID int64 Available int Reserved int } func (i Inventory) CanReserve(qty int) bool { return qty > 0 && i.Available >= qty } func (i *Inventory) Reserve(qty int) error { if !i.CanReserve(qty) { return errors.New("insufficient inventory to reserve") } i.Available -= qty i.Reserved += qty return nil }
Product.Quantity adalah angka stok ringkas yang sudah kita pakai sejak modul tipe, cukup untuk validasi katalog. Inventory adalah model stok yang lebih teliti, memisahkan yang tersedia dari yang sudah dipesan agar checkout tidak oversell. Di roadmap concurrency, Inventory inilah yang dijaga saat banyak order terjadi bersamaan.
Payment mencatat hasil dari provider pembayaran, dengan nominal tetap dalam rupiah int64.
internal/payment/payment.gopackage payment import "time" type Payment struct { ID int64 OrderID int64 AmountRupiah int64 Provider string ReferenceNo string PaidAt time.Time }
Karena Go tidak punya constructor, kita memakai fungsi pembuat seperti NewOrder untuk menjamin order lahir dalam keadaan valid. Pola ini meneruskan gaya (Data, error) dari modul fungsi dan memanggil item.LineTotal() yang kini sudah jadi method.
internal/order/factory.gopackage order import ( "errors" "time" "github.com/kamu/skincare-backend/internal/checkout" ) func NewOrder(id, userID int64, items []checkout.CartItem, now time.Time) (Order, error) { if userID == 0 { return Order{}, errors.New("user id is required") } if len(items) == 0 { return Order{}, errors.New("order must contain at least one item") } var total int64 for _, item := range items { if item.Qty <= 0 { return Order{}, errors.New("item quantity must be positive") } total += item.LineTotal() } return Order{ ID: id, UserID: userID, Items: items, Total: total, Status: OrderStatusPending, CreatedAt: now, }, nil }
Keenam entitas saling terhubung membentuk peta domain online shop skincare.
erDiagram
USER ||--o{ ORDER : "membuat"
ORDER ||--|{ CART_ITEM : "berisi"
CART_ITEM }o--|| PRODUCT : "merujuk"
PRODUCT ||--o| INVENTORY : "punya stok"
ORDER ||--o| PAYMENT : "dibayar oleh"Gambar 5. Peta entitas inti. Satu user bisa punya banyak order, satu order berisi banyak cart item yang merujuk produk, tiap produk punya satu inventory, dan satu order dibayar oleh paling banyak satu payment.
Order sendiri bergerak melalui daur hidup status, dan MarkPaid adalah transisi terjaga dari pending ke paid.
stateDiagram-v2 [*] --> Pending: NewOrder Pending --> Paid: MarkPaid Pending --> Cancelled: batalkan Paid --> [*] Cancelled --> [*]
Gambar 6. Daur hidup order. Method pointer receiver seperti MarkPaid menjaga agar transisi hanya terjadi dari status yang sah, persis seperti guard clause yang kamu pelajari di modul control flow.
User
Akun pelanggan: email, nama, dan pemilik order.
Product
Item katalog skincare: harga, SKU, stok ringkas, dan status publikasi.
CartItem
Produk dan jumlah yang dipilih, dengan LineTotal sebagai method.
Order
Hasil checkout: item, total, status, dan jejak pembayaran.
Payment
Pembayaran dari provider: nominal, reference, dan waktu paid.
Inventory
Stok tersedia dan reserved agar checkout tidak oversell.
Hands-on Ringan
Buat model domain kecil, jalankan test, lalu rasakan efek pointer receiver.
Latihan ini tidak butuh database. Kita hanya menguji perilaku struct dan method.
Letakkan tipe inti di internal/checkout dan internal/order agar belum tercampur dengan HTTP handler atau repository.
Mulai dari CartItem.LineTotal (value receiver) dan Order.MarkPaid (pointer receiver), karena keduanya langsung terasa dalam alur checkout.
Uji method perhitungan dan method mutasi state, lalu jalankan go test ./....
internal/checkout/cart_test.gopackage checkout import ( "testing" "github.com/kamu/skincare-backend/internal/product" ) func TestCartItemLineTotal(t *testing.T) { item := CartItem{ Product: product.Product{ Name: "Niacinamide Serum 30ml", PriceRupiah: 129000, Quantity: 25, Status: product.ProductStatusActive, }, Qty: 2, } got := item.LineTotal() want := int64(258000) if got != want { t.Fatalf("line total mismatch: got %d want %d", got, want) } }
internal/order/order_test.gopackage order import ( "testing" "time" ) func TestOrderMarkPaid(t *testing.T) { createdAt := time.Date(2026, 6, 6, 10, 0, 0, 0, time.UTC) paidAt := time.Date(2026, 6, 6, 10, 3, 0, 0, time.UTC) o := Order{ ID: 1001, UserID: 7, Total: 258000, Status: OrderStatusPending, CreatedAt: createdAt, } if err := o.MarkPaid(5001, paidAt); err != nil { t.Fatalf("mark paid failed: %v", err) } if o.Status != OrderStatusPaid { t.Fatalf("status mismatch: got %s want %s", o.Status, OrderStatusPaid) } if o.PaidAt == nil { t.Fatal("paid at should be set after MarkPaid") } }
Terminalgo test ./...
Ubah func (o *Order) MarkPaid menjadi func (o Order) MarkPaid, lalu jalankan test lagi. Test akan gagal karena status tidak berubah pada order asli. Itulah cara paling cepat merasakan kenapa pointer receiver penting untuk mutasi.
Jebakan Umum dari JS dan PHP
Sebagian bug Go pemula lahir dari membawa kebiasaan class dan object terlalu jauh.
Go terasa sederhana, tetapi beberapa aturan kecil sering mengejutkan developer JavaScript dan PHP.
Menganggap struct sebagai class
Struct tidak punya constructor otomatis, inheritance, keyword private, decorator, atau magic method. Pakai fungsi seperti NewOrder untuk membuat nilai yang valid.
Lupa pointer receiver untuk mutasi
Value receiver bekerja pada salinan. Jika method harus mengubah Status, Quantity, atau Reserved, pakai pointer receiver agar perubahan menempel.
Semua field dibuat exported
Huruf besar membuka akses lintas package. Untuk invariant yang harus dijaga ketat, pertimbangkan field unexported plus fungsi pembuat.
omitempty dipakai terlalu agresif
Nilai bisnis seperti total: 0 atau stock: 0 bisa hilang dari response. Pakai omitempty hanya untuk field yang benar-benar opsional, dan omitzero untuk time.Time.
Embedded struct dianggap inheritance
Embedded struct adalah composition. Jangan membangun pohon tipe panjang seperti OOP klasik.
DTO dan domain dicampur
Request JSON sering berbeda dari aturan domain. Pisahkan agar API bisa berubah tanpa merusak inti bisnis.
Jangan menjadikan satu struct domain sebagai tempat query database, validasi HTTP, dan format response sekaligus. Beban itu membuat modul Web API, PostgreSQL, dan clean architecture berikutnya jauh lebih sulit dipisahkan.
Ringkasan & Poin Penting
Struct dan method adalah fondasi cara Go memodelkan domain backend tanpa class.
Yang Wajib Menempel
structadalah wadah data eksplisit yang hidup saat runtime, bukan anotasi compile-time seperti interface TypeScript.methodadalah fungsi dengan receiver; ia bisa menempel pada struct maupun tipe bernama lain sepertiProductStatus.- Value receiver untuk operasi baca, pointer receiver untuk mutasi, struct besar, dan konsistensi method set.
- Embedded struct adalah composition dengan promosi field dan method, bukan inheritance.
- JSON tag mengatur bentuk API;
omitemptyuntuk nilai kosong dasar,omitzerountuktime.Timedan tipe ber-IsZero. - DTO menjaga kontrak HTTP, domain model menjaga aturan bisnis, dan fungsi mapping menghubungkan keduanya.
- Tanpa constructor, fungsi pembuat seperti
NewOrdermemastikan entitas lahir dalam keadaan valid.
Pemetaan ke proyek online shop skincare
Entitas inti siap pakai
User, Product, CartItem, Order, Payment, dan Inventory menjadi kosakata domain yang dipakai ulang di roadmap API, database, dan checkout.
Batas yang sehat
Pemisahan DTO dan domain plus fungsi NewOrderResponse membuat kontrak HTTP dan aturan bisnis bisa berkembang sendiri-sendiri.
Modul berikutnya memperdalam pointer dan dasar memori: apa itu pointer, pass by value vs pass by reference, nil, kapan memakai pointer untuk mutasi dan nilai opsional, serta hasil dari repository. Pointer receiver yang kamu pakai di MarkPaid dan Reserve adalah pintu masuknya.
Pastikan kamu bisa menjelaskan beda struct dengan class, memilih antara value dan pointer receiver, menerangkan kenapa embedded struct bukan inheritance, membedakan omitempty dari omitzero, dan menulis satu fungsi NewOrder lengkap dengan test untuk LineTotal dan MarkPaid.
Progress disimpan lokal di browser ini.