Interface adalah kumpulan method
Tipe interface berisi daftar signature method. Tipe apa pun yang punya method-method itu memenuhi interface tersebut, tanpa perlu deklarasi yang menyatakannya.
Baik Rect maupun Circle tidak menyebut Shape sama sekali. Itulah implementasi implisit, ciri khas interface di Go. Artinya kamu bisa mendefinisikan interface di package-mu sendiri yang sudah dipenuhi oleh tipe dari package lain, tanpa menyentuh kode mereka.
Interface kecil dari standard library
Kode Go lebih menyukai interface dengan satu atau dua method. Yang paling penting:
| Interface | Method | Dipakai oleh |
|---|---|---|
fmt.Stringer | String() string | pencetakan di fmt |
error | Error() string | setiap fungsi yang bisa gagal |
io.Reader | Read(p []byte) (n int, err error) | file, jaringan, gzip, body HTTP |
io.Writer | Write(p []byte) (n int, err error) | file, buffer, hash, response HTTP |
sort.Interface | Len, Less, Swap | package sort |
http.Handler | ServeHTTP(w, r) | net/http |
Karena io.Reader hanya punya satu method, puluhan tipe mengimplementasikannya, dan fungsi apa pun yang menerima io.Reader bisa bekerja dengan semuanya:
Pepatah Go berbunyi "the bigger the interface, the weaker the abstraction" (makin besar interface, makin lemah abstraksinya). Interface yang lebih besar dibangun dengan menggabungkan yang kecil: io.ReadWriter adalah Reader ditambah Writer, ditulis dengan menyematkan (embed) satu interface di dalam interface lain.
any: interface kosong
interface{} tidak punya method, jadi setiap tipe memenuhinya. Go 1.18 menambahkan any sebagai alias; keduanya identik.
Nilai any bisa menampung apa saja, tapi hampir tidak ada yang bisa kamu lakukan dengannya sampai tipe konkretnya dipulihkan dengan type assertion atau type switch. Pilih interface sungguhan atau generics kalau kumpulan tipenya sudah diketahui. any cocok untuk data yang benar-benar dinamis, seperti JSON hasil decode yang bentuknya tidak diketahui, dan untuk mencetak.
Isi sebuah nilai interface
Nilai interface adalah pasangan: tipe dinamis dan nilai dinamis. var s Shape = Rect{3, 4} menyimpan tipe Rect dan salinan nilainya. Memanggil s.Area() akan mencari method milik Rect saat program berjalan.
Interface bernilai nil hanya kalau kedua bagiannya kosong. Aturan ini memicu bug paling membingungkan di Go.
Jebakan interface nil
Pointer nil yang disimpan di interface menghasilkan interface yang tidak nil.
Output:
false
*main.MyError true
true
validate(true) mengembalikan interface error yang menyimpan tipe *MyError dan nilai nil. Interface itu punya tipe, jadi ia tidak sama dengan nil, dan cabang if err != nil milik pemanggil pun dijalankan. Memanggil err.Error() di sana lalu akan panic saat mengakses field dari receiver yang nil.
Solusinya sederhana: deklarasikan variabelnya sebagai error, bukan sebagai tipe pointer konkret, atau kembalikan nil literal di jalur sukses. Jangan pernah mengembalikan tipe pointer error konkret dari fungsi yang hasilnya error. Jebakan yang sama berlaku untuk interface apa pun, bukan hanya error.
Memeriksa bahwa sebuah tipe mengimplementasikan interface
Implementasi diperiksa di tempat sebuah nilai di-assign ke interface. Kalau belum ada kode yang melakukannya, kesalahan pada signature method tidak akan ketahuan. Blank assignment di level package membuat pemeriksaan itu eksplisit:
var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)
Ini tidak menambah biaya apa pun saat program berjalan. Kalau *LogWriter punya Write(p []byte) error dan bukan Write(p []byte) (int, error), build akan gagal:
cannot use (*LogWriter)(nil) (value of type *LogWriter) as io.Writer value in variable declaration: *LogWriter does not implement io.Writer (wrong type for method Write)
have Write([]byte) error
want Write([]byte) (int, error)
Pointer receiver dan interface
Kalau sebuah method punya pointer receiver, hanya tipe pointer yang memiliki method itu. *Counter memenuhi interface lewat method tersebut; Counter tidak. Compiler akan bilang Counter does not implement Incrementer (method Inc has pointer receiver). Simpan &Counter{} di interface. Halaman method menjelaskan method set.
Accept interfaces, return structs
Pedoman Go yang umum: fungsi sebaiknya menerima parameter berupa interface dan mengembalikan tipe konkret.
- Menerima interface memungkinkan pemanggil mengirim apa pun yang cocok, termasuk tiruan (fake) untuk pengujian. Fungsi yang membaca data sebaiknya menerima
io.Reader, bukan*os.File. - Mengembalikan tipe konkret memungkinkan pemanggil memakai semua method dan field-nya, sekaligus menghindari jebakan interface nil.
os.Openmengembalikan*os.File, bukanio.Reader.
Kebiasaan yang berkaitan: definisikan interface di tempat ia dipakai, bukan di tempat ia diimplementasikan. Kalau service-mu butuh sesuatu yang bisa Get(id) seorang user, deklarasikan interface satu method di package service-mu, dan biarkan package database cukup meng-export struct-nya.
Membandingkan nilai interface
Dua nilai interface sama kalau tipe dinamisnya identik dan nilai dinamisnya sama. Kalau tipe dinamisnya tidak bisa dibandingkan (slice, map), == tetap bisa dikompilasi tapi panic saat program berjalan: runtime error: comparing uncomparable type []int.
Kesalahan umum
- Mengembalikan pointer nil bertipe sebagai interface. Kembalikan
nilliteral. - Membuat interface terlalu dini. Tulis tipe konkretnya dulu. Tambahkan interface saat implementasi kedua atau sebuah pengujian membutuhkannya.
- Pointer ke interface.
*io.Readerhampir tidak pernah benar. Interface sudah menyimpan pointer kalau kamu menyimpan pointer di dalamnya. - Interface yang besar. Interface dengan sepuluh method sulit diimplementasikan dan sulit dibuat tiruannya. Pecah menjadi beberapa interface.
Pertanyaan yang sering diajukan
Bagaimana cara mengimplementasikan interface di Go?
Definisikan method yang tercantum di interface, dengan nama dan signature yang sama, pada tipemu. Tidak ada kata kunci implements. Kalau *File punya Read(p []byte) (int, error), berarti ia adalah io.Reader, secara otomatis. Compiler memeriksanya di setiap tempat kamu meng-assign nilai itu ke tipe interface.
Apa itu interface kosong atau any di Go?
interface{} tidak punya method, jadi setiap tipe memenuhinya. Sejak Go 1.18, any adalah alias bawaan untuk interface{}. Nilai bertipe any bisa menampung apa saja, tapi kamu butuh type assertion atau type switch untuk mengambil kembali tipe konkretnya.
Kenapa interface Go saya tidak nil padahal saya meng-assign pointer nil?
Nilai interface menyimpan sebuah tipe dan sebuah nilai. Meng-assign *MyError yang nil ke error menghasilkan interface dengan tipe *MyError dan nilai nil, dan interface itu tidak sama dengan nil. Kembalikan nil literal, bukan pointer nil yang bertipe, saat tidak ada error.
Bagaimana cara memeriksa saat kompilasi bahwa sebuah tipe mengimplementasikan interface?
Tambahkan blank assignment di level package: var _ io.Reader = (*MyReader)(nil). Kalau *MyReader kekurangan method, build akan gagal dengan pesan yang menyebut method yang hilang.