Menu

Interface di Golang: Implementasi Implisit, any, dan Jebakan nil

Interface di Go dipenuhi secara implisit: tipe apa pun yang punya method yang tepat otomatis mengimplementasikannya. Pelajari interface kecil seperti io.Reader dan fmt.Stringer, interface kosong any, jebakan interface nil, dan prinsip accept interfaces, return structs.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

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:

InterfaceMethodDipakai oleh
fmt.StringerString() stringpencetakan di fmt
errorError() stringsetiap fungsi yang bisa gagal
io.ReaderRead(p []byte) (n int, err error)file, jaringan, gzip, body HTTP
io.WriterWrite(p []byte) (n int, err error)file, buffer, hash, response HTTP
sort.InterfaceLen, Less, Swappackage sort
http.HandlerServeHTTP(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.Open mengembalikan *os.File, bukan io.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 nil literal.
  • Membuat interface terlalu dini. Tulis tipe konkretnya dulu. Tambahkan interface saat implementasi kedua atau sebuah pengujian membutuhkannya.
  • Pointer ke interface. *io.Reader hampir 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.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai