Sebagian kegagalan memiliki makna yang tetap: data tidak ada, berkas sudah ditutup, kuota habis. Untuk kasus seperti ini, Go memakai pola sentinel error — satu nilai error yang dideklarasikan di level package sebagai titik acuan, lalu dibandingkan pemanggilnya dengan errors.Is. Pada chapter ini kita akan mendefinisikan sentinel sendiri, melihat contoh nyata io.EOF dari standard library, dan menimbang kapan pola ini layak dipakai di API publik.

Apa Itu Sentinel Error #

Sentinel error adalah nilai error yang dibuat sekali dan menjadi penanda kondisi tertentu:

go
var ErrTidakDitemukan = errors.New("data tidak ditemukan")

Tiga ciri menentukan pola ini. Pertama, nilai dideklarasi sebagai variabel global dengan awalan Err sesuai konvensi penamaan Go. Kedua, nilai dialokasikan sekali saat package dimuat, sehingga setiap kejadian gagal berbagi objek yang identik. Ketiga, pemanggil tidak membandingkan teks pesannya, melainkan identitas nilainya — dan pemeriksaan identitas dilakukan lewat errors.Is, bukan operator ==.

Sentinel di Standard Library #

Contoh paling terkenal adalah io.EOF dari package io. Seluruh pembaca bertipe io.Reader menandai akhir data dengan mengembalikan io.EOF, bukan error buatan sendiri. Karena satu titik acuan itu disepakati, kode pembaca berkas, koneksi jaringan, dan buffer memori memakai pemeriksaan yang sama:

go
if errors.Is(err, io.EOF) {
	fmt.Println("selesai membaca")
}

Pola serupa tersebar di standard library: sql.ErrNoRows untuk query tanpa hasil dan fs.ErrNotExist untuk berkas yang tidak ada.

Kapan Sentinel Cocok #

Sentinel paling berguna pada batas API publik, ketika pemanggil butuh mengambil keputusan berbeda untuk kegagalan yang berbeda. Daftar berikut memetakan pertimbangannya:

  • Cocok: kondisi yang pemanggil ingin tangani secara khusus, misalnya not found yang harus dijawab dengan HTTP 404.
  • Cocok: nilai yang bisa dijamin stabil dalam jangka panjang karena sudah menjadi kontrak API.
  • Tidak cocok: klasifikasi kegagalan yang granular dan terus bertambah — setiap sentinel baru adalah kontrak baru yang harus dirawat.
  • Tidak cocok: detail yang hanya perlu dibaca manusia; cukup fmt.Errorf dengan konteks.

Program Utuh #

Berkas sentinel-demo.go membandingkan sentinel buatan sendiri dan sentinel standard library dalam satu program:

go
package main

import (
	"errors"
	"fmt"
	"io"
	"strings"
)

// ErrTidakDitemukan adalah sentinel untuk data yang tidak ada.
var ErrTidakDitemukan = errors.New("data tidak ditemukan")

func cariPengguna(kunci string) (string, error) {
	pengguna := map[string]string{
		"budi": "Budi Santoso",
		"siti": "Siti Aminah",
	}
	if nama, ok := pengguna[kunci]; ok {
		return nama, nil
	}
	return "", ErrTidakDitemukan
}

func bacaPotongan(r io.Reader) {
	buf := make([]byte, 4)
	for {
		n, err := r.Read(buf)
		if n > 0 {
			fmt.Printf("terbaca %d byte: %q\n", n, buf[:n])
		}
		if err != nil {
			if errors.Is(err, io.EOF) {
				fmt.Println("akhir data tercapai")
			} else {
				fmt.Println("error pembacaan lain:", err)
			}
			return
		}
	}
}

func main() {
	nama, err := cariPengguna("budi")
	fmt.Printf("nama=%q err=%v\n", nama, err)

	nama, err = cariPengguna("rudi")
	fmt.Printf("nama=%q err=%v\n", nama, err)

	fmt.Println("errors.Is ErrTidakDitemukan:", errors.Is(err, ErrTidakDitemukan))
	fmt.Println("errors.Is io.ErrUnexpectedEOF:", errors.Is(err, io.ErrUnexpectedEOF))

	bacaPotongan(strings.NewReader("Golang"))
}

Jalankan program dari direktori chapter dengan perintah berikut:

bash
go run sentinel-demo.go

Output yang Dihasilkan:

text
nama="Budi Santoso" err=<nil>
nama="" err=data tidak ditemukan
errors.Is ErrTidakDitemukan: true
errors.Is io.ErrUnexpectedEOF: false
terbaca 4 byte: "Gola"
terbaca 2 byte: "ng"
akhir data tercapai

Mari kita telaah baris demi baris program di atas:

  1. Baris 10-11 (var ErrTidakDitemukan): sentinel dideklarasikan di level package sehingga fungsi cariPengguna dan pemanggilnya memakai objek error yang identik.
  2. Baris 18-21 (if nama, ok := pengguna[kunci]): idiom comma ok pada akses map memisahkan jalur ketemu dan tidak ketemu; jalur gagal mengembalikan sentinel, bukan error baru.
  3. Baris 27-30 (n, err := r.Read(buf)): method Read boleh mengembalikan data dan error sekaligus, sehingga program mencetak data yang berhasil dibaca lebih dulu sebelum memeriksa err.
  4. Baris 31-37 (if err != nil ... errors.Is(err, io.EOF)): pemeriksaan akhir data memakai errors.Is terhadap sentinel standard library; cabang lain menangani error pembacaan yang berbeda.
  5. Baris 49-50 (fmt.Println("errors.Is ...")): pemeriksaan terhadap sentinel sendiri menghasilkan true, sedangkan io.ErrUnexpectedEOF yang berbeda menghasilkan false — sentinel tidak tertukar walaupun sama-sama berhubungan dengan pembacaan.
  6. Baris 52 (bacaPotongan(strings.NewReader("Golang"))): pembacaan string enam byte dengan buffer empat byte; strings.Reader mengembalikan potongan terakhir ng sekaligus io.EOF, sehingga output menampilkan dua baris terbaca lalu berakhir.

Sentinel menjawab pertanyaan “kegagalan jenis apa ini?”, tetapi jawabannya terbatas pada satu bit: cocok atau tidak. Ketika pemanggil butuh detail — field mana yang salah, berapa nilainya — kita perlu error yang membawa data. Chapter berikutnya, Custom Error Type , membangun tipe error milik kita sendiri.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

sentinel-demo.go

Unduh berkas
go
package main

import (
	"errors"
	"fmt"
	"io"
	"strings"
)

// ErrTidakDitemukan adalah sentinel untuk data yang tidak ada.
var ErrTidakDitemukan = errors.New("data tidak ditemukan")

func cariPengguna(kunci string) (string, error) {
	pengguna := map[string]string{
		"budi": "Budi Santoso",
		"siti": "Siti Aminah",
	}
	if nama, ok := pengguna[kunci]; ok {
		return nama, nil
	}
	return "", ErrTidakDitemukan
}

func bacaPotongan(r io.Reader) {
	buf := make([]byte, 4)
	for {
		n, err := r.Read(buf)
		if n > 0 {
			fmt.Printf("terbaca %d byte: %q\n", n, buf[:n])
		}
		if err != nil {
			if errors.Is(err, io.EOF) {
				fmt.Println("akhir data tercapai")
			} else {
				fmt.Println("error pembacaan lain:", err)
			}
			return
		}
	}
}

func main() {
	nama, err := cariPengguna("budi")
	fmt.Printf("nama=%q err=%v\n", nama, err)

	nama, err = cariPengguna("rudi")
	fmt.Printf("nama=%q err=%v\n", nama, err)

	fmt.Println("errors.Is ErrTidakDitemukan:", errors.Is(err, ErrTidakDitemukan))
	fmt.Println("errors.Is io.ErrUnexpectedEOF:", errors.Is(err, io.ErrUnexpectedEOF))

	bacaPotongan(strings.NewReader("Golang"))
}