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.