Spesifikasi Penanganan Kesalahan Golang

Jenis Kesalahan

Terdapat beberapa opsi untuk mendeklarasikan kesalahan. Sebelum memilih opsi yang paling cocok untuk kasus penggunaan Anda, pertimbangkan hal berikut:

  • Apakah pemanggil perlu mencocokkan kesalahan untuk menanganinya? Jika ya, kita harus mendukung fungsi errors.Is atau errors.As dengan mendeklarasikan variabel kesalahan tingkat atas atau jenis kustom.
  • Apakah pesan kesalahan berupa string statis atau string dinamis yang memerlukan informasi kontekstual? Untuk string statis, kita dapat menggunakan errors.New, namun untuk yang terakhir, kita harus menggunakan fmt.Errorf atau jenis kesalahan kustom.
  • Apakah kita melewatkan kesalahan baru yang dikembalikan oleh fungsi turunan? Jika ya, lihat bagian pembungkus kesalahan.
Cocokan Kesalahan? Pesan Kesalahan Panduan
Tidak statis errors.New
Tidak dinamis fmt.Errorf
Ya statis variabel var tingkat atas dengan errors.New
Ya dinamis jenis kesalahan kustom

Sebagai contoh, gunakan errors.New untuk merepresentasikan kesalahan dengan string statis. Jika pemanggil perlu mencocokkan dan menangan kesalahan ini, ekspor sebagai variabel untuk mendukung pencocokan dengan errors.Is.

Tidak Ada Pencocokan Kesalahan

// package foo

func Open() error {
  return errors.New("tidak dapat membuka")
}

// package bar

if err := foo.Open(); err != nil {
  // Tidak dapat menangani kesalahan.
  panic("kesalahan tidak diketahui")
}

Pencocokan Kesalahan

// package foo

var ErrTidakDapatMembuka = errors.New("tidak dapat membuka")

func Open() error {
  return ErrTidakDapatMembuka
}

// package bar

if err := foo.Open(); err != nil {
  if errors.Is(err, foo.ErrTidakDapatMembuka) {
    // menangani kesalahan
  } else {
    panic("kesalahan tidak diketahui")
  }
}

Untuk kesalahan dengan string dinamis, gunakan fmt.Errorf jika pemanggil tidak perlu mencocokkannya. Jika pemanggil memang perlu mencocokkannya, maka gunakan kesalahan error kustom.

Tidak Ada Pencocokan Kesalahan

// package foo

func Open(file string) error {
  return fmt.Errorf("file %q tidak ditemukan", file)
}

// package bar

if err := foo.Open("testfile.txt"); err != nil {
  // Tidak dapat menangani kesalahan.
  panic("kesalahan tidak diketahui")
}

Pencocokan Kesalahan

// package foo

type KesalahanTidakDitemukan struct {
  File string
}

func (e *KesalahanTidakDitemukan) Error() string {
  return fmt.Sprintf("file %q tidak ditemukan", e.File)
}

func Open(file string) error {
  return &KesalahanTidakDitemukan{File: file}
}

// package bar

if err := foo.Open("testfile.txt"); err != nil {
  var tidakDitemukan *KesalahanTidakDitemukan
  if errors.As(err, &tidakDitemukan) {
    // menangani kesalahan
  } else {
    panic("kesalahan tidak diketahui")
  }
}

Perhatikan bahwa jika Anda mengekspor variabel atau jenis kesalahan dari sebuah paket, maka itu menjadi bagian dari API publik paket tersebut.

Pembungkusan Kesalahan

Ketika terjadi kesalahan saat memanggil metode lain, biasanya ada tiga cara untuk menanganinya:

  • Mengembalikan kesalahan aslinya apa adanya.
  • Menggunakan fmt.Errorf dengan %w untuk menambahkan konteks ke kesalahan dan kemudian mengembalikannya.
  • Menggunakan fmt.Errorf dengan %v untuk menambahkan konteks ke kesalahan dan kemudian mengembalikannya.

