Golang Dasar: Dari Nol hingga CLI Production-Ready6/10

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.

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

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

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

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

Memuat diagram…

Embedding mempromosikan field dan method, tetapi nilai embedded tetap merupakan bagian objek, bukan hubungan “adalah”.

Code
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:

Code
type Service struct {
	store TaskStore
}

5. Contoh: Service dengan Store

Code
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

  1. Semua hal dijadikan interface. Mulailah dengan tipe konkret; ekstrak interface saat ada kebutuhan substitusi atau pengujian.
  2. Receiver nilai untuk mutasi. Perubahan hanya terjadi pada salinan.
  3. Interface terlalu besar. Pecah berdasarkan kebutuhan pemakai; satu sampai tiga method sering cukup.
  4. Menganggap embedding sebagai inheritance. Embedded type tidak membentuk hierarki subtype.
  5. Mengekspor seluruh field. Ekspor hanya bagian API yang memang diperlukan.
  6. Menyalin struct yang mengandung mutex. Gunakan pointer dan jangan menyalin nilai sinkronisasi.
  7. Nil pointer di dalam interface. Interface dapat tidak nil meskipun nilai konkret di dalamnya berupa pointer nil.

Latihan

  1. Tambahkan Priority int pada Task dan method Validate() error dengan rentang 1–5.
  2. Buat interface TaskReader yang hanya memiliki All() []Task.
  3. Implementasikan FindByID(int) (Task, bool) pada MemoryStore.
  4. Buat Logger kecil dengan method Printf(string, ...any) dan injeksikan ke Service.
  5. Jelaskan kapan field bernama lebih baik daripada embedding pada contoh Metadata.

Ringkasan

  • struct memodelkan 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.
Struct, Method, Interface, dan Komposisi · Golang Dasar: Dari Nol hingga CLI Production-Ready