Skip to content

Penanganan Error

Ada tiga level exception di Go:

  • error: Error流程 normal, perlu ditangani, langsung mengabaikan tidak menangani program juga tidak akan crash
  • panic: Masalah yang sangat serius, program harus keluar segera setelah menangani masalah
  • fatal: Masalah yang sangat fatal, program harus segera keluar

Secara akurat, Go tidak memiliki exception, ia direfleksikan melalui error. Sama juga, Go juga tidak memiliki statement try-catch-finally seperti ini. Pendiri Go berharap dapat mengontrol error, mereka tidak berharap melakukan apapun perlu nested一堆 try-catch, oleh karena itu sebagian besar kasus akan mengembalikannya sebagai nilai return fungsi, seperti contoh kode berikut:

go
func main() {
  // Membuka file
  if file, err := os.Open("README.txt"); err != nil {
    fmt.Println(err)
        return
  }
    fmt.Println(file.Name())
}

Niat kode ini sangat jelas, membuka file bernama README.txt, jika gagal membuka fungsi akan mengembalikan error, output informasi error, jika error adalah nil maka berhasil membuka, output nama file.

Tampaknya lebih ringkas daripada try-catch, tetapi jika ada sangat banyak pemanggilan fungsi, akan di mana-mana充斥着 statement判断 if err != nil, seperti contoh berikut, ini adalah demo menghitung hash file, dalam一小段 kode ini total muncul tiga kali if err != nil.

go
func main() {
  sum, err := checksum("main.go")
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println(sum)
}

func checksum(path string) (string, error) {
  file, err := os.Open(path)
  if err != nil {
    return "", err
  }
  defer file.Close()

  hash := sha256.New()
  _, err = io.Copy(hash, file)
  if err != nil {
    return "", err
  }

  var hexSum [64]byte
  sum := hash.Sum(nil)
  hex.Encode(hexSum[:], sum)

  return string(hexSum[:]), nil
}

Karena itu, poin paling dikritik pihak luar tentang Go ada di penanganan error. if err != nil di source code Go menempati bagian yang cukup besar. Rust juga mengembalikan nilai error, tetapi tidak ada yang akan mengatakan hal ini, karena ia menyelesaikan masalah这类 melalui syntax sugar. Dibandingkan Rust, syntax sugar Go tidak bisa dibilang banyak, hanya bisa dibilang hampir tidak ada.

Tetapi kita harus melihat事物 secara dialektis, segala sesuatu ada baik dan buruknya, keunggulan penanganan error Go ada beberapa

  • Beban mental kecil: Ada error langsung tangani, tidak tangani langsung kembalikan
  • Keterbacaan: Karena cara penanganan sangat sederhana, sebagian besar kasus sangat mudah memahami kode
  • Mudah debug: Setiap error dihasilkan dari nilai return pemanggilan fungsi, dapat dilapis demi lapis kembali mencari, jarang terjadi tiba-tiba muncul error tetapi tidak tahu dari mana

Tetapi kekurangannya juga banyak

  • Tidak ada informasi stack di error (perlu paket pihak ketiga menyelesaikan atau enkapsulasi sendiri)
  • Jelek, kode repetitif banyak (tergantung preferensi pribadi)
  • Custom error dideklarasikan melalui var, ia adalah variabel bukan konstanta (memang tidak seharusnya)
  • Masalah variable shadowing

Di komunitas, proposal dan diskusi tentang penanganan error Go sejak Go lahir tidak pernah berhenti, ada lelucon seperti ini: Jika Anda dapat menerima penanganan error Go, maka Anda baru Gopher yang kompeten.

TIP

Ada dua artikel tim Go tentang penanganan error, tertarik dapat lihat

error

error属于 adalah error流程 normal, kemunculannya dapat diterima, sebagian besar kasus harus menanganinya, tentu juga dapat mengabaikan, level keparahan error tidak cukup untuk menghentikan seluruh program. error sendiri adalah interface yang sudah didefinisikan, interface ini hanya memiliki satu metode Error(), nilai return metode ini adalah string, digunakan untuk output informasi error.

go
type error interface {
   Error() string
}

error dalam sejarah juga pernah mengalami perubahan besar, di versi 1.13 tim Go meluncurkan chained error, dan menyediakan mekanisme pemeriksaan error yang lebih sempurna, selanjutnya akan一一介绍.

Membuat

Membuat error memiliki beberapa metode berikut, pertama adalah menggunakan fungsi New di bawah paket errors.

go
err := errors.New("ini adalah error")

Kedua adalah menggunakan fungsi Errorf di bawah paket fmt, dapat mendapatkan error dengan parameter terformat.

go
err := fmt.Errorf("ini adalah error dengan%d parameter terformat", 1)

Berikut adalah contoh lengkap

go
func sumPositive(i, j int) (int, error) {
   if i <= 0 || j <= 0 {
      return -1, errors.New("harus bilangan bulat positif")
   }
   return i + j, nil
}

Sebagian besar kasus, demi maintainability yang lebih baik, umumnya tidak akan membuat error临时, tetapi akan menggunakan error yang umum digunakan sebagai variabel global, seperti kode berikut yang diambil dari file os\erros.go

go
var (
  ErrInvalid = fs.ErrInvalid // "invalid argument"

  ErrPermission = fs.ErrPermission // "permission denied"
  ErrExist      = fs.ErrExist      // "file already exists"
  ErrNotExist   = fs.ErrNotExist   // "file does not exist"
  ErrClosed     = fs.ErrClosed     // "file already closed"

  ErrNoDeadline       = errNoDeadline()       // "file type does not support deadline"
  ErrDeadlineExceeded = errDeadlineExceeded() // "i/o timeout"
)

Dapat dilihat semuanya didefinisikan oleh var sebagai variabel

Custom Error

Melalui implementasi metode Error(), dapat dengan mudah custom error, misalnya errorString di bawah paket erros adalah implementasi yang sangat sederhana.

go
func New(text string) error {
   return &errorString{text}
}

// struct errorString
type errorString struct {
   s string
}

func (e *errorString) Error() string {
   return e.s
}

Karena implementasi errorString terlalu sederhana, kemampuan ekspresi tidak cukup, oleh karena itu banyak open source library termasuk library官方 akan memilih custom error, untuk memenuhi kebutuhan error yang berbeda.

Passing

Dalam beberapa situasi, caller memanggil fungsi mengembalikan error, tetapi caller sendiri tidak bertanggung jawab menangani error, oleh karena itu juga mengembalikan error sebagai nilai return, dilempar ke caller layer atas, proses ini disebut passing. Error selama proses passing mungkin dikemas berlapis-lapis, ketika caller layer atas ingin判断 tipe error untuk做出 penanganan berbeda, mungkin tidak dapat membedakan kategori error atau salah判断, dan chained error lahir untuk menyelesaikan situasi ini.

go
type wrapError struct {
   msg string
   err error
}

func (e *wrapError) Error() string {
   return e.msg
}

func (e *wrapError) Unwrap() error {
   return e.err
}

wrappError juga mengimplementasikan interface error, juga memiliki satu metode Unwrap, digunakan untuk mengembalikan referensi untuk error asli di dalamnya, dikemas berlapis-lapis membentuk rantai error,顺着 rantai mencari, sangat mudah dapat menemukan error asli. Karena struct ini tidak diekspos ke luar, oleh karena itu hanya dapat menggunakan fungsi fmt.Errorf untuk membuat, misalnya

go
err := errors.New("ini adalah error asli")
wrapErr := fmt.Errorf("error, %w", err)

Saat menggunakan, harus menggunakan verb format %w, dan parameter hanya boleh satu error yang valid.

Penanganan

Langkah terakhir dalam penanganan error adalah bagaimana menangani dan memeriksa error, paket errors menyediakan beberapa fungsi mudah untuk menangani error.

go
func Unwrap(err error) error

Fungsi errors.Unwrap() digunakan untuk unwrap rantai error, implementasi internalnya juga sangat sederhana

