Gestione degli Errori
In Go ci sono tre livelli di eccezioni:
error: errore normale del flusso, deve essere gestito. Ignorarlo non causerà il crash del programmapanic: problema molto grave, il programma dovrebbe uscire immediatamente dopo aver gestito il problemafatal: problema molto critico, il programma dovrebbe uscire immediatamente
Per essere precisi, Go non ha eccezioni. Sono rappresentate tramite errori. Allo stesso modo, Go non ha istruzioni try-catch-finally. I fondatori di Go sperano che gli errori siano controllabili. Non vogliono dover annidare molti try-catch per ogni operazione. Quindi nella maggior parte dei casi gli errori vengono restituiti come valori di ritorno delle funzioni. Ad esempio, il seguente codice:
func main() {
// Apre un file
if file, err := os.Open("README.txt"); err != nil {
fmt.Println(err)
return
}
fmt.Println(file.Name())
}L'intenzione di questo codice è chiara: aprire un file chiamato README.txt. Se l'apertura fallisce, la funzione restituirà un errore e stamperà il messaggio di errore. Se l'errore è nil, allora l'apertura è riuscita e stampa il nome del file.
Sembra essere più conciso di try-catch, ma se ci sono molte chiamate di funzioni, ci saranno ovunque istruzioni di判断 if err != nil. Ad esempio, il codice seguente è un demo per calcolare l'hash di un file. In questo breve codice ci sono tre if err != nil:
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
}Proprio per questo, il punto più criticato di Go dalla comunità esterna è la gestione degli errori. if err != nil occupa una parte considerevole nel codice sorgente di Go. Anche Rust restituisce valori di errore, ma nessuno lo critica per questo, perché risolve questo tipo di problema tramite zucchero sintattico. In confronto a Rust, lo zucchero sintattico di Go non può essere definito molto, si può dire che quasi non esiste.
Tuttavia, dobbiamo vedere le cose in modo dialettico. Tutto ha pro e contro. I vantaggi della gestione degli errori di Go sono diversi:
- Carico mentale ridotto: se c'è un errore, lo si gestisce. Se non lo si gestisce, lo si restituisce
- Leggibilità: poiché il modo di gestire gli errori è molto semplice, nella maggior parte dei casi è facile capire il codice
- Facile debug: ogni errore viene generato dal valore di ritorno di una chiamata di funzione. Si può risalire livello per livello. Raramente accade che appaia improvvisamente un errore senza sapere da dove proviene
Ma ci sono anche molti svantaggi:
- Gli errori non contengono informazioni sullo stack (devono essere risolti con pacchetti di terze parti o incapsulamento proprio)
- Brutti, molte ripetizioni di codice (dipende dalle preferenze personali)
- Gli errori personalizzati sono dichiarati tramite
var, sono variabili non costanti (effettivamente non dovrebbe essere così) - Problemi di shadowing delle variabili
Le proposte e le discussioni sulla gestione degli errori di Go nella comunità non si sono mai fermate dalla nascita di Go. C'è un detto scherzoso: se puoi accettare la gestione degli errori di Go, allora sei un Gopher qualificato.
TIP
Ecco due articoli del team Go sulla gestione degli errori. Chi è interessato può darci un'occhiata:
error
error è un errore normale del flusso. La sua apparizione è accettabile. Nella maggior parte dei casi dovrebbe essere gestito,当然 può anche essere ignorato. La gravità di error non è sufficiente per fermare l'intero programma. error è di per sé un'interfaccia predefinita. Questa interfaccia ha solo un metodo Error(), il cui valore di ritorno è una stringa utilizzata per outputtare le informazioni di errore:
type error interface {
Error() string
}error ha subito grandi modifiche nella storia. Nella versione 1.13, il team Go ha introdotto gli errori a catena e ha fornito un meccanismo di controllo degli errori più completo. Verranno introdotti di seguito.
Creazione
Ci sono diversi modi per creare un error. Il primo è utilizzare la funzione New del pacchetto errors:
err := errors.New("questo è un errore")Il secondo è utilizzare la funzione Errorf del pacchetto fmt per ottenere un error con parametri formattati:
err := fmt.Errorf("questo è un errore con %d parametri formattati", 1)Di seguito è riportato un esempio completo:
func sumPositive(i, j int) (int, error) {
if i <= 0 || j <= 0 {
return -1, errors.New("devono essere numeri interi positivi")
}
return i + j, nil
}Nella maggior parte dei casi, per una migliore manutenibilità, generalmente non si crea temporaneamente un error, ma si utilizzano error comuni come variabili globali. Ad esempio, il seguente codice estratto dal file os\erros.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"
)Come si può vedere, sono tutte variabili definite con var.
Errori Personalizzati
Implementando il metodo Error(), è facile personalizzare un error. Ad esempio, errorString nel pacchetto erros è un'implementazione molto semplice:
func New(text string) error {
return &errorString{text}
}
// struttura errorString
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}Poiché l'implementazione di errorString è troppo semplice e non ha sufficiente capacità espressiva, molte librerie open source, incluse le librerie ufficiali, scelgono di personalizzare gli error per soddisfare diverse esigenze di errore.
Trasmissione
In alcune situazioni, il chiamante chiama una funzione che restituisce un errore, ma il chiamante stesso non è responsabile della gestione dell'errore, quindi restituisce anche l'errore come valore di ritorno, passandolo al chiamante di livello superiore. Questo processo si chiama trasmissione. Gli errori possono essere confezionati a più livelli durante la trasmissione. Quando il chiamante di livello superiore vuole判断 il tipo di errore per fare diversi trattamenti, potrebbe non essere in grado di判别 il tipo di errore o potrebbe giudicare erroneamente. Gli errori a catena sono apparsi proprio per risolvere questa situazione.
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}wrappError implementa anche l'interfaccia error e ha un metodo in più Unwrap, utilizzato per restituire un riferimento all'error originale interno. Sotto livelli di confezionamento si forma una catena di errori. Risalendo la catena, è facile trovare l'errore originale. Poiché questa struttura non è esposta esternamente, può essere creata solo utilizzando la funzione fmt.Errorf. Ad esempio:
err := errors.New("questo è un errore originale")
wrapErr := fmt.Errorf("errore, %w", err)Durante l'uso, è necessario utilizzare il verbo di formato %w e il parametro può essere solo un error valido.
Gestione
L'ultimo passo nella gestione degli errori è come gestire e controllare gli errori. Il pacchetto errors fornisce alcune funzioni utili per gestire gli errori:
func Unwrap(err error) errorLa funzione errors.Unwrap() viene utilizzata per decapsulare una catena di errori. La sua implementazione interna è molto semplice:
func Unwrap(err error) error {
u, ok := err.(interface { // 类型断言,是否实现该方法
Unwrap() error
})
if !ok { //没有实现说明是一个基础的 error
return nil
}
return u.Unwrap() // 否则调用 Unwrap
}Dopo il decapsulamento, restituisce l'errore avvolto dalla catena di errori corrente. L'errore avvolto potrebbe essere ancora una catena di errori. Se si desidera trovare il valore o il tipo corrispondente nella catena di errori, è possibile eseguire una ricerca ricorsiva per la corrispondenza. Tuttavia, la libreria standard ha già fornito funzioni simili:
func Is(err, target error) boolLa funzione errors.Is viene utilizzata per判断 se una catena di errori contiene un errore specificato. Ecco un esempio:
var originalErr = errors.New("this is an error")
func wrap1() error { // 包裹原始错误
return fmt.Errorf("wrapp error %w", wrap2())
}
func wrap2() error { // 原始错误
return originalErr
}
func main() {
err := wrap1()
if errors.Is(err, originalErr) { // 如果使用 if err == originalErr 将会是 false
fmt.Println("original")
}
}Quindi, quando si判断 gli errori, non si dovrebbe utilizzare l'operatore ==, ma si dovrebbe utilizzare errors.Is():
func As(err error, target any) boolLa funzione errors.As() viene utilizzata per cercare il primo errore di tipo corrispondente nella catena di errori e assegnare il valore al err passato. In alcuni casi, è necessario convertire l'errore di tipo error nel tipo di implementazione dell'errore specifico per ottenere dettagli di errore più dettagliati. L'uso di type assertion su una catena di errori è inefficace, poiché l'errore originale è avvolto da una struttura. Questo è anche il motivo per cui è necessaria la funzione As. Ecco un esempio:
type TimeError struct { // 自定义 error
Msg string
Time time.Time //记录发生错误的时间
}
func (m TimeError) Error() string {
return m.Msg
}
func NewMyError(msg string) error {
return &TimeError{
Msg: msg,
Time: time.Now(),
}
}
func wrap1() error { // 包裹原始错误
return fmt.Errorf("wrapp error %w", wrap2())
}
func wrap2() error { // 原始错误
return NewMyError("original error")
}
func main() {
var myerr *TimeError
err := wrap1()
// 检查错误链中是否有*TimeError 类型的错误
if errors.As(err, &myerr) { // 输出 TimeError 的时间
fmt.Println("original", myerr.Time)
}
}target deve essere un puntatore a error. Poiché quando si crea la struttura si restituisce un puntatore alla struttura, error è effettivamente di tipo *TimeError, quindi target deve essere di tipo **TimeError.
Tuttavia, il pacchetto errors fornito ufficialmente non è effettivamente sufficiente, perché non ha informazioni sullo stack e non può posizionare. Generalmente si consiglia di utilizzare un altro pacchetto di potenziamento ufficiale:
github.com/pkg/errorsEsempio:
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:1650Utilizzando l'output formattato, è possibile vedere le informazioni sullo stack. Per impostazione predefinita, non vengono outputtate. Questo pacchetto è essenzialmente una versione potenziata del pacchetto errors della libreria standard. Entrambi sono scritti ufficialmente. Non si sa perché non sia stato incorporato nella libreria standard.
panic
panic, tradotto in cinese come 恐慌 (panico), indica un problema di programma molto grave. Il programma deve interrompersi immediatamente per gestire il problema, altrimenti il programma si interrompe immediatamente e outputta le informazioni sullo stack. panic è la forma di espressione delle eccezioni runtime in Go. Di solito appare in alcune operazioni pericolose, principalmente per fermare le perdite in tempo, evitando così conseguenze più gravi. Tuttavia, panic farà anche un buon lavoro di post-elaborazione del programma prima di uscire, e panic può anche essere recuperato per garantire la continuazione dell'esecuzione del programma.
Di seguito è riportato un esempio di scrittura di un valore in una map nil, che sicuramente attiverà un panic:
func main() {
var dic map[string]int
dic["a"] = 'a'
}panic: assignment to entry in nil mapTIP
Quando ci sono più goroutine nel programma,只要 una qualsiasi goroutine verifica un panic, se non viene catturato, l'intero programma andrà in crash.
Creazione
Creare esplicitamente un panic è molto semplice. Basta utilizzare la funzione built-in panic. La firma della funzione è la seguente:
func panic(v any)La funzione panic accetta un parametro v di tipo any. Quando si outputtano le informazioni sullo stack di errore, anche v verrà outputtato. Ecco un esempio di utilizzo:
func main() {
initDataBase("", 0)
}
func initDataBase(host string, port int) {
if len(host) == 0 || port == 0 {
panic("Parametri di connessione dati non validi")
}
// ...altra logica
}Quando l'inizializzazione della connessione al database fallisce, il programma non dovrebbe avviarsi, perché senza database il programma non ha senso. Quindi qui dovrebbe essere lanciato un panic:
panic: Parametri di connessione dati non validiPost-Elaborazione
Prima che il programma esca a causa di un panic, eseguirà alcuni lavori di post-elaborazione, come l'esecuzione di istruzioni defer:
func main() {
defer fmt.Println("A")
defer fmt.Println("B")
fmt.Println("C")
panic("panic")
defer fmt.Println("D")
}Output:
C
B
A
panic: panicE le istruzioni defer delle funzioni a monte verranno eseguite allo stesso modo. Ecco un esempio:
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: panicAnche nei defer possono essere annidati panic. Di seguito è riportato un esempio più complesso:
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)
}L'ordine di esecuzione dei panic annidati nei defer è coerente. Quando si verifica un panic, la logica successiva non potrà essere eseguita:
C
2
1
A
panic: panicB
panic: panicAIn sintesi, quando si verifica un panic, la funzione in cui si trova uscirà immediatamente ed eseguirà il lavoro di post-elaborazione della funzione corrente, come defer, quindi salirà livello per livello. Anche le funzioni a monte eseguiranno il lavoro di post-elaborazione fino a quando il programma non smette di funzionare.
Quando una goroutine secondaria verifica un panic, non attiverà il lavoro di post-elaborazione della goroutine corrente. Se il panic non viene ripristinato fino a quando la goroutine secondaria non esce, il programma uscirà direttamente:
var waitGroup sync.WaitGroup
func main() {
demo()
}
func demo() {
waitGroup.Add(1)
defer func() {
fmt.Println("A")
}()
fmt.Println("C")
go dangerOp()
waitGroup.Wait() // 父协程阻塞等待子协程执行完毕
defer fmt.Println("D")
}
func dangerOp() {
defer fmt.Println(1)
defer fmt.Println(2)
panic("panicB")
defer fmt.Println(3)
waitGroup.Done()
}Output:
C
2
1
panic: panicBCome si può vedere, nessuna delle istruzioni defer in demo() è stata eseguita e il programma è uscito direttamente. È importante notare che, senza waitGroup per bloccare la goroutine padre, l'esecuzione di demo() potrebbe essere più veloce dell'esecuzione della goroutine secondaria, e il risultato outputtato sarebbe molto confuso. Modifichiamo leggermente il codice:
func main() {
demo()
}
func demo() {
defer func() {
// 父协程善后工作要花费 20ms
time.Sleep(time.Millisecond * 20)
fmt.Println("A")
}()
fmt.Println("C")
go dangerOp()
defer fmt.Println("D")
}
func dangerOp() {
// 子协程要执行一些逻辑,要花费 1ms
time.Sleep(time.Millisecond)
defer fmt.Println(1)
defer fmt.Println(2)
panic("panicB")
defer fmt.Println(3)
}Output:
C
D
2
1
panic: panicBIn questo esempio, quando la goroutine secondaria verifica un panic, la goroutine padre ha già completato l'esecuzione della funzione ed è entrata nel lavoro di post-elaborazione. Durante l'esecuzione dell'ultimo defer, ha incontrato per caso un panic della goroutine secondaria, quindi il programma è uscito direttamente.
Recupero
Quando si verifica un panic, utilizzando la funzione built-in recover() è possibile gestirlo in tempo e garantire la continuazione dell'esecuzione del programma. Deve essere eseguito in un'istruzione defer. Ecco un esempio di utilizzo:
func main() {
dangerOp()
fmt.Println("Programma uscito normalmente")
}
func dangerOp() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
fmt.Println("panic recuperato")
}
}()
panic("Si è verificato un panic")
}Il chiamante non sa affatto che si è verificato un panic all'interno della funzione dangerOp(). Il programma esegue la logica rimanente ed esce normalmente. Quindi l'output è il seguente:
Si è verificato un panic
panic recuperato
Programma uscito normalmenteMa in realtà, l'uso di recover() ha molte insidie implicite. Ad esempio, utilizzare nuovamente una closure in defer per utilizzare recover:
func main() {
dangerOp()
fmt.Println("Programma uscito normalmente")
}
func dangerOp() {
defer func() {
func() {
if err := recover(); err != nil {
fmt.Println(err)
fmt.Println("panic recuperato")
}
}()
}()
panic("Si è verificato un panic")
}La funzione closure può essere considerata come la chiamata a una funzione. panic viene trasmesso verso l'alto, non verso il basso, quindi naturalmente la funzione closure non può recuperare il panic. Quindi l'output è il seguente:
panic: 发生 panicOltre a questo, c'è anche una situazione molto estrema, ovvero quando il parametro di panic() è nil:
func main() {
dangerOp()
fmt.Println("Programma uscito normalmente")
}
func dangerOp() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
fmt.Println("panic recuperato")
}
}()
panic(nil)
}In questo caso, il panic verrà effettivamente recuperato, ma non verrà outputtato alcun messaggio di errore:
Output:
Programma uscito normalmenteIn sintesi, la funzione recover ha alcuni punti di attenzione:
- Deve essere utilizzata in
defer - Anche se utilizzata più volte, solo una può recuperare il
panic - Una closure
recovernon recupererà alcunpanicnelle funzioni esterne - Il parametro di
panicnon può utilizzarenil
fatal
fatal è un problema estremamente grave. Quando si verifica un fatal, il programma deve interrompersi immediatamente senza eseguire alcun lavoro di post-elaborazione. Di solito si tratta di chiamare la funzione Exit del pacchetto os per uscire dal programma, come mostrato di seguito:
func main() {
dangerOp("")
}
func dangerOp(str string) {
if len(str) == 0 {
fmt.Println("fatal")
os.Exit(1)
}
fmt.Println("Logica normale")
}Output:
fatalI problemi di livello fatal raramente vengono attivati esplicitamente. Nella maggior parte dei casi, vengono attivati passivamente.
