Interface Pustaka Standar yang Umum

Stringer, error, sort.Interface, dan komposisi Reader/Writer.

Daftar isi

Pustaka standar Go menaruh sebagian besar abstraksinya pada interface kecil yang dipenuhi tipe buatan pengembang. Topik ini membahas empat di antaranya yang paling sering ditemui, yaitu fmt.Stringer untuk pencetakan, error untuk pelaporan kegagalan, sort.Interface untuk pengurutan, serta komposisi Reader dan Writer menjadi kontrak yang lebih besar. Setelah membacanya, tipe buatan sendiri dapat menyatu dengan paket fmt, sort, dan io tanpa adaptor apa pun.

fmt.Stringer#

Paket fmt dapat mencetak nilai apa pun, tetapi apabila nilainya memiliki metode String() string, hasil cetaknya diambil dari metode itu. Kontrak tersebut bernama fmt.Stringer dan didefinisikan sebagai berikut.

go Definisi fmt.Stringer pada pustaka standar
// tidak-dijalankan
type Stringer interface {
	String() string
}

Metode String memainkan peran yang sama seperti metode toString pada bahasa berorientasi objek, tetapi hubungannya terbentuk lewat pemenuhan interface dan bukan pewarisan. Program berikut memberi tipe Suhu cara mencetak dirinya sendiri.

go stringer-suhu.go
package main

import "fmt"

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

// String memenuhi fmt.Stringer sehingga Suhu tahu cara mencetak dirinya.
func (s Suhu) String() string {
	return fmt.Sprintf("%.1f °C", float64(s))
}

func main() {
	pengukuran := []Suhu{22.5, 24, 19.75}
	for _, p := range pengukuran {
		fmt.Println(p)
	}
	fmt.Printf("rekap: pertama %v, terakhir %v\n", pengukuran[0], pengukuran[2])
}
text Keluaran
22.5 °C
24.0 °C
19.8 °C
rekap: pertama 22.5 °C, terakhir 19.8 °C

fmt.Println dan verb %v memeriksa fmt.Stringer lebih dahulu, sehingga ketiga pengukuran dicetak melalui metode String. Pemformatan yang tadinya tersebar di tempat pemanggil kini berkumpul di satu tempat, yaitu metode milik tipenya sendiri. Ketika format berubah, cukup metode itu yang disunting. Manfaat yang sama berlaku pada tipe yang menyimpan nilai Suhu, karena pencetakan struct yang memiliki field bertipe Suhu otomatis menampilkan format yang sama. Pustaka standar memanfaatkan mekanisme ini pada beberapa tipenya, misalnya time.Duration yang mencetak durasinya dalam bentuk ringkas.

Program tersebut dapat dijalankan lewat terminal sebagaimana berikut.

bash
# siapkan folder kerja untuk topik ini
mkdir -p /tmp/gointerface/interface-stdlib
cp stringer-suhu.go /tmp/gointerface/interface-stdlib/
cd /tmp/gointerface/interface-stdlib

# periksa format sebelum menjalankan programnya
gofmt -l .

# jalankan programnya
go run stringer-suhu.go
text Keluaran
22.5 °C
24.0 °C
19.8 °C
rekap: pertama 22.5 °C, terakhir 19.8 °C

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

error sebagai interface#

Tipe error adalah interface dengan satu metode Error() string, dan setiap tipe yang memiliki metode itu memenuhinya tanpa deklarasi apa pun. Kelebihan desain ini terasa ketika error perlu membawa data, karena struct pembawa data dapat sekaligus menjadi error. Program berikut mendefinisikan tipe error yang menyimpan nilai dan batas pelanggarannya.

go error-ambang.go
package main

import "fmt"

// ErrAmbang adalah tipe error buatan sendiri untuk pelanggaran batas.
type ErrAmbang struct {
	Nilai int
	Batas int
}

// Error memenuhi interface error bawaan bahasa Go.
func (e ErrAmbang) Error() string {
	return fmt.Sprintf("nilai %d melebihi batas %d", e.Nilai, e.Batas)
}

// validasiBatas menolak nilai yang melewati ambang tertinggi.
func validasiBatas(n int) error {
	if n > 100 {
		return ErrAmbang{Nilai: n, Batas: 100}
	}
	return nil
}

func main() {
	for _, n := range []int{80, 120} {
		if err := validasiBatas(n); err != nil {
			fmt.Println("ditolak:", err)
		} else {
			fmt.Println("diterima:", n)
		}
	}
}
text Keluaran
diterima: 80
ditolak: nilai 120 melebihi batas 100

Pernyataan return ErrAmbang{Nilai: n, Batas: 100} mengembalikan nilai struct, sedangkan fungsi menyatakan kembaliannya bertipe error. Keduanya cocok karena ErrAmbang memenuhi interface error, sehingga pemanggil dapat mencetaknya melalui metode Error. Apabila errornya kelak perlu diperiksa lebih dalam, field seperti Nilai dan Batas tetap tersedia dan dapat dibaca melalui type assertion, yaitu pemeriksaan tipe yang dilakukan saat program berjalan.

sort.Interface#

Paket sort tidak mengenal tipe data yang diurutkannya. Yang diperlukannya hanya interface sort.Interface dengan tiga metode berikut.

go Definisi sort.Interface pada pustaka standar
// tidak-dijalankan
type Interface interface {
	Len() int
	Less(i, j int) bool
	Swap(i, j int)
}