Jika tidak ada konteks tambahan yang perlu ditambahkan, kembalikan kesalahan aslinya apa adanya. Ini akan mempertahankan tipe kesalahan asli dan pesannya. Ini sangat cocok ketika pesan kesalahan yang mendasarinya sudah cukup untuk melacak dari mana kesalahan berasal.

Namun, tambahkan konteks ke pesan kesalahan sebanyak mungkin agar kesalahan yang ambigu seperti "connection refused" tidak terjadi. Sebagai gantinya, Anda akan menerima kesalahan yang lebih berguna, seperti "memanggil layanan foo: connection refused".

Gunakan fmt.Errorf untuk menambahkan konteks ke kesalahan Anda dan pilih di antara kata kerja %w atau %v berdasarkan apakah pemanggil harus dapat mencocokkan dan mengekstrak penyebab akar.

  • Gunakan %w jika pemanggil harus memiliki akses ke kesalahan yang mendasarinya. Ini adalah standar yang baik untuk sebagian besar pembungkusan kesalahan, tetapi perlu diingat bahwa pemanggil dapat mulai mengandalkan perilaku ini. Oleh karena itu, untuk kesalahan pembungkusan yang merupakan variabel atau tipe yang dikenal, cek dan uji mereka sebagai bagian dari kontrak fungsi.
  • Gunakan %v untuk menyamarkan kesalahan yang mendasarinya. Pemanggil tidak akan dapat mencocokkannya, tetapi Anda dapat beralih ke %w di masa depan jika diperlukan.

Saat menambahkan konteks ke kesalahan yang dikembalikan, hindari menggunakan frasa seperti "failed to" agar konteksnya ringkas. Ketika kesalahan menyebar melalui tumpukan, akan ditumpuk lapis demi lapis:

Tidak Disarankan:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "failed to create new store: %w", err)
}

// failed to x: failed to y: failed to create new store: the error

Disarankan:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %w", err)
}
// x: y: new store: the error

Namun, setelah kesalahan dikirim ke sistem lain, harus jelas bahwa pesan tersebut adalah sebuah kesalahan (mis., tag "err" atau awalan "Gagal" di log).

Penamaan yang Salah

Untuk nilai kesalahan yang disimpan sebagai variabel global, gunakan awalan Err atau err tergantung apakah mereka diekspor. Silakan lihat panduan. Untuk konstanta dan variabel tingkat atas yang tidak diekspor, gunakan garis bawah (_) sebagai awalan.

var (
  // Ekspor dua kesalahan berikut agar pengguna paket ini dapat mencocokkannya dengan errors.Is.
  ErrBrokenLink = errors.New("link is broken")
  ErrCouldNotOpen = errors.New("could not open")

  // Kesalahan ini tidak diekspor karena kami tidak ingin itu menjadi bagian dari API publik kami. Kami masih dapat menggunakannya dalam paket dengan errors.
  errNotFound = errors.New("not found")
)

Untuk jenis kesalahan kustom, gunakan akhiran Error.

// Demikian pula, kesalahan ini diekspor agar pengguna paket ini dapat mencocokkannya dengan errors.As.
type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("file %q not found", e.File)
}

// Kesalahan ini tidak diekspor karena kami tidak ingin itu menjadi bagian dari API publik. Kami masih bisa menggunakannya dalam paket dengan errors.As.
type resolveError struct {
  Path string
}

func (e *resolveError) Error() string {
  return fmt.Sprintf("resolve %q", e.Path)
}

Penanganan Kesalahan

Ketika pemanggil menerima kesalahan dari pemanggil, dapat menangani kesalahan dengan berbagai cara berdasarkan pemahaman atas kesalahan tersebut.

Ini termasuk tetapi tidak terbatas pada:

  • Memadankan kesalahan dengan errors.Is atau errors.As jika pemanggil telah menyetujui definisi kesalahan tertentu, dan menangani percabangan dengan cara yang berbeda
  • Mencatat kesalahan dan merosot dengan indah jika kesalahan dapat dipulihkan
  • Mengembalikan kesalahan yang telah ditentukan dengan baik jika itu mewakili kondisi kegagalan spesifik domain
  • Mengembalikan kesalahan, baik itu dibungkus atau apa adanya

