26. Struct dan metode
Sejauh ini, nilai-nilai yang saling berkaitan hidup terpisah: hadiah
quest di satu map dan langkah-langkahnya di map lain, ruangan dan
inventaris pemain di dua variabel di luar main. Bab ini
memperkenalkan struct, yang menyimpan nilai-nilai yang
berkaitan di satu tempat, dan metode, yaitu fungsi yang
menjadi milik sebuah tipe. Bagian 6 dibangun di atas dua gagasan
ini.
Menyimpan data yang berkaitan bersama-sama
Bab 23 berakhir dengan quest yang disimpan dalam dua map, dan mengatakan bahwa tidak ada yang menjaga keduanya tetap selaras. Bab itu juga mengatakan bahwa satu map punya satu tipe nilai, jadi nama dan hadiah tidak bisa berbagi map. Struct menyelesaikan keduanya.
Struct adalah nilai yang terdiri dari
field bernama, dan setiap field punya tipenya sendiri.
Kamu mendefinisikannya sekali dengan type, di tingkat
teratas file, di samping fungsi-fungsimu:
type quest struct {
name string
reward int
steps []string
}
func main() {
errand := quest{
name: "Healer's Errand",
reward: 50,
steps: []string{"Speak to the elder", "Collect 5 herbs", "Return to camp"},
}
fmt.Println(errand.reward) // 50
fmt.Println(errand.steps[0]) // Speak to the elder
fmt.Println(len(errand.steps)) // 3
}Baca sebagai: definisikan tipe baru bernama quest,
yang punya tiga field ini. Nilai di dalam main adalah
literal struct: tipenya, lalu pasangan
field: nilai di dalam kurung kurawal. Seperti map yang
ditulis dalam beberapa baris, setiap baris butuh koma di belakangnya,
termasuk yang terakhir.
Kamu membaca field dengan titik, dan menugaskan nilai ke field dengan cara yang sama:
errand := quest{name: "Healer's Errand", reward: 50}
errand.reward = 60
fmt.Println(errand.reward) // 60Inilah penulisan dengan titik yang tidak dimiliki map, seperti yang ditunjukkan Bab 23. Go memeriksa nama field sebelum program berjalan, jadi nama yang salah ketik menghentikannya. Dengan map, kesalahan yang sama diam-diam memberimu nol:
errand := quest{name: "Healer's Errand", rewrd: 50}
fmt.Println(errand)unknown field rewrd in struct literal of type quest
Nilai nol
Field yang tidak kamu tulis di literal mendapat nilai
nol dari tipenya: 0 untuk int, ""
untuk string, false untuk bool, dan nil untuk
slice atau map. Menulis tipenya dengan kurung kurawal kosong memberikan
struct yang semua isinya nilai nol:
blank := quest{name: "Unwritten"}
fmt.Printf("%q %d %d\n", blank.name, blank.reward, len(blank.steps)) // "Unwritten" 0 0
empty := quest{}
fmt.Printf("%q\n", empty.name) // ""Untuk mencetak seluruh struct, %v memberikan nilainya,
dan %+v menambahkan nama field-nya:
errand := quest{name: "Lost Cat", reward: 5}
fmt.Printf("%v\n", errand) // {Lost Cat 5 []}
fmt.Printf("%+v\n", errand) // {name:Lost Cat reward:5 steps:[]}Go juga mengizinkan kamu tidak menulis nama field, seperti
quest{"Lost Cat", 5, nil}. Semua nilainya harus ada, dan
harus urut. Menambah field nantinya akan merusak setiap literal seperti
itu, jadi buku ini selalu menulis namanya.
Satu map, bukan dua
Dengan struct, dua map dari Bab 23 menjadi satu. Kuncinya menemukan quest, dan quest menyimpan semua hal tentang dirinya:
quests := map[string]quest{
"Healer's Errand": {
name: "Healer's Errand",
reward: 50,
steps: []string{"Speak to the elder", "Collect 5 herbs", "Return to camp"},
},
"Lost Cat": {name: "Lost Cat", reward: 5, steps: []string{"Find the cat"}},
}
fmt.Println(quests["Lost Cat"].reward) // 5
fmt.Println(len(quests["Healer's Errand"].steps)) // 3Kedua map tidak bisa lagi menjadi tidak selaras, karena sekarang
hanya ada satu. Sebuah quest tidak mungkin punya hadiah tetapi tidak
punya entri untuk langkah-langkahnya. Di dalam literal, Go mengizinkan
kamu tidak menulis tipe bagian dalamnya, sama seperti pada map
bersarang. Contoh-contoh ini ada di
exercises/26/01-quest.go.
Satu ruangan dalam satu nilai
Petualangan Teks menyimpan ruangan dalam empat map, karena nama,
deskripsi, pintu keluar, dan barang adalah tipe yang berbeda. Struct
menyatukan satu ruangan dalam satu nilai. Ini bagian utama dari
exercises/26/02-room.go:
type room struct {
name string
description string
exits map[string]int
items []string
}
func main() {
rooms := map[int]room{
1: {
name: "Forest Path",
description: "You are on a moss covered forest path. Birdsong drifts on the air.",
exits: map[string]int{"north": 2},
},
2: {
name: "Clearing",
description: "A small clearing of wildflowers.",
exits: map[string]int{"south": 1, "east": 3},
items: []string{"torch"},
},
}
here := rooms[2]
fmt.Println("--", here.name, "--")
fmt.Println(here.description)
fmt.Println("Items here:", strings.Join(here.items, ", "))
fmt.Println("East leads to room", here.exits["east"])
}Output:
-- Clearing --
A small clearing of wildflowers.
Items here: torch
East leads to room 3
Semua hal tentang satu ruangan sekarang bepergian bersama. Untuk memberikan ruangan ke sebuah fungsi, kamu memberikan satu nilai, bukan satu id dan empat map.
Fungsi pada struct, cara panjang
Bab 21 menyimpan hitungan antar pemanggilan dengan variabel di luar semua fungsi, dan menjanjikan cara yang lebih rapi. Struct bisa menyimpan hitungan itu, dan fungsi bisa menerima struct lalu mengembalikan versi yang sudah diubah:
type counter struct {
count int
}
func step(c counter) counter {
c.count = c.count + 1
return c
}
func main() {
c := counter{}
c = step(c)
c = step(c)
c = step(c)
fmt.Println(c.count) // 3
}Tidak ada yang hidup di luar fungsi, dan kamu bisa melihat dari
setiap pemanggilan apa yang dibaca dan apa yang berubah. Kamu juga bisa
punya dua penghitung sekaligus, yang tidak bisa dilakukan oleh satu
variabel di luar fungsi. Ini berhasil, tetapi step adalah
nama yang terlalu umum, dan setiap pemanggilan harus memberikan
penghitungnya secara eksplisit.
Metode dan receiver
Metode adalah fungsi yang menjadi milik sebuah tipe. Go tidak punya class: kamu mendefinisikan tipenya, lalu menulis metode-metodenya sebagai fungsi terpisah, di sampingnya. Yang membuat sebuah fungsi menjadi metode adalah satu parameter tambahan, yang ditulis dalam kurungnya sendiri sebelum nama fungsi:
type counter struct {
count int
}
func (c counter) show() {
fmt.Println("Count:", c.count)
}
func main() {
c := counter{count: 3}
c.show() // Count: 3
}Parameter tambahan itu, (c counter), disebut
receiver. Receiver adalah nilai di depan titik ketika
metode dipanggil. Pada c.show(), Go memberikan nilai
c kepada show, dan di dalam badannya nilai itu
bernama c, sama seperti parameter apa pun punya nama. Nama
receiver biasanya satu atau dua huruf pertama dari nama tipenya.
Di dalam badan metode, receiver adalah variabel biasa:
c.count membaca sebuah field. Dua nilai dari satu tipe
menjawab untuk dirinya masing-masing, karena setiap pemanggilan menerima
nilainya sendiri:
type counter struct {
count int
}
func (c counter) show() {
fmt.Println("Count:", c.count)
}
func main() {
a := counter{count: 1}
b := counter{count: 2}
a.show() // Count: 1
b.show() // Count: 2
}Metode juga memungkinkan tipe yang berbeda memakai nama yang sama:
dog dan door boleh sama-sama punya metode
open, karena tipe di depan titik menunjukkan mana yang kamu
maksud.
Metode mendapat salinan
Receiver pada (c counter) adalah receiver
nilai: metode mendapat salinan struct, sama seperti fungsi
mendapat salinan parameter int, seperti kata Bab 21. Jadi metode seperti
ini tidak bisa mengubah aslinya:
type counter struct {
count int
}
func (c counter) stepWrong() {
c.count = c.count + 1
}
func main() {
c := counter{}
c.stepWrong()
c.stepWrong()
fmt.Println(c.count) // 0
}Program ini bisa dikompilasi dan berjalan, dan jawabannya
0. Setiap pemanggilan menambahkan 1 ke salinannya sendiri,
dan salinan itu hilang ketika pemanggilan berakhir.
Pada tahap ini, jalan keluarnya sama dengan versi panjang tadi:
kembalikan salinan yang sudah diubah, dan tugaskan ke variabel yang
sama. Ini kebiasaan yang sama dengan append di Bab 22:
// Chapter 26 example: a counter, with a method that gives back a changed copy.
// Run with: go run exercises/26/03-counter.go
package main
import "fmt"
type counter struct {
count int
}
func (c counter) show() {
fmt.Println("Count:", c.count)
}
func (c counter) step() counter {
c.count = c.count + 1
return c
}
func main() {
c := counter{}
c = c.step()
c = c.step()
c = c.step()
c.show() // Count: 3
// Try this: add a method reset that gives back a counter with count set
// to 0, assign its result back to c, and call c.show() again.
}Program ini mencetak Count: 3.
Buka exercises/26/03-counter.go. Tambahkan metode
reset yang mengembalikan penghitung dengan
count bernilai 0. Tugaskan hasilnya kembali ke
c, lalu panggil c.show() lagi.
Menugaskan kembali memang berhasil, tetapi mudah terlupa. Go
menghentikanmu kalau kamu membuang hasil append, tetapi
tidak kalau kamu membuang hasil metodemu sendiri. c.step()
di satu baris tersendiri bisa dikompilasi, dan tidak melakukan apa-apa
pada c. Bab 27 menghilangkan masalah ini dengan
receiver pointer, yang memungkinkan metode mengubah
aslinya.
Metode yang sudah pernah kamu panggil
Sejak Bab 15 kamu sudah memanggil
reader.ReadString('\n'). reader adalah sebuah
nilai, dan ReadString adalah metode padanya. Itulah
sebabnya dia ingat sudah membaca sampai mana dari satu pemanggilan ke
pemanggilan berikutnya.
Tidak semua yang memakai titik adalah metode. Pada
strings.ToUpper(name), bagian di depan titik adalah
package, dan ToUpper adalah fungsi biasa di dalamnya.
String-nya masuk sebagai argumen. String sama sekali tidak punya metode,
jadi kode ini tidak bisa dikompilasi:
name := "keiko"
fmt.Println(name.ToUpper())name.ToUpper undefined (type string has no field or method ToUpper)
error punya metode. err.Error() memberikan
pesannya sebagai string. Ini isi main di
exercises/26/04-method-or-function.go:
name := "keiko"
// A function from the strings package. The string goes in as an argument.
fmt.Println(strings.ToUpper(name)) // KEIKO
// A method. err is a value, and Error is a method you can call on it.
err := errors.New("out of arrows")
fmt.Println(err.Error()) // out of arrowsBab 29 menunjukkan mengapa fmt.Println(err) mencetak
pesan yang sama.
Nilai kecil untuk game
Struct dan metode paling berguna ketika beberapa keadaan berjalan bersama. Ini seekor anjing dengan nama dan sisa jumlah gonggongan:
// Chapter 26 example: a dog with state, a newDog function and a method.
// Run with: go run exercises/26/05-dog.go
package main
import "fmt"
type dog struct {
name string
barksLeft int
}
func newDog(name string, barksLeft int) dog {
return dog{name: name, barksLeft: barksLeft}
}
func (d dog) bark() dog {
if d.barksLeft <= 0 {
fmt.Printf("%s is hoarse.\n", d.name)
return d
}
fmt.Printf("%s: Woof!\n", d.name)
d.barksLeft = d.barksLeft - 1
return d
}
func main() {
rex := newDog("Rex", 3)
rex = rex.bark()
rex = rex.bark()
rex = rex.bark()
rex = rex.bark()
}Output:
Rex: Woof!
Rex: Woof!
Rex: Woof!
Rex is hoarse.
Struct menyimpan keadaannya, dan metode bekerja padanya. Tipe dengan datanya dan metode yang bekerja pada data itu adalah bentuk yang dipakai di sepanjang bagian ini.
Perhatikan newDog. Go tidak punya fungsi khusus yang
berjalan saat kamu membuat sebuah nilai. Sesuai kebiasaan, kamu menulis
fungsi biasa bernama newThing yang membangun nilainya dan
mengembalikannya, seperti yang dilakukan bufio.NewReader.
Bahasa Go tidak memperlakukan nama ini secara khusus. Fungsi ini berguna
ketika membuat sebuah nilai butuh lebih dari sekadar mengisi field. Bab
30 memakainya untuk memeriksa nilai-nilainya lebih dulu.
Kesalahan umum: melupakan receiver
Kalau receiver tidak ditulis, kamu punya fungsi biasa yang tidak tahu
apa-apa tentang c.
exercises/26/06-missing-receiver.go sengaja berisi
kesalahan ini:
func step() counter {
c.count = c.count + 1
return c
}
func main() {
c := counter{}
c = c.step()
fmt.Println(c.count)
}Menjalankannya berhenti dengan:
# command-line-arguments
exercises\26\06-missing-receiver.go:17:2: undefined: c
exercises\26\06-missing-receiver.go:18:9: undefined: c
exercises\26\06-missing-receiver.go:23:8: c.step undefined (type counter has no field or method step)
Dua error pertama mengatakan bahwa step tidak punya
c. Yang terakhir mengatakan bahwa counter
tidak punya metode step, karena step tidak
terhubung dengannya. Perbaikannya adalah menambahkan receiver:
func (c counter) step() counter {.
PR
Starter PR ada di exercises/26/homework/. Setiap starter
bisa langsung dijalankan dan mencetak satu baris pengganti. Definisikan
tipe dan metodemu di luar main. Semua metode di PR ini
memakai receiver nilai, jadi metode yang mengubah sesuatu mengembalikan
salinan yang sudah diubah.
Soal 1 — Penghitung
Buka exercises/26/homework/01-counter-object.go. Buat
struct counter dengan satu field dan tiga metode:
count: field int, yang dimulai dari nilai nolnya.inc(): mengembalikan penghitung dengancountbertambah 1.get(): mengembalikan hitungan saat ini.reset(): mengembalikan penghitung dengancountbernilai0.
Panggil secara berurutan, dan cetak nilainya di setiap langkah.
Soal 2 — Anjing yang menggonggong
Buka exercises/26/homework/02-dog.go. Buat struct
dog dengan field name dan metode
bark() yang mencetak <name>: Woof!. Buat
anjing kedua dengan nama yang berbeda. Panggil bark() pada
keduanya, dan pastikan masing-masing mencetak namanya sendiri.
Soal 3 — Kalkulator
Buka exercises/26/homework/03-calculator.go. Buat struct
calc dengan:
value: field int, yang dimulai dari nilai nolnya.add(n): mengembalikan calc yang sudah ditambahn.sub(n): mengembalikan calc yang sudah dikurangin.mul(n): mengembalikan calc yang sudah dikalin.show(): mencetak nilai saat ini dengan label, sepertiValue: 10.
Lakukan beberapa pemanggilan, dan pastikan nilainya berubah sesuai harapan.
Tantangan — Stack
Buka exercises/26/homework/04-stack.go. Buat struct
stack yang menyimpan string dan berperilaku seperti
tumpukan, yaitu item terakhir yang masuk menjadi yang pertama
keluar:
items: field[]string.push(v): mengembalikan stack denganvdi puncak.pop(): mengembalikan dua nilai, stack tanpa item puncak, dan item puncak itu.peek(): mengembalikan item puncak dantrue, atau""danfalsekalau stack kosong, seperti bentukv, okdari Bab 23.size(): mengembalikan jumlah item yang tersimpan.
Puncak stack adalah ujung slice. Pakai append untuk
push, dan pemotongan slice dari Bab 22 untuk pop.
Stuck atau sudah selesai? Buka halaman solusi PR.