Bab 6 dari 10
Struct, Method, Interface, dan Komposisi
Tujuan Pembelajaran
Setelah menyelesaikan bab ini, Anda mampu:
- memodelkan data dengan
struct; - menulis method dengan receiver nilai dan pointer;
- mendefinisikan interface kecil berdasarkan perilaku;
- memakai komposisi dan embedding tanpa pewarisan;
- memilih tipe konkret atau interface secara tepat.
1. Struct: Mengelompokkan Data yang Berkaitan
struct adalah tipe bentukan yang menyatukan beberapa field. Gunakan nama field berawalan huruf besar jika field perlu diakses dari package lain.
package main
import "fmt"
type Task struct {
ID int
Title string
Done bool
}
func main() {
t := Task{ID: 1, Title: "Belajar struct"}
t.Done = true
fmt.Printf("%d: %s, selesai=%t\n", t.ID, t.Title, t.Done)
}
Utamakan literal bernama seperti Task{ID: 1, Title: "..."}. Bentuk ini tetap jelas ketika urutan field berubah.
2. Method dan Receiver
Method adalah fungsi yang terikat pada tipe. Receiver nilai menerima salinan; receiver pointer dapat mengubah nilai asal.
package main
import (
"errors"
"fmt"
"strings"
)
type Task struct {
ID int
Title string
Done bool
}
func NewTask(id int, title string) (Task, error) {
title = strings.TrimSpace(title)
if id < 1 {
return Task{}, errors.New("ID harus positif")
}
if title == "" {
return Task{}, errors.New("judul tidak boleh kosong")
}
return Task{ID: id, Title: title}, nil
}
func (t *Task) Complete() {
t.Done = true
}
func (t Task) String() string {
status := "[ ]"
if t.Done {
status = "[x]"
}
return fmt.Sprintf("%s %d %s", status, t.ID, t.Title)
}
func main() {
t, err := NewTask(1, "Belajar method")
if err != nil {
panic(err)
}
t.Complete()
fmt.Println(t)
}
Gunakan receiver pointer ketika method memodifikasi objek atau struct cukup besar. Gunakan receiver nilai untuk operasi baca pada nilai kecil. Jangan mencampur keduanya tanpa alasan yang jelas.
3. Interface: Kontrak Perilaku
Implementasi interface di Go bersifat implisit. Sebuah tipe memenuhi interface jika memiliki seluruh method yang diminta.
package main
import "fmt"
type Describer interface {
Describe() string
}
type Task struct{ Title string }
func (t Task) Describe() string { return "Tugas: " + t.Title }
type Note struct{ Text string }
func (n Note) Describe() string { return "Catatan: " + n.Text }
func PrintDescription(d Describer) {
fmt.Println(d.Describe())
}
func main() {
PrintDescription(Task{Title: "Belajar interface"})
PrintDescription(Note{Text: "Interface sebaiknya kecil"})
}
Definisikan interface di sisi pemakai, bukan otomatis di sisi implementasi. Terima interface jika fungsi benar-benar membutuhkan variasi perilaku; kembalikan tipe konkret agar API tetap mudah digunakan.
Method Set dan Pointer
Jika method didefinisikan pada *Task, biasanya hanya *Task yang memenuhi interface terkait. Method dengan receiver nilai tersedia bagi Task dan *Task.
var _ fmt.Stringer = Task{}
Pernyataan di atas merupakan pemeriksaan saat kompilasi bahwa Task memenuhi fmt.Stringer.
4. Komposisi dan Embedding
Go tidak memiliki pewarisan kelas. Bangun tipe kompleks dengan menyusun tipe yang lebih kecil.
Embedding mempromosikan field dan method, tetapi nilai embedded tetap merupakan bagian objek, bukan hubungan “adalah”.
package main
import "fmt"
type Metadata struct {
CreatedBy string
}
type Task struct {
Metadata
Title string
}
func main() {
t := Task{
Metadata: Metadata{CreatedBy: "Ayu"},
Title: "Belajar komposisi",
}
fmt.Println(t.CreatedBy, t.Title)
}
Gunakan field bernama ketika embedding membuat API ambigu. Komposisi eksplisit sering lebih mudah dibaca:
type Service struct {
store TaskStore
}
5. Contoh: Service dengan Store
package main
import (
"errors"
"fmt"
"strings"
)
type Task struct {
ID int
Title string
}
type TaskStore interface {
Save(Task) error
All() []Task
}
type MemoryStore struct {
tasks []Task
}
func (s *MemoryStore) Save(t Task) error {
s.tasks = append(s.tasks, t)
return nil
}
func (s *MemoryStore) All() []Task {
return append([]Task(nil), s.tasks...)
}
type Service struct {
store TaskStore
}
func NewService(store TaskStore) Service {
return Service{store: store}
}
func (s Service) Add(id int, title string) error {
title = strings.TrimSpace(title)
if id < 1 || title == "" {
return errors.New("ID dan judul tidak valid")
}
return s.store.Save(Task{ID: id, Title: title})
}
func main() {
store := &MemoryStore{}
service := NewService(store)
if err := service.Add(1, "Susun model domain"); err != nil {
panic(err)
}
fmt.Println(store.All())
}
All mengembalikan salinan slice agar pemanggil tidak dapat mengubah penyimpanan secara tidak sengaja.
Kesalahan Umum
- Semua hal dijadikan interface. Mulailah dengan tipe konkret; ekstrak interface saat ada kebutuhan substitusi atau pengujian.
- Receiver nilai untuk mutasi. Perubahan hanya terjadi pada salinan.
- Interface terlalu besar. Pecah berdasarkan kebutuhan pemakai; satu sampai tiga method sering cukup.
- Menganggap embedding sebagai inheritance. Embedded type tidak membentuk hierarki subtype.
- Mengekspor seluruh field. Ekspor hanya bagian API yang memang diperlukan.
- Menyalin struct yang mengandung mutex. Gunakan pointer dan jangan menyalin nilai sinkronisasi.
- Nil pointer di dalam interface. Interface dapat tidak
nilmeskipun nilai konkret di dalamnya berupa pointernil.
Latihan
- Tambahkan
Priority intpadaTaskdan methodValidate() errordengan rentang 1–5. - Buat interface
TaskReaderyang hanya memilikiAll() []Task. - Implementasikan
FindByID(int) (Task, bool)padaMemoryStore. - Buat
Loggerkecil dengan methodPrintf(string, ...any)dan injeksikan keService. - Jelaskan kapan field bernama lebih baik daripada embedding pada contoh
Metadata.
Ringkasan
structmemodelkan data; method menempatkan perilaku dekat dengan data.- Receiver pointer diperlukan untuk mutasi, sedangkan receiver nilai cocok untuk operasi baca pada nilai kecil.
- Interface Go dipenuhi secara implisit dan sebaiknya kecil serta berpusat pada kebutuhan pemakai.
- Komposisi menggantikan pewarisan dan menjaga hubungan antarkomponen tetap eksplisit.