go
func Unwrap(err error) error {
   u, ok := err.(interface { // type assertion, apakah mengimplementasikan metode ini
      Unwrap() error
   })
   if !ok { // tidak mengimplementasikan说明 adalah error dasar
      return nil
   }
   return u.Unwrap() // jika tidak memanggil Unwrap
}

Setelah unwrap akan mengembalikan error yang dibungkus rantai error saat ini, error yang dibungkus mungkin masih adalah rantai error, jika ingin menemukan nilai atau tipe yang sesuai di rantai error, dapat melakukan pencarian matching secara rekursif, tetapi standard library sudah menyediakan fungsi serupa.

go
func Is(err, target error) bool

Fungsi errors.Is berfungsi untuk判断 apakah rantai error mengandung error yang ditentukan, contoh sebagai berikut

go
var originalErr = errors.New("this is an error")

func wrap1() error { // Membungkus error asli
   return fmt.Errorf("wrapp error %w", wrap2())
}

func wrap2() error { // Error asli
   return originalErr
}

func main() {
   err := wrap1()
   if errors.Is(err, originalErr) { // Jika menggunakan if err == originalErr akan false
      fmt.Println("original")
   }
}

Oleh karena itu saat判断 error, tidak boleh menggunakan operator ==, tetapi harus menggunakan errors.Is().

go
func As(err error, target any) bool

Fungsi errors.As() berfungsi untuk mencari error dengan tipe matching pertama di rantai error, dan memberikan nilai ke err yang diteruskan. Beberapa kasus perlu mengkonversi error tipe error menjadi tipe implementasi error spesifik, untuk mendapatkan detail error yang lebih rinci, dan type assertion pada rantai error tidak efektif, karena error asli dibungkus oleh struct, ini juga alasan mengapa perlu fungsi As. Contoh sebagai berikut

go
type TimeError struct { // custom error
   Msg  string
   Time time.Time // mencatat waktu terjadi error
}

func (m TimeError) Error() string {
   return m.Msg
}

func NewMyError(msg string) error {
   return &TimeError{
      Msg:  msg,
      Time: time.Now(),
   }
}

func wrap1() error { // Membungkus error asli
   return fmt.Errorf("wrapp error %w", wrap2())
}

func wrap2() error { // Error asli
   return NewMyError("original error")
}

func main() {
   var myerr *TimeError
   err := wrap1()
   // Memeriksa apakah rantai error memiliki error tipe *TimeError
   if errors.As(err, &myerr) { // Output waktu TimeError
      fmt.Println("original", myerr.Time)
   }
}

target harus pointer ke error, karena saat membuat struct mengembalikan pointer struct, oleh karena itu error sebenarnya adalah tipe *TimeError, maka target harus tipe **TimeError.

Tetapi paket errors yang disediakan官方 sebenarnya tidak cukup, karena tidak ada informasi stack, tidak dapat positioning, umumnya lebih direkomendasikan menggunakan paket enhancement官方 lainnya

github.com/pkg/errors

Contoh

go
import (
  "fmt"
  "github.com/pkg/errors"
)

func Do() error {
  return errors.New("error")
}

func main() {
  if err := Do(); err != nil {
    fmt.Printf("%+v", err)
  }
}

Output

some unexpected error happened
main.Do
        D:/WorkSpace/Code/GoLeran/golearn/main.go:9
main.main
        D:/WorkSpace/Code/GoLeran/golearn/main.go:13
runtime.main
        D:/WorkSpace/Library/go/root/go1.21.3/src/runtime/proc.go:267
runtime.goexit
        D:/WorkSpace/Library/go/root/go1.21.3/src/runtime/asm_amd64.s:1650

Melalui output terformat, dapat melihat informasi stack, secara default tidak akan output stack. Paket ini setara dengan versi enhancement paket standard library errors, sama-sama ditulis oleh官方, tidak tahu mengapa tidak dimasukkan ke standard library.

panic

panic diterjemahkan sebagai panic, menunjukkan masalah program yang sangat serius, program perlu segera berhenti untuk menangani masalah ini, jika tidak program segera berhenti berjalan dan output informasi stack, panic adalah bentuk ekspresi exception runtime Go, biasanya muncul di beberapa operasi berbahaya, terutama untuk timely stop loss, sehingga menghindari menyebabkan konsekuensi yang lebih serius. Tetapi panic juga akan melakukan pekerjaan善后 program sebelum keluar, sekaligus panic juga dapat dipulihkan untuk menjamin program terus berjalan.

