2012-06-10 9 views
8

Diciamo che vogliamo implementare seguente calcolo:funzione di annidamento chiama GO

outval/err = f3(f3(f1(inval))

dove ognuno di f1, f2, f3 può fallire con un errore in quel momento ci fermiamo il calcolo e impostare err all'errore restituito dalla funzione in errore. (Naturalmente, nidificazione può essere arbitrariamente lungo)

in linguaggi come C++/Java/C# può essere facilmente fatto avendo f1, f2 e f3 gettare un'eccezione e che racchiude il calcolo in un blocco try-catch, mentre in lingue come Haskell possiamo invece usare le monadi.

Ora sto cercando di implementarlo in GO e l'unico approccio a cui riesco a pensare è la scala if if else che è piuttosto prolissa. Non ho problemi se non siamo in grado di nidificare le chiamate, ma a mio parere aggiungendo un controllo degli errori dopo ogni riga nel codice sembra brutto e si interrompe il flusso. Mi piacerebbe sapere se c'è un modo migliore per farlo.

Modifica: Modifica secondo il commento di peterSO
Di seguito si riporta l'esempio concreto e l'attuazione immediata

package main 

import "fmt" 

func f1(in int) (out int, err error) { 
    return in + 1, err 
} 

func f2(in int) (out int, err error) { 
    return in + 2, err 
} 

func f3(in int) (out int, err error) { 
    return in + 3, err 
} 

func calc(in int) (out int, err error) { 
    var temp1, temp2 int 
    temp1, err = f1(in) 
    if err != nil { 
     return temp1, err 
    } 
    temp2, err = f2(temp1) 
    if err != nil { 
     return temp2, err 
    } 
    return f3(temp2) 
} 

func main() { 
    inval := 0 
    outval, err := calc3(inval) 
    fmt.Println(inval, outval, err) 
} 

Quello che sto cercando di illustrare è, funzione Calc fa alcune calcolo, eventualmente con l'ausilio di funzioni di libreria che può fallire e la semantica è se qualche chiamata fallisce calc fa propagare l'errore al chiamante (simile a non gestire l'eccezione). Secondo me, il codice per calc è brutto.

Tra per questo caso particolare in cui tutte le funzioni di libreria hanno esattamente stessa firma, siamo in grado di rendere il codice migliore (sto usando idea da http://golang.org/doc/articles/wiki/#tmp_269)

func saferun(f func (int) (int, error)) func (int, error) (int, error) { 
    return func (in int, err error) (int, error) { 
     if err != nil { 
      return in, err 
     } 
     return f(in) 
    } 
} 

Poi possiamo ridefinire calc come

func calc(in int) (out int, err error) { 
    return saferun(f3)(saferun(f2)(f1(in))) 
} 

o come

func calc(in int) (out int, err error) { 
    sf2 := saferun(f2) 
    sf3 := saferun(f3) 
    return sf3(sf2(f1(in))) 
} 

Ma senza generici s Tuttavia, non sono sicuro di come posso utilizzare questo approccio per qualsiasi set di funzioni di libreria.

risposta

5

La discussione tra Errori vs Eccezioni è lunga e noiosa. Perciò non ci entrerò.

La risposta più semplice alla tua domanda riguarda Go built-in defer, panic, e recover funzioni come discusso in this blog posta. Possono offrire un comportamento simile alle eccezioni.

package main 

import "fmt" 

func main() { 
    defer func() { 
     // This recovers from a panic if one occurred. 
     if x := recover(); x != nil { 
      fmt.Printf("%v\n", x) 
     } 
    }() 

    value := f(f(f(1))) 
    fmt.Printf("%d\n", value) 
} 

func f(i int) int { 
    value := i*i + 1 

    // something goes wrong, panic instead of returning an error. 
    panic("ohnoes") 

    return value 
} 
+0

Grazie per la risposta, ma non sono sicuro di aver capito il tuo approccio. Cosa succede se non controllo la firma di f1, f2, f3, ecc. Perché sono funzioni di libreria.A mio modesto parere, l'errore nella gestione delle infrastrutture in altre lingue non richiede che tu abbia un tale controllo, – Suyog

+2

Suyog, citando dalla tua domanda originale, "... fatto avendo f1, f2 e f3 lanciare un'eccezione ...." sembra che tu permetta di modificare f1, f2 e f3 per generare eccezioni. Avere il panico non è diverso. Il panico è il meccanismo disponibile in Vai per separare lo stack da una profondità arbitraria, restituendo un valore nel processo. Non è il modo più idiomatico e preferito per gestire gli errori in Go, ma è il meccanismo che farà ciò che chiedi. – Sonia

+0

Intendevo che f1, f2, f3 sono già definiti per generare un'eccezione. Scusa per una formulazione errata. In linguaggio come JAVA, molte funzioni di libreria sono definite per generare un'eccezione, mentre in GO sembra che lo schema sia che le funzioni di libreria restituiscano un errore. Quindi il problema che sto affrontando è che devo verificare immediatamente gli errori che si traducono nella scrittura di codice ripetitivo. – Suyog

0

Senza un esempio concreto, si sono mulini a vento. Ad esempio, per la tua definizione, le funzioni fn restituiscono un valore e qualsiasi errore. Le funzioni fn sono funzioni del pacchetto la cui firma non può essere modificata. Usando il tuo esempio,

package main 

import "fmt" 

func f1(in int) (out int, err error) { 
    return in + 1, err 
} 

func f2(in int) (out int, err error) { 
    return in + 2, err 
} 

func f3(in int) (out int, err error) { 
    return in + 3, err 
} 

func main() { 
    inval := 0 
    outval, err := f3(f2(f1(inval))) 
    fmt.Println(inval, outval, err) 
} 

Come hai intenzione di ottenere il tuo esempio per compilare ed eseguire?

+0

Grazie per la risposta, aggiorno la domanda come da suggerimenti – Suyog

7

Se si desidera poter eseguire questa operazione, è possibile utilizzare una funzione di composizione.

func compose(fs ...func(Value) (OutVal, error)) func(Value) (OutVal, error) { 
    return func(val Value) OutVal, Error { 
    sVal := val 
    var err error 
    for _, f := range fs { 
     sval, err = f(val) 
     if err != nil { 
     // bail here and return the val 
     return nil, err 
     } 
    } 
    return sval, nil 
    } 
} 

outVal, err := compose(f1, f2)(inVal) 

maggior parte del tempo anche se probabilmente si vuole essere più semplice di questo in quanto può essere difficile per gli altri a capire il codice quando incontrano esso.

+0

Ciò è stato utile, grazie! – Suyog

7

In primo luogo, una versione estesa dello stile try-catch a cui sei abituato, prendendo in prestito ovviamente la risposta di jimt e la risposta di PeterSO.

package main 

import "fmt" 

// Some dummy library functions with different signatures. 
// Per idiomatic Go, they return error values if they have a problem. 
func f1(in string) (out int, err error) { 
    return len(in), err 
} 

func f2(in int) (out int, err error) { 
    return in + 1, err 
} 

func f3(in int) (out float64, err error) { 
    return float64(in) + .5, err 
} 

func main() { 
    inval := "one" 

    // calc3 three is the function you want to call that does a computation 
    // involving f1, f2, and f3 and returns any error that crops up. 
    outval, err := calc3(inval) 

    fmt.Println("inval: ", inval) 
    fmt.Println("outval:", outval) 
    fmt.Println("err: ", err) 
} 

func calc3(in string) (out float64, err error) { 
    // Ignore the following big comment and the deferred function for a moment, 
    // skip to the comment on the return statement, the last line of calc3... 
    defer func() { 
     // After viewing what the fXp function do, this function can make 
     // sense. As a deferred function it runs whenever calc3 returns-- 
     // whether a panic has happened or not. 
     // 
     // It first calls recover. If no panic has happened, recover returns 
     // nil and calc3 is allowed to return normally. 
     // 
     // Otherwise it does a type assertion (the value.(type) syntax) 
     // to make sure that x is of type error and to get the actual error 
     // value. 
     // 
     // It does a tricky thing then. The deferred function, being a 
     // function literal, is a closure. Specifically, it has access to 
     // calc3's return value "err" and can force calc3 to return an error. 
     // A line simply saying "err = xErr" would be enough, but we can 
     // do better by annotating the error (something specific from f1, 
     // f2, or f3) with the context in which it occurred (calc3). 
     // It allows calc3 to return then, with this descriptive error. 
     // 
     // If x is somehow non-nil and yet not an error value that we are 
     // expecting, we re-panic with this value, effectively passing it on 
     // to allow a higer level function to catch it. 
     if x := recover(); x != nil { 
      if xErr, ok := x.(error); ok { 
       err = fmt.Errorf("calc3 error: %v", xErr) 
       return 
      } 
      panic(x) 
     } 
    }() 
    // ... this is the way you want to write your code, without "breaking 
    // the flow." 
    return f3p(f2p(f1p(in))), nil 
} 

// So, notice that we wrote the computation in calc3 not with the original 
// fX functions, but with fXp functions. These are wrappers that catch 
// any error and panic, removing the error from the function signature. 
// Yes, you must write a wrapper for each library function you want to call. 
// It's pretty easy though: 
func f1p(in string) int { 
    v, err := f1(in) 
    if err != nil { 
     panic(err) 
    } 
    return v 
} 

func f2p(in int) int { 
    v, err := f2(in) 
    if err != nil { 
     panic(err) 
    } 
    return v 
} 

func f3p(in int) float64 { 
    v, err := f3(in) 
    if err != nil { 
     panic(err) 
    } 
    return v 
} 
// Now that you've seen the wrappers that panic rather than returning errors, 
// go back and look at the big comment in the deferred function in calc3. 

Quindi, potresti protestare che hai chiesto più facile e questo non lo è. Nessun argomento nel suo insieme, ma se la libreria funziona tutti restituiscono valori di errore e si desidera concatenare le chiamate di funzione senza i valori di errore, la soluzione disponibile è di racchiudere le funzioni della libreria ei wrapper sono molto sottili e facili da scrivere. L'unica altra parte difficile è quella della funzione differita, ma è uno schema che puoi imparare e riutilizzare e sono solo poche righe di codice.

Non voglio promuovere troppo questa soluzione perché non è quella che viene usata spesso. È comunque un modello valido e ha alcuni casi d'uso in cui è appropriato.

La gestione degli errori è un argomento importante, come menzionato da jimt. "Quali sono i modi migliori per gestire gli errori in Go?" sarebbe una buona domanda per SO, tranne che per il problema che fallisce la critica di "tutto il libro". I può immaginare un intero libro sul tema della gestione degli errori in Go.

Invece, offrirò la mia osservazione generale che se inizi a lavorare con i valori di errore invece di cercare di farli sparire, dopo un po 'inizi a capire i vantaggi di farlo. Ciò che appare come una scala prolissa di istruzioni if ​​in un esempio di gioco come quello che abbiamo usato qui potrebbe sembrare ancora una scala prolissa di istruzioni if ​​se la si scrive per la prima volta in un programma del mondo reale. Quando in realtà hai bisogno di gestire quegli errori, però, ritorni al codice e all'improvviso lo vedi come stub che ti aspettano tutti a riempire di codice di gestione degli errori. Puoi vedere solo cosa fare perché il codice che ha causato l'errore è proprio lì. Puoi mostrare a un utente di non vedere un oscuro messaggio di errore di basso livello e mostrare invece qualcosa di significativo. Tu, come programmatore, ti viene chiesto di fare la cosa giusta invece di accettare una cosa predefinita.

Per risposte più complete, una buona risorsa per iniziare è l'articolo Error Handling and Go. Se cerchi attraverso lo Go-Nuts messages ci sono anche lunghe discussioni sull'argomento. Le funzioni nella libreria standard si richiamano un po ', (sorpresa) e quindi il codice sorgente della libreria standard contiene molti esempi di errori di gestione. Questi sono esempi eccellenti da studiare poiché il codice è scritto dagli autori di Go che promuovono questo stile di programmazione di lavorare con i valori di errore.

+0

Grazie mille per un'ampia risposta! – Suyog

0

Trovato mailing thread su go-nuts per questo argomento. Aggiungendolo per riferimento

0

Troppo male questo è chiuso già ... Questo:

value := f(f(f(1))) 

non è un esempio di concatenamento ma di nesting. Concatenamento dovrebbe essere simile:

c.funA().funB().funC() 

Ecco una example lavoro.

+0

Punto giusto; questa non è una risposta, ma un commento, però – leonbloy

Problemi correlati