27. Pointer
Bab 26 berakhir dengan sebuah masalah. Metode dengan receiver nilai mendapat salinan, jadi tidak bisa mengubah aslinya, dan menugaskan kembali setelah setiap pemanggilan mudah terlupa. Bab ini memperkenalkan pointer, yang memungkinkan fungsi atau metode mencapai nilai aslinya.
Apa itu pointer
Setiap variabel tinggal di suatu tempat di memori komputer. Pointer adalah nilai yang menyimpan lokasi itu. Kalau variabel adalah rumah, pointer adalah alamatnya yang ditulis di secarik kertas: menyalin kertasnya tidak menyalin rumahnya.
Dua simbol melakukan pekerjaannya:
&scoreberarti "lokasiscore". Ini membuat pointer.*pberarti "nilai di lokasi yang disimpanp". Ini mengikuti pointer.
score := 10
p := &score
fmt.Println(*p) // 10
*p = 20
fmt.Println(score) // 20p bukan skor kedua. Dia mengarah ke satu
score itu, jadi menugaskan lewat *p mengubah
score. Tipe p ditulis *int,
dibaca "pointer ke int". Kalau kamu mencetak p sendiri,
kamu mendapat lokasinya sebagai angka, seperti
0xc000012120, yang berubah dari satu kali jalan ke kali
jalan berikutnya. Angka itu jarang berguna.
Go tidak punya aritmetika pada pointer. Kamu bisa mengikuti dan menyalinnya, tetapi tidak bisa menambahkan angka ke pointer untuk mencapai lokasi di sebelahnya.
Memberikan pointer ke fungsi
Bab 21 mengatakan bahwa mengubah parameter tidak mengubah variabel milik pemanggil. Berikan pointer, dan fungsi bisa mengikutinya kembali ke aslinya:
func addBonus(n *int, bonus int) {
*n = *n + bonus
}
func main() {
score := 20
addBonus(&score, 5)
fmt.Println(score) // 25
}Tipe parameternya *int, dan pemanggilannya memberikan
&score. Pemanggilan itu menunjukkan bahwa
score mungkin berubah, dan itu hal yang baik untuk dilihat
pembaca. Contoh-contoh ini ada di
exercises/27/01-pointer-basics.go.
Memberikan yang salah dari keduanya tidak bisa dikompilasi:
func addBonus(n *int, bonus int) {
*n = *n + bonus
}
func main() {
score := 20
addBonus(score, 5)
fmt.Println(score)
}cannot use score (variable of type int) as *int value in argument to addBonus
Receiver pointer
Receiver metode adalah parameter, jadi receiver juga bisa berupa
pointer. Receiver pointer punya * di depan
tipenya. Ini penghitung dari Bab 26, tanpa menugaskan kembali:
// Chapter 27 example: a method with a pointer receiver changes the original.
// Run with: go run exercises/27/02-counter.go
package main
import "fmt"
type counter struct {
count int
}
func (c *counter) step() {
c.count = c.count + 1
}
func main() {
c := counter{}
c.step()
c.step()
c.step()
fmt.Println(c.count) // 3
}Baca (c *counter) sebagai step menerima
pointer ke sebuah counter. Dua hal yang perlu diperhatikan:
- Di dalam metode, kamu menulis
c.count, bukan(*c).count. Ketika nilai di depan titik adalah pointer ke struct, Go mengikuti pointer itu untukmu. - Pemanggilannya
c.step(), padahalcadalah counter danstepmeminta pointer. Ketika sebuah variabel menyimpan nilainya, Go mengambil lokasinya untukmu, seolah-olah kamu menulis(&c).step(). Kamu akan sering melihat ini.
Go hanya bisa melakukan itu untuk sesuatu yang punya lokasi, seperti variabel. Nilai yang tidak pernah disimpan di mana pun tidak punya lokasi, jadi kode ini tidak bisa dikompilasi:
type counter struct {
count int
}
func (c *counter) step() {
c.count = c.count + 1
}
func main() {
counter{}.step()
}cannot call pointer method step on counter
Titik yang bisa berpindah
Sebuah titik punya x dan y, dan metode yang
mengukur jarak ke titik lain:
// Chapter 27 example: a point with a pointer receiver.
// Run with: go run exercises/27/03-point.go
package main
import (
"fmt"
"math"
)
type point struct {
x float64
y float64
}
func (p *point) distance(other point) float64 {
dx := p.x - other.x
dy := p.y - other.y
return math.Sqrt(dx*dx + dy*dy)
}
func main() {
a := point{x: 0, y: 0}
b := point{x: 3, y: 4}
fmt.Println(a.distance(b)) // 5
// Try this: add a method move(dx float64, dy float64) with a pointer
// receiver, call it on a, and print a.distance(b) again.
}a.distance(b) memanggil metode dengan a
sebagai receiver dan b sebagai argumen. Hasil cetaknya
5, karena hasilnya float64 tanpa angka di belakang
koma.
distance tidak mengubah apa pun, jadi receiver nilai
juga bisa. Tetapi titik yang berpindah butuh receiver pointer, dan
programmer Go tidak mencampur keduanya pada satu tipe. Bagian terakhir
bab ini kembali membahas hal ini.
Buka exercises/27/03-point.go. Tambahkan metode
move(dx float64, dy float64) dengan receiver pointer, yang
menambahkan dx ke p.x dan dy ke
p.y. Panggil pada a, lalu cetak
a.distance(b) lagi.
Tipe kedua: character
Karakter kecil yang bisa menerima serangan. Filenya
exercises/27/04-character.go:
type character struct {
name string
hp int
maxHP int
}
func newCharacter(name string, hp int) *character {
return &character{name: name, hp: hp, maxHP: hp}
}
func (c *character) takeDamage(amount int) {
c.hp = c.hp - amount
if c.hp < 0 {
c.hp = 0
}
}
func (c *character) heal(amount int) {
c.hp = c.hp + amount
if c.hp > c.maxHP {
c.hp = c.maxHP
}
}
func (c *character) isAlive() bool {
return c.hp > 0
}
func main() {
keiko := newCharacter("Keiko", 100)
keiko.takeDamage(30)
fmt.Println(keiko.hp) // 70
keiko.heal(50)
fmt.Println(keiko.hp) // 100
fmt.Println(keiko.isAlive()) // true
}&character{...} membuat karakter baru dan memberimu
pointer ke karakter itu, jadi newCharacter mengembalikan
*character. Mengembalikan pointer ke sesuatu yang dibuat di
dalam fungsi itu aman di Go. Karakternya tetap hidup selama masih ada
pointer yang mengarah ke dia. Setiap pemanggilan
newCharacter membuat karakter terpisah dengan field-nya
sendiri.
Menyalin atau berbagi
Menugaskan struct berarti menyalinnya. Menugaskan pointer hanya menyalin pointernya, jadi kedua variabel mengarah ke struct yang sama:
a := character{name: "Ada", hp: 100}
b := a
b.hp = 1
fmt.Println(a.hp) // 100
c := &character{name: "Ben", hp: 100}
d := c
d.hp = 1
fmt.Println(c.hp) // 1Kamu sudah pernah melihat pembagian seperti ini. Bab 22 menunjukkan bahwa menugaskan slice berarti berbagi item-nya, dan bahwa fungsi bisa mengubah item slice tanpa pointer yang terlihat. Itu karena slice menyimpan pointer ke item-nya di dalam dirinya. Hanya item-nya yang dibagi, itulah sebabnya fungsi yang menumbuhkan slice tetap harus mengembalikannya. Map berperilaku sama, seperti yang ditunjukkan Bab 23. Jadi kamu jarang butuh pointer ke slice atau ke map.
Salinan struct menyalin setiap field, dan field slice tetap mengarah ke item yang sama:
type team struct {
names []string
}
func main() {
a := team{names: []string{"Ada"}}
b := a
b.names[0] = "Ben"
fmt.Println(a.names[0]) // Ben
}nil: pointer ke ketiadaan
Nilai nol dari pointer adalah nil, nil yang
sama dengan yang kamu periksa pada err != nil. Pointer nil
tidak mengarah ke mana pun, jadi mengikutinya membuat program crash saat
berjalan. Ini exercises/27/05-nil-panic.go, di dalam
main:
var keiko *character
fmt.Println(keiko == nil) // true
fmt.Println(keiko.hp)Program ini mencetak true, lalu:
panic: runtime error: invalid memory address or nil pointer dereference
Beberapa baris lagi menyusul setelah baris ini. Di antaranya, Go
memberikan file dan baris tempat crash terjadi. Ini crash yang paling
sering akan kamu temui di Go. Artinya, ada pointer yang bernilai
nil di baris yang disebutkannya. Dereference
pointer berarti mengikutinya. Crash yang sama terjadi di dalam metode,
pada baris pertama yang membaca field lewat receiver nil.
nil juga berguna, karena bisa berarti "tidak ada
jawaban". Fungsi yang mengembalikan pointer bisa mengembalikan
nil ketika tidak punya apa-apa untuk diberikan, dan
pemanggilnya memeriksa dulu. Ini
exercises/27/06-nil-check.go:
// Chapter 27 example: a function that returns nil for "no answer", and a
// caller that checks for it.
// Run with: go run exercises/27/06-nil-check.go
package main
import "fmt"
type character struct {
name string
hp int
}
// stronger returns whichever character has more hp, or nil for a tie.
func stronger(a *character, b *character) *character {
if a.hp > b.hp {
return a
}
if b.hp > a.hp {
return b
}
return nil
}
func announce(winner *character) {
if winner == nil {
fmt.Println("It is a tie.")
return
}
fmt.Println(winner.name, "is stronger.")
}
func main() {
ada := &character{name: "Ada", hp: 100}
ben := &character{name: "Ben", hp: 80}
cara := &character{name: "Cara", hp: 100}
announce(stronger(ada, ben))
announce(stronger(ada, cara))
}Output:
Ada is stronger.
It is a tie.
Fungsi yang mengembalikan pointer yang mungkin nil
sebaiknya mengatakannya di komentarnya, seperti stronger.
Siapa pun yang memanggilnya harus memeriksa.
Pointer yang sudah pernah kamu pakai
bufio.NewReader mengembalikan
*bufio.Reader, sebuah pointer. Reader harus mengingat sudah
membaca sampai mana, jadi metode-metodenya punya receiver pointer.
Ketika sebuah fungsi membutuhkan reader sebagai parameter, itulah tipe
yang ditulis:
func readLine(reader *bufio.Reader, prompt string) (string, error) {
fmt.Print(prompt)
text, err := reader.ReadString('\n')
line := strings.TrimSpace(text)
if err != nil && line == "" {
return "", err
}
return line, nil
}
func main() {
reader := bufio.NewReader(os.Stdin)
name, err := readLine(reader, "Name: ")
if err != nil {
fmt.Println("No more input.")
return
}
fmt.Println("Hello,", name)
}Salinan pointer semuanya mengarah ke satu reader itu, jadi apa yang sudah dibacanya tidak hilang. Versi di Bab 21 menyimpan reader di variabel di luar fungsi. Keduanya berhasil, dan yang ini menunjukkan dari pemanggilannya apa yang dipakai fungsi.
Kapan memakai receiver pointer
Untuk tipe yang kamu tulis di buku ini, tiga aturan sudah cukup:
- Kalau metode harus mengubah nilainya, metode itu butuh receiver pointer.
- Kalau suatu tipe punya satu metode seperti itu, beri semua metodenya receiver pointer, supaya tidak ada yang perlu mengingat mana yang mana.
- Kalau tidak ada metode yang mengubah nilainya, dan field-nya hanya sedikit dan kecil, seperti persegi panjang, pakai receiver nilai.
Kalau ragu, pakai receiver pointer. Sebagian besar struct yang punya keadaan akhirnya memakainya.
PR
Starter PR ada di exercises/27/homework/. Setiap starter
bisa langsung dijalankan dan mencetak satu baris pengganti. Definisikan
tipe dan metodemu di luar main.
Soal 1 — Titik dengan move
Buka exercises/27/homework/01-point.go. Buat struct
point seperti di atas, ditambah metode
move(dx, dy) yang menambah posisinya. Buat dua titik, cetak
jarak di antara keduanya, pindahkan salah satunya, cetak posisinya, lalu
cetak jaraknya lagi.
Soal 2 — Karakter
Buka exercises/27/homework/02-character.go. Buat struct
character dengan newCharacter(name, hp),
takeDamage(amount), heal(amount), dan
isAlive(). Tambahkan metode report() yang
mencetak baris seperti Keiko: 70 / 100 HP (alive: true).
Jalankan pertarungan kecil: report, serangan 30, report, serangan 80,
report, sembuh 20, report.
Soal 3 — Persegi panjang
Buka exercises/27/homework/03-rectangle.go. Buat struct
rectangle dengan field w dan h,
serta metode area() dan perimeter(). Tentukan
apakah receiver-nya sebaiknya nilai atau pointer. Uji dengan dua persegi
panjang yang berbeda ukuran.
Tantangan — Anjing dan temannya
Buka exercises/27/homework/04-dog-friend.go. Buat struct
dog dengan field name, dan field
friend bertipe *dog. Friend yang nil berarti
anjing itu tidak punya teman. Tulis newDog(name), yang
mengembalikan *dog, dan metode describe() yang
mencetak <name> has no friend. atau
<name> is friends with <friend's name>.
Buat dua anjing, dan panggil describe() pada keduanya.
Jadikan yang satu teman yang lain, dan panggil describe()
pada keduanya lagi. Lalu ubah nama temannya lewat field
friend, dengan rex.friend.name = "Lady", dan
panggil describe() pada keduanya sekali lagi.
Stuck atau sudah selesai? Buka halaman solusi PR.