Berikut adalah contoh menulis nilai ke map nil, pasti akan memicu panic

go
func main() {
   var dic map[string]int
   dic["a"] = 'a'
}
panic: assignment to entry in nil map

TIP

Ketika program ada banyak goroutine, selama任一 goroutine terjadi panic, jika tidak menangkapnya, seluruh program akan crash

Membuat

Secara eksplisit membuat panic sangat sederhana, menggunakan fungsi built-in panic即可, signature fungsi sebagai berikut

go
func panic(v any)

Fungsi panic menerima parameter v dengan tipe any, saat output informasi stack error, v juga akan dioutput. Contoh penggunaan sebagai berikut

go
func main() {
  initDataBase("", 0)
}

func initDataBase(host string, port int) {
  if len(host) == 0 || port == 0 {
    panic("parameter koneksi data ilegal")
  }
    // ... logika lainnya
}

Ketika inisialisasi koneksi database gagal, program tidak boleh启动, karena tanpa database program berjalan tidak ada artinya, oleh karena itu di sini harus melempar panic

panic: parameter koneksi data ilegal

###善后

Program sebelum keluar karena panic akan melakukan beberapa pekerjaan善后, misalnya mengeksekusi statement defer.

go
func main() {
   defer fmt.Println("A")
   defer fmt.Println("B")
   fmt.Println("C")
   panic("panic")
   defer fmt.Println("D")
}

Output adalah

C
B
A
panic: panic

Dan statement defer fungsi upstream juga akan dieksekusi, contoh sebagai berikut

go
func main() {
   defer fmt.Println("A")
   defer fmt.Println("B")
   fmt.Println("C")
   dangerOp()
   defer fmt.Println("D")
}

func dangerOp() {
   defer fmt.Println(1)
   defer fmt.Println(2)
   panic("panic")
   defer fmt.Println(3)
}

Output

C
2
1
B
A
panic: panic

defer juga dapat nested panic, berikut adalah contoh yang lebih kompleks

go
func main() {
  defer fmt.Println("A")
  defer func() {
    func() {
      panic("panicA")
      defer fmt.Println("E")
    }()
  }()
  fmt.Println("C")
  dangerOp()
  defer fmt.Println("D")
}

func dangerOp() {
  defer fmt.Println(1)
  defer fmt.Println(2)
  panic("panicB")
  defer fmt.Println(3)
}

Urutan eksekusi panic yang nested di defer tetap konsisten, saat terjadi panic logika selanjutnya tidak akan dapat dieksekusi.

C
2
1
A
panic: panicB
        panic: panicA

Berdasarkan hal di atas, saat terjadi panic, akan segera keluar dari fungsi所在, dan mengeksekusi pekerjaan善后 fungsi saat ini, misalnya defer, lalu melempar layer demi layer ke atas, fungsi upstream sama juga melakukan pekerjaan善后, sampai program berhenti berjalan.

Ketika goroutine anak terjadi panic, tidak akan memicu pekerjaan善后 goroutine saat ini, jika sampai goroutine anak keluar belum memulihkan panic, maka program akan langsung berhenti berjalan.

go
var waitGroup sync.WaitGroup

func main() {
  demo()
}

func demo() {
  waitGroup.Add(1)
  defer func() {
    fmt.Println("A")
  }()
  fmt.Println("C")
  go dangerOp()
  waitGroup.Wait() // Goroutine induk阻塞 menunggu goroutine anak selesai eksekusi
  defer fmt.Println("D")
}
func dangerOp() {
  defer fmt.Println(1)
  defer fmt.Println(2)
  panic("panicB")
  defer fmt.Println(3)
  waitGroup.Done()
}

Output adalah

C
2
1
panic: panicB

