Skip to content

Hata İşleme

Go'da üç seviye anormallik vardır:

  • error: Normal akış hatası, işlenmelidir. Doğrudan göz ardı edilirse program çökmez
  • panic: Çok ciddi sorun, program sorunu处理后 hemen çıkmalıdır
  • fatal: Çok ölümcül sorun, program hemen çıkmalıdır

Doğru söylemek gerekirse, Go dilinde anormallik yoktur, hatalar aracılığıyla yansıtılır. Aynı şekilde, Go'da try-catch-finally gibi ifadeler de yoktur. Go'nun kurucuları hataları kontrol edilebilir olmasını umar. Her iş için bir sürü try-catch iç içe geçmesini istemezler. Bu nedenle çoğu durumda fonksiyon dönüş değeri olarak döndürülür. Aşağıdaki kod örneği:

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

Bu kodun amacı çok açık, README.txt adında bir dosya açmaktır. Açma başarısız olursa fonksiyon bir hata döndürür, hata bilgisi çıktılanır. Eğer hata nil ise açma başarılıdır, dosya adı çıktılanır.

Görünüşe göre try-catch'ten daha basit. Ancak çok sayıda fonksiyon çağrısı varsa, her yerde if err != nil gibi判断 ifadeleri olacaktır. Aşağıdaki örnek gibi, bu bir dosya hash değeri hesaplama demosudur. Bu küçük kod parçasında toplam üç kez if err != nil görünür.

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
}

Bu nedenle, Go'nun en çok eleştirilen noktası hata işlemedir. Go kaynak kodunda if err != nil oldukça büyük bir bölümü kaplar. Rust da hata değeri döndürür, ancak kimse bu konuda bir şey söylemez. Çünkü sözdizimi şekeri ile bu tür sorunları çözer. Rust ile karşılaştırıldığında, Go'nun sözdizimi şekeri çok fazla değil, neredeyse hiç yok denecek kadar azdır.

Ancak事物ya diyalektik bakmalıyız. Her şeyin iyi ve kötü yanları vardır. Go'nun hata işleminin birkaç avantajı vardır:

  • Zihinsel yük küçüktür: Hata varsa işle, işlemezsen döndür
  • Okunabilirlik: İşleme yöntemi çok basit olduğu için, çoğu durumda kodu anlamak kolaydır
  • Hata ayıklama kolaydır: Her hata fonksiyon çağrısı dönüş değerinden oluşur, katman katman geriye bulunabilir. Aniden bir hata ortaya çıkar ama nereden geldiği bilinmez durumu nadiren oluşur

Ancak dezavantajları da az değil:

  • Hata yığın bilgisi içermez (üçüncü taraf paket veya kendi paketleme ile çözülür)
  • Çirkin, tekrarlayan kod çok (kişisel beğeniye bağlı)
  • Özel hata var ile bildirilir, bir değişkendir sabit değil (gerçekten olmamalı)
  • Değişken gölgeleme sorunu

Toplulukta Go hata işleme ile ilgili öneriler ve tartışmalar Go'nun doğuşundan beri hiç durmadı. Şaka olarak şöyle bir söz var: Eğer Go'nun hata işlemini kabul edebiliyorsanız, o zaman ancak yeterli bir Gopher olabilirsiniz.

::: ipucu

Go ekibinin hata işleme hakkında iki makalesi var, ilgileniyorsanız bakabilirsiniz

:::

error

error normal bir akış hatasıdır. Ortaya çıkması kabul edilebilirdir. Çoğu durumda işlenmelidir. Tabii ki göz ardı edilebilir. error'un şiddet seviyesi tüm programı durdurmaya yetmez. error önceden tanımlanmış bir arayüzdür. Bu arayüzün sadece bir yöntemi vardır: Error(). Bu yöntemin dönüş değeri string'dir, hata bilgisi çıktısı için kullanılır.

go
type error interface {
   Error() string
}

error tarihinde büyük değişiklikler geçirdi. 1.13 sürümünde Go ekibi zincirleme hata çıkardı ve daha eksiksiz hata kontrol mekanizması sağladı. Sonraki bölümlerde hepsi tanıtılacaktır.

Oluşturma

error oluşturmanın birkaç yöntemi vardır. Birincisi errors paketi altındaki New fonksiyonunu kullanmaktır.

go
err := errors.New("bu bir hata")

İkincisi fmt paketi altındaki Errorf fonksiyonunu kullanmaktır. Biçimlendirme parametreli bir error elde edebilirsiniz.

go
err := fmt.Errorf("bu %d parametreli bir hata", 1)

