Dasar Interface

Pemenuhan implisit tanpa kata kunci implements.

Daftar isi

Interface mendefinisikan perilaku yang diharapkan dari sebuah nilai dalam bentuk sekumpulan metode, tanpa menyebut tipe yang memenuhinya. Topik ini membahas cara mendeklarasikan interface, cara pemenuhan implisit menghubungkan tipe konkret dengan interface, serta struktur nilai interface yang tersusun atas tipe dinamis dan nilai dinamis. Setelah membacanya, fungsi dapat dirancang menerima sembarang tipe yang memenuhi kontrak tertentu, dan perbedaan antara interface yang nil dengan interface yang berisi nilai nil dapat dijelaskan.

Interface sebagai kontrak metode#

Interface (interface) adalah tipe yang menuntut sekumpulan metode tanpa menetapkan implementasinya. Deklarasi interface hanya mencantumkan nama metode beserta tanda tangannya (signature), yaitu daftar parameter dan nilai kembalian tanpa isi fungsi. Tipe apa pun yang memiliki seluruh metode tersebut dengan bentuk yang sama persis otomatis memenuhi interface itu. Bentuk umum deklarasi interface adalah sebagai berikut.

go Bentuk umum deklarasi interface
// tidak-dijalankan
type NamaInterface interface {
	MetodePertama()
	MetodeKedua(nilai int) (string, error)
}

Definisi di atas tidak memuat satu baris pun kode implementasi, sehingga isinya murni kontrak perilaku. Urutan metode di dalam deklarasi tidak berpengaruh, dan satu interface boleh menuntut satu metode saja maupun beberapa metode sekaligus. Semakin sedikit metode yang dituntut, semakin banyak tipe yang dapat memenuhinya. Prinsip tersebut menjadi dasar filosofi interface kecil yang menjadi inti pembahasan modul ini.

Pemenuhan implisit#

Go tidak memiliki kata kunci implements maupun deklarasi turunan. Hubungan antara tipe konkret dan interface terbentuk secara implisit, yaitu saat kompilator memastikan tipe tersebut memiliki seluruh metode yang dituntut. Konsekuensinya besar untuk desain program. Tipe yang ditulis lebih dahulu, misalnya oleh pustaka lain, tetap dapat memenuhi interface yang dideklarasikan kemudian tanpa menyentuh kode tipe tersebut. Program berikut memperlihatkan dua tipe yang berbeda memenuhi satu interface yang sama, lengkap dengan fungsi yang menerima keduanya.

go catatan-ringkas.go
package main

import "fmt"

// Ringkas menuntut metode yang mengembalikan ringkasan satu baris.
type Ringkas interface {
	Ringkas() string
}

// Catatan adalah satu entri buku catatan.
type Catatan struct {
	Judul string
	Isi   string
}

// Ringkas memenuhi interface Ringkas tanpa deklarasi apa pun.
func (c Catatan) Ringkas() string {
	return c.Judul + ": " + c.Isi
}

// Suhu adalah pengukuran suhu dalam derajat Celsius.
type Suhu float64

// Ringkas memenuhi interface Ringkas yang sama.
func (s Suhu) Ringkas() string {
	return fmt.Sprintf("%.1f derajat Celsius", float64(s))
}

// tampilkan menerima sembarang nilai yang memenuhi interface Ringkas.
func tampilkan(r Ringkas) {
	fmt.Println(r.Ringkas())
}

func main() {
	daftar := []Ringkas{
		Catatan{Judul: "Rapat", Isi: "pukul 10.00 di ruang sidang"},
		Suhu(24.5),
	}
	for _, item := range daftar {
		tampilkan(item)
	}
}
text Keluaran
Rapat: pukul 10.00 di ruang sidang
24.5 derajat Celsius

Fungsi tampilkan tidak mengetahui keberadaan Catatan maupun Suhu. Yang diperhatikannya hanya kontrak Ringkas, sehingga tipe baru dapat ditambahkan tanpa mengubah fungsi tersebut. Pemenuhan diperiksa saat program dikompilasi, dan apabila satu metode tidak ada, kompilasi gagal dengan pesan yang menyebut metode mana yang kurang. Cakupan inilah yang membuat interface pada Go sering dideklarasikan di sisi pemakainya dan bukan di sisi tipe penyedianya.

Program di atas dapat dicoba lewat terminal dengan go run sebagaimana berikut.

bash
# siapkan folder kerja untuk topik ini
mkdir -p /tmp/gointerface/dasar-interface
cp catatan-ringkas.go /tmp/gointerface/dasar-interface/
cd /tmp/gointerface/dasar-interface

# periksa format sebelum menjalankan programnya
gofmt -l .

# jalankan programnya
go run catatan-ringkas.go
text Keluaran
Rapat: pukul 10.00 di ruang sidang
24.5 derajat Celsius

Keluarannya sama dengan blok sebelumnya karena keduanya menjalankan program yang sama, sedangkan gofmt -l tidak mencetak apa pun ketika format kode sudah sesuai.

Nilai interface: tipe dinamis dan nilai dinamis#

Variabel bertipe interface tidak menyimpan nilainya saja. Isinya adalah sepasang informasi, yaitu tipe dinamis (dynamic type), yakni tipe konkret yang sedang dipegang, dan nilai dinamis (dynamic value) milik tipe tersebut. Variabel interface yang belum diberi nilai tidak memiliki tipe dinamis, dan perbandingannya dengan nil bernilai benar. Program berikut mengamati kedua keadaan itu pada satu variabel yang sama.

go tipe-dinamis.go
package main

import "fmt"

// Statistik menuntut dua metode penghitungan.
type Statistik interface {
	Tambah(n int)
	Rekap() string
}

// RataRata menghitung rata-rata nilai ujian.
type RataRata struct {
	jumlah int
	banyak int
}

// Tambah menambahkan satu nilai ke penghitung.
func (r *RataRata) Tambah(n int) {
	r.jumlah += n
	r.banyak++
}

// Rekap mengembalikan ringkasan penghitungan sejauh ini.
func (r *RataRata) Rekap() string {
	return fmt.Sprintf("banyak %d, rata-rata %.1f", r.banyak, float64(r.jumlah)/float64(r.banyak))
}

func main() {
	var s Statistik
	fmt.Println("s masih nil:", s == nil)

	s = &RataRata{}
	fmt.Printf("tipe dinamisnya %T\n", s)
	for _, n := range []int{80, 90, 100} {
		s.Tambah(n)
	}
	fmt.Println(s.Rekap())

	// baris berikut tidak dapat dikompilasi karena int tidak memenuhi Statistik
	// s = 42
}
text Keluaran
s masih nil: true
tipe dinamisnya *main.RataRata
banyak 3, rata-rata 90.0

Baris pertama keluaran menunjukkan s masih nil sebelum diberi nilai. Setelah s = &RataRata{}, tipe dinamisnya menjadi *main.RataRata, dan pemanggilan s.Tambah(n) diteruskan ke metode milik tipe dinamis itu. Mekanisme ini disebut pemanggilan metode dinamis (dynamic dispatch), yaitu pemilihan metode yang dilakukan saat program berjalan dan bukan saat dikompilasi. Karena itu, satu variabel interface dapat memegang tipe yang berbeda dari waktu ke waktu selama semuanya memenuhi kontrak yang sama.

Kesalahan umum#

Dua kesalahan berikut paling sering muncul ketika interface mulai digunakan.

  • Lupa bahwa pemenuhan membedakan receiver nilai dan receiver pointer. Apabila metode dideklarasikan dengan receiver *RataRata, yang memenuhi interface adalah *RataRata dan bukan RataRata. Deklarasi var s Statistik = RataRata{} ditolak kompilator dengan pesan RataRata does not implement Statistik (method Rekap has pointer receiver). Perbaikannya, gunakan receiver nilai untuk tipe kecil yang tidak diubah, atau simpan pointer pada variabel interface.
  • Mengira setiap interface yang berisi pointer nil juga bernilai nil. Pointer yang bertipe konkret tetap membawa tipe dinamisnya ketika dimasukkan ke variabel interface, sehingga perbandingan dengan nil bernilai salah. Program berikut memperlihatkan perbedaannya.
go nil-dalam-interface.go
package main

import "fmt"

// Penyimpan menuntut metode penyimpanan satu entri.
type Penyimpan interface {
	Simpan(kunci string)
}

// Arsip menyimpan daftar kunci catatan.
type Arsip struct {
	Berisi []string
}

// Simpan menambahkan satu kunci ke arsip.
func (a *Arsip) Simpan(kunci string) {
	a.Berisi = append(a.Berisi, kunci)
}

func main() {
	var p *Arsip // pointer bertipe konkret yang belum menunjuk nilai apa pun
	var s Penyimpan = p

	fmt.Println("p nil:", p == nil)
	fmt.Println("s nil:", s == nil)
}
text Keluaran
p nil: true
s nil: false

Nilai p memang nil sebagai pointer, tetapi s menyimpan sepasang tipe dinamis *main.Arsip dengan nilai dinamis nil. Perbandingan s == nil memeriksa pasangan itu, bukan nilai yang ada di dalamnya. Perbaikan yang lazim adalah mengembalikan nil secara eksplisit pada jalur kegagalan, yaitu menulis return nil dan bukan variabel pointer yang belum diinisialisasi.

Ringkasan#

  • Interface menuntut sekumpulan metode tanpa kode implementasi, dan tipe apa pun yang memiliki seluruh metode tersebut memenuhinya secara implisit.
  • Pemenuhan diperiksa saat kompilasi, sehingga hubungan antara tipe konkret dan interface tidak perlu dideklarasikan.
  • Nilai interface menyimpan pasangan tipe dinamis dan nilai dinamis, sedangkan variabel interface yang belum diberi nilai adalah nil.
  • Interface yang berisi pointer bertipe konkret yang nil tidak sama dengan interface nil.

Referensi#