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:

  • &score berarti "lokasi score". Ini membuat pointer.
  • *p berarti "nilai di lokasi yang disimpan p". Ini mengikuti pointer.
score := 10

p := &score
fmt.Println(*p) // 10

*p = 20
fmt.Println(score) // 20

p 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(), padahal c adalah counter dan step meminta 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) // 1

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

  1. Kalau metode harus mengubah nilainya, metode itu butuh receiver pointer.
  2. Kalau suatu tipe punya satu metode seperti itu, beri semua metodenya receiver pointer, supaya tidak ada yang perlu mengingat mana yang mana.
  3. 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.