Aşağıda tam bir örnek bulunmaktadır

go
func sumPositive(i, j int) (int, error) {
   if i <= 0 || j <= 0 {
      return -1, errors.New("pozitif tamsayı olmalı")
   }
   return i + j, nil
}

Çoğu durumda, daha iyi bakım için genellikle geçici error oluşturulmaz. Bunun yerine yaygın olarak kullanılan error'lar全局 değişken olarak kullanılır. Aşağıda os\erros.go dosyasından alınan kod örneği

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"
)

Hepsinin var ile tanımlandığı değişkenler olduğu görülebilir

Özel Hata

Error() yöntemini uygulayarak kolayca özel error oluşturabilirsiniz. Örneğin erros paketi altındaki errorString çok basit bir uygulamadır.

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

// errorString yapısı
type errorString struct {
   s string
}

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

errorString uygulaması çok basit olduğu için, ifade yeteneği yetersizdir. Bu nedenle birçok açık kaynak kütüphane ve resmi kütüphane özel error seçer. Farklı hata gereksinimlerini karşılamak için.

İletim

Bazı durumlarda, çağrılan fonksiyon bir hata döndürür. Ancak çağrılan kendisi hatayı işlemez ve hatayı dönüş değeri olarak döndürür. Bir üst çağrıana iletir. Bu sürece iletim denir. Hata iletim过程中 katman katman paketlenebilir. Üst çağrılan hatanın türünü判断 edip farklı işlemler yapmak istediğinde, hatanın kategorisini ayırt edemeyebilir veya yanlış判断 yapabilir. Zincirleme hata tam olarak bu tür durumu çözmek için ortaya çıktı.

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 aynı zamanda error arayüzünü uygular. Bir yöntemi daha vardır: Unwrap. Orijinal error'un referansı için kullanılır. Katman katman paketleme altında bir hata zinciri oluşur. Zincir boyunca arayarak, orijinal hatayı kolayca bulabilirsiniz. Bu yapı dışarıya açık olmadığı için, sadece fmt.Errorf fonksiyonu kullanılarak oluşturulabilir. Örneğin

go
err := errors.New("bu bir orijinal hata")
wrapErr := fmt.Errorf("hata, %w", err)

Kullanırken, mutlaka %w biçim fiili kullanılmalıdır ve parametre sadece bir geçerli error olabilir.

İşleme

Hata işleminin son adımı hatayı nasıl işleyip kontrol etmektir. errors paketi hata işlemek için birkaç kolay fonksiyon sağlar.

go
func Unwrap(err error) error

errors.Unwrap() fonksiyonu bir hata zincirini açmak için kullanılır. İç uygulaması çok basittir

go
func Unwrap(err error) error {
   u, ok := err.(interface { // Tür断言, yöntemi uygulayıp uygulamadığı
      Unwrap() error
   })
   if !ok { // Uygulanmamışsa temel bir error olduğu anlaşılır
      return nil
   }
   return u.Unwrap() // Aksi takdirde Unwrap çağrılır
}

Açıldıktan sonra mevcut hata zincirinin sardığı hatayı döndürür. Sarılan hata hala bir hata zinciri olabilir. Zincirde karşılık gelen değeri veya türü bulmak istiyorsanız, yinelemeli olarak arama eşleştirmesi yapabilirsiniz. Ancak standart kütüphane zaten benzer fonksiyonlar sağlamıştır.

go
func Is(err, target error) bool

errors.Is fonksiyonunun işlevi hata zincirinde belirtilen hatanın bulunup bulunmadığını判断 etmektir. Örnek aşağıdaki gibidir

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

func wrap1() error { // Orijinal hatayı sar
   return fmt.Errorf("wrapp error %w", wrap2())
}

func wrap2() error { // Orijinal hata
   return originalErr
}

func main() {
   err := wrap1()
   if errors.Is(err, originalErr) { // if err == originalErr kullanılırsa false olur
      fmt.Println("original")
   }
}

Bu nedenle hata判断 yaparken, == operatörü kullanılmamalıdır. Bunun yerine errors.Is() kullanılmalıdır.

go
func As(err error, target any) bool

errors.As() fonksiyonunun işlevi hata zincirinde ilk eşleşen tür hatasını bulmak ve değeri iletilen err'a atamaktır. Bazı durumlarda error türü hatayı özel hata uygulama türüne dönüştürmek gerekir. Daha detaylı hata detayları elde etmek için. Ancak hata zincirine tür断言 yapmak etkisizdir. Çünkü orijinal hata yapı tarafından sarılmıştır. Bu da As fonksiyonunun neden gerekli olduğunun nedenidir. Örnek aşağıdaki gibidir

