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) // 3Perulangan 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
[]critterdengan perulangan posisi untuk nilai kecil, ketika setiap nilai hanya tinggal di slice ini. - Pakai
[]*critterketika 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: 1if 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) // 3Menambah 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) // 2Yang 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) // 1Kunci 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.