28. Banyak struct bersama

Bab 26 dan 27 membangun satu nilai dalam satu waktu. Struct paling berguna ketika ada banyak nilai dari satu jenis yang diperlakukan sebagai kelompok: sebuah regu karakter, gerombolan musuh. Bab ini menggabungkan dua hal yang sudah kamu tahu, struct dengan metode dan slice, menjadi pola sehari-hari berupa slice berisi struct.

Tipe untuk dibuat banyak

Tipe critter kecil, dengan dua metode. Tipe ini mengikuti aturan dari Bab 27:

type critter struct {
    name   string
    energy int
}

func (c *critter) play() {
    c.energy = c.energy - 1
    fmt.Printf("%s plays. Energy: %d\n", c.name, c.energy)
}

func (c *critter) isTired() bool {
    return c.energy <= 0
}

Simpan struct dalam slice

Slice bisa menyimpan satu tipe apa pun, termasuk tipemu sendiri. Ini sebuah []critter:

critters := []critter{
    {name: "Spark", energy: 3},
    {name: "Bolt", energy: 2},
    {name: "Nova", energy: 1},
}
fmt.Println(critters) // [{Spark 3} {Bolt 2} {Nova 1}]

Di dalam literal, Go mengizinkan kamu tidak menulis tipe di depan setiap {. critters adalah slice biasa dari Bab 22. Yang baru adalah setiap item punya field dan metodenya sendiri, yang kamu capai lewat posisinya:

critters := []critter{
    {name: "Spark", energy: 3},
    {name: "Bolt", energy: 2},
}

critters[1].energy = 10
critters[1].play()              // Bolt plays. Energy: 9
fmt.Println(critters[0].energy) // 3

Perulangan dan memanggil metode pada setiap item

Perulangan yang paling jelas tampak berhasil:

critters := []critter{
    {name: "Spark", energy: 3},
    {name: "Bolt", energy: 2},
}

for _, c := range critters {
    c.play()
}

Output:

Spark plays. Energy: 2
Bolt plays. Energy: 1

Angkanya tampak benar. Sekarang cetak slice-nya sesudah itu, seperti yang dilakukan exercises/28/02-range-copy.go:

critters := []critter{
    {name: "Spark", energy: 3},
    {name: "Bolt", energy: 2},
}

for _, c := range critters {
    c.play()
}
fmt.Println(critters) // [{Spark 3} {Bolt 2}]

range memberimu salinan dari setiap struct. Bab 22 sudah memperingatkan hal ini untuk int. Pada perulangan di atas, c adalah variabel yang menyimpan salinan item. c.play() mengambil lokasi salinan itu, lalu mengubahnya. Critter di dalam slice tidak pernah kehilangan energi.

Kode ini bisa dikompilasi dan berjalan, dan angka yang tercetak tampak benar di putaran pertama. Tidak ada yang memberi tahu bahwa ada yang salah sampai perulangan kedua mencetak Energy: 2 lagi. Mengambil &c di dalam perulangan juga tidak menolong, karena itu pointer ke salinannya.

Ada dua perbaikan. Yang pertama adalah memakai posisi, seperti di Bab 22:

critters := []critter{
    {name: "Spark", energy: 3},
    {name: "Bolt", energy: 2},
}

for i := range critters {
    critters[i].play()
}
fmt.Println(critters) // [{Spark 2} {Bolt 1}]

critters[i] adalah item itu sendiri, bukan salinan. Satu perulangan, dan setiap critter melacak energinya sendiri. Inilah gerakan inti bab ini: simpan sebuah kumpulan, bertindak padanya sebagai kelompok, dan biarkan setiap nilai melacak dirinya sendiri.

Buka exercises/28/01-critters.go. File ini membuat setiap critter bermain sekali dengan perulangan posisi, lalu menghitung yang lelah. Tambahkan perulangan bermain kedua sebelum penghitungan, dan lihat energinya turun. Lalu tulis perulangannya sebagai for _, c := range critters, dan lihat apa kata hitungannya.

Slice berisi pointer

Perbaikan kedua adalah menyimpan pointer di dalam slice. Tipenya []*critter. Sekarang range menyalin pointer, dan salinannya mengarah ke critter yang sama:

// Chapter 28 example: a slice of pointers. range copies the pointer, and the
// copy still points at the same critter.
// Run with: go run exercises/28/03-pointer-slice.go

package main

import "fmt"

type critter struct {
    name   string
    energy int
}

func newCritter(name string, energy int) *critter {
    return &critter{name: name, energy: energy}
}

func (c *critter) play() {
    c.energy = c.energy - 1
    fmt.Printf("%s plays. Energy: %d\n", c.name, c.energy)
}

func main() {
    critters := []*critter{
        newCritter("Spark", 3),
        newCritter("Bolt", 2),
    }

    for _, c := range critters {
        c.play()
    }
    for _, c := range critters {
        c.play()
    }

    fmt.Println(critters[0].energy) // 1
    fmt.Println(critters[1].energy) // 0
}

Output:

Spark plays. Energy: 2
Bolt plays. Energy: 1
Spark plays. Energy: 1
Bolt plays. Energy: 0
1
0

c.play() sekarang mengubah critter yang sebenarnya, jadi perulangan range biasa sudah benar. Nama kedua untuk critter yang sama juga tetap selaras: first := critters[0] berbagi critter itu, sedangkan pada []critter itu akan menjadi salinan.

Pilih yang mana? Keduanya umum:

  • Pakai []critter dengan perulangan posisi untuk nilai kecil, ketika setiap nilai hanya tinggal di slice ini.
  • Pakai []*critter ketika nilainya sering berubah, atau ketika ada hal lain yang juga perlu memegang nilai yang sama.

Pada []*critter, sebuah item bisa bernilai nil, dan memanggil play padanya membuat crash, seperti yang ditunjukkan Bab 27. Isi slice dengan critter sungguhan.

Menyaring dan menghitung

Ini tetap sebuah slice, jadi setiap pola perulangan dari Bab 20 berlaku. Hitung berapa critter yang lelah:

critters := []critter{
    {name: "Spark", energy: 2},
    {name: "Bolt", energy: 1},
    {name: "Nova", energy: 0},
}

tired := 0
for i := range critters {
    if critters[i].isTired() {
        tired = tired + 1
    }
}
fmt.Println("Tired critters:", tired) // Tired critters: 1

if memanggil metode, bukan menguji variabel biasa, dan pola penghitungnya tidak berubah.

Ketika perulangan hanya membaca, salinan dari range tidak merugikan, dan bentuk pendeknya baik-baik saja kalau kamu membaca field secara langsung:

critters := []critter{
    {name: "Spark", energy: 2},
    {name: "Bolt", energy: 1},
}

total := 0
for _, c := range critters {
    total = total + c.energy
}
fmt.Println(total) // 3

Menambah dan menghapus

append dan pemotongan slice dari Bab 22 berlaku pada slice berisi struct, sama seperti pada slice berisi angka:

critters := []critter{
    {name: "Spark", energy: 3},
    {name: "Bolt", energy: 2},
}

critters = append(critters, critter{name: "Zip", energy: 5}) // a new critter joins
critters = critters[1:]                                      // the first one leaves
fmt.Println(critters)                                        // [{Bolt 2} {Zip 5}]

Mengurutkan

slices.SortFunc dari Bab 24 menerima fungsi yang membandingkan dua item. Untuk struct, fungsi itu membandingkan field mana pun yang ingin kamu jadikan dasar pengurutan. Ini exercises/28/04-sort.go:

// Chapter 28 example: sorting a slice of structs with a named function.
// Run with: go run exercises/28/04-sort.go

package main

import (
    "fmt"
    "slices"
    "strings"
)

type critter struct {
    name   string
    energy int
}

func byEnergy(a critter, b critter) int {
    return a.energy - b.energy
}

func byName(a critter, b critter) int {
    return strings.Compare(a.name, b.name)
}

func main() {
    critters := []critter{
        {name: "Spark", energy: 3},
        {name: "Bolt", energy: 2},
        {name: "Nova", energy: 1},
    }

    slices.SortFunc(critters, byEnergy)
    fmt.Println(critters) // [{Nova 1} {Bolt 2} {Spark 3}]

    slices.SortFunc(critters, byName)
    fmt.Println(critters) // [{Bolt 2} {Nova 1} {Spark 3}]
}

byEnergy mengembalikan angka negatif kalau a didahulukan, seperti kata Bab 24. strings.Compare memberikan jawaban yang sejenis untuk dua string. Untuk []*critter, parameter fungsinya menjadi *critter.

Map berisi struct

Map juga bisa menyimpan struct, dan membaca field-nya mudah: team["spark"].energy. Menugaskan ke field-nya tidak begitu. Ini exercises/28/06-map-field-error.go, di dalam main:

team := map[string]critter{
    "spark": {name: "Spark", energy: 3},
}

team["spark"].energy = 2
fmt.Println(team["spark"].energy)

Menjalankannya berhenti dengan:

# command-line-arguments
exercises\28\06-map-field-error.go:21:2: cannot assign to struct field team["spark"].energy in map

Map memberimu salinan dari yang disimpannya, dan Go menolak mengubah satu field dari salinan yang akan segera hilang. Dengan alasan yang sama, kamu tidak bisa memanggil metode pointer pada item di dalam map:

team := map[string]critter{
    "spark": {name: "Spark", energy: 3},
}

team["spark"].play()
cannot call pointer method play on critter

Ada dua perbaikan, di exercises/28/05-map-of-structs.go. Yang pertama adalah menyalin struct keluar, mengubah salinannya, lalu menaruhnya kembali:

team := map[string]critter{
    "spark": {name: "Spark", energy: 3},
}

c := team["spark"]
c.energy = 2
team["spark"] = c
fmt.Println(team["spark"].energy) // 2

Yang kedua adalah menyimpan pointer di dalam map. Menugaskan lewat pointer tidak mengubah apa yang disimpan map, jadi Go mengizinkannya:

pets := map[string]*critter{
    "bolt": {name: "Bolt", energy: 2},
}

pets["bolt"].energy = 1
fmt.Println(pets["bolt"].energy) // 1

Kunci yang tidak ada pada map berisi pointer memberikan nilai nol, yaitu nil, jadi pets["nova"].energy membuat crash. Periksa dulu dengan bentuk v, ok dari Bab 23:

pets := map[string]*critter{
    "bolt": {name: "Bolt", energy: 2},
}

pet, ok := pets["nova"]
if !ok {
    fmt.Println("No critter called nova.") // No critter called nova.
    return
}
fmt.Println(pet.energy)

Seperti biasa, range pada map tidak punya urutan. Untuk mencetak map berisi struct, urutkan kuncinya lebih dulu, seperti di Bab 23.

PR

Starter PR ada di exercises/28/homework/. Setiap starter bisa langsung dijalankan dan mencetak satu baris pengganti. Keempatnya punya struct hero kecil dan fungsi newHero yang sama di bagian atas.

Soal 1 — Regu pahlawan

Buka exercises/28/homework/01-party.go. Buat slice berisi tiga pahlawan dengan nama dan hit point yang berbeda, lalu ulangi slice itu dan cetak nama serta HP setiap pahlawan di barisnya sendiri.

Soal 2 — Total HP

Buka exercises/28/homework/02-total-hp.go. Pakai pola akumulasi untuk menjumlahkan HP semua pahlawan, dan cetak total regunya.

Soal 3 — Siapa yang terluka?

Buka exercises/28/homework/03-who-is-hurt.go. Setiap pahlawan punya hp dan maxHP. Turunkan hp salah satu pahlawan lewat posisinya di slice, lalu ulangi regu itu dan cetak setiap pahlawan yang hp-nya di bawah maxHP.

Tantangan — Pahlawan terkuat

Buka exercises/28/homework/04-strongest.go. Ulangi regu itu dan cetak nama pahlawan dengan HP terbanyak. Pakai gagasan mengakumulasi nilai maksimum: ingat yang terbaik sejauh ini, dan ganti kalau kamu menemukan yang lebih kuat.

Stuck atau sudah selesai? Buka halaman solusi PR.