29. Komposisi dan interface

Anjing dan kucing punya banyak kesamaan: masing-masing punya nama, dan masing-masing bisa dideskripsikan. Hanya beberapa hal yang berbeda. Menulis bagian yang sama dua kali itu pemborosan, dan keduanya bisa menjadi tidak selaras, seperti dua map di Bab 23.

Go memberimu dua alat untuk ini. Embedding menaruh satu struct di dalam struct lain, sehingga yang di luar bisa memakai field dan metode yang di dalam. Interface memungkinkan tipe yang berbeda diperlakukan sama, karena punya metode yang sama. Bab ini membahas keduanya.

Bagian yang sama

Mulai dengan struct yang menyimpan kesamaan seluruh keluarga:

type animal struct {
    name string
}

func (a animal) describe() {
    fmt.Printf("%s is an animal.\n", a.name)
}

Embedding

Untuk menaruh animal di dalam dog, tulis tipenya di dalam struct tanpa nama field:

type dog struct {
    animal
}

func main() {
    rex := dog{animal: animal{name: "Rex"}}
    rex.describe()               // Rex is an animal.
    fmt.Println(rex.name)        // Rex
    fmt.Println(rex.animal.name) // Rex
}

Inilah embedding. Sebuah dog sekarang berisi sebuah animal, dalam field yang memakai nama tipenya. Di literal, kamu mengisi field itu dengan animal: animal{...}.

Field dan metode dari tipe di dalam dipromosikan: kamu bisa memakainya langsung pada anjing itu. rex.name adalah kependekan dari rex.animal.name, dan rex.describe() adalah kependekan dari rex.animal.describe(). dog tidak punya describe sendiri. Go mencarinya di dog, tidak menemukannya, lalu menemukannya di animal yang di-embed. Metode dengan receiver pointer dipromosikan dengan cara yang sama.

Menambah perilaku yang hanya dimiliki satu tipe

Beri tipe di luar metodenya sendiri. Metode itu milik dog, dan animal tidak tahu apa-apa tentangnya:

func (d dog) fetch() {
    fmt.Printf("%s fetches the ball.\n", d.name)
}

func main() {
    rex := dog{animal: animal{name: "Rex"}}
    rex.fetch() // Rex fetches the ball.
}

Mengganti metode

Kalau tipe di luar punya metode dengan nama yang sama dengan metode yang dipromosikan, metode milik tipe di luar yang menang:

func (d dog) describe() {
    fmt.Printf("%s is a dog.\n", d.name)
}

func main() {
    rex := dog{animal: animal{name: "Rex"}}
    rex.describe()        // Rex is a dog.
    rex.animal.describe() // Rex is an animal.
}

animal masih ada di dalam, dan metodenya juga masih ada, seperti yang ditunjukkan baris terakhir.

Mengganti metode hanya berpengaruh pada pemanggilan lewat tipe di luar. Metode animal yang memanggil a.describe() selalu memanggil milik animal, walaupun animal itu berada di dalam dog. Tipe di dalam tidak tahu dia di-embed di mana.

Memanggil versi yang di-embed juga

Kadang kamu ingin menambah metode di dalam, bukan menggantinya: lakukan apa yang dia lakukan, lalu sedikit lagi. Panggil lewat field-nya:

// Chapter 29 example: a dog that embeds an animal, and adds to its describe.
// Run with: go run exercises/29/01-animals.go

package main

import "fmt"

type animal struct {
    name string
}

func (a animal) describe() {
    fmt.Printf("%s is an animal.\n", a.name)
}

type dog struct {
    animal
}

func (d dog) describe() {
    d.animal.describe()
    fmt.Println("  ...specifically, a dog.")
}

func (d dog) fetch() {
    fmt.Printf("%s fetches the ball.\n", d.name)
}

func main() {
    rex := dog{animal: animal{name: "Rex"}}
    rex.describe()
    rex.fetch()

    // Try this: add a cat struct that embeds animal, give it its own describe
    // that says it is a cat, and add a meow method. Make one cat and call both.
}

Output:

Rex is an animal.
  ...specifically, a dog.
Rex fetches the ball.

Buka exercises/29/01-animals.go. Tambahkan struct cat yang meng-embed animal, beri dia describe sendiri yang mengatakan bahwa dia kucing, dan tambahkan metode meow. Buat satu kucing dan panggil keduanya.

Tipe yang di-embed bukan induk

