2012-06-10 15 views
8

Digamos que queremos implementar cálculo siguiente:Función de inserción llama en GO

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

donde cada uno de f1, f2, f3 puede fallar con un error en ese momento nos detenemos el cálculo y ajuste err al error devuelto por la función de falla. (Por supuesto, la anidación puede ser arbitrariamente larga)

En lenguajes como C++/Java/C# que se puede hacer fácilmente por tener f1, f2 y f3 lanzar una excepción y que encierra el cálculo en un bloque try-catch, mientras que en idiomas como Haskell podemos usar mónadas en su lugar.

Ahora estoy tratando de implementarlo en GO y el único enfoque que puedo pensar es obvio if-else ladder que es bastante detallado. No tengo problemas si no podemos anidar las llamadas, pero en mi opinión agregar una verificación de error después de cada línea en el código se ve feo y rompe el flujo. Me gustaría saber si hay alguna forma mejor de hacerlo.

edición: Edición de acuerdo con el comentario de peterSO
A continuación se muestra el ejemplo de aplicación concreta y directa

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

Lo que estoy tratando de ilustrar es, la función Calc hace algún cálculo, posiblemente con la ayuda de las funciones de biblioteca que puede fallar y la semántica es que si falla una llamada, calc propaga el error a la persona que llama (similar a no manejar la excepción). En mi opinión, el código para calc es feo.

Entre para este caso particular en que todas las funciones de la biblioteca tienen exactamente misma firma, podemos hacer que el código mejor (estoy usando la idea de 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) 
    } 
} 

entonces podemos redefinir Calc como

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

o como

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

Pero sin genéricos s upport, no estoy seguro de cómo puedo utilizar este enfoque para cualquier conjunto de funciones de la biblioteca.

Respuesta

5

La discusión entre Errores vs Excepciones es larga y tediosa. Por lo tanto, no entraré en eso.

La respuesta más simple a su pregunta se refiere a Go incorporada defer, panic y recover funciones como se discute en this blog puesto. Pueden ofrecer un comportamiento similar a las excepciones.

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

Gracias por responder, pero no estoy seguro de entender su enfoque. ¿Qué ocurre si no controlo la firma de f1, f2, f3, etc., porque son funciones de la biblioteca?En mi opinión modesta, el manejo incorrecto de las infraestructuras en otros idiomas no requiere que tenga dicho control – Suyog

+2

Suyog, citando su pregunta original, "... hecho al tener f1, f2 y f3 arrojar una excepción ..." Seguro suena como si permitieras cambiar f1, f2 y f3 para lanzar excepciones. Tenerlos en pánico no es diferente. El pánico es el mecanismo disponible en Ir para desenrollar la pila de una profundidad arbitraria, devolviendo un valor en el proceso. No es la forma idiomática y preferida de manejar los errores en Go, pero es el mecanismo que hará lo que usted pida. – Sonia

+0

Quise decir que f1, f2, f3 ya están definidos para lanzar una excepción. Perdón por malas palabras. En lenguaje como JAVA, muchas funciones de la biblioteca están definidas para arrojar una excepción, mientras que en el patrón GO parece que las funciones de la biblioteca devuelven un error. Por lo tanto, el problema al que me enfrento es que debo verificar los errores de inmediato, lo que da como resultado la escritura de código repetitivo. – Suyog

0

Sin un ejemplo concreto, se está inclinando en los molinos de viento. Por ejemplo, según su definición, las funciones fn devuelven un valor y cualquier error. Las funciones fn son funciones de paquete cuya firma no se puede cambiar. Usando su ejemplo,

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

¿Cómo va a obtener su ejemplo para compilar y ejecutar?

+0

Gracias por responder, actualizo la pregunta según las sugerencias – Suyog

7

Si realmente desea poder hacer esto, podría usar una función de composición.

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) 

La mayor parte del tiempo, aunque es probable que desee ser más sencillo que esto, ya que puede ser difícil de entender para los demás su código cuando se encuentran con él.

+0

¡Eso fue útil, gracias! – Suyog

7

En primer lugar, una versión ampliada del estilo try-catch al que está acostumbrado, tomando prestado obviamente de la respuesta de jimt y la respuesta de 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. 

Por lo tanto, podría protestar que haya pedido más fácil y esto no es así. No hay argumento en general, pero si la biblioteca funciona con todos los valores de retorno de error y desea encadenar llamadas de función sin los valores de error, la solución disponible es ajustar las funciones de la biblioteca, y los envoltorios son muy delgados y fáciles de escribir. La única otra parte difícil es la función diferida, pero es un patrón que puedes aprender y reutilizar y solo unas pocas líneas de código.

No quiero promocionar demasiado esta solución porque no se usa con frecuencia. Sin embargo, es un patrón válido y tiene algunos casos de uso donde es apropiado.

El manejo de errores es un gran tema, como se mencionó anteriormente. "¿Cuáles son buenas maneras de manejar errores en Go?" sería una buena pregunta para SO, excepto por el problema de que falla el critereón de "todo el libro". I puede imaginar un libro completo sobre el tema de manejo de errores en Go.

En su lugar, le ofreceré mi observación general de que si acaba de empezar a trabajar con valores de error en lugar de tratar de hacerlos desaparecer, después de un tiempo comienza a entender las ventajas de hacerlo. Lo que parece una escalera detallada de sentencias if en un ejemplo de juguete como el que usamos aquí podría parecer una escalera detallada de sentencias if cuando la escribe por primera vez en un programa del mundo real. Sin embargo, cuando realmente necesita manejar esos errores, vuelve al código y de repente lo ve como un talón esperando que se desarrolle con un verdadero código de manejo de errores. Puede ver exactamente qué hacer porque el código que causó el error está ahí. Puede evitar que un usuario vea un oscuro mensaje de error de bajo nivel y, en su lugar, muestre algo significativo. A usted, como programador, se le pide que haga lo correcto en lugar de aceptar una cosa predeterminada.

Para respuestas más completas, un buen recurso para comenzar es el artículo Error Handling and Go. Si busca a través del Go-Nuts messages, también hay largas discusiones sobre el asunto. Las funciones en la biblioteca estándar se llaman entre sí bastante (sorpresa) y, por lo tanto, el código fuente de la biblioteca estándar contiene muchos ejemplos de errores de manejo. Estos son excelentes ejemplos para estudiar dado que el código está escrito por los autores de Go que están promoviendo este estilo de programación de trabajar con valores de error.

+0

¡Muchas gracias por la extensa respuesta! – Suyog

0

Se ha encontrado el correo thread en go-nuts para este tema. Agregándolo como referencia.

0

Lástima que éste está ya cerrada ... Este:

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

no es un ejemplo de encadenamiento sino de anidación. Encadenando debería ser algo como:

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

Aquí está una example de trabajo.

+0

Punto razonable; esto no es una respuesta sino un comentario, aunque – leonbloy