go
type TimeError struct { // Özel error
   Msg  string
   Time time.Time // Hatanın oluştuğu zamanı kaydet
}

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

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

func wrap1() error { // Orijinal hatayı sar
   return fmt.Errorf("wrapp error %w", wrap2())
}

func wrap2() error { // Orijinal hata
   return NewMyError("original error")
}

func main() {
   var myerr *TimeError
   err := wrap1()
   // Hata zincirinde *TimeError türü hata olup olmadığını kontrol et
   if errors.As(err, &myerr) { // TimeError zamanını çıktıla
      fmt.Println("original", myerr.Time)
   }
}

target mutlaka error'a işaret eden işaretçi olmalıdır. Yapı oluştururken yapı işaretçisi döndürüldüğü için, error aslında *TimeError türündendir. O zaman target mutlaka **TimeError türünde olmalıdır.

Ancak resmi errors paketi aslında yeterli değildir. Çünkü yığın bilgisi yoktur, konumlandırılamaz. Genellikle resmi başka bir geliştirilmiş paket önerilir

github.com/pkg/errors

Örnek

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)
  }
}

Çıktı

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

Biçimlendirilmiş çıktı ile, yığın bilgisi görülebilir. Varsayılan olarak yığın çıktılanmaz. Bu paket standart kütüphane errors paketinin geliştirilmiş versiyonudur. İkisi de resmi tarafından yazılmıştır. Neden standart kütüphaneye dahil edilmediği bilinmiyor.

panic

panic Çince'de panik olarak çevrilir. Çok ciddi program sorununu ifade eder. Programın bu sorunu işlemek için hemen durması gerekir. Aksi takdirde program hemen durur ve yığın bilgisi çıktılanır. panic Go'da çalışma zamanı anormalliğinin ifade biçimidir. Genellikle bazı tehlikeli işlemlerde ortaya çıkar. Temel olarak zamanında zararı durdurmak içindir. Daha ciddi sonuçlara neden olmaktan kaçınmak için. Ancak panic çıkmadan önce programın sonrasını iyi yapar. Aynı zamanda panic kurtarılabilir ve programın çalışmaya devam etmesi sağlanabilir.

Aşağıda nil haritaya değer yazan bir örnek var. Kesinlikle panic tetikler

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

::: ipucu

Programda birden fazla goroutine olduğunda, herhangi bir goroutine panic oluşursa, kurtarılmazsa tüm program çöker

:::

Oluşturma

Açıkça panic oluşturmak çok basittir. Yerleşik panic fonksiyonu kullanılır. Fonksiyon imzası şu şekildedir

go
func panic(v any)

panic fonksiyonu any türünde bir v parametresi alır. Hata yığın bilgisi çıktılanırken, v de çıktılanır. Kullanım örneği aşağıdaki gibidir

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

func initDataBase(host string, port int) {
  if len(host) == 0 || port == 0 {
    panic("Geçersiz veri bağlantı parametresi")
  }
    // ...diğer mantık
}

Veritabanı bağlantısı başlatma başarısız olduğunda, program başlamamalıdır. Çünkü veritabanı olmadan program çalışmanın anlamı yoktur. Bu nedenle burada panic fırlatılmalıdır

panic: Geçersiz veri bağlantı parametresi

Sonrası İş

Program panic nedeniyle çıkmadan önce bazı sonrası işler yapar. Örneğin defer ifadesini çalıştırır.

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

Çıktı

C
B
A
panic: panic

Ve üst fonksiyonun defer ifadesi de çalışır. Örnek aşağıdaki gibidir

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)
}

Çıktı

C
2
1
B
A
panic: panic

defer içinde panic iç içe geçebilir. Aşağıda daha karmaşık bir örnek var

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)
}

defer içinde iç içe panic çalışma sırası hala tutarlıdır. panic oluştuğunda sonraki mantık çalıştırılamaz.

C
2
1
A
panic: panicB
        panic: panicA

Yukarıdaki gibi, panic oluştuğunda, bulunduğu fonksiyondan hemen çıkar. Ve mevcut fonksiyonun sonrası işini yapar. Örneğin defer. Sonra katman katman yukarı fırlatır. Üst fonksiyon da aynı şekilde sonrası işini yapar. Program çalışmayı durdurana kadar.

Alt goroutine panic oluştuğunda, mevcut goroutine'in sonrası işi tetiklenmez. Alt goroutine çıkana kadar panic kurtarılmazsa, program doğrudan çalışmayı durdurur.

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() // Üst goroutine alt goroutine'in bitmesini bekler
  defer fmt.Println("D")
}
func dangerOp() {
  defer fmt.Println(1)
  defer fmt.Println(2)
  panic("panicB")
  defer fmt.Println(3)
  waitGroup.Done()
}

Çıktı

C
2
1
panic: panicB

demo() içindeki defer ifadesinin hiçbirinin çalıştırılmadığını ve programın doğrudan çıktığını görebilirsiniz. Dikkat edilmesi gereken, eğer waitGroup ile üst goroutine engellenmezse, demo() çalışma hızı alt goroutine'in çalışma hızından hızlı olabilir. Çıktı sonucu çok kafa karıştırıcı olabilir. Aşağıda kodu biraz değiştiriyoruz

go
func main() {
  demo()
}

func demo() {
  defer func() {
    // Üst goroutine sonrası işi 20ms harcar
    time.Sleep(time.Millisecond * 20)
    fmt.Println("A")
  }()
  fmt.Println("C")
  go dangerOp()
  defer fmt.Println("D")
}
func dangerOp() {
  // Alt goroutine bazı mantıklar çalıştırır, 1ms harcar
  time.Sleep(time.Millisecond)
  defer fmt.Println(1)
  defer fmt.Println(2)
  panic("panicB")
  defer fmt.Println(3)
}

Çıktı

C
D
2
1
panic: panicB

Bu örnekte, alt goroutine panic oluştuğunda, üst goroutine çoktan fonksiyon çalışmasını tamamlamış ve sonrası işine girmiştir. Son defer çalıştırılırken, tesadüfen alt goroutine panic oluşur. Bu nedenle program doğrudan çalışmayı durdurur.

Kurtarma

panic oluştuğunda, yerleşik recover() fonksiyonu zamanında işleyebilir ve programın çalışmaya devam etmesini sağlayabilir. Mutlaka defer ifadesinde çalıştırılmalıdır. Kullanım örneği aşağıdaki gibidir.

go
func main() {
   dangerOp()
   fmt.Println("Program normal çıktı")
}

func dangerOp() {
   defer func() {
      if err := recover(); err != nil {
         fmt.Println(err)
         fmt.Println("panic kurtarıldı")
      }
   }()
   panic("panic oluştu")
}

Çağrılan dangerOp() fonksiyonunun içinde panic oluştuğunu hiç bilmez. Program kalan mantığı çalıştırdıktan sonra normal çıkar. Bu nedenle çıktı şu şekildedir

panic oluştu
panic kurtarıldı
Program normal çıktı

Ancak aslında recover() kullanımında birçok gizli tuzak vardır. Örneğin defer içinde tekrar kapama kullanarak recover kullanmak.

go
func main() {
  dangerOp()
  fmt.Println("Program normal çıktı")
}

func dangerOp() {
  defer func() {
    func() {
      if err := recover(); err != nil {
        fmt.Println(err)
        fmt.Println("panic kurtarıldı")
      }
    }()
  }()
  panic("panic oluştu")
}

Kapama fonksiyonu bir fonksiyon çağrısı olarak düşünülebilir. panic aşağıya değil yukarıya iletilir. Doğal olarak kapama fonksiyonu da panic'i kurtaramaz. Bu nedenle çıktı şu şekildedir.

panic: panic oluştu

Bunun dışında, bir çok uç durum daha var. Yani panic() parametresi nil'dir.

go
func main() {
   dangerOp()
   fmt.Println("Program normal çıktı")
}

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

Bu durumda panic gerçekten kurtarılır. Ancak hiçbir hata bilgisi çıktılanmaz.

Çıktı

Program normal çıktı

Genel olarak recover fonksiyonunun birkaç dikkat noktası vardır

  1. Mutlaka defer içinde kullanılmalıdır
  2. Çoklu kullanım sadece birinin panic'i kurtarabilir
  3. Kapama recover dış fonksiyonun herhangi bir panic'ini kurtarmaz
  4. panic parametresi nil kullanamaz

fatal

fatal son derece ciddi bir sorundur. fatal oluştuğunda, program hemen durmalıdır. Herhangi bir sonrası işi çalıştırılmaz. Genellikle os paketi altındaki Exit fonksiyonu çağrılarak programdan çıkılır. Aşağıda gösterildiği gibi

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

func dangerOp(str string) {
  if len(str) == 0 {
    fmt.Println("fatal")
    os.Exit(1)
  }
  fmt.Println("Normal mantık")
}

Çıktı

fatal

fatal seviyesi sorunlar genellikle açıkça tetiklenmez. Çoğu durumda pasif olarak tetiklenir.

Golang by www.golangdev.cn edit