Go tidak punya pewarisan. Seekor anjing berisi seekor hewan, dan hanya itu: dog adalah tipe yang berbeda dari animal, dan Go tidak menerima yang satu sebagai pengganti yang lain. Ini exercises/29/02-not-a-parent.go:

type animal struct {
    name string
}

type dog struct {
    animal
}

func show(a animal) {
    fmt.Println("Animal:", a.name)
}

func main() {
    rex := dog{animal: animal{name: "Rex"}}
    show(rex)
}

Menjalankannya berhenti dengan:

# command-line-arguments
exercises\29\02-not-a-parent.go:25:7: cannot use rex (variable of type dog) as animal value in argument to show

Kamu bisa memberikan hewan yang ada di dalam: show(rex.animal). Tetapi show lalu hanya melihat hewan, dan anjingnya hilang. Dengan alasan yang sama, satu []animal tidak bisa menyimpan anjing dan kucing. Untuk memperlakukan tipe yang berbeda dengan cara yang sama, kamu butuh interface.

Interface

Interface adalah tipe yang didefinisikan oleh sekumpulan metode. Interface mengatakan apa yang harus bisa dilakukan sebuah nilai, dan tidak mengatakan apa-apa tentang nilai itu sendiri:

type describer interface {
    describe()
}

Baca sebagai: describer adalah apa saja yang punya metode describe(). Tipe apa pun yang punya metode itu memenuhi interface tersebut. Tidak ada baris tempat sebuah tipe menyatakan hal itu. Punya metodenya sudah cukup.

Variabel atau parameter bertipe interface bisa menyimpan nilai apa pun yang memenuhinya. Memanggil metode lewat interface menjalankan metode milik nilai di dalamnya. Ini exercises/29/03-interface.go, tempat dog dan cat masing-masing punya describe sendiri, dan parrot mendapat metode yang dipromosikan dari animal:

func introduce(d describer) {
    fmt.Print("Meet this one: ")
    d.describe()
}

func main() {
    rex := dog{animal: animal{name: "Rex"}}
    tom := cat{animal: animal{name: "Tom"}}
    polly := parrot{animal: animal{name: "Polly"}}

    introduce(rex)

    creatures := []describer{rex, tom, polly, animal{name: "Bug"}}
    for _, c := range creatures {
        c.describe()
    }
}

Output:

Meet this one: Rex is a dog.
Rex is a dog.
Tom is a cat.
Polly is an animal.
Bug is an animal.

Satu fungsi, dan satu slice, untuk empat tipe yang berbeda. Setiap nilai menjawab dengan metodenya sendiri. Perulangannya tidak perlu tahu tipe masing-masing, dan tipe baru yang punya metode describe langsung berjalan tanpa perubahan di sini.

Nama interface sering berupa kata kerja ditambah er, seperti describer. Buat interface tetap kecil. Banyak yang hanya punya satu metode.

Kalau suatu tipe kekurangan metode, Go berhenti sebelum program berjalan, dan menyebutkan apa yang kurang. exercises/29/04-missing-method.go memberikan rock, yang tidak bisa mendeskripsikan dirinya:

# command-line-arguments
exercises\29\04-missing-method.go:33:12: cannot use rock{…} (value of type rock) as describer value in argument to introduce: rock does not implement describer (missing method describe)

Metode dengan receiver pointer adalah milik pointernya. Kalau describe punya receiver (c *cat), maka *cat memenuhi describer, sedangkan cat tidak:

type describer interface {
    describe()
}

type cat struct {
    name string
}

func (c *cat) describe() {
    fmt.Printf("%s is a cat.\n", c.name)
}

func main() {
    tom := cat{name: "Tom"}
    creatures := []describer{tom}
    fmt.Println(len(creatures))
}
cannot use tom (variable of type cat) as describer value in array or slice literal: cat does not implement describer (method describe has pointer receiver)

Perbaikannya adalah memberikan pointernya: []describer{&tom}.

Contoh nyata: fmt.Stringer

Pustaka standar penuh dengan interface kecil. Package fmt mendeklarasikan yang satu ini:

type Stringer interface {
    String() string
}

Ketika kamu mencetak nilai dengan fmt.Println atau %v, fmt memeriksa apakah nilai itu punya metode ini, dan kalau punya, memakai teksnya. Ini exercises/29/05-stringer.go:

// Chapter 29 example: fmt.Stringer, an interface from the standard library.
// Run with: go run exercises/29/05-stringer.go

package main

import "fmt"