Terlepas dari bagaimana pemanggil menangani kesalahan, biasanya seharusnya hanya menangani setiap kesalahan sekali. Misalnya, pemanggil seharusnya tidak mencatat kesalahan dan kemudian mengembalikannya, karena pemanggilnya juga mungkin menangani kesalahan tersebut.

Sebagai contoh, pertimbangkan skenario berikut:

Buruk: Mencatat kesalahan dan mengembalikannya

Pemanggil lain lebih tinggi di tumpukan mungkin mengambil tindakan serupa terhadap kesalahan ini. Ini akan menciptakan banyak kebisingan dalam log aplikasi dengan sedikit manfaat.

u, err := getUser(id)
if err != nil {
  // BURUK: Lihat deskripsi
  log.Printf("Tidak dapat mendapatkan pengguna %q: %v", id, err)
  return err
}

Baik: Membungkus kesalahan dan mengembalikkannya

Kesalahan lebih tinggi di tumpukan akan menangani kesalahan ini. Menggunakan %w memastikan bahwa mereka dapat memadankan kesalahan dengan errors.Is atau errors.As jika relevan.

u, err := getUser(id)
if err != nil {
  return fmt.Errorf("mendapatkan pengguna %q: %w", id, err)
}

Baik: Mencatat kesalahan dan merosot dengan indah

Jika operasi tidak mutlak diperlukan, kita dapat memberikan keruntuhan yang mulus dengan pulih darinya tanpa mengganggu pengalaman.

if err := emitMetrics(); err != nil {
  // Kegagalan menulis metrik seharusnya tidak
  // memutuskan aplikasi.
  log.Printf("Tidak dapat mengeluarkan metrik: %v", err)
}

Baik: Memadankan kesalahan dan merosot dengan indah sesuai

Jika pemanggil telah menentukan kesalahan spesifik dalam persetujuannya dan kegagalan dapat dipulihkan, cocokkan kasus kesalahan tersebut dan merosot dengan indah. Untuk semua kasus lain, bungkus kesalahan dan kembalikan. Kesalahan lebih tinggi di tumpukan akan menangani kesalahan lain.

tz, err := getUserTimeZone(id)
if err != nil {
  if errors.Is(err, ErrUserNotFound) {
    // Pengguna tidak ada. Gunakan UTC.
    tz = time.UTC
  } else {
    return fmt.Errorf("mendapatkan pengguna %q: %w", id, err)
  }
}

Penanganan Gagal Asersi

Asumsi tipe akan memunculkan panic dengan satu nilai pengembalian jika deteksi tipe tidak benar. Oleh karena itu, selalu gunakan idiom "koma, OK".

Tidak Direkomendasikan:

t := i.(string)

Direkomendasikan:

t, ok := i.(string)
if !ok {
  // Tangani kesalahan dengan indah
}

Hindari menggunakan panic

Kode yang dijalankan di lingkungan produksi harus menghindari panic. Panic adalah sumber utama kegagalan beruntun. Jika terjadi kesalahan, fungsi harus mengembalikan kesalahan dan membiarkan pemanggil untuk menentukan bagaimana cara menanganinya.

Tidak disarankan:

func run(args []string) {
  if len(args) == 0 {
    panic("diperlukan satu argumen")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}

Disarankan:

func run(args []string) error {
  if len(args) == 0 {
    return errors.New("diperlukan satu argumen")
  }
  // ...
  return nil
}

func main() {
  if err := run(os.Args[1:]); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

panic/recover bukanlah strategi penanganan kesalahan. Panic hanya boleh terjadi ketika suatu kejadian tak dapat dipulihkan (misalnya, referensi nil) terjadi. Satu pengecualian adalah selama inisialisasi program: situasi yang menyebabkan program panic harus ditangani selama startup program.

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

Bahkan dalam kode uji coba, disarankan untuk menggunakan t.Fatal atau t.FailNow daripada panic untuk memastikan bahwa kegagalan ditandai.

Tidak disarankan:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  panic("gagal menyiapkan uji coba")
}

Disarankan:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  t.Fatal("gagal menyiapkan uji coba")
}