BAB 20: Interface — Kontrak Perilaku
Pelajari cara mendefinisikan dan mengimplementasikan interface di Go secara implisit, type assertion, interface kosong, dan komposisi interface.
Di bab sebelumnya, kamu belajar bahwa package menyembunyikan detail implementasi lewat exported dan unexported. Tapi ada pertanyaan yang lebih dalam: bagaimana jika kamu ingin menulis function yang bisa bekerja dengan berbagai tipe struct berbeda, tanpa perlu tahu persis struct mana yang digunakan? Bagaimana toko online bisa memproses berbagai metode pembayaran — transfer bank, kartu kredit, dompet digital — tanpa kode yang tercecer di mana-mana?
Jawabannya adalah interface.
Apa Itu Interface?
Interface adalah kumpulan method signature tanpa implementasi. Interface mendefinisikan kontrak — siapa pun yang memiliki semua method yang disyaratkan, secara otomatis memenuhi interface tersebut. Tidak ada deklarasi eksplisit, tidak ada kata kunci implements.
Di Go, ini disebut implicit implementation — tipe mengimplementasikan interface hanya dengan memiliki method-method yang sesuai. Sistem ini sering dibandingkan dengan duck typing: “Jika ia bisa berjalan seperti bebek dan bersuara seperti bebek, maka ia adalah bebek.”
type Hitung interface {
Luas() float64
Keliling() float64
}
Interface Hitung di atas menyatakan: “Tipe apapun yang punya method Luas() float64 dan Keliling() float64 memenuhi interface ini.”
Implementasi Pertama
Mari kita lihat ini bekerja. Persegi dan Lingkaran adalah tipe yang berbeda, tapi keduanya punya method yang sama:
// main.go
package main
import (
"fmt"
"math"
)
type Hitung interface {
Luas() float64
Keliling() float64
}
type Persegi struct {
Sisi float64
}
func (p Persegi) Luas() float64 {
return p.Sisi * p.Sisi
}
func (p Persegi) Keliling() float64 {
return 4 * p.Sisi
}
type Lingkaran struct {
Jari float64
}
func (l Lingkaran) Luas() float64 {
return math.Pi * l.Jari * l.Jari
}
func (l Lingkaran) Keliling() float64 {
return 2 * math.Pi * l.Jari
}
func cetakInfo(h Hitung) {
fmt.Printf("Luas: %.2f, Keliling: %.2f\n", h.Luas(), h.Keliling())
}
func main() {
p := Persegi{Sisi: 5}
l := Lingkaran{Jari: 7}
cetakInfo(p)
cetakInfo(l)
}
Luas: 25.00, Keliling: 20.00
Luas: 153.94, Keliling: 43.98
Function cetakInfo menerima parameter bertipe Hitung — bukan Persegi atau Lingkaran. Ia tidak peduli tipe spesifik apa yang dikirim, selama tipe itu memiliki method Luas() dan Keliling(). Ini kekuatan interface: kode yang tidak bergantung pada implementasi konkret.
Interface di Slice
Karena interface adalah tipe, kamu bisa membuat slice of interface dan menyimpan berbagai tipe di dalamnya:
// main.go
package main
import (
"fmt"
"math"
)
type Hitung interface {
Luas() float64
Keliling() float64
}
type Persegi struct{ Sisi float64 }
func (p Persegi) Luas() float64 { return p.Sisi * p.Sisi }
func (p Persegi) Keliling() float64 { return 4 * p.Sisi }
type Lingkaran struct{ Jari float64 }
func (l Lingkaran) Luas() float64 { return math.Pi * l.Jari * l.Jari }
func (l Lingkaran) Keliling() float64 { return 2 * math.Pi * l.Jari }
type Segitiga struct{ Alas, Tinggi, SisiA, SisiB, SisiC float64 }
func (s Segitiga) Luas() float64 { return 0.5 * s.Alas * s.Tinggi }
func (s Segitiga) Keliling() float64 { return s.SisiA + s.SisiB + s.SisiC }
func main() {
bentuk := []Hitung{
Persegi{Sisi: 5},
Lingkaran{Jari: 3},
Segitiga{Alas: 6, Tinggi: 4, SisiA: 5, SisiB: 5, SisiC: 6},
}
totalLuas := 0.0
for _, b := range bentuk {
fmt.Printf("Luas: %.2f\n", b.Luas())
totalLuas += b.Luas()
}
fmt.Printf("Total luas semua bentuk: %.2f\n", totalLuas)
}
Luas: 25.00
Luas: 28.27
Luas: 12.00
Total luas semua bentuk: 65.27
Tiga tipe yang berbeda disimpan dalam satu slice, diiterasi dengan satu loop, dan diperlakukan secara seragam. Menambah bentuk baru di masa depan tidak memerlukan perubahan apapun pada kode loop atau function cetakInfo.
Type Assertion
Terkadang kamu butuh mengakses method atau field yang spesifik pada tipe konkret, yang tidak ada di interface. Di sinilah type assertion berguna.
// main.go
package main
import (
"fmt"
"math"
)
type Hitung interface {
Luas() float64
}
type Lingkaran struct {
Jari float64
}
func (l Lingkaran) Luas() float64 {
return math.Pi * l.Jari * l.Jari
}
func (l Lingkaran) Diameter() float64 {
return 2 * l.Jari
}
func main() {
var h Hitung = Lingkaran{Jari: 5}
fmt.Println("Luas:", h.Luas())
// type assertion — dapatkan kembali tipe konkretnya
l, ok := h.(Lingkaran)
if ok {
fmt.Println("Diameter:", l.Diameter())
}
// type assertion tanpa pengecekan — panic jika salah tipe
// l2 := h.(Persegi) // ini akan panic
}
Luas: 78.54
Diameter: 10.00
Bentuk nilai, ok := variabel.(Tipe) adalah safe type assertion — ok bernilai false jika konversi gagal, tanpa menyebabkan panic. Selalu gunakan bentuk ini kecuali kamu benar-benar yakin tipe konkretnya.
Type Switch
Jika kamu perlu membedakan beberapa kemungkinan tipe, type switch lebih bersih daripada berantai if-assertion:
// main.go
package main
import (
"fmt"
"math"
)
type Hitung interface {
Luas() float64
}
type Persegi struct{ Sisi float64 }
func (p Persegi) Luas() float64 { return p.Sisi * p.Sisi }
type Lingkaran struct{ Jari float64 }
func (l Lingkaran) Luas() float64 { return math.Pi * l.Jari * l.Jari }
func deskripsikan(h Hitung) {
switch v := h.(type) {
case Persegi:
fmt.Printf("Persegi dengan sisi %.1f\n", v.Sisi)
case Lingkaran:
fmt.Printf("Lingkaran dengan jari-jari %.1f\n", v.Jari)
default:
fmt.Printf("Bentuk tidak dikenal: %T\n", v)
}
}
func main() {
deskripsikan(Persegi{Sisi: 4})
deskripsikan(Lingkaran{Jari: 6})
}
Persegi dengan sisi 4.0
Lingkaran dengan jari-jari 6.0
Interface Kosong
Go punya satu interface yang istimewa: interface tanpa method apapun. Di versi Go lama ditulis interface{}, di Go 1.18+ ada alias yang lebih jelas: any.
// main.go
package main
import "fmt"
func cetakApaSaja(nilai any) {
fmt.Printf("Tipe: %T, Nilai: %v\n", nilai, nilai)
}
func main() {
cetakApaSaja(42)
cetakApaSaja("halo")
cetakApaSaja(true)
cetakApaSaja([]int{1, 2, 3})
}
Tipe: int, Nilai: 42
Tipe: string, Nilai: halo
Tipe: bool, Nilai: true
Tipe: []int, Nilai: [1 2 3]
Karena setiap tipe secara otomatis memenuhi interface kosong, any bisa menyimpan nilai dari tipe apapun. Ini berguna untuk function utilitas seperti logger atau serializer, tapi jangan berlebihan — kehilangan informasi tipe membuat kode sulit di-maintain dan rawan runtime error.
any adalah alias dari interface{} yang diperkenalkan di Go 1.18. Keduanya identik. Lebih suka any untuk kode baru karena lebih mudah dibaca.
Type Assertion pada any
Menyimpan nilai ke dalam any mudah, tapi mengambilnya kembali butuh satu langkah tambahan. Ketika nilai sudah masuk ke dalam any, Go tidak lagi tahu tipe aslinya — dan kamu perlu memberi tahu kompiler tipe mana yang ingin diambil. Inilah yang disebut type assertion.
// main.go
package main
import (
"fmt"
"strings"
)
func main() {
var data any
// simpan string
data = "bandung,jakarta,surabaya"
kota, ok := data.(string)
if ok {
daftar := strings.Split(kota, ",")
fmt.Println("Kota:", daftar)
}
// simpan int
data = 2024
tahun, ok := data.(int)
if ok {
fmt.Println("Tahun:", tahun+1)
}
// type assertion yang salah — ok = false, tidak panic
angka, ok := data.(float64)
fmt.Printf("float64 assertion: nilai=%v, berhasil=%v\n", angka, ok)
}
Kota: [bandung jakarta surabaya]
Tahun: 2025
float64 assertion: nilai=0, berhasil=false
Bentuk nilai, ok := variable.(Tipe) adalah safe assertion — jika tipe tidak cocok, ok bernilai false dan nilai mendapat zero value dari tipe tujuan, tanpa panic. Ini berbeda dengan assertion tanpa ok yang langsung panic jika tipe salah.
Untuk struct yang disimpan sebagai pointer, assertion-nya menyertakan tanda bintang:
// main.go
package main
import "fmt"
type Pegawai struct {
Nama string
Departemen string
}
func tampilkanInfo(data any) {
p, ok := data.(*Pegawai)
if !ok {
fmt.Println("bukan data pegawai")
return
}
fmt.Printf("%s (%s)\n", p.Nama, p.Departemen)
}
func main() {
pegawai := &Pegawai{Nama: "Rina", Departemen: "Engineering"}
tampilkanInfo(pegawai)
tampilkanInfo("bukan struct")
}
Rina (Engineering)
bukan data pegawai
Pola []map[string]any
Salah satu kombinasi yang sering muncul dalam kode Go — terutama saat berurusan dengan data JSON atau konfigurasi dinamis — adalah slice dari map[string]any. Pola ini memungkinkan kamu menyimpan kumpulan record yang setiap kolomnya bisa punya tipe berbeda.
// main.go
package main
import "fmt"
func main() {
// data karyawan dengan tipe nilai yang beragam
karyawan := []map[string]any{
{
"nama": "Budi Santoso",
"usia": 28,
"aktif": true,
"skills": []string{"Go", "PostgreSQL"},
},
{
"nama": "Dewi Lestari",
"usia": 32,
"aktif": true,
"skills": []string{"Go", "Redis", "Kafka"},
},
{
"nama": "Hendra Wijaya",
"usia": 25,
"aktif": false,
"skills": []string{"Go"},
},
}
for _, k := range karyawan {
nama := k["nama"].(string)
usia := k["usia"].(int)
aktif := k["aktif"].(bool)
skills := k["skills"].([]string)
status := "aktif"
if !aktif {
status = "tidak aktif"
}
fmt.Printf("%s (%d tahun) — %s — skills: %v\n", nama, usia, status, skills)
}
}
Budi Santoso (28 tahun) — aktif — skills: [Go PostgreSQL]
Dewi Lestari (32 tahun) — aktif — skills: [Go Redis Kafka]
Hendra Wijaya (25 tahun) — tidak aktif — skills: [Go]
Pola ini fleksibel, tapi ada harga yang dibayar: setiap akses nilai membutuhkan type assertion, dan jika assertion salah di runtime program akan panic. Untuk data yang strukturnya sudah diketahui sejak compile time, struct selalu lebih aman dan lebih efisien.
Hindari menggunakan any atau map[string]any untuk data yang strukturnya sudah kamu ketahui. Buat struct yang tepat — kode jadi lebih mudah dibaca, lebih aman, dan IDE bisa membantu autocomplete. Gunakan any hanya ketika tipe data benar-benar tidak diketahui saat program dikompilasi, misalnya saat mem-parsing JSON dengan struktur yang dinamis.
Komposisi Interface
Seperti struct bisa di-embed ke struct lain, interface bisa di-embed ke interface lain. Ini memungkinkan kamu membangun interface yang lebih besar dari interface yang lebih kecil:
// main.go
package main
import "fmt"
type Pembaca interface {
Baca() string
}
type Penulis interface {
Tulis(data string)
}
// ReadWriter menggabungkan Pembaca dan Penulis
type ReadWriter interface {
Pembaca
Penulis
}
type Buffer struct {
data string
}
func (b *Buffer) Baca() string {
return b.data
}
func (b *Buffer) Tulis(data string) {
b.data += data
}
func proses(rw ReadWriter) {
rw.Tulis("Go ")
rw.Tulis("sangat ")
rw.Tulis("ekspresif")
fmt.Println(rw.Baca())
}
func main() {
buf := &Buffer{}
proses(buf)
}
Go sangat ekspresif
Pola ini dipakai secara luas di standard library Go. io.ReadWriter adalah komposisi dari io.Reader dan io.Writer. io.ReadWriteCloser menambahkan io.Closer. Kamu bisa membangun kontrak yang tepat tanpa duplikasi.
Pointer Receiver dan Interface
Ada satu gotcha yang perlu dipahami: jika method menggunakan pointer receiver, maka hanya pointer yang memenuhi interface, bukan value.
// main.go
package main
import "fmt"
type Greeter interface {
Sapa() string
}
type Orang struct {
Nama string
}
// pointer receiver
func (o *Orang) Sapa() string {
return "Halo, " + o.Nama
}
func main() {
o := Orang{Nama: "Budi"}
// var g Greeter = o // ERROR: Orang tidak memenuhi Greeter
var g Greeter = &o // benar: *Orang memenuhi Greeter
fmt.Println(g.Sapa())
}
Halo, Budi
Kalau method menggunakan value receiver, maka baik value maupun pointer keduanya memenuhi interface. Tapi pointer receiver hanya dipenuhi oleh pointer. Ini konsisten dengan perilaku receiver yang sudah dipelajari di bab method.
Pastikan kamu mengirim &struct bukan struct ketika method yang dibutuhkan interface menggunakan pointer receiver. Kompiler akan memberikan error yang cukup jelas jika kamu salah, tapi memahami penyebabnya dari awal menghemat waktu debugging.
Latihan
Latihan 1 — Pembayaran:
Buat interface MetodePembayaran dengan method Bayar(jumlah float64) string. Implementasikan tiga tipe: Transfer, Kartu, dan Dompet, masing-masing dengan nama bank/provider sebagai field. Buat function prosesPembayaran(mp MetodePembayaran, jumlah float64) yang memanggil Bayar dan mencetak hasilnya. Uji dengan slice of MetodePembayaran.
Latihan 2 — Type switch:
Buat function identifikasi(nilai any) yang menggunakan type switch untuk mencetak deskripsi berbeda tergantung tipe: untuk int cetak “angka bulat”, untuk float64 cetak “angka desimal”, untuk string cetak panjang stringnya, untuk slice cetak jumlah elemennya, dan untuk tipe lain cetak nama tipenya.
Latihan 3 — Interface komposisi:
Buat tiga interface kecil: Stringer (method String() string), Comparer (method SamaWith(other any) bool), dan Validator (method Valid() bool). Gabungkan ketiganya menjadi interface Model. Implementasikan Model pada struct Email yang memvalidasi format email sederhana (harus mengandung ”@”).
Dengan interface, kode Go menjadi fleksibel tanpa kehilangan keamanan tipe. Kamu bisa menulis komponen yang bisa diganti implementasinya — dari database sungguhan ke mock untuk testing, dari HTTP client ke implementasi lokal — tanpa mengubah kode yang menggunakannya. Inilah pondasi dari hampir semua arsitektur Go yang baik.