Dapat dilihat statement defer di demo() satu pun tidak dieksekusi, program langsung keluar. Perlu diperhatikan, jika tidak ada waitGroup untuk mem-blok goroutine induk, eksekusi demo() mungkin lebih cepat daripada kecepatan eksekusi goroutine anak, hasil output akan menjadi sangat membingungkan, berikut sedikit modifikasi kode

go
func main() {
  demo()
}

func demo() {
  defer func() {
    // Pekerjaan善后 goroutine induk membutuhkan 20ms
    time.Sleep(time.Millisecond * 20)
    fmt.Println("A")
  }()
  fmt.Println("C")
  go dangerOp()
  defer fmt.Println("D")
}
func dangerOp() {
  // Goroutine anak perlu mengeksekusi beberapa logika, membutuhkan 1ms
  time.Sleep(time.Millisecond)
  defer fmt.Println(1)
  defer fmt.Println(2)
  panic("panicB")
  defer fmt.Println(3)
}

Output adalah

C
D
2
1
panic: panicB

Dalam contoh ini, ketika goroutine anak terjadi panic, goroutine induk sudah selesai eksekusi fungsi, masuk ke pekerjaan善后, saat mengeksekusi defer terakhir, kebetulan bertemu goroutine anak terjadi panic, oleh karena itu program langsung keluar berjalan.

Pemulihan

Saat terjadi panic, menggunakan fungsi built-in recover() dapat timely menangani dan menjamin program terus berjalan, harus dijalankan di statement defer, contoh penggunaan sebagai berikut.

go
func main() {
   dangerOp()
   fmt.Println("program keluar normal")
}

func dangerOp() {
   defer func() {
      if err := recover(); err != nil {
         fmt.Println(err)
         fmt.Println("panic pulih")
      }
   }()
   panic("terjadi panic")
}

Caller sama sekali tidak tahu di dalam fungsi dangerOp() terjadi panic, program mengeksekusi logika tersisa lalu keluar normal, oleh karena itu output sebagai berikut

terjadi panic
panic pulih
program keluar normal

Tetapi sebenarnya penggunaan recover() memiliki banyak jebakan implisit. Misalnya menggunakan recover lagi di closure di defer.

go
func main() {
  dangerOp()
  fmt.Println("program keluar normal")
}

func dangerOp() {
  defer func() {
    func() {
      if err := recover(); err != nil {
        fmt.Println(err)
        fmt.Println("panic pulih")
      }
    }()
  }()
  panic("terjadi panic")
}

Fungsi closure dapat dianggap memanggil fungsi, panic adalah meneruskan ke atas bukan ke bawah, tentu fungsi closure juga tidak dapat memulihkan panic, oleh karena itu output sebagai berikut.

panic: terjadi panic

Selain itu, ada situasi yang sangat ekstrem, yaitu parameter panic() adalah nil.

go
func main() {
   dangerOp()
   fmt.Println("program keluar normal")
}

func dangerOp() {
   defer func() {
      if err := recover(); err != nil {
         fmt.Println(err)
         fmt.Println("panic pulih")
      }
   }()
   panic(nil)
}

Dalam situasi ini panic memang akan pulih, tetapi tidak akan output informasi error apapun.

Output

program keluar normal

Secara keseluruhan fungsi recover memiliki beberapa poin perhatian

  1. Harus digunakan di defer
  2. Banyak kali menggunakan hanya satu yang dapat memulihkan panic
  3. Closure recover tidak akan memulihkan panic fungsi eksternal apapun
  4. Parameter panic dilarang menggunakan nil

fatal

fatal adalah masalah yang sangat serius, saat terjadi fatal, program perlu segera berhenti berjalan, tidak akan mengeksekusi pekerjaan善后 apapun, biasanya adalah memanggil fungsi Exit di bawah paket os untuk keluar program, seperti ditunjukkan berikut

go
func main() {
  dangerOp("")
}

func dangerOp(str string) {
  if len(str) == 0 {
    fmt.Println("fatal")
    os.Exit(1)
  }
  fmt.Println("logika normal")
}

Output

fatal

Masalah level fatal umumnya jarang secara eksplisit memicu, sebagian besar kasus adalah pasif memicu.

Golang by www.golangdev.cn edit