type point struct {
    x int
    y int
}

func (p point) String() string {
    return fmt.Sprintf("(%d, %d)", p.x, p.y)
}

type size struct {
    w int
    h int
}

func main() {
    p := point{x: 3, y: 4}
    fmt.Println(p)                                   // (3, 4)
    fmt.Printf("Position: %v\n", p)                  // Position: (3, 4)
    fmt.Println([]point{{x: 1, y: 2}, {x: 5, y: 6}}) // [(1, 2) (5, 6)]

    // size has no String method, so it prints in the usual struct form.
    fmt.Println(size{w: 3, h: 4}) // {3 4}

    var s fmt.Stringer = p
    fmt.Println(s.String()) // (3, 4)
}

point tidak pernah menyebut fmt.Stringer. Dia punya metodenya, dan itu cukup. Namanya harus persis String, dengan huruf kapital, karena fmt adalah package lain, dan Bab 25 menjelaskan bahwa package lain hanya bisa melihat nama yang diekspor. Metode bernama string diabaikan.

error adalah interface

Bab 21 mengatakan bahwa error menyimpan nil atau deskripsi masalahnya. Sekarang kamu bisa melihat apa itu sebenarnya. Dia dideklarasikan seperti ini:

type error interface {
    Error() string
}

Jadi tipe apa pun yang punya metode Error() string adalah error, dan nil berarti interface itu tidak menyimpan apa-apa. Itulah sebabnya fmt.Println(err) mencetak pesannya: fmt juga memeriksa metode ini. Tipemu sendiri bisa menyimpan lebih dari sekadar pesan. Ini checkLevel dari Bab 21 dengan tipe seperti itu, di exercises/29/06-custom-error.go:

type levelError struct {
    level int
}

func (e levelError) Error() string {
    return fmt.Sprintf("level %d is not from 1 to 99", e.level)
}

func checkLevel(level int) error {
    if level < 1 || level > 99 {
        return levelError{level: level}
    }
    return nil
}

func main() {
    err := checkLevel(7)
    fmt.Println(err) // <nil>

    err = checkLevel(150)
    if err != nil {
        fmt.Println("Error:", err) // Error: level 150 is not from 1 to 99
    }
}

Pemanggil memeriksa err != nil seperti biasa. Untuk sebagian besar program, errors.New dan fmt.Errorf sudah cukup.

Meminta tipe yang sebenarnya

Kadang kamu butuh nilai di dalam sebuah interface. Type assertion memintanya, dengan bentuk v, ok yang sama seperti mencari di map:

bad, ok := err.(levelError)
if ok {
    fmt.Println("The level was", bad.level) // The level was 150
}

ok bernilai false kalau interface itu menyimpan tipe lain. Type switch melakukan hal yang sama untuk beberapa tipe sekaligus, dan kamu akan melihatnya di kode orang lain. Jarang-jarang saja memakai keduanya. Kalau kamu sampai bertanya tipe apa yang kamu pegang, metode pada interface biasanya jawaban yang lebih baik.

PR

Starter PR ada di exercises/29/homework/. Setiap starter bisa langsung dijalankan dan mencetak satu baris pengganti.

Soal 1 — Kendaraan dan mobil

Buka exercises/29/homework/01-vehicle.go. File ini punya struct vehicle dengan field name dan metode describe() yang mencetak <name> is a vehicle., dan car yang meng-embed-nya. Buat sebuah mobil di main dan panggil describe(). Metode yang dipakai seharusnya metode yang dipromosikan.

Soal 2 — Mengganti metode

Buka exercises/29/homework/02-override.go. Beri car metode describe() sendiri yang mencetak <name> is a car. Pastikan vehicle tetap mengatakan "vehicle", dan car mengatakan "car".

Soal 3 — Memperluas metode yang di-embed

Buka exercises/29/homework/03-extend.go. Beri car metode describe() yang lebih dulu memanggil describe() milik vehicle yang di-embed, lalu mencetak It has four wheels. Kedua baris harus muncul.

Tantangan — Dua jenis

Buka exercises/29/homework/04-two-kinds.go. Buat struct animal dengan name, dan dua struct yang meng-embed-nya, bird dan fish. Masing-masing punya move() sendiri yang mencetak caranya bergerak (flies, swims). Deklarasikan interface mover, taruh satu dari masing-masing dalam []mover, dan ulangi slice itu sambil memanggil move().

Stuck atau sudah selesai? Buka halaman solusi PR.