Metode Len melaporkan banyaknya elemen, Less memutuskan urutan dua elemen, dan Swap menukarkan dua elemen. Pemisahan tiga tanggung jawab ini membuat satu algoritme pengurutan melayani tipe data apa pun. Program berikut mengurutkan catatan menurut tebal halamannya.

go urutkan-catatan.go
package main

import (
	"fmt"
	"sort"
)

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

// DaftarCatatan adalah irisan Catatan yang dapat diurutkan.
type DaftarCatatan []Catatan

// Len, Less, dan Swap memenuhi sort.Interface.
func (d DaftarCatatan) Len() int {
	return len(d)
}

func (d DaftarCatatan) Less(i, j int) bool {
	return d[i].Halaman < d[j].Halaman
}

func (d DaftarCatatan) Swap(i, j int) {
	d[i], d[j] = d[j], d[i]
}

func main() {
	daftar := DaftarCatatan{
		{Judul: "Rapat mingguan", Halaman: 4},
		{Judul: "Ide proyek", Halaman: 2},
		{Judul: "Log sensor", Halaman: 7},
	}
	sort.Sort(daftar)
	for _, c := range daftar {
		fmt.Printf("%s (%d halaman)\n", c.Judul, c.Halaman)
	}
}
text Keluaran
Ide proyek (2 halaman)
Rapat mingguan (4 halaman)
Log sensor (7 halaman)

Tipe DaftarCatatan adalah irisan Catatan yang diberi tiga metode, sehingga sort.Sort dapat mengurutkannya tanpa mengetahui isi elemennya. Untuk pengurutan menurut kunci lain, misalnya judul, cukup mengganti isi Less, sedangkan dua metode lainnya tetap dipakai. Untuk pengurutan sekali pakai, paket slices menyediakan slices.SortFunc yang menerima fungsi perbandingan, sehingga ketiga metode tadi tidak perlu ditulis.

Komposisi interface kecil#

Interface dapat disusun dari interface lain melalui penyematan, yaitu mencantumkan nama interface di dalam deklarasi interface baru. Gabungan io.Reader dan io.Writer muncul begitu sering sehingga pustaka standar menyediakannya sebagai io.ReadWriter.

go Komposisi interface pada pustaka standar
// tidak-dijalankan
type ReadWriter interface {
	Reader
	Writer
}

Tipe yang memenuhi io.ReadWriter harus memiliki Read sekaligus Write, sedangkan pemenuhan tiap bagiannya mengikuti kontrak kecil masing-masing. bytes.Buffer adalah contoh tipe yang memenuhi keduanya sekaligus. Program berikut memanfaatkan sifat itu untuk membaca dan menulis pada satu buffer yang sama.

go kanal-dua-arah.go
package main

import (
	"bytes"
	"fmt"
	"io"
)

// gandakan membaca seluruh isi kanal, lalu menulisnya dua kali ke kanal yang sama.
func gandakan(kanal io.ReadWriter) error {
	isi, err := io.ReadAll(kanal)
	if err != nil {
		return err
	}
	teks := string(isi)
	_, err = io.WriteString(kanal, teks+" "+teks)
	return err
}

func main() {
	var kanal bytes.Buffer
	kanal.WriteString("catatan singkat")

	if err := gandakan(&kanal); err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(kanal.String())
}
text Keluaran
catatan singkat catatan singkat

Fungsi gandakan menerima io.ReadWriter, sehingga ia dapat dipakai pada bytes.Buffer, koneksi jaringan, maupun bufio.ReadWriter tanpa perubahan kode. Fungsi ini hanya memperhatikan gabungan kontraknya dan bukan tipe konkretnya. Prinsip yang sama berlaku pada varian lain di paket io, seperti io.ReadWriteCloser, yang menyusun tiga kontrak kecil menjadi satu.

Kesalahan umum#

Tiga kesalahan berikut muncul ketika interface pustaka standar mulai diimplementasikan.

  • Mendefinisikan String dengan receiver pointer. Pemenuhan fmt.Stringer menempel pada *Catatan, sedangkan fmt.Println(catatan) menerima nilai Catatan dan mencetaknya mentah sebagai {Rapat mingguan 4}. Perbaikannya, gunakan receiver nilai untuk tipe kecil, atau teruskan pointer secara konsisten di seluruh kode.
  • Mengembalikan nilai error dari tipe dengan metode Error bereceiver pointer. Deklarasi func (e *ErrAmbang) Error() string membuat nilai ErrAmbang{} tidak memenuhi error, sehingga pernyataan pengembaliannya ditolak kompilator. Perbaikannya, jaga konsistensi dengan receiver nilai untuk error ringan, atau kembalikan &ErrAmbang{...} bila receiver pointer memang diperlukan.
  • Menempelkan ketiga metode sort.Interface pada tipe yang salah. Metode Len, Less, dan Swap harus menempel pada tipe irisan yang diurutkan, misalnya DaftarCatatan, dan bukan pada tipe elemennya seperti Catatan. Apabila metodenya menempel pada elemen, irisannya tidak memenuhi interface dan sort.Sort menolaknya saat kompilasi.

Ringkasan#

  • fmt.Stringer mengubah cara sebuah nilai dicetak oleh paket fmt melalui metode String() string.
  • error adalah interface satu metode, sehingga struct pembawa data dapat sekaligus menjadi error.
  • sort.Interface menuntut Len, Less, dan Swap, dan satu algoritme mengurutkan tipe apa pun yang memenuhinya.
  • Interface disusun melalui penyematan, dan io.ReadWriter adalah gabungan io.Reader dengan io.Writer.

Referensi#