Hata İşleme
Go'da üç seviye anormallik vardır:
error: Normal akış hatası, işlenmelidir. Doğrudan göz ardı edilirse program çökmezpanic: Çok ciddi sorun, program sorunu处理后 hemen çıkmalıdırfatal: Ç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:
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.
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
varile 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.
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.
err := errors.New("bu bir hata")İkincisi fmt paketi altındaki Errorf fonksiyonunu kullanmaktır. Biçimlendirme parametreli bir error elde edebilirsiniz.
err := fmt.Errorf("bu %d parametreli bir hata", 1)Aşağıda tam bir örnek bulunmaktadır
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
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.
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ı.
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
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.
func Unwrap(err error) errorerrors.Unwrap() fonksiyonu bir hata zincirini açmak için kullanılır. İç uygulaması çok basittir
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.
func Is(err, target error) boolerrors.Is fonksiyonunun işlevi hata zincirinde belirtilen hatanın bulunup bulunmadığını判断 etmektir. Örnek aşağıdaki gibidir
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.
func As(err error, target any) boolerrors.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
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
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:1650Biç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
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
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
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ı parametresiSonrası İş
Program panic nedeniyle çıkmadan önce bazı sonrası işler yapar. Örneğin defer ifadesini çalıştırır.
func main() {
defer fmt.Println("A")
defer fmt.Println("B")
fmt.Println("C")
panic("panic")
defer fmt.Println("D")
}Çıktı
C
B
A
panic: panicVe üst fonksiyonun defer ifadesi de çalışır. Örnek aşağıdaki gibidir
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: panicdefer içinde panic iç içe geçebilir. Aşağıda daha karmaşık bir örnek var
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: panicAYukarı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.
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: panicBdemo() 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
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: panicBBu ö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.
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.
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ştuBunun dışında, bir çok uç durum daha var. Yani panic() parametresi nil'dir.
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
- Mutlaka
deferiçinde kullanılmalıdır - Çoklu kullanım sadece birinin
panic'i kurtarabilir - Kapama
recoverdış fonksiyonun herhangi birpanic'ini kurtarmaz panicparametresinilkullanamaz
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
func main() {
dangerOp("")
}
func dangerOp(str string) {
if len(str) == 0 {
fmt.Println("fatal")
os.Exit(1)
}
fmt.Println("Normal mantık")
}Çıktı
fatalfatal seviyesi sorunlar genellikle açıkça tetiklenmez. Çoğu durumda pasif olarak tetiklenir.
