Go: propuesta: Ir 2: simplificar el manejo de errores con || err sufijo

Creado en 25 jul. 2017  ·  519Comentarios  ·  Fuente: golang/go

Ha habido muchas propuestas sobre cómo simplificar el manejo de errores en Go, todas basadas en la queja general de que demasiado código Go contiene las líneas

if err != nil {
    return err
}

No estoy seguro de que haya un problema aquí que deba resolverse, pero como sigue apareciendo, voy a presentar esta idea.

Uno de los problemas centrales con la mayoría de las sugerencias para simplificar el manejo de errores es que solo simplifican dos formas de manejar errores, pero en realidad hay tres:

  1. ignora el error
  2. devolver el error sin modificar
  3. devolver el error con información contextual adicional

Ya es fácil (quizás demasiado fácil) ignorar el error (ver # 20803). Muchas propuestas existentes para el manejo de errores facilitan la devolución del error sin modificar (por ejemplo, # 16225, # 18721, # 21146, # 21155). Pocos facilitan la devolución del error con información adicional.

Esta propuesta se basa libremente en los lenguajes shell Perl y Bourne, fuentes fértiles de ideas lingüísticas. Introducimos un nuevo tipo de declaración, similar a una declaración de expresión: una expresión de llamada seguida de || . La gramática es:

PrimaryExpr Arguments "||" Expression

De manera similar, presentamos un nuevo tipo de declaración de asignación:

ExpressionList assign_op PrimaryExpr Arguments "||" Expression

Aunque la gramática acepta cualquier tipo después de || en el caso de no asignación, el único tipo permitido es el tipo predeclarado error . La expresión que sigue a || debe tener un tipo asignable a error . Puede que no sea un tipo booleano, ni siquiera un tipo booleano con nombre asignable a error . (Esta última restricción es necesaria para que esta propuesta sea compatible con versiones anteriores del lenguaje existente).

Estos nuevos tipos de declaración solo se permiten en el cuerpo de una función que tiene al menos un parámetro de resultado, y el tipo del último parámetro de resultado debe ser del tipo predeclarado error . De manera similar, la función que se llama debe tener al menos un parámetro de resultado, y el tipo del último parámetro de resultado debe ser el tipo predeclarado error .

Al ejecutar estas declaraciones, la expresión de llamada se evalúa como de costumbre. Si se trata de una instrucción de asignación, los resultados de la llamada se asignan a los operandos del lado izquierdo como de costumbre. Luego, el resultado de la última llamada, que como se describe arriba debe ser del tipo error , se compara con nil . Si el resultado de la última llamada no es nil , se ejecuta implícitamente una declaración de devolución. Si la función de llamada tiene varios resultados, se devuelve el valor cero para todos los resultados excepto el último. La expresión que sigue a || se devuelve como último resultado. Como se describió anteriormente, el último resultado de la función de llamada debe tener el tipo error , y la expresión debe ser asignable al tipo error .

En el caso de no asignación, la expresión se evalúa en un ámbito en el que se introduce una nueva variable err y se le asigna el valor del último resultado de la llamada a la función. Esto permite que la expresión se refiera fácilmente al error devuelto por la llamada. En el caso de asignación, la expresión se evalúa en el ámbito de los resultados de la llamada y, por lo tanto, puede referirse directamente al error.

Esa es la propuesta completa.

Por ejemplo, la función os.Chdir está actualmente

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

Según esta propuesta, podría redactarse como

func Chdir(dir string) error {
    syscall.Chdir(dir) || &PathError{"chdir", dir, err}
    return nil
}

Estoy escribiendo esta propuesta principalmente para alentar a las personas que desean simplificar el manejo de errores de Go a pensar en formas de facilitar el contexto de los errores, no solo para devolver el error sin modificar.

FrozenDueToAge Go2 LanguageChange NeedsInvestigation Proposal error-handling

Comentario más útil

Una idea sencilla, con soporte para decoración de errores, pero que requiere un cambio de idioma más drástico (obviamente no para go1.10) es la introducción de una nueva palabra clave check .

Tendría dos formas: check A y check A, B .

Tanto A como B deben ser error . La segunda forma solo se usaría al decorar con errores; las personas que no necesiten o deseen decorar sus errores utilizarán la forma más sencilla.

1er formulario (marque A)

check A evalúa A . Si nil , no hace nada. Si no es nil , check actúa como un return {<zero>}*, A .

Ejemplos de

  • Si una función solo devuelve un error, se puede usar en línea con check , por lo que
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

se convierte en

check UpdateDB()
  • Para una función con múltiples valores de retorno, deberá asignar, como lo hacemos ahora.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

se convierte en

a, b, err := Foo()
check err

// use a and b

2da forma (marque A, B)

check A, B evalúa A . Si nil , no hace nada. Si no es nil , check actúa como un return {<zero>}*, B .

Esto es para necesidades de decoración de errores. Seguimos comprobando A , pero es B que se usa en el return implícito.

Ejemplo

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

se convierte en

a, err := Foo()
check err, &BarError{"Bar", err}

Notas

Es un error de compilación

  • use la instrucción check en cosas que no se evalúen como error
  • use check en una función con valores de retorno que no estén en la forma { type }*, error

La forma dos-expr check A, B está en cortocircuito. B no se evalúa si A es nil .

Notas sobre practicidad

Hay soporte para decorar errores, pero pagas por la sintaxis más torpe de check A, B solo cuando realmente necesitas decorar errores.

Para el texto estándar de if err != nil { return nil, nil, err } (que es muy común) check err es tan breve como podría ser sin sacrificar la claridad (vea la nota sobre la sintaxis a continuación).

Notas sobre la sintaxis

Yo diría que este tipo de sintaxis ( check .. , al principio de la línea, similar a return ) es una buena manera de eliminar el código estándar de verificación de errores sin ocultar la interrupción del flujo de control que los retornos implícitos introducen.

Una desventaja de ideas como <do-stuff> || <handle-err> y <do-stuff> catch <handle-err> anteriores, o las a, b = foo()? propuestas en otro hilo, es que ocultan la modificación del flujo de control de una manera que dificulta el flujo. seguir; el primero con || <handle-err> maquinaria agregada al final de una línea de apariencia simple, el último con un pequeño símbolo que puede aparecer en todas partes, incluso en el medio y al final de una línea de código simple, posiblemente varias veces.

Una sentencia check siempre será de nivel superior en el bloque actual, teniendo la misma prominencia de otras sentencias que modifican el flujo de control (por ejemplo, una return anticipada).

Todos 519 comentarios

    syscall.Chdir(dir) || &PathError{"chdir", dir, e}

¿De dónde viene e de allí? ¿Error de tipografía?

O quiso decir:

func Chdir(dir string) (e error) {
    syscall.Chdir(dir) || &PathError{"chdir", dir, e}
    return nil
}

(Es decir, ¿la comprobación implícita err! = Nil primero asigna un error al parámetro de resultado, que se puede nombrar para modificarlo nuevamente antes del retorno implícito?)

Suspiro, arruiné mi propio ejemplo. Ahora arreglado: el e debería ser err . La propuesta coloca err en el alcance para contener el valor de error de la llamada a la función cuando no está en una declaración de asignación.

Si bien no estoy seguro de estar de acuerdo con la idea o la sintaxis, debo darle crédito por prestar atención a agregar contexto a los errores antes de devolverlos.

Esto podría ser de interés para @davecheney , quien escribió https://github.com/pkg/errors.

Qué sucede en este código:

if foo, err := thing.Nope() || &PathError{"chdir", dir, err}; err == nil || ignoreError {
}

(Mis disculpas si esto ni siquiera es posible sin la parte || &PathError{"chdir", dir, e} ; estoy tratando de expresar que esto se siente como una anulación confusa del comportamiento existente, y los retornos implícitos son ... ¿furtivos?)

@ object88 Estaría bien si no permitiera este nuevo caso en un SimpleStmt como se usa en las declaraciones if y for y switch . Probablemente sería mejor, aunque complicaría un poco la gramática.

Pero si no hacemos eso, entonces lo que sucede es que si thing.Nope() devuelve un error no nulo, entonces la función que llama regresa con &PathError{"chdir", dir, err} (donde err es el variable establecida por la llamada a thing.Nope() ). Si thing.Nope() devuelve un nil error, entonces sabemos con certeza que err == nil es verdadero en la condición de la declaración if , y por lo tanto el cuerpo de la si se ejecuta la sentencia. La variable ignoreError nunca se lee. No hay ambigüedad o anulación del comportamiento existente aquí; el manejo de || introducido aquí solo se acepta cuando la expresión después de || no es un valor booleano, lo que significa que no se compilaría actualmente.

Estoy de acuerdo en que los retornos implícitos son engañosos.

Sí, mi ejemplo es bastante pobre. Pero no permitir la operación dentro de un if , for o switch resolvería mucha confusión potencial.

Dado que la barra de examen es generalmente algo difícil de hacer en el idioma tal como está, decidí ver qué tan difícil era codificar esta variante en el idioma. No mucho más difícil que los demás: https://play.golang.org/p/9B3Sr7kj39

Realmente no me gustan todas estas propuestas para hacer que un tipo de valor y una posición en los argumentos de retorno sean especiales. Este es de alguna manera peor porque también hace que err un nombre especial en este contexto específico.

Aunque ciertamente estoy de acuerdo en que la gente (¡incluyéndome a mí!) Debería estar más cansada de devolver errores sin contexto adicional.

Cuando hay otros valores de retorno, como

if err != nil {
  return 0, nil, "", Struct{}, wrap(err)
}

Definitivamente puede volverse tedioso de leer. Me gustó un poco la sugerencia de return ..., err en https://github.com/golang/go/issues/19642#issuecomment -288559297

Si entiendo correctamente, para construir el árbol de sintaxis, el analizador necesitaría saber los tipos de variables para distinguir entre

boolean := BoolFunc() || BoolExpr

y

err := FuncReturningError() || Expr

No luce bien.

menos es más...

Cuando la ExpressionList de retorno contiene dos o más elementos, ¿cómo funciona?

Por cierto, quiero panicIf en su lugar.

err := doSomeThing()
panicIf(err)

err = doAnotherThing()
panicIf(err)

@ianlancetaylor En el ejemplo de su propuesta, err todavía no se declara explícitamente y se incluye como 'mágico' (lenguaje predefinido), ¿verdad?

¿O será algo como

func Chdir(dir string) error {
    return (err := syscall.Chdir(dir)) || &PathError{"chdir", dir, err}
}

?

Por otro lado (dado que ya está marcado como un "cambio de idioma" ...)
Introduzca un nuevo operador (!! o ??) que haga el atajo en caso de error! = Nil (¿o cualquier nullable?)

func DirCh(dir string) (string, error) {
    return dir, (err := syscall.Chdir(dir)) !! &PathError{"chdir", dir, err}
}

Lo siento si esto está demasiado lejos :)

Estoy de acuerdo en que el manejo de errores en Go puede ser repetitivo. No me importa la repetición, pero muchos de ellos afectan la legibilidad. Hay una razón por la cual la "Complejidad Ciclomática" (ya sea que usted crea en ella o no) usa los flujos de control como medida de complejidad. La declaración "si" agrega ruido adicional.

Sin embargo, la sintaxis propuesta "||" no es muy intuitivo de leer, especialmente porque el símbolo se conoce comúnmente como operador OR. Además, ¿cómo se manejan las funciones que devuelven múltiples valores y errores?

Solo estoy lanzando algunas ideas aquí. ¿Qué tal en lugar de usar el error como salida, usamos el error como entrada? Ejemplo: https://play.golang.org/p/rtfoCIMGAb

Gracias por todos los comentarios.

@opennota Buen punto. Todavía podría funcionar, pero estoy de acuerdo en que ese aspecto es incómodo.

@mattn No creo que haya una ExpressionList de retorno, así que no estoy seguro de lo que está preguntando. Si la función de llamada tiene varios resultados, todos menos el último se devuelven como el valor cero del tipo.

@mattn panicif no aborda uno de los elementos clave de esta propuesta, que es una manera fácil de devolver un error con contexto adicional. Y, por supuesto, uno puede escribir panicif hoy con bastante facilidad.

@tandr Sí, err se define mágicamente, lo cual es bastante horrible. Otra posibilidad sería permitir que la expresión-error use error para referirse al error, que es terrible de otra manera.

@tandr Podríamos usar un operador diferente, pero no veo ninguna gran ventaja. No parece hacer que el resultado sea más legible.

@henryas Creo que la propuesta explica cómo maneja múltiples resultados.

@henryas Gracias por el ejemplo. Lo que no me gusta de ese tipo de enfoque es que hace que el manejo de errores sea el aspecto más destacado del código. Quiero que el manejo de errores esté presente y visible, pero no quiero que sea lo primero en la línea. Eso es cierto hoy, con el modismo if err != nil y la sangría del código de manejo de errores, y debería permanecer así si se agregan nuevas características para el manejo de errores.

Gracias de nuevo.

@ianlancetaylor No sé si

Reproduciré una versión algo simplificada aquí:

func panicIf(err error, transforms ...func(error) error) {
  if err == nil {
    return
  }
  for _, transform := range transforms {
    err = transform(err)
  }
  panic(err)
}

Casualmente, acabo de dar una charla relámpago en GopherCon donde usé (pero no

func DirCh(dir string) (string, error) {
    dir := syscall.Chdir(dir)        =: err; if err != nil { return "", err }
}

donde =: es el nuevo bit de sintaxis, un espejo de := que asigna en la otra dirección. Obviamente, también necesitaríamos algo por = , lo cual es ciertamente problemático. Pero la idea general es facilitar al lector la comprensión del camino feliz, sin perder información.

Por otro lado, la forma actual de manejo de errores tiene algunos méritos, ya que sirve como un recordatorio evidente de que puede estar haciendo demasiadas cosas en una sola función y que algunas refactorizaciones pueden estar atrasadas.

Me gusta mucho la sintaxis propuesta por @billyh aquí

func Chdir(dir string) error {
    e := syscall.Chdir(dir) catch: &PathError{"chdir", dir, e}
    return nil
}

o un ejemplo más complejo usando https://github.com/pkg/errors

func parse(input io.Reader) (*point, error) {
    var p point

    err := read(&p.Longitude) catch: nil, errors.Wrap(err, "Failed to read longitude")
    err = read(&p.Latitude) catch: nil, errors.Wrap(err, "Failed to read Latitude")
    err = read(&p.Distance) catch: nil, errors.Wrap(err, "Failed to read Distance")
    err = read(&p.ElevationGain) catch: nil, errors.Wrap(err, "Failed to read ElevationGain")
    err = read(&p.ElevationLoss) catch: nil, errors.Wrap(err, "Failed to read ElevationLoss")

    return &p, nil
}

Una idea sencilla, con soporte para decoración de errores, pero que requiere un cambio de idioma más drástico (obviamente no para go1.10) es la introducción de una nueva palabra clave check .

Tendría dos formas: check A y check A, B .

Tanto A como B deben ser error . La segunda forma solo se usaría al decorar con errores; las personas que no necesiten o deseen decorar sus errores utilizarán la forma más sencilla.

1er formulario (marque A)

check A evalúa A . Si nil , no hace nada. Si no es nil , check actúa como un return {<zero>}*, A .

Ejemplos de

  • Si una función solo devuelve un error, se puede usar en línea con check , por lo que
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

se convierte en

check UpdateDB()
  • Para una función con múltiples valores de retorno, deberá asignar, como lo hacemos ahora.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

se convierte en

a, b, err := Foo()
check err

// use a and b

2da forma (marque A, B)

check A, B evalúa A . Si nil , no hace nada. Si no es nil , check actúa como un return {<zero>}*, B .

Esto es para necesidades de decoración de errores. Seguimos comprobando A , pero es B que se usa en el return implícito.

Ejemplo

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

se convierte en

a, err := Foo()
check err, &BarError{"Bar", err}

Notas

Es un error de compilación

  • use la instrucción check en cosas que no se evalúen como error
  • use check en una función con valores de retorno que no estén en la forma { type }*, error

La forma dos-expr check A, B está en cortocircuito. B no se evalúa si A es nil .

Notas sobre practicidad

Hay soporte para decorar errores, pero pagas por la sintaxis más torpe de check A, B solo cuando realmente necesitas decorar errores.

Para el texto estándar de if err != nil { return nil, nil, err } (que es muy común) check err es tan breve como podría ser sin sacrificar la claridad (vea la nota sobre la sintaxis a continuación).

Notas sobre la sintaxis

Yo diría que este tipo de sintaxis ( check .. , al principio de la línea, similar a return ) es una buena manera de eliminar el código estándar de verificación de errores sin ocultar la interrupción del flujo de control que los retornos implícitos introducen.

Una desventaja de ideas como <do-stuff> || <handle-err> y <do-stuff> catch <handle-err> anteriores, o las a, b = foo()? propuestas en otro hilo, es que ocultan la modificación del flujo de control de una manera que dificulta el flujo. seguir; el primero con || <handle-err> maquinaria agregada al final de una línea de apariencia simple, el último con un pequeño símbolo que puede aparecer en todas partes, incluso en el medio y al final de una línea de código simple, posiblemente varias veces.

Una sentencia check siempre será de nivel superior en el bloque actual, teniendo la misma prominencia de otras sentencias que modifican el flujo de control (por ejemplo, una return anticipada).

@ALTree , no entendí cómo tu ejemplo:

a, b, err := Foo()
check err

Logra los tres valiosos rendimientos del original:

return "", "", err

¿Solo devuelve valores cero para todas las devoluciones declaradas, excepto el error final? ¿Qué pasa con los casos en los que le gustaría devolver un valor válido junto con un error, por ejemplo, el número de bytes escritos cuando un Write () falla?

Cualquiera que sea la solución con la que vayamos, debería restringir mínimamente la generalidad del manejo de errores.

Con respecto al valor de tener check al comienzo de la línea, mi preferencia personal es ver el flujo de control primario al comienzo de cada línea y que el manejo de errores interfiera con la legibilidad de ese flujo de control primario tan poco. como sea posible. Además, si el manejo de errores se distingue por una palabra reservada como check o catch , entonces prácticamente cualquier editor moderno resaltará la sintaxis de la palabra reservada de alguna manera y la hará perceptible incluso si está en el lado derecho.

@billyh esto se explica arriba, en la línea que dice:

Si no es nulo, el cheque actúa como return {<zero>}*, A

check devolverá el valor cero de cualquier valor devuelto, excepto el error (en la última posición).

¿Qué pasa con los casos en los que le gustaría devolver un valor válido junto con un error?

Entonces usarás el modismo if err != nil { .

Hay muchos casos en los que necesitará un procedimiento de recuperación de errores más sofisticado. Por ejemplo, puede que necesite, después de detectar un error, revertir algo o escribir algo en un archivo de registro. En todos estos casos, todavía tendrá el idioma habitual if err en su caja de herramientas, y puede usarlo para iniciar un nuevo bloque, donde cualquier tipo de operación relacionada con el manejo de errores, sin importar cuán articulada sea, puede llevarse a cabo.

Cualquiera que sea la solución con la que vayamos, debería restringir mínimamente la generalidad del manejo de errores.

Vea mi respuesta arriba. Todavía tendrá if y cualquier otra cosa que le proporcione el idioma ahora.

Prácticamente cualquier editor moderno destacará la palabra reservada

Quizás. Pero introducir una sintaxis opaca, que requiere resaltar la sintaxis para ser legible, no es ideal.

Este error en particular se puede solucionar introduciendo una función de retorno doble al idioma.
en este caso, la función a () devuelve 123:

func a () int {
B()
volver 456
}
func b () {
retorno retorno int (123)
}

Esta función se puede utilizar para simplificar el manejo de errores de la siguiente manera:

func handle (var * foo, err error) (var * foo, err error) {
if err! = nil {
return return nil, err
}
return var, nil
}

func client_code () (* client_object, error) {
var obj, err = handle (algo_que_puede_fallar ())
// esto solo se alcanza si algo no falla
// de lo contrario, la función client_code propagaría el error por la pila
afirmar (err == nil)
}

Esto permite a las personas escribir funciones de manejo de errores que pueden propagar los errores por la pila.
tales funciones de manejo de errores pueden estar separadas del código principal

Lo siento si me equivoqué, pero quiero aclarar un punto, la función a continuación producirá un error, vet advertencia o ¿será aceptada?

func Chdir(dir string) (err error) {
    syscall.Chdir(dir) || err
    return nil
}

@rodcorsi Bajo esta propuesta, su ejemplo sería aceptado sin advertencia veterinaria. Sería equivalente a

if err := syscall.Chdir(dir); err != nil {
    return err
}

¿Qué tal expandir el uso de Context para manejar errores? Por ejemplo, dada la siguiente definición:
type ErrorContext interface { HasError() bool SetError(msg string) Error() string }
Ahora en la función propensa a errores ...
func MyFunction(number int, ctx ErrorContext) int { if ctx.HasError() { return 0 } return number + 1 }
En la función intermedia ...
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) return number }
Y en la función de nivel superior
func main() { ctx := context.New() no := MyIntermediateFunction(ctx) if ctx.HasError() { log.Fatalf("Error: %s", ctx.Error()) return } fmt.Printf("%d\n", no) }
Hay varios beneficios al utilizar este enfoque. Primero, no distrae al lector de la ruta de ejecución principal. Hay declaraciones "if" mínimas para indicar una desviación de la ruta de ejecución principal.

En segundo lugar, no oculta el error. De la firma del método se desprende claramente que si acepta ErrorContext, entonces la función puede tener errores. Dentro de la función, usa las declaraciones de bifurcación normales (por ejemplo, "si") que muestra cómo se maneja el error usando el código Go normal.

En tercer lugar, el error se envía automáticamente a la parte interesada, que en este caso es el propietario del contexto. En caso de que haya un procesamiento de error adicional, se mostrará claramente. Por ejemplo, hagamos algunos cambios en la función intermedia para envolver cualquier error existente:
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) if ctx.HasError() { ctx.SetError(fmt.Sprintf("wrap msg: %s", ctx.Error()) return } number *= 20 number = MyFunction(number, ctx) return number }
Básicamente, solo escribe el código de manejo de errores según sea necesario. No es necesario que los burbujee manualmente.

Por último, usted, como escritor de la función, puede decidir si se debe manejar el error. Usando el enfoque actual de Go, es fácil hacer esto ...
`` ``
// dada la siguiente definición
error de func MyFunction (número int)

// entonces haz esto
MyFunction (8) // sin comprobar el error
With the ErrorContext, you as the function owner can make the error checking optional with this:
func MyFunction (ctx ErrorContext) {
if ctx! = nil && ctx.HasError () {
regreso
}
// ...
}
Or make it compulsory with this:
func MyFunction (ctx ErrorContext) {
if ctx.HasError () {// entrará en pánico si ctx es nulo
regreso
}
// ...
}
If you make error handling compulsory and yet the user insists on ignoring error, they can still do that. However, they have to be very explicit about it (to prevent accidental ignore). For instance:
func UpperFunction (ctx ErrorContext) {
ignorado: = context.New ()
MyFunction (ignorado) // este es ignorado

 MyFunction(ctx) //this one is handled

}
`` ``
Este enfoque no cambia nada en el lenguaje existente.

@ALTree Alberto, ¿qué tal mezclar tu check y lo que propuso @ianlancetaylor ?

entonces

func F() (int, string, error) {
   i, s, err := OhNo()
   if err != nil {
      return i, s, &BadStuffHappened(err, "oopsie-daisy")
   }
   // all is good
   return i+1, s+" ok", nil
}

se convierte en

func F() (int, string, error) {
   i, s, err := OhNo()
   check i, s, err || &BadStuffHappened(err, "oopsie-daisy")
   // all is good
   return i+1, s+" ok", nil
}

Además, podemos limitar check para tratar solo con tipos de error, por lo que si necesita varios valores de retorno, deben ser nombrados y asignados, por lo que asigna "en el lugar" de alguna manera y se comporta como un simple "retorno"

func F() (a int, s string, err error) {
   i, s, err = OhNo()
   check err |=  &BadStuffHappened(err, "oopsy-daisy")  // assigns in place and behaves like simple "return"
   // all is good
   return i+1, s+" ok", nil
}

Si return se convertiría en aceptable en expresión un día, entonces check no es necesario o se convierte en una función estándar

func check(e error) bool {
   return e != nil
}

func F() (a int, s string, err error) {
   i, s, err = OhNo()
   check(err) || return &BadStuffHappened(err, "oopsy-daisy")
   // all is good
   return i+1, s+" ok", nil
}

Aunque la última solución se siente como Perl 😄

No recuerdo quién lo propuso originalmente, pero aquí hay otra idea de sintaxis (el bici favorito de todos :-). No digo que sea buena, pero si estamos lanzando ideas al bote ...

x, y := try foo()

sería equivalente a:

x, y, err := foo()
if err != nil {
    return (an appropriate number of zero values), err
}

y

x, y := try foo() catch &FooErr{E:$, S:"bad"}

sería equivalente a:

x, y, err := foo()
if err != nil {
    return (an appropriate number of zero values), &FooErr{E:err, S:"bad"}
}

La forma try ciertamente se ha propuesto antes, varias veces, diferencias de sintaxis modulo superficiales. La forma try ... catch se propone con menos frecuencia, pero es claramente similar a la construcción check A, B @ALTree y la @tandr . Una diferencia es que esta es una expresión, no una declaración, por lo que puede decir:

z(try foo() catch &FooErr{E:$, S:"bad"})

Podría tener múltiples intentos / capturas en una sola declaración:

p = try q(0) + try q(1)
a = try b(c, d() + try e(), f, try g() catch &GErr{E:$}, h()) catch $BErr{E:$}

aunque no creo que queramos fomentar eso. También debería tener cuidado aquí con el orden de evaluación. Por ejemplo, si h() se evalúa para efectos secundarios si e() devuelve un error que no es nulo.

Obviamente, nuevas palabras clave como try y catch romperían la compatibilidad con Go 1.x.

Sugiero que deberíamos exprimir el objetivo de esta propuesta. ¿Qué problema solucionará esta propuesta? ¿Reducir las siguientes tres líneas en dos o una? Esto podría ser un cambio de idioma de devolución / if.

if err != nil {
    return err
}

¿O reducir el número de veces para comprobar err? Puede ser una solución de prueba / captura para esto.

Me gustaría sugerir que cualquier sintaxis de acceso directo razonable para el manejo de errores tiene tres propiedades:

  1. No debería aparecer antes del código que está verificando, de modo que la ruta sin errores sea prominente.
  2. No debe introducir variables implícitas en el alcance, para que los lectores no se confundan cuando hay una variable explícita con el mismo nombre.
  3. No debería hacer que una acción de recuperación (por ejemplo, return err ) sea más fácil que otra. A veces, puede ser preferible una acción completamente diferente (como llamar a t.Fatal ). Tampoco queremos disuadir a las personas de que agreguen contexto adicional.

Dadas esas limitaciones, parece que una sintaxis casi mínima sería algo así como

STMT SEPARATOR_TOKEN VAR BLOCK

Por ejemplo,

syscall.Chdir(dir) :: err { return err }

que es equivalente a

if err := syscall.Chdir(dir); err != nil {
    return err
}
````
Even though it's not much shorter, the new syntax moves the error path out of the way. Part of the change would be to modify `gofmt` so it doesn't line-break one-line error-handling blocks, and it indents multi-line error-handling blocks past the opening `}`.

We could make it a bit shorter by declaring the error variable in place with a special marker, like

syscall.Chdir (dir) :: {return @err }
''

Me pregunto cómo se comporta esto en cuanto a valores distintos de cero y error devuelto. Por ejemplo, bufio.Peek posiblemente devuelva un valor distinto de cero y ErrBufferFull ambos al mismo tiempo.

@mattn todavía puede usar la sintaxis anterior.

@nigeltao Sí, lo entiendo. Sospecho que este comportamiento posiblemente genere un error en el código del usuario ya que bufio.Peek también devuelve un valor distinto de cero y nulo. El código no debe tener valores implícitos ni errores a la vez. Por lo tanto, el valor y el error deben devolverse a la persona que llama (en este caso).

ret, err := doSomething() :: err { return err }
return ret, err

@jba Lo que estás describiendo se parece un poco a un operador de composición de función transpuesta:

syscall.Chdir(dir) ⫱ func (err error) { return &PathError{"chdir", dir, err} }

Pero el hecho de que estemos escribiendo código principalmente imperativo requiere que no usemos una función en la segunda posición, porque parte del punto es poder regresar temprano.

Así que ahora estoy pensando en tres observaciones que están relacionadas:

  1. El manejo de errores es como la composición de funciones, pero la forma en que hacemos las cosas en Go es algo opuesto a la mónada de error de Haskell: debido a que principalmente escribimos código imperativo en lugar de secuencial, queremos transformar el error (para agregar contexto) en lugar de el valor sin error (que preferimos vincular a una variable).

  2. Las funciones de Go que devuelven (x, y, error) generalmente significan algo más como una unión (# 19412) de (x, y) | error .

  3. En los lenguajes que descomprimen o uniones de coincidencia de patrones, los casos son ámbitos separados, y muchos de los problemas que tenemos con los errores en Go se deben al sombreado inesperado de las variables redeclaradas que podrían mejorarse separando esos ámbitos (# 21114).

Entonces, tal vez lo que realmente queremos es como el operador =: , pero con una especie de condicional de coincidencia de unión:

syscall.Chdir(dir) =? err { return &PathError{"chdir", dir, err} }

ir
n: = io.WriteString (w, s) =? err {return err}

and perhaps a boolean version for `, ok` index expressions and type assertions:
```go
y := m[x] =! { return ErrNotFound }

Excepto por el alcance, eso no es muy diferente de simplemente cambiar gofmt para ser más receptivo a las frases breves:

err := syscall.Chdir(dir); if err != nil { return &PathError{"chdir", dir, err} }

ir
n, err: = io.WriteString (w, s); if err! = nil {return err}

```go
y, ok := m[x]; if !ok { return ErrNotFound }

¡Pero el alcance es un gran problema! Los problemas de alcance son donde este tipo de código cruza la línea de "algo feo" a "errores sutiles".

@ianlancetaylor
Si bien soy un fanático de la idea general, no soy un gran partidario de la sintaxis críptica similar a la de Perl. Quizás una sintaxis más prolija sería menos confusa, como:

syscall.Chdir(dir) or dump(err): errors.Wrap(err, "chdir failed")

syscall.Chdir(dir) or dump

Además, no entendí si el último argumento aparece en caso de asignación, por ejemplo:

resp := http.Get("https://example.com") or dump

No olvidemos que los errores son valores en go y no de algún tipo especial.
No hay nada que podamos hacer con otras estructuras que no podamos hacer con los errores y al revés. Esto significa que si comprende las estructuras en general, comprenderá los errores y cómo se manejan (incluso si cree que es detallado)

Esta sintaxis requeriría que los desarrolladores nuevos y antiguos aprendan un poco de información nueva antes de que puedan comenzar a comprender el código que la usa.

Eso solo hace que esta propuesta no valga la pena en mi humilde opinión.

Personalmente prefiero esta sintaxis

err := syscall.Chdir(dir)
if err != nil {
    return err
}
return nil

encima

if err := syscall.Chdir(dir); err != nil {
    return err
}
return nil

Es una línea más, pero separa la acción prevista del manejo de errores. Este formulario es el más legible para mí.

@bcmills :

Excepto por el alcance, eso no es muy diferente de simplemente cambiar gofmt para ser más receptivo a las frases ingeniosas

No solo el alcance; también está el borde izquierdo. Creo que eso realmente afecta la legibilidad. creo

syscall.Chdir(dir) =: err; if err != nil { return &PathError{"chdir", dir, err} } 

es mucho más claro que

err := syscall.Chdir(dir); if err != nil { return &PathError{"chdir", dir, err} } 

especialmente cuando ocurre en varias líneas consecutivas, porque su ojo puede escanear el borde izquierdo para ignorar el manejo de errores.

Mezclando la idea @bcmills podemos introducir el operador de reenvío de tubería condicional.

La función F2 se ejecutará si el último valor no es

func F1() (foo, bar){}

first := F1() ?> last: F2(first, last)

Un caso especial de reenvío de tubería con declaración de devolución

func Chdir(dir string) error {
    syscall.Chdir(dir) ?> err: return &PathError{"chdir", dir, err}
    return nil
}

Ejemplo real aportado por @urandom en otro número
Para mí, mucho más legible con enfoque en flujo primario.

func configureCloudinit(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig, error) {
    // When bootstrapping, we only want to apt-get update/upgrade
    // and setup the SSH keys. The rest we leave to cloudinit/sshinit.
    udata := cloudconfig.NewUserdataConfig(icfg, cloudcfg) ?> err: return nil, err
    if icfg.Bootstrap != nil {
        udata.ConfigureBasic() ?> err: return nil, err
        return udata, nil
    }
    udata.Configure() ?> err: return nil, err
    return udata, nil
}

func ComposeUserData(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig, renderer renderers.ProviderRenderer) ([]byte, error) {
    if cloudcfg == nil {
        cloudcfg = cloudinit.New(icfg.Series) ?> err: return nil, errors.Trace(err)
    }
    _ = configureCloudinit(icfg, cloudcfg) ?> err: return nil, errors.Trace(err)
    operatingSystem := series.GetOSFromSeries(icfg.Series) ?> err: return nil, errors.Trace(err)
    udata := renderer.Render(cloudcfg, operatingSystem) ?> err: return nil, errors.Trace(err)
    logger.Tracef("Generated cloud init:\n%s", string(udata))
    return udata, nil
}

Estoy de acuerdo en que el manejo de errores no es ergonómico. Es decir, cuando lee el código a continuación, debe vocalizarlo en if error not nil then que se traduce en if there is an error then .

if err != nil {
    // handle error
}

Me gustaría tener la capacidad de expresar el código anterior de tal manera, que en mi opinión es más legible.

if err {
    // handle error
}

Solo mi humilde sugerencia :)

Se parece a Perl, incluso tiene la variable mágica.
Como referencia, en perl harías

abrir (ARCHIVO, $ archivo) o morir ("no se puede abrir $ archivo: $!");

En mi humilde opinión, no vale la pena, un punto que me gusta de ir es que el manejo de errores
es explícito y 'en tu cara'

Si nos atenemos a él, me gustaría no tener variables mágicas, deberíamos ser
capaz de nombrar la variable de error

e: = syscall.Chdir (dir)?> e: & PathError {"chdir", dir, e}

Y también podríamos usar un símbolo diferente de || específico para esta tarea,
Supongo que los símbolos de texto como "o" no son posibles debido a que
compatibilidad

n, _, err, _ = somecall (...)?> err: & PathError {"somecall", n, err}

El martes 1 de agosto de 2017 a las 2:47 p.m., Rodrigo [email protected] escribió:

Mezclando la idea @bcmills https://github.com/bcmills podemos presentar
operador de reenvío de tubería condicional.

La función F2 se ejecutará si el último valor no es

func F1 () (foo, bar) {}
primero: = F1 ()?> último: F2 (primero, último)

Un caso especial de reenvío de tubería con declaración de devolución

error de func Chdir (cadena de directorio) {
syscall.Chdir (dir)?> err: return & PathError {"chdir", dir, err}
retorno nulo
}

Ejemplo real
https://github.com/juju/juju/blob/01b24551ecdf20921cf620b844ef6c2948fcc9f8/cloudconfig/providerinit/providerinit.go
presentado por @urandom https://github.com/urandom en otro número
Para mí, mucho más legible con enfoque en flujo primario.

func configureCloudinit (icfg * instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig, error) {
// Al arrancar, solo queremos apt-get update / upgrade
// y configura las claves SSH. El resto lo dejamos a cloudinit / sshinit.
udata: = cloudconfig.NewUserdataConfig (icfg, cloudcfg)?> err: return nil, err
if icfg.Bootstrap! = nil {
udata.ConfigureBasic ()?> err: return nil, err
volver udata, nada
}
udata.Configure ()?> err: return nil, err
volver udata, nada
}
func ComposeUserData (icfg * instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig, renderer renderers.ProviderRenderer) ([] byte, error) {
if cloudcfg == nil {
cloudcfg = cloudinit.New (icfg.Series)?> err: return nil, errors.Trace (err)
}
configureCloudinit (icfg, cloudcfg)?> err: return nil, errors.Trace (err)
operatingSystem: = series.GetOSFromSeries (icfg.Series)?> err: return nil, errors.Trace (err)
udata: = renderer.Render (cloudcfg, operatingSystem)?> err: return nil, errors.Trace (err)
logger.Tracef ("Inicialización en la nube generada: \ n% s", cadena (udata))
volver udata, nada
}

-
Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/21161#issuecomment-319359614 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AbwRO_J0h2dQHqfysf2roA866vFN4_1Jks5sTx5hgaJpZM4Oi1c-
.

¿Soy el único que piensa que todos estos cambios propuestos serían más complicados que el formulario actual?

Creo que la sencillez y la brevedad no son iguales ni intercambiables. Sí, todos estos cambios serían una o más líneas más cortos pero introducirían operadores o palabras clave que un usuario del idioma tendría que aprender.

@rodcorsi Sé que parece menor, pero creo que es importante que la segunda parte sea un bloque : las declaraciones existentes if y for usan bloques, y select y switch ambos usan una sintaxis delimitada por llaves, por lo que parece incómodo omitir las llaves para esta operación de flujo de control en particular.

También es mucho más fácil asegurarse de que el árbol de análisis no sea ambiguo si no tiene que preocuparse por las expresiones arbitrarias que siguen a los nuevos símbolos.

La sintaxis y la semántica que tenía en mente para mi boceto son:


NonZeroGuardStmt = ( Expression | IdentifierList ":=" Expression |
                     ExpressionList assign_op Expression ) "=?" [ identifier ] Block .
ZeroGuardStmt = ( Expression | IdentifierList ":=" Expression |
                  ExpressionList assign_op Expression ) "=!" Block .

Un NonZeroGuardStmt ejecuta Block si el último valor de un Expression no es igual al valor cero de su tipo. Si un identifier está presente, está vinculado a ese valor dentro de Block . Un ZeroGuardStmt ejecuta Block si el último valor de un Expression es igual al valor cero de su tipo.

Para la forma := , los otros valores (principales) de Expression están vinculados a IdentifierList como en un ShortVarDecl . Los identificadores se declaran en el ámbito contenedor, lo que implica que también son visibles dentro de Block .

Para el formulario assign_op , cada operando del lado izquierdo debe ser direccionable, una expresión de índice de mapa o (solo para asignaciones de = ) el identificador en blanco. Los operandos pueden estar entre paréntesis. Los otros valores (iniciales) del lado derecho Expression se evalúan como en un Assignment . La asignación ocurre antes de la ejecución de Block e independientemente de si se ejecuta luego Block .


Creo que la gramática propuesta aquí es compatible con Go 1: ? no es un identificador válido y no hay operadores Go existentes que utilicen ese carácter, y aunque ! es un operador válido, no hay producción existente en la que puede ir seguida de un { .

@bcmills LGTM, con cambios concomitantes en gofmt.

Pensé que haría =? y =! cada uno de ellos un token por derecho propio, lo que haría que la gramática fuera trivialmente compatible.

Pensé que harías =? y =! cada uno es un símbolo por derecho propio, lo que haría que la gramática fuera trivialmente compatible.

Podemos hacer eso en la gramática, pero no en el lexer: la secuencia "=!" puede aparecer en un código Go 1 válido (https://play.golang.org/p/pMTtUWgBN9).

La llave es lo que hace que el análisis no sea ambiguo en mi propuesta: =! actualmente solo puede aparecer en una declaración o asignación a una variable booleana, y las declaraciones y asignaciones no pueden aparecer inmediatamente antes de una llave (https : //play.golang.org/p/ncJyg-GMuL) a menos que estén separados por un punto y

@romainmenke Nop . No eres el único. No veo el valor de un manejo de errores de una sola línea. Puede guardar una línea pero agregar mucha más complejidad. El problema es que en muchas de estas propuestas, la parte de manejo de errores queda oculta. La idea no es hacerlos menos notorios porque el manejo de errores es importante, sino hacer que el código sea más fácil de leer. Brevedad no equivale a legibilidad fácil. Si tiene que hacer cambios en el sistema de manejo de errores existente, creo que el convencional try-catch-finalmente es mucho más atractivo que muchas ideas aquí.

Me gusta la propuesta check porque también puede extenderla para manejar

f, err := os.Open(myfile)
check err
defer check f.Close()

Otras propuestas no parecen poder combinarse con defer también. check también es muy legible y simple para Google si no lo sabe. No creo que deba limitarse al tipo error . Cualquier cosa que sea un parámetro de retorno en la última posición podría usarlo. Entonces, un iterador puede tener un check por un Next() bool .

Una vez escribí un escáner que parece

func (s *Scanner) Next() bool {
    if s.Error != nil || s.pos >= s.RecordCount {
        return false
    }
    s.pos++

    var rt uint8
    if !s.read(&rt) {
        return false
    }
...

Ese último bit podría ser check s.read(&rt) lugar.

@carlmjohnson

Otras propuestas no parecen poder combinarse con defer también.

Si está asumiendo que expandiremos defer para permitir regresar desde la función externa usando la nueva sintaxis, puede aplicar esa suposición igualmente bien a otras propuestas.

defer f.Close() =? err { return err }

Dado que la propuesta check @ALTree presenta una declaración separada, no veo cómo se podría mezclar eso con un defer que hace algo más que simplemente devolver el error.

defer func() {
  err := f.Close()
  check err, fmt.Errorf(…, err) // But this func() doesn't return an error!
}()

Contraste:

defer f.Close() =? err { return fmt.Errorf(…, err) }

La justificación de muchas de estas propuestas es una mejor "ergonomía", pero realmente no veo cómo cualquiera de ellas es mejor que no sea hacer que haya un poco menos para escribir. ¿Cómo aumentan la capacidad de mantenimiento del código? ¿La composibilidad? ¿La legibilidad? ¿La facilidad para comprender el flujo de control?

@jimmyfrasche

¿Cómo aumentan la capacidad de mantenimiento del código? ¿La composibilidad? ¿La legibilidad? ¿La facilidad para comprender el flujo de control?

Como señalé anteriormente, la principal ventaja de cualquiera de estas propuestas probablemente tendría que provenir de un alcance más claro de asignaciones y variables err : vea # 19727, # 20148, # 5634, # 21114, y probablemente otros para varios formas en que las personas se encuentran con problemas de alcance en relación con el manejo de errores.

@bcmills, gracias por haberme perdido en su publicación anterior.

Sin embargo, dada esa premisa, ¿no sería mejor proporcionar una facilidad más general para "establecer un alcance más claro de las asignaciones" que pudieran ser utilizadas por todas las variables? Sin querer, también he sombreado mi parte de variables que no son de error, ciertamente.

Recuerdo cuando se introdujo el comportamiento actual de := : mucho de ese hilo en el que se vuelven locos † era un clamor por una forma de anotar explícitamente qué nombres se reutilizarían en lugar de la implícita "reutilización solo si esa variable existe en exactamente el alcance actual "que es donde se manifiestan todos los problemas sutiles difíciles de ver, en mi experiencia.

† No puedo encontrar ese hilo, ¿alguien tiene un enlace?

Hay muchas cosas que creo que podrían mejorarse en Go, pero el comportamiento de := siempre me pareció el único error grave. ¿Quizás revisar el comportamiento de := es la forma de resolver el problema de raíz o al menos reducir la necesidad de otros cambios más extremos?

@jimmyfrasche

Sin embargo, dada esa premisa, ¿no sería mejor proporcionar una facilidad más general para "establecer un alcance más claro de las asignaciones" que pudieran ser utilizadas por todas las variables?

Si. Esa es una de las cosas que me gustan del operador =? o :: que @jba y yo hemos propuesto: también se extiende muy bien a (un subconjunto ciertamente limitado de) no errores.

Personalmente, sospecho que sería más feliz a largo plazo con una característica de tipo de datos tagged-union / varint / algebraic más explícita (ver también # 19412), pero ese es un cambio mucho mayor en el lenguaje: es difícil ver cómo lo haríamos eso en las API existentes en un entorno mixto Go 1 / Go 2.

¿La facilidad para comprender el flujo de control?

En las propuestas de my y @bcmills , su ojo puede escanear el lado izquierdo y

@bcmills Creo que soy responsable de al menos la mitad de las palabras en # 19412, por lo que no es necesario que me venda en tipos de suma;)

Cuando se trata de devolver cosas con un error, hay cuatro casos

  1. solo un error (no es necesario que haga nada, solo devuelva un error)
  2. cosas Y un error (lo manejarías exactamente como lo harías ahora)
  3. una cosa O un error (¡podría usar tipos de suma!: tada :)
  4. dos o más cosas O un error

Si aciertas 4, ahí es donde las cosas se complican. Sin introducir tipos de tuplas (tipos de productos sin etiquetar que van con los tipos de productos etiquetados de la estructura), tendría que reducir el problema al caso 3 agrupando todo en una estructura si desea utilizar tipos de suma para modelar "esto o un error".

La introducción de tipos de tupla causaría todo tipo de problemas y problemas de compatibilidad y superposiciones extrañas (¿es func() (int, string, error) una tupla definida implícitamente o son los valores de retorno múltiples un concepto separado? Si es una tupla definida implícitamente, ¿eso significa func() (n int, msg string, err error) es una estructura definida implícitamente? Si es una estructura, ¿cómo accedo a los campos si no estoy en el mismo paquete?)

Sigo pensando que los tipos de suma brindan muchos beneficios, pero no hacen nada para solucionar los problemas con el alcance, por supuesto. En todo caso, podrían empeorar las cosas porque podría sombrear la suma completa del 'resultado o error' en lugar de simplemente sombrear el caso de error cuando tenía algo en el caso de resultado.

@jba No veo cómo eso es una propiedad deseable. Aparte de una falta general de facilidad con el concepto de hacer que el control fluya en dos dimensiones, por así decirlo, tampoco puedo pensar en por qué no lo es. ¿Puedes explicarme el beneficio?

Sin introducir tipos de tupla […] tendrías que [agrupar] todo en una estructura si quieres usar tipos de suma para modelar "esto o un error".

Estoy de acuerdo con eso: creo que tendríamos sitios de llamadas mucho más legibles de esa manera (¡no más enlaces posicionales transpuestos accidentalmente!), Y # 12854 mitigaría gran parte de la sobrecarga actualmente asociada con los retornos de estructura.

El gran problema es la migración: ¿cómo pasaríamos del modelo "valores y error" de Go 1 a un modelo potencial de "valores o error" en Go 2, especialmente dadas las API como io.Writer que realmente devuelven "valores? y error "?

Sigo pensando que los tipos de suma brindan muchos beneficios, pero no hacen nada para solucionar los problemas con el alcance, por supuesto.

Eso depende de cómo los desempaques, lo que supongo que nos lleva de vuelta a donde estamos hoy. Si prefieres las uniones, quizás puedas imaginar una versión de =? como una API de "coincidencia de patrones asimétrica":

i := match strconv.Atoi(str) | err error { return err }

Donde match sería la operación tradicional de coincidencia de patrones de estilo ML, pero en el caso de una coincidencia no exhaustiva devolvería el valor (como interface{} si la unión tiene más de una alternativa incomparable) en lugar de entrar en pánico con un fallo de partido no exhaustivo.

Acabo de registrar un paquete en https://github.com/mpvl/errd que aborda los problemas discutidos aquí mediante programación (sin cambios de idioma). El aspecto más importante de este paquete es que no solo acorta el manejo de errores, sino que también facilita hacerlo correctamente. Doy ejemplos en los documentos sobre cómo el manejo tradicional de errores idiomáticos es más complicado de lo que parece, especialmente en la interacción con aplazar.

Sin embargo, considero que este es un paquete "quemador"; el objetivo es obtener una buena experiencia y conocimientos sobre la mejor manera de extender el idioma. Interactúa bastante bien con los genéricos, por cierto, si esto se convierte en algo.

Todavía estoy trabajando en algunos ejemplos más, pero este paquete está listo para experimentar.

@bcmills un millón: +1: para # 12854

Como notó, hay "retorno X y error" y "retorno X o error", por lo que realmente no podría solucionarlo sin alguna macro que traduzca la forma anterior en la nueva forma a pedido (y, por supuesto, habría errores o al menos el tiempo de ejecución entra en pánico cuando se usa inevitablemente para una función de "X y error").

Realmente no me gusta la idea de introducir macros especiales en el lenguaje, especialmente si es solo para el manejo de errores, que es mi mayor problema con muchas de estas propuestas.

Go no es grande en azúcar o magia y eso es una ventaja.

Hay demasiada inercia y muy poca información codificada en la práctica actual para manejar un salto masivo hacia un paradigma de manejo de errores más funcional.

Si Go 2 obtiene tipos de suma, lo que francamente me sorprendería (¡en el buen sentido!), Tendría que ser, en todo caso, un proceso gradual muy lento para pasar al "nuevo estilo" y, mientras tanto, habría incluso más fragmentación y confusión en cómo manejar los errores, por lo que no veo que sea un resultado neto positivo. (Sin embargo, comenzaría a usarlo inmediatamente para cosas como chan union { Msg1 T; Msg2 S; Err error } lugar de tres canales).

Si esto fuera antes del Go1 y el equipo de Go pudiera decir "vamos a pasar los próximos seis meses moviendo todo y cuando se rompa, sigamos", eso sería una cosa, pero en este momento estamos básicamente estancados incluso si obtenemos tipos de suma.

Como notó, hay "retorno X y error" y "retorno X o error", por lo que no podría solucionarlo sin alguna macro que traduzca la forma antigua en la nueva forma a pedido.

Como intenté decir anteriormente, no creo que sea necesario que la nueva forma, sea lo que sea, cubra "retorno X y error". Si la gran mayoría de los casos son "devolver X o error", y la nueva forma solo mejora eso, entonces eso es genial, y aún puede usar la antigua forma compatible con Go 1 para el más raro "devolver X y error".

@nigeltao Es cierto, pero aún necesitaríamos alguna forma de diferenciarlos durante la transición, a menos que esté proponiendo que mantengamos toda la biblioteca estándar en el estilo existente.

@jimmyfrasche No creo que pueda construir un argumento para ello. Puede ver mi charla o ver el ejemplo en el README del repositorio. Pero si la evidencia visual no es convincente para usted, entonces no hay nada que pueda decir.

@jba vio la charla y leyó el

Si el objetivo es dejar a un lado, a falta de un término mejor, la ruta infeliz, entonces un complemento $ EDITOR funcionaría sin cambio de idioma y funcionaría con todo el código existente, independientemente de las preferencias del autor del código.

Un cambio de idioma hace que la sintaxis sea algo más compacta. @bcmills menciona que esto mejora el alcance, pero realmente no veo cómo podría hacerlo a menos que tenga reglas de alcance diferentes a las de := pero eso parece que causaría más confusión.

@bcmills No entiendo tu comentario. Obviamente, puedes distinguirlos. La forma antigua se ve así:

err := foo()
if err != nil {
  return n, err  // n can be non-zero
}

La nueva forma parece

check foo()

o

foo() || &FooError{err}

o cualquiera que sea el color de la bici. Supongo que la mayor parte de la biblioteca estándar puede realizar la transición, pero no toda tiene que hacerlo.

Para agregar a los requisitos de @ianlancetaylor : la simplificación de los mensajes de error no solo debería acortar las cosas, sino también facilitar el manejo correcto de los errores. Pasar pánicos y errores posteriores para diferir funciones es complicado de hacer bien.

Considere, por ejemplo, escribir en un archivo de Google Cloud Storage, donde queremos cancelar la escritura del archivo ante cualquier error:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer func() {
        if r := recover(); r != nil {
            w.CloseWithError(fmt.Errorf("panic: %v", r))
            panic(r)
        }
        if err != nil {
            _ = w.CloseWithError(err)
        } else {
            err = w.Close()
        }
    }
    _, err = io.Copy(w, r)
    return err
}

Las sutilezas de este código incluyen:

  • El error de Copiar se pasa furtivamente a través del argumento de retorno con nombre a la función aplazar.
  • Para estar completamente seguros, detectamos el pánico de r y nos aseguramos de abortar la escritura antes de reanudar el pánico.
  • Ignorar el error del primer cierre es intencional, pero parece un artefacto de programador perezoso.

Usando el paquete errd, este código se ve así:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client, err := storage.NewClient(ctx)
        e.Must(err)
        e.Defer(client.Close, errd.Discard)

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        e.Defer(w.CloseWithError)

        _, err = io.Copy(w, r)
        e.Must(err)
    })
}

errd.Discard es un controlador de errores. Los controladores de errores también se pueden utilizar para ajustar, registrar o cualquier error.

e.Must es el equivalente a foo() || wrapError

e.Defer es adicional y se ocupa de pasar errores a aplazamientos.

Usando genéricos, este fragmento de código podría verse así:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client := e.Must(storage.NewClient(ctx))
        e.Defer(client.Close, errd.Discard)

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        e.Defer(w.CloseWithError)

        _ = e.Must(io.Copy(w, r))
    })
}

Si estandarizamos los métodos que se utilizarán para Aplazar, incluso podría verse así:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client := e.DeferClose(e.Must(storage.NewClient(ctx)), errd.Discard)
       e.Must(io.Copy(e.DeferClose(client.Bucket(bucket).Object(dst).NewWriter(ctx)), r)
    })
}

Donde DeferClose elige Close o CloseWithError. No decir que esto es mejor, solo mostrar las posibilidades.

De todos modos, di una presentación en una reunión de Ámsterdam la semana pasada sobre este tema y parecía que la capacidad de facilitar el manejo correcto de errores se considera más útil que hacerlo más corto.

Una solución que mejore los errores debería centrarse al menos tanto en hacer las cosas bien más fáciles como en acortarlas.

@ALTree errd maneja la "sofisticada recuperación de errores"

@jimmyfrasche : errd hace más o menos lo que hace el ejemplo de su patio de recreo, pero también teje errores de paso y pánico a los aplazamientos.

@jimmyfrasche : Estoy de acuerdo en que la mayoría de las propuestas no agregan mucho a lo que ya se puede lograr en el código.

@romainmenke : estoy de acuerdo en que se

@jba : el enfoque errd hace que sea bastante fácil escanear el flujo de errores en comparación con el flujo sin errores con solo mirar el lado izquierdo (cualquier cosa que comience con e. es error o manejo diferido). También hace que sea muy fácil escanear cuáles de los valores devueltos se manejan por error o aplazamiento y cuáles no.

@bcmills : aunque errd no soluciona los problemas de alcance en sí mismo, elimina la necesidad de pasar errores posteriores a las variables de error declaradas anteriormente y todo, mitigando así el problema considerablemente para el manejo de errores, AFAICT.

errd parece depender completamente del pánico y la recuperación. eso parece que viene con una importante penalización de rendimiento. No estoy seguro de que sea una solución general debido a esto.

@urandom : Bajo el capó se implementa como un
Si el código original:

  • no usa diferir: la penalización de usar errd es grande, alrededor de 100ns *.
  • usa diferir idiomático: el tiempo de ejecución o errd es del mismo orden, aunque algo más lento
  • utiliza el manejo adecuado de errores para aplazar: el tiempo de ejecución es aproximadamente igual; errd puede ser más rápido si el número de aplazamientos es> 1

Otros gastos generales:

  • Pasar cierres (w.Close) a Aplazar actualmente también agrega una sobrecarga de aproximadamente 25ns *, en comparación con el uso de DeferClose o DeferFunc API (consulte la versión v0.1.0). Después de discutir con @rsc, lo
  • Envolver cadenas de error en línea como controladores ( e.Must(err, msg("oh noes!") ) cuesta alrededor de 30 ns con Go 1.8. Sin embargo, con la propina (1.9), aunque sigue siendo una asignación, calculé el costo en 2ns. Por supuesto, para los mensajes de error declarados previamente, el costo sigue siendo insignificante.

(*) todos los números que se ejecutan en mi MacBook Pro 2016.

Con todo, el costo parece aceptable si su código original usa diferir. De lo contrario, Austin está trabajando para reducir significativamente el costo del aplazamiento, por lo que el costo puede incluso disminuir con el tiempo.

De todos modos, el objetivo de este paquete es adquirir experiencia sobre cómo se sentiría y sería útil usar el manejo de errores alternativo ahora para que podamos construir la mejor adición de lenguaje en Go 2. El caso en cuestión es la discusión actual, se enfoca demasiado en reducir un pocas líneas para casos simples, mientras que hay mucho más que ganar y posiblemente otros puntos son más importantes.

@jimmyfrasche :

entonces un complemento $ EDITOR funcionaría sin cambio de idioma

Sí, eso es precisamente lo que argumento en la charla. Aquí estoy argumentando que si hacemos un cambio de idioma, debería estar de acuerdo con el concepto de "nota al margen".

@nigeltao

Obviamente, puedes distinguirlos. La forma antigua se ve así:

Me refiero al punto de declaración, no al punto de uso.

Algunas de las propuestas discutidas aquí no distinguen entre las dos en el sitio de la convocatoria, pero otras sí. Si elegimos una de las opciones que asume "valor o error", como || , try … catch o match , entonces debería ser un error de tiempo de compilación. esa sintaxis con una función de "valor y error", y el implementador de la función debe decidir cuál es.

En el momento de la declaración, actualmente no hay forma de distinguir entre "valor y error" y "valor o error":

func Atoi(string) (int, error)

y

func WriteString(Writer, String) (int, error)

tienen los mismos tipos de retorno, pero una semántica de error diferente.

@mpvl Estoy mirando los documentos y src para errd. Creo que estoy empezando a entender cómo funciona, pero parece que tiene una gran cantidad de API que se interponen en el camino de la comprensión que parece que podría implementarse en un paquete separado. Estoy seguro de que todo lo hace más útil en la práctica pero a modo de ilustración añade mucho ruido.

Si ignoramos los ayudantes comunes como las funciones de nivel superior para operar en el resultado de WithDefault (), y asumimos, en aras de la simplicidad, que siempre usamos el contexto, e ignoramos cualquier decisión tomada para el rendimiento, la API barebone mínima absoluta se reduciría a la debajo de las operaciones?

type Handler = func(ctx context.Context, panicing bool, err error) error
Run(context.Context, func(*E), defaults ...Handler) //egregious style but most minimal
type struct E {...}
func (*E) Must(err error, handlers ...Handler)
func (*E) Defer(func() error, handlers ...Handler)

Al mirar el código, veo algunas buenas razones por las que no está definido como se indicó anteriormente, pero estoy tratando de llegar a la semántica central para comprender mejor el concepto. Por ejemplo, no estoy seguro de si IsSentinel está en el núcleo o no.

@jimmyfrasche

@bcmills menciona que esto mejora el alcance, pero realmente no veo cómo podría

La principal mejora es mantener la variable err fuera de alcance. Eso evitaría errores como los vinculados desde https://github.com/golang/go/issues/19727. Para ilustrar con un fragmento de uno de ellos:

    res, err := ctxhttp.Get(ctx, c.HTTPClient, dirURL)
    if err != nil {
        return Directory{}, err
    }
    defer res.Body.Close()
    c.addNonce(res.Header)
    if res.StatusCode != http.StatusOK {
        return Directory{}, responseError(res)
    }

    var v struct {
        …
    }
    if json.NewDecoder(res.Body).Decode(&v); err != nil {
        return Directory{}, err
    }

El error ocurre en la última instrucción if: el error de Decode se elimina, pero no es obvio porque un err de una verificación anterior todavía estaba dentro del alcance. En contraste, usando el operador :: o =? , se escribiría:

    res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err }
    defer res.Body.Close()
    c.addNonce(res.Header)
    (res.StatusCode == http.StatusOK) =! { return Directory{}, responseError(res) }

    var v struct {
        …
    }
    json.NewDecoder(res.Body).Decode(&v) =? err { return Directory{}, err }

Aquí hay dos mejoras de alcance que ayudan:

  1. El primer err (de la llamada anterior Get ) solo está dentro del alcance del bloque return , por lo que no se puede usar accidentalmente en comprobaciones posteriores.
  2. Debido a que el err de Decode se declara en la misma declaración en la que se verifica que no sea nulo, no puede haber distorsión entre la declaración y el cheque.

(1) por sí solo hubiera sido suficiente para revelar el error en el momento de la compilación, pero (2) hace que sea fácil de evitar cuando se usa la declaración de protección de la manera obvia.

@bcmills gracias por la aclaración

Entonces, en res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err } la macro =? expande a

var res *http.Reponse
{
  var err error
  res, err = ctxhttp.Get(ctx, c.HTTPClient, dirURL)
  if err != nil {
    return Directory{}, err 
  }
}

Si eso es correcto, eso es lo que quise decir cuando dije que tendría que tener una semántica diferente de := .

Parece que causaría sus propias confusiones como:

func f() error {
  var err error
  g() =? err {
    if err != io.EOF {
      return err
    }
  }
  //one could expect that err could be io.EOF here but it will never be so
}

A menos que haya entendido mal algo.

Sí, esa es la expansión correcta. Tienes razón en que difiere de := , y eso es intencional.

Parece que causaría sus propias confusiones.

Eso es cierto. No es obvio para mí si eso sería confuso en la práctica. Si es así, podríamos proporcionar variantes ":" de la declaración de protección para la declaración (y hacer que las variantes "=" solo se asignen).

(Y ahora eso me hace pensar que los operadores deberían escribirse ? y ! lugar de =? y =! ).

res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) ?: err { return Directory{}, err }

pero

func f() error {
  var err error
  g() ?= err { (err == io.EOF) ! { return err } }
  // err may be io.EOF here.
}

@mpvl Mi principal preocupación acerca de errd es con la interfaz Handler: parece fomentar las canalizaciones de devoluciones de llamada de estilo funcional, pero mi experiencia con el código de estilo de devolución de llamada / continuación (tanto en lenguajes imperativos como Go y C ++ como en funcional lenguajes como ML y Haskell) es que a menudo es mucho más difícil de seguir que el estilo secuencial / imperativo equivalente, que también se alinea con el resto de modismos de Go.

¿Imagina cadenas de estilo Handler como parte de la API, o es su Handler un sustituto de alguna otra sintaxis que está considerando (como algo que opere en Block ¿s?)

@bcmills Todavía no estoy de acuerdo con las características mágicas que introducen docenas de conceptos en el lenguaje en una sola línea y solo funcionan con una cosa, pero finalmente entiendo por qué son más que una forma ligeramente más corta de escribir x, err := f(); if err != nil { return err } . Gracias por ayudarme a entender y lamento que haya tardado tanto.

@bcmills Reescribí el ejemplo motivador de @mpvl , que tiene un manejo de errores retorcido, usando la última propuesta =? que no siempre declara una nueva variable err:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)

        defer func() {
                if r := recover(); r != nil { // r is interface{} not error so we can't use it here
                        _ = w.CloseWithError(fmt.Errorf("panic: %v", r))
                        panic(r)
                }

                if err != nil { // could use =! here but I don't see how that simplifies anything
                        _ = w.CloseWithError(err)
                } else {
                        err = w.Close()
                }
        }()

        io.Copy(w, r) =? err { return err } // what about n? does this need to be prefixed by a '_ ='?
        return nil
}

La mayor parte del manejo de errores no se modifica. Solo pude usar =? en dos lugares. En primer lugar, realmente no proporcionó ningún beneficio que pudiera ver. En el segundo, hizo el código más largo y oscurece el hecho de que io.Copy devuelve dos cosas, por lo que probablemente hubiera sido mejor no usarlo allí.

@jimmyfrasche Ese código es la excepción, no la regla. No deberíamos diseñar funciones para facilitar la escritura.

Además, me pregunto si el recover debería estar allí. Si w.Write o r.Read (o io.Copy !) Entra en pánico, probablemente sea mejor terminar.

Sin recover , no hay necesidad real de defer , y la parte inferior de la función podría convertirse

_ = io.Copy(w, r) =? err { _ = w.CloseWithError(err); return err }
return w.Close()

@jimmyfrasche

// r is interface{} not error so we can't use it here

Tenga en cuenta que mi redacción específica (en https://github.com/golang/go/issues/21161#issuecomment-319434101) se trata de valores cero, no de errores específicamente.

// what about n? does this need to be prefixed by a '_ ='?

No es así, aunque podría haber sido más explícito al respecto.

No me gusta particularmente el uso de @mpvl de recover en ese ejemplo: fomenta el uso del pánico sobre el flujo de control idiomático, mientras que, en todo caso, creo que deberíamos eliminar las llamadas recover extrañas ( como los de fmt ) de la biblioteca estándar en Go 2.

Con ese enfoque, escribiría ese código como:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        io.Copy(w, r) =? err {
                w.CloseWithError(err)
                return err
        }
        return w.Close()
}

Por otro lado, tiene razón en que con la recuperación unidiomática hay pocas oportunidades de aplicar funciones destinadas a admitir el manejo de errores idiomáticos. Sin embargo, separar la recuperación de la operación Close conduce a un código IMO algo más limpio.

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        defer func() {
                if err != nil {
                        _ = w.CloseWithError(err)
                } else {
                        err = w.Close()
                }
        }()
        defer func() {
                recover() =? r {
                        err = fmt.Errorf("panic: %v", r)
                        panic(r)
                }
        }()

        io.Copy(w, r) =? err { return err }
        return nil
}

@jba vuelve a entrar en pánico el controlador de

@bcmills esa última revisión se ve mejor (incluso si =? , de verdad)

@jba :

_ = io.Copy(w, r) =? err { _ = w.CloseWithError(err); return err }
return w.Close()

eso todavía no cubre el caso de pánico en el lector. Es cierto que este es un caso raro, pero bastante importante: llamar a Close aquí en caso de pánico es muy malo.

Ese código es la excepción, no la regla. No deberíamos diseñar funciones para facilitar la escritura.

@jba : Estoy totalmente en desacuerdo en este caso. Es importante manejar correctamente los errores. Permitir que el caso simple sea más fácil alentará aún menos a las personas a pensar en el manejo adecuado de errores. Me gustaría ver algún enfoque que, como errd , haga que el manejo conservador de errores sea trivialmente fácil, al tiempo que requiere un poco de esfuerzo para relajar las reglas, nada que se mueva ni siquiera ligeramente en la otra dirección.

@jimmyfrasche : con respecto a tu simplificación: tienes más o menos razón.

  • IsSentinel no es esencial, solo práctico y común. Lo dejé caer, al menos por ahora.
  • Err in State es diferente de err, por lo que su API lo elimina. Sin embargo, no es fundamental para la comprensión.
  • Los controladores pueden ser funciones, pero son interfaces principalmente por razones de rendimiento. Solo sé que un buen grupo de personas no usará el paquete si no está optimizado. (vea algunos de los primeros comentarios sobre errd en este número)
  • El contexto es lamentable. AppEngine lo necesita, pero no mucho más, creo. Estaría bien quitando el soporte hasta que la gente se niegue.

@mpvl Solo estaba tratando de reducirlo a algunas cosas para que fuera más fácil entender cómo funcionaba, cómo usarlo e imaginar cómo encajaría con el código que escribí.

@jimmyfrasche : entendido, aunque es bueno si una API no requiere que lo hagas. :)

@bcmills : los

  • envolver un error
  • definir para ignorar errores (para hacerlo explícito. Ver ejemplo)
  • errores de registro
  • métricas de error

Nuevamente, en orden de importancia, estos deben estar delimitados por:

  • cuadra
  • línea
  • paquete

Los errores predeterminados solo están ahí para facilitar la garantía de que un error se maneja en algún lugar,
pero podría vivir solo a nivel de bloque. Originalmente tenía una API con opciones en lugar de controladores. Sin embargo, eso resultó en una API más grande y más torpe, además de ser más lenta.

No veo que el problema de la devolución de llamada sea tan malo aquí. Los usuarios definen un Runner pasándole un Handler al que se llama si hay un error. El corredor específico se especifica explícitamente en el bloque donde se manejan los errores. En muchos casos, un controlador será simplemente un literal de cadena envuelto pasado en línea. Voy a probar un poco para ver qué es útil y qué no.

Por cierto, si no deberíamos fomentar los errores de registro en los controladores, entonces probablemente se pueda eliminar el soporte de contexto.

@jba :

Además, me pregunto si la recuperación debería estar allí. Si w.Write o r.Read (o io.Copy!) Entra en pánico, probablemente sea mejor terminar.

writeToGS aún termina si hay un pánico, como debería (!!!), simplemente asegura que llama a CloseWithError con un error que no sea nulo. Si no se maneja el pánico, el aplazamiento aún se llama, pero con err == nil, lo que da como resultado un archivo potencialmente corrupto que se materializa en Cloud Storage. Lo correcto aquí es llamar a CloseWithError con algún error temporal y luego continuar con el pánico.

Encontré un montón de ejemplos como este en el código Go. Tratar con io.Pipes también a menudo resulta en un código un poco demasiado sutil. El manejo de errores a menudo no es tan sencillo como parece ahora.

@bcmills

No me gusta particularmente el uso de @mpvl de

Sin intentar fomentar el uso del pánico en absoluto. Tenga en cuenta que el pánico vuelve a aparecer justo después de CloseWithError y, por lo tanto, no cambia el flujo de control. El pánico sigue siendo pánico.
Sin embargo, no usar la recuperación aquí es incorrecto, ya que un pánico hará que se llame al aplazamiento con un error nulo, lo que indica que lo que se ha escrito hasta ahora se puede cometer.

El único argumento algo válido para no usar la recuperación aquí es que es muy poco probable que ocurra un pánico, incluso para un lector arbitrario (el lector es de tipo desconocido en este ejemplo por una razón :)).
Sin embargo, para el código de producción, esta es una postura inaceptable. Especialmente cuando se programa a una escala lo suficientemente grande, es probable que esto suceda en algún momento (los pánicos pueden ser causados ​​por otras cosas que no sean errores en el código).

Por cierto, tenga en cuenta que el paquete errd elimina la necesidad de que el usuario piense en esto. Sin embargo, cualquier otro mecanismo que indique un error en caso de pánico para diferir está bien. No llamar a aplazamientos por pánico también funcionaría, pero eso viene con sus propios problemas.

En el momento de la declaración, actualmente no hay forma de distinguir entre "valor y error" y "valor o error":

@bcmills Oh, ya veo. Para abrir otra lata de bicis, supongo que podrías decir

func Atoi(string) ?int

en vez de

func Atoi(string) (int, error)

pero WriteString permanecería sin cambios:

func WriteString(Writer, String) (int, error)

Me gusta la propuesta =? / =! / :=? / :=! de @bcmills / @jba más que propuestas similares. Tiene algunas propiedades agradables:

  • componible (puede usar =? dentro de un bloque =? )
  • general (solo se preocupa por el valor cero, no específico del tipo de error)
  • alcance mejorado
  • podría funcionar con diferir (en una variación anterior)

También tiene algunas propiedades que no encuentro tan agradables.

Nidos de composición. El uso repetido continuará sangrando cada vez más a la derecha. Eso no es necesariamente algo malo en sí mismo, pero me imagino que en situaciones con un manejo de errores muy complicado que requiere lidiar con errores que causan errores, el código para tratarlo rápidamente se volvería mucho menos claro que el status quo actual. En tal situación, uno podría usar =? para el error más externo y if err != nil en los errores internos, pero ¿esto realmente ha mejorado el manejo de errores en general o solo en el caso común? Quizás mejorar el caso común es todo lo que se necesita, pero personalmente no lo encuentro convincente.

Introduce falsedad en el lenguaje para ganar su generalidad. La falsedad se define como "es (no) el valor cero" es perfectamente razonable, pero if err != nil { es mejor que if err { ya que es explícito, en mi opinión. Esperaría ver contorsiones en la naturaleza que intentan usar =? / etc. sobre un flujo de control más natural para intentar acceder a su falsedad. Eso ciertamente sería unidiomático y mal visto, pero sucedería. Si bien el abuso potencial de una característica no es en sí mismo un argumento en contra de una característica, es algo a considerar.

El alcance mejorado (para las variantes que declaran su parámetro) es bueno en algunos casos, pero si es necesario corregir el alcance, corríjalo en general.

La semántica del "único resultado situado más a la derecha" tiene sentido, pero me parece un poco extraño. Eso es más un sentimiento que una discusión.

Esta propuesta agrega brevedad al lenguaje pero no potencia adicional. Podría implementarse completamente como un preprocesador que realiza una macro expansión. Por supuesto, eso sería indeseable: complicaría las construcciones y el desarrollo de fragmentos y cualquier preprocesador de este tipo sería extremadamente complicado, ya que tiene que ser higiénico y tener en cuenta los tipos. No estoy tratando de disimular diciendo "simplemente crea un preprocesador". Menciono esto únicamente para señalar que esta propuesta es enteramente azúcar. No le permite hacer nada que no pueda hacer en Go ahora; simplemente te permite escribirlo de forma más compacta. No me opongo dogmáticamente al azúcar. Hay poder en una abstracción lingüística cuidadosamente elegida, pero el hecho de que sea azúcar significa que debe considerarse 👎 hasta que se demuestre su inocencia, por así decirlo.

Los lhs de los operadores son una declaración, pero un subconjunto muy limitado de declaraciones. Qué elementos incluir en ese subconjunto es bastante evidente, pero, al menos, requeriría refactorizar la gramática en la especificación del lenguaje para adaptarse al cambio.

Quisiera algo como

func F() (S, T, error)

func MustF() (S, T) {
  return F() =? err { panic(err) }
}

¿ser permitido?

Si

defer f.Close() :=? err {
    return err
}

está permitido que debe ser (de alguna manera) equivalente a

func theOuterFunc() (err error) {
  //...
  defer func() {
    if err2 := f.Close(); err2 != nil {
      err = err2
    }
  }()
  //...
}

lo que parece profundamente problemático y probablemente cause situaciones muy confusas, incluso ignorando que de una manera muy poco parecida a Go, oculta las implicaciones de rendimiento de la asignación implícita de un cierre. La alternativa es tener return retorno del cierre implícito y luego tener un mensaje de error que no puede devolver un valor de tipo error de un func() que es un poco obtuso.

Sin embargo, en realidad, aparte de una corrección de alcance ligeramente mejorada, esto no soluciona ninguno de los problemas que enfrento al lidiar con los errores en Go. A lo sumo, escribir if err != nil { return err } es una molestia, módulo las ligeras preocupaciones de legibilidad que expresé en # 21182. Los dos mayores problemas son

  1. pensando en cómo manejar el error, y no hay nada que un idioma pueda hacer al respecto
  2. introspección de un error para determinar qué hacer en algunas situaciones; alguna convención adicional con soporte del paquete errors sería de gran

Me doy cuenta de que esos no son los únicos problemas y que muchos encuentran otros aspectos más inmediatamente preocupantes, pero son con los que paso más tiempo y los encuentro más irritantes y problemáticos que cualquier otra cosa.

Por supuesto, siempre se agradecería un mejor análisis estático para detectar cuándo me he equivocado en algo (y en general, no solo en este escenario). Los cambios de idioma y las convenciones que facilitan el análisis de la fuente para que sean más útiles también serían de interés.

Acabo de escribir mucho (¡MUCHO! ¡Lo siento!) Sobre esto, pero no descarto la propuesta. Creo que tiene mérito, pero no estoy convencido de que se salga del listón o de que haga lo suyo.

@jimmyfrasche

Recuerdo cuando se introdujo el comportamiento actual de: =, una gran cantidad de ese hilo en volverse loco † era un clamor por una forma de anotar explícitamente qué nombres se reutilizarían en lugar de la implícita "reutilización solo si esa variable existe exactamente en el ámbito actual "que es donde se manifiestan todos los problemas sutiles difíciles de ver, en mi experiencia.

† No puedo encontrar ese hilo, ¿alguien tiene un enlace?

Creo que debes estar recordando un hilo diferente, a menos que estuvieras involucrado en Go cuando se lanzó. La especificación del 09/11/2009, justo antes de su lanzamiento, tiene:

A diferencia de las declaraciones de variables regulares, una declaración de variable corta puede volver a declarar variables siempre que hayan sido declaradas originalmente en el mismo bloque con el mismo tipo y al menos una de las variables que no están en blanco sea nueva.

Recuerdo haber visto eso cuando leí la especificación por primera vez y pensé que era una gran regla, ya que anteriormente había usado un lenguaje con: = pero sin esa regla de reutilización, y pensar en nuevos nombres para lo mismo era tedioso.

@mpvl
Creo que la astucia de su ejemplo original es más el resultado de la
la API que está utilizando allí que el manejo de errores de Go en sí.

Sin embargo, es un ejemplo interesante, sobre todo por el hecho de que
no desea cerrar el archivo normalmente si entra en pánico, por lo que
El modismo normal "diferir w.Close ()" no funciona.

Si no necesita evitar llamar a Close cuando hay un
pánico, entonces podrías hacer:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer w.Close()
    _, err = io.Copy(w, r)
    if err != nil {
        w.CloseWithError(err)
    }
    return err
}

asumiendo que la semántica fue cambiada de modo que llamar a Close
después de llamar a CloseWithError es un no-op.

Ya no creo que eso se vea tan mal.

Incluso con el requisito de que el archivo no se escriba sin errores cuando hay un pánico, no debería ser demasiado difícil de adaptar; por ejemplo, agregando una función Finalizar que se debe llamar explícitamente antes de Cerrar.

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer w.Close()
    _, err = io.Copy(w, r)
    return w.Finalize(err)

Sin embargo, eso no puede adjuntar el mensaje de error de pánico, pero un registro decente podría aclararlo.
(el método Close incluso podría tener una llamada de recuperación dentro, aunque no estoy seguro si eso es
en realidad una muy mala idea ...)

Sin embargo, creo que el aspecto de recuperación de pánico de este ejemplo es algo así como una pista falsa en este contexto, ya que más del 99% de los casos de manejo de errores no hacen la recuperación de pánico.

@rogpeppe :

Sin embargo, eso no puede adjuntar el mensaje de error de pánico, pero un registro decente podría aclararlo.

No creo que eso sea un problema.

Sin embargo, el cambio de API propuesto mitiga, pero aún no resuelve por completo el problema. La semántica requerida requiere que otro código también se comporte correctamente. Considere el siguiente ejemplo:

r, w := io.Pipe()
go func() {
    var err error                // used to intercept downstream errors
    defer func() {
        w.CloseWithError(err)
    }()

    r, err := newReader()
    if err != nil {
        return
    }
    defer func() {
        if errC := r.Close(); errC != nil && err == nil {
            err = errC
        }
    }
    _, err = io.Copy(w, r)
}()
return r

Por derecho propio, este código muestra que el manejo de errores puede ser complicado o al menos complicado (y me gustaría saber cómo se podría mejorar esto con las otras propuestas): pasa sigilosamente los errores posteriores a través de una variable y también tiene un poco declaración if torpe para garantizar que se pase el error correcto. Ambos distraen demasiado de la "lógica empresarial". El manejo de errores domina el código. Y este ejemplo ni siquiera maneja los pánicos todavía.

Para completar, en errd esto _ manejaría los pánicos correctamente y se vería así:

r, w := io.Pipe()
go errd.Run(func(e *errd.E) {
    e.Defer(w.CloseWithError)

    r, err := newReader()
    e.Must(err)
    e.Defer(r.Close)

    _, err = io.Copy(w, r)
    e.Must(err)
})
return r

Si el lector anterior (que no usa errd ) se pasa como el lector para writeToGS, y el io.Reader devuelto por el pánico newReader, aún resultaría en una semántica defectuosa con su solución de API propuesta (podría correr para cerrarse con éxito el archivo GS después de que la tubería se cierra en caso de pánico con un error nulo).

Esto también prueba el punto. No es trivial razonar sobre el manejo adecuado de errores en Go. Cuando miré cómo se vería el código reescribiéndolo con errd , encontré un montón de código con errores. Sin embargo, solo aprendí lo difícil y sutil que era escribir el manejo idiomático de errores de Go adecuado al escribir las pruebas unitarias para el paquete errd . :)

Una alternativa al cambio de API propuesto sería no manejar ningún aplazamiento en caso de pánico. Esto tiene sus propios problemas y no resolvería completamente el problema y probablemente se pueda deshacer, pero tendría algunas buenas cualidades.

De cualquier manera, lo mejor sería algún cambio de lenguaje que mitiga las sutilezas del manejo de errores, en lugar de uno que se centre en la brevedad.

@mpvl
A menudo encuentro con el código de manejo de errores en Go que la creación de otra función puede limpiar las cosas. Escribiría su código arriba de algo como esto:

func something() {
    r, w := io.Pipe()
    go func() {
        err := copyFromNewReader(w)
        w.CloseWithError(err)
    }()
    ...
}

func copyFromNewReader(w io.Writer) error {
    r, err := newReader()
    if err != nil {
        return err
    }
    defer r.Close()
    _, err = io.Copy(w, r)
    return err
}()

Supongo que r.Close no devuelve un error útil: si ha leído todo el contenido de un lector y acaba de encontrar solo io.EOF, es casi seguro que no importa si devuelve un error cuando se cierra.

No estoy interesado en la API errd, es demasiado sensible a que se inicien goroutines. Por ejemplo: https://play.golang.org/p/iT441gO5us Si doSomething inicia o no una goroutine para ejecutar el
function in no debería afectar la corrección del programa, pero cuando se usa errd, lo hace. Espera que los pánicos atraviesen con seguridad los límites de abstracción, y no es así en Go.

@mpvl

aplazar w.CloseWithError (err)

Por cierto, esta línea siempre llama a CloseWithError con un valor de error nulo. Creo que tu pretendías
escribir:

defer func() { 
   w.CloseWithError(err)
}()

@mpvl

Tenga en cuenta que el error devuelto por el método Close en un io.Reader casi nunca es útil (consulte la lista en https://github.com/golang/go/issues/20803#issuecomment-312318808 ).

Eso sugiere que deberíamos escribir su ejemplo hoy como:

r, w := io.Pipe()
go func() (err error) {
    defer func() { w.CloseWithError(err) }()

    r, err := newReader()
    if err != nil {
        return err
    }
    defer r.Close()

    _, err = io.Copy(w, r)
    return err
}()
return r

... lo que me parece perfectamente bien, además de ser un poco detallado.

Es cierto que pasa un error nulo a w.CloseWithError en caso de pánico, pero todo el programa termina en ese punto de todos modos. Si es importante no cerrar nunca con un error nulo, es un simple cambio de nombre más una línea adicional:

-go func() (err error) {
-   defer func() { w.CloseWithError(err) }()
+go func() (rerr error) {
+   rerr = errors.New("goroutine exited by panic")
+   defer func() { w.CloseWithError(rerr) }()

@rogpeppe : de hecho, gracias. :)

Sí, estoy al tanto del problema de la gorutina. Es desagradable, pero probablemente algo que no sea difícil de detectar con un control veterinario. De todos modos, no veo a errd como una solución final, sino más como una forma de obtener experiencia sobre cómo abordar mejor el manejo de errores. Idealmente, habría un cambio de idioma que resuelva los mismos problemas, pero con las restricciones adecuadas impuestas.

Espera que los pánicos atraviesen con seguridad los límites de abstracción, y no es así en Go.

Eso no es lo que esperaba. En este caso, espero que las API no informen el éxito cuando no lo hubo. Su último fragmento de código lo maneja correctamente, porque no usa diferir para el escritor. Pero esto es muy sutil. Muchos usuarios usarían diferir en este caso porque se considera idiomático.

Quizás un conjunto de controles veterinarios podría detectar usos problemáticos de aplazamientos. Aún así, tanto en el código "idiomático" original como en su última pieza refactorizada hay muchos retoques para solucionar las sutilezas del manejo de errores para algo que de otra manera sería una pieza de código bastante simple. El código de solución no es para averiguar cómo manejar ciertos casos de error, es simplemente un desperdicio de ciclos cerebrales que podría usarse para un uso productivo.

Específicamente, lo que estoy tratando de aprender de errd es si hace que el manejo de errores sea más sencillo cuando se usa directamente. Por lo que puedo ver, se desvanecen muchas complicaciones y sutilezas. Sería bueno ver si podemos codificar aspectos de su semántica en nuevas características del lenguaje.

@jimmyfrasche

Introduce falsedad en el lenguaje para ganar su generalidad.

Ese es un muy buen punto. Los problemas habituales con la falsedad provienen de olvidar invocar una función booleana o de desreferenciar un puntero a nil.

Podríamos abordar este último definiendo el operador para que solo funcione con tipos nillables (y probablemente descartando =! como resultado, ya que sería en su mayoría inútil).

Podríamos abordar el primero al restringirlo aún más para que no funcione con tipos de función, o para que solo funcione con tipos de puntero o interfaz: entonces estaría claro que la variable no es booleana, y los intentos de usarla para comparaciones booleanas serían más obviamente mal.

¿Se permitiría algo como [ MustF ]?

Si.

Si se permite [ defer f.Close() :=? err { ], debe ser (de alguna manera) equivalente a
[ defer func() { … }() ].

No necesariamente, no. Podría tener su propia semántica (más como call/cc que como una función anónima). No he propuesto un cambio de especificación para usar =? en defer (requeriría al menos un cambio en la gramática), por lo que no estoy seguro de cuán complicada sería tal definición .

Los dos mayores problemas son […] 2. introspección de un error para determinar qué hacer en algunas situaciones

Estoy de acuerdo en que ese es un problema mayor en la práctica, pero parece más o menos ortogonal a este problema (que se trata más de reducir el texto estándar y el potencial asociado de errores).

( @rogpeppe , @davecheney , @dsnet , @crawshaw , yo y algunos otros seguramente estoy olvidando que tuvimos una buena discusión en GopherCon sobre API para inspeccionar errores, y espero que veamos algunas buenas propuestas en ese frente también , pero realmente creo que es un tema para otro tema).

@bcmills : este código tiene dos problemas 1) lo mismo que mencionó @rogpeppe : err pasado a CloseWithError es siempre nil, y 2) todavía no maneja pánicos, por lo que significa que la API informará el éxito explícitamente cuando haya un pánico (el r devuelto podría emitir un io.EOF incluso cuando no se hayan escrito todos los bytes), incluso si 1 es fijo.

De lo contrario, estoy de acuerdo en que el error devuelto por Close a menudo se puede ignorar. Sin embargo, no siempre (vea el primer ejemplo).

Me parece algo sorprendente que se hayan hecho 4 o 5 sugerencias defectuosas en mis ejemplos bastante sencillos (incluido uno de mí) y todavía siento que tengo que argumentar que el manejo de errores en Go no es trivial. :)

@bcmills :

Es cierto que pasa un error nulo a w.CloseWithError en caso de pánico, pero todo el programa termina en ese punto de todos modos.

¿Lo hace? Los diferidos de esa goroutine todavía se llaman. Por lo que tengo entendido, se ejecutarán hasta su finalización. En este caso, el cierre señalará un io.EOF.

Consulte, por ejemplo, https://play.golang.org/p/5CFbsAe8zF. Después de que la goroutine entre en pánico, todavía pasa felizmente "foo" a la otra goroutine, que luego sigue escribiendo en Stdout.

De manera similar, otro código puede recibir un io.EOF incorrecto de una goroutine en pánico (como la de su ejemplo), concluir con éxito y enviar felizmente un archivo a GS antes de que la goroutine en pánico reanude su pánico.

Su siguiente argumento puede ser: bueno, no escriba código con errores, pero:

  • luego facilite la prevención de estos errores, y
  • Los pánicos pueden ser causados ​​por factores externos, como OOM.

Si es importante no cerrar nunca con un error nulo, es un simple cambio de nombre más una línea adicional:

Todavía debería cerrarse con nil para señalar io.EOF cuando haya terminado, por lo que no funcionará.

Si es importante no cerrar nunca con un error nulo, es un simple cambio de nombre más una línea adicional:

Todavía debería cerrarse con nil para señalar io.EOF cuando haya terminado, por lo que no funcionará.

¿Por qué no? El return err al final establecerá rerr en nil .

@bcmills : ah ya veo lo que quieres decir ahora. Sí, eso debería funcionar. Sin embargo, no me preocupa la cantidad de líneas, sino la sutileza del código.

Encuentro que esto está en la misma categoría de problemas que el sombreado de variables, solo que es menos probable que se ejecute (posiblemente empeorando). La mayoría de los errores de sombreado de variables con los que se puede discutir con buenas pruebas unitarias. Los pánicos arbitrarios son más difíciles de detectar.

Cuando se opera a escala, está prácticamente garantizado que verá errores como este manifestándose. Puede que sea paranoico, pero he visto escenarios mucho menos probables que conducen a la pérdida y corrupción de datos. Normalmente esto está bien, pero no para el procesamiento de transacciones (como escribir archivos gs).

Espero que no le importe que secuestro su propuesta con una sintaxis alternativa: ¿cómo se siente la gente acerca de algo como esto?

return err if f, err := os.Open("..."); err != nil

@SirCmpwn Eso entierra al lede. Lo más fácil de leer en una función debería ser el flujo normal de control, no el manejo de errores.

Eso es justo, pero tu propuesta también me incomoda: introduce una sintaxis opaca (||) que se comporta de manera diferente a la que esperan los usuarios || a comportarse. No estoy seguro de cuál es la solución correcta, lo reflexionaré un poco más.

@SirCmpwn Sí, como dije en la publicación original "Estoy escribiendo esta propuesta principalmente para alentar a las personas que desean simplificar el manejo de errores de Go a pensar en formas de facilitar el contexto de los errores, no solo para devolver el error sin modificar . " Escribí mi propuesta lo mejor que pude, pero no espero que sea adoptada.

Comprendido.

Esto es un poco más radical, pero tal vez un enfoque basado en macros funcionaría mejor.

f = try!(os.Open("..."))

try! comería el último valor de la tupla y lo devolvería si no es nulo, y de lo contrario devolvería el resto de la tupla.

Me gustaría sugerir que nuestro enunciado del problema es,

El manejo de errores en Go es detallado y repetitivo. El formato idiomático del manejo de errores de Go hace que sea más difícil ver el flujo de control sin errores y la verbosidad es poco atractiva, especialmente para los recién llegados. Hasta la fecha, las soluciones propuestas para este problema requieren típicamente funciones de manejo de errores artesanales, reducen la ubicación del manejo de errores y aumentan la complejidad. Debido a que uno de los objetivos de Go es obligar al escritor a considerar el manejo y la recuperación de errores, cualquier mejora en el manejo de errores también debe basarse en ese objetivo.

Para abordar esta declaración de problema, propongo estos objetivos para mejorar el manejo de errores en Go 2.x:

  1. Reduce la repetición repetitiva de manejo de errores y maximiza el enfoque en la intención principal de la ruta del código.
  2. Fomenta el manejo adecuado de errores, incluida la envoltura de errores al propagarlos hacia adelante.
  3. Se adhiere a los principios de diseño de Go de claridad y simplicidad.
  4. Es aplicable en la gama más amplia posible de situaciones de manejo de errores.

Evaluando esta propuesta:

f.Close() =? err { return fmt.Errorf(…, err) }

de acuerdo con esos objetivos, concluiría que tiene un buen éxito en el objetivo n. ° 1. No estoy seguro de cómo ayuda con el n. ° 2, pero tampoco hace que agregar contexto sea menos probable (mi propia propuesta compartió esta debilidad en el n. ° 2). Sin embargo, realmente no tiene éxito en el n. ° 3 y en el n. ° 4:
1) Como han dicho otros, la comprobación y asignación del valor de error es opaca e inusual; y
2) La sintaxis =? también es inusual. Es especialmente confuso si se combina con la sintaxis =! similar pero diferente. La gente tardará un tiempo en acostumbrarse a sus significados; y
3) Devolver un valor válido junto con el error es lo suficientemente común como para que cualquier nueva solución también maneje ese caso.

Sin embargo, realizar el manejo de errores en un bloque puede ser una buena idea si, como han sugerido otros, se combina con cambios en gofmt . En relación con mi propuesta, mejora la generalidad, lo que debería ayudar con el objetivo n. ° 4 y la familiaridad que ayuda al objetivo n. ° 3 a costa de sacrificar la brevedad para el caso común de simplemente devolver el error con contexto adicional.

Si me hubiera preguntado en abstracto, podría haber estado de acuerdo en que una solución más general sería preferible a una solución específica de manejo de errores siempre que cumpliera con los objetivos de mejora de manejo de errores anteriores. Ahora, sin embargo, después de leer esta discusión y pensar más en ella, me inclino a creer que una solución específica para el manejo de errores dará como resultado una mayor claridad y simplicidad. Si bien los errores en Go son solo valores, el manejo de errores constituye una parte tan importante de cualquier programación que tener alguna sintaxis específica para hacer que el código de manejo de errores sea claro y conciso parece apropiado. Me temo que haremos un problema que ya es difícil (encontrar una solución limpia para el manejo de errores) aún más difícil y complicado si lo combinamos con otros objetivos como el alcance y la componibilidad.

Independientemente, sin embargo, como @rsc señala en su artículo, Toward Go 2 , ni el enunciado del problema, los objetivos ni ninguna propuesta de sintaxis es probable que avance sin informes de experiencia que demuestren que el problema es significativo. ¿Quizás en lugar de debatir varias propuestas de sintaxis, deberíamos empezar a buscar datos de apoyo?

Independientemente, sin embargo, como @rsc señala en su artículo, Toward Go 2, ni el enunciado del problema, los objetivos ni ninguna propuesta de sintaxis es probable que avance sin informes de experiencia que demuestren que el problema es significativo. ¿Quizás en lugar de debatir varias propuestas de sintaxis, deberíamos empezar a buscar datos de apoyo?

Creo que esto es evidente si asumimos que la ergonomía es importante. Abra cualquier base de código de Go y busque lugares donde haya oportunidades para SECAR las cosas y / o mejorar la ergonomía que el lenguaje pueda abordar; en este momento, el manejo de errores es un claro valor atípico. Creo que el enfoque Toward Go 2 puede abogar erróneamente por ignorar los problemas que tienen soluciones, en este caso, que la gente simplemente sonríe y lo soporta.

if $val, err := $operation($args); err != nil {
  return err
}

Cuando hay más texto estándar que código, el problema es evidente en mi humilde opinión.

@billyh

Siento que el formato: f.Close() =? err { return fmt.Errorf(…, err) } es demasiado detallado y confuso. Personalmente, no creo que la parte del error deba estar en un bloque. Inevitablemente, eso lo llevaría a distribuirse en 3 líneas en lugar de 1. Además, en el cambio de apagado que necesita hacer más que solo modificar un error antes de devolverlo, uno puede usar el actual if err != nil { ... } sintaxis.

El operador =? también es un poco confuso. No es inmediatamente obvio lo que está sucediendo allí.

Con algo como esto:
file := os.Open("/some/file") or raise(err) errors.Wrap(err, "extra context")
o la taquigrafía:
file := os.Open("/some/file") or raise
y el diferido:
defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2)
es un poco más prolijo y la elección de la palabra podría reducir la confusión inicial (es decir, la gente podría asociar inmediatamente raise con una palabra clave similar de otros lenguajes como python, o simplemente deducir que el aumento genera el error / last-non- valor predeterminado en la pila hasta la persona que llama).

También es una buena solución imperativa, que no intenta resolver todos los posibles errores oscuros bajo el sol. Con mucho, la mayor parte del manejo de errores en la naturaleza es de la naturaleza mencionada anteriormente. Para lo más tarde, la sintaxis actual también está ahí para ayudar.

Editar:
Si queremos reducir un poco la "magia", los ejemplos anteriores también podrían verse así:
file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")
file, err := os.Open("/some/file") or raise err
defer err2 := f.Close() or errors.ReplaceIfNil(err, err2)
Personalmente, creo que los ejemplos anteriores son mejores, ya que mueven el manejo de errores completo hacia la derecha, en lugar de dividirlo como es el caso aquí. Sin embargo, esto podría ser más claro.

Me gustaría sugerir que nuestro enunciado del problema es ...

No estoy de acuerdo con el planteamiento del problema. Me gustaría sugerir una alternativa:


El manejo de errores no existe desde el punto de vista del lenguaje. Lo único que proporciona Go es un tipo de error predeclarado e incluso eso es solo por conveniencia porque no habilita nada realmente nuevo. Los errores son solo valores . El manejo de errores es solo un código de usuario normal. No hay nada especial en él desde el punto de vista del idioma y no debería haber nada especial en él. El único problema con el manejo de errores es que algunas personas creen que esta valiosa y hermosa simplicidad debe eliminarse a cualquier costo.

En la línea de lo que dice Cznic, sería bueno tener una solución que sea útil para algo más que el manejo de errores.

Una forma de hacer que el manejo de errores sea más general es pensarlo en términos de tipos de unión / tipos de suma y desenvolvimiento. Swift y Rust tienen soluciones con? ! sintaxis, aunque creo que la de Rust ha sido un poco inestable.

Si no queremos convertir los tipos de suma en un concepto de alto nivel, podríamos convertirlo en parte de un retorno múltiple, de la misma forma en que las tuplas no son realmente parte de Go, pero aún puede hacer un retorno múltiple.

Una prueba de sintaxis inspirada en Swift:

func Failable() (*Thingie | error) {
    ...
}

guard thingie, err := Failable() else { 
    return wrap(err, "Could not make thingie)
}
// err is not in scope here

También puede usar esto para otras cosas, como:

guard val := myMap[key] else { val = "default" }

La solución =? propuesta por @bcmills y @jba no es solo por error, el concepto es distinto de cero. este ejemplo funcionará normalmente.

func Foo()(Bar, Recover){}
bar := Foo() =? recover { log.Println("[Info] Recovered:", recover)}

La idea principal de esta propuesta son las notas al margen, separar el propósito principal del código y dejar a un lado los casos secundarios, para facilitar la lectura.
Para mi la lectura de un código Go, en algunos casos, no es continua, muchas veces tienes la idea de parar con if err!= nil {return err} , entonces la idea de notas al margen me parece interesante, como en un libro que leemos la idea principal continuamente y luego lea las notas al margen. ( @jba hablar )
En situaciones muy raras, el error es el propósito principal de una función, tal vez en una recuperación. Normalmente, cuando tenemos un error, agregamos algo de contexto, registramos y devolvemos, en estos casos, las notas al margen pueden hacer que su código sea más legible.
No sé si es la mejor sintaxis, particularmente no me gusta el bloque en la segunda parte, una nota al margen debe ser pequeña, una línea debería ser suficiente

bar := Foo() =? recover: log.Println("[Info] Recovered:", recover)

@billyh

  1. Como han dicho otros, la comprobación y asignación de valores de error es opaca e inusual; y

Sea más concreto: "opacos e inusuales" son terriblemente subjetivos. ¿Puede dar algunos ejemplos de código en los que crea que la propuesta sería confusa?

  1. El =? la sintaxis también es inusual. […]

En mi opinión, eso es una característica. Si alguien ve un operador inusual, sospecho que está más inclinado a buscar lo que hace en lugar de simplemente asumir algo que puede o no ser exacto.

  1. Devolver un valor válido junto con el error es lo suficientemente común como para que cualquier nueva solución también maneje ese caso.

¿Lo hace?

Lea la propuesta con atención: =? realiza asignaciones antes de evaluar el Block , por lo que también podría usarse para ese caso:

n := r.Read(buf) =? err {
  if err == io.EOF {
    […]
  }
  return err
}

Y como señaló @nigeltao , siempre puede usar el patrón existente 'n, err: = r.Read (buf) `. Agregar una función para ayudar con el alcance y el texto estándar para el caso común no implica que debamos usarla también para casos poco comunes.

¿Quizás en lugar de debatir varias propuestas de sintaxis, deberíamos empezar a buscar datos de apoyo?

Vea los numerosos problemas (y sus ejemplos) que Ian vinculó en la publicación original.
Consulte también https://github.com/golang/go/wiki/ExperienceReports#error -handling.

Si ha obtenido información específica de esos informes, compártala.

@urandom

Personalmente, no creo que la parte del error deba estar en un bloque. Inevitablemente, eso llevaría a que se distribuyera en 3 líneas en lugar de 1.

El propósito del bloque es doble:

  1. para proporcionar una clara ruptura visual y gramatical entre la expresión que produce el error y su manejador, y
  2. para permitir una gama más amplia de manejo de errores (según el objetivo declarado de @ianlancetaylor en la publicación original).

3 líneas frente a 1 ni siquiera es un cambio de idioma: si el número de líneas es su mayor preocupación, podríamos abordarlo con un simple cambio a gofmt .

file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")
file, err := os.Open("/some/file") or raise err

Ya tenemos return y panic ; agregar raise encima parece que agrega demasiadas formas de salir de una función con muy poca ganancia.

defer err2 := f.Close() or errors.ReplaceIfNil(err, err2)

errors.ReplaceIfNil(err, err2) requeriría una semántica de paso por referencia muy inusual.
En su lugar, podría pasar err por puntero, supongo:

defer err2 := f.Close() or errors.ReplaceIfNil(&err, err2)

pero todavía me parece muy extraño. ¿El token or construye una expresión, una declaración u otra cosa? (Una propuesta más concreta ayudaría).

@carlmjohnson

¿Cuál sería la sintaxis y la semántica concretas de su declaración guard … else ? Para mí, se parece mucho a =? o :: con los tokens y las posiciones variables intercambiadas. (Una vez más, una propuesta más concreta ayudaría: ¿cuáles son la sintaxis y la semántica reales que tiene en mente?)

@bcmills
El hipotético ReplaceIfNil sería simple:

func ReplaceIfNil(original, replacement error) error {
   if original == nil {
       return replacement
   }
   return original
}

Nada inusual en eso. Quizás el nombre ...

or sería un operador binario, donde el operando izquierdo sería una IdentifierList o una PrimaryExpr. En el caso del primero, se reduce al identificador más a la derecha. Luego permite que se ejecute el operando de la derecha si el de la izquierda no es un valor predeterminado.

Es por eso que necesitaba otro token después, para hacer la magia de devolver los valores predeterminados, para todos excepto el último parámetro en la función Resultado, que tomaría el valor de la expresión después.
IIRC, hubo otra propuesta no hace mucho tiempo que haría que el lenguaje agregara un '...' o algo así, que tomaría el lugar de la tediosa inicialización del valor predeterminado. Por esa causa, todo podría verse así:

f, err := os.Open("/some/file") or return ..., errors.Wrap(err, "more context")

En cuanto al bloque, entiendo que permite un manejo más amplio. Personalmente, no estoy seguro de si el alcance de esta propuesta debería ser tratar de satisfacer todos los escenarios posibles, en lugar de cubrir un hipotético 80%. Y personalmente creo que importa cuántas líneas tomaría un resultado (aunque nunca dije que era mi mayor preocupación, de hecho es la legibilidad, o la falta de ella, cuando se usan tokens oscuros como =?). Si esta nueva propuesta abarca varias líneas en el caso general, personalmente no veo sus beneficios sobre algo como:

if f, err := os.Open("/some/file"); err != nil {
     return errors.Wrap(err, "more context")
}
  • si las variables definidas anteriormente estuvieran disponibles fuera del alcance if .
    Y eso aún haría que una función con solo un par de declaraciones de este tipo sea más difícil de leer, debido al ruido visual de estos bloques de manejo de errores. Y esa es una de las quejas que tiene la gente cuando habla sobre el manejo de errores en marcha.

@urandom

or sería un operador binario, donde el operando izquierdo sería una IdentifierList o una PrimaryExpr. […] Luego permite que se ejecute el operando de la derecha si el de la izquierda no es un valor predeterminado.

Los operadores binarios de Go son expresiones, no declaraciones, por lo que hacer de or un operador binario generaría muchas preguntas. (¿Cuál es la semántica de or como parte de una expresión más grande, y cómo concuerda con los ejemplos que ha publicado con := ?)

Suponiendo que en realidad es una declaración, ¿cuál es el operando de la derecha? Si es una expresión, ¿cuál es su tipo? ¿Se puede usar raise como expresión en otros contextos? Si es una declaración, ¿cuál es su semántica si no es raise ? ¿O está proponiendo que or raise sea ​​esencialmente una declaración única (por ejemplo, or raise como una alternativa de sintaxis a :: o =? )?

Puedo escribir

defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2) or raise(err3) Transform(err3)

?

Puedo escribir

f(r.Read(buf) or raise err)

?

defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2) or raise(err3) Transform(err3)

No, esto no sería válido debido al segundo raise . Si eso no estaba allí, entonces toda la cadena de transformación debería pasar y el resultado final debería devolverse a la persona que llama. Aunque tal semántica en su conjunto probablemente no sea necesaria, ya que solo puede escribir:

defer f.Close() or raise(err2) Transform(errors.ReplaceIfNil(err, err2)


f(r.Read(buf) or raise err)

Si asumimos mi comentario original, donde o tomaríamos el último valor del lado izquierdo, de modo que si fuera un valor predeterminado, la expresión final se evaluaría al resto de la lista de resultados; entonces sí, esto debería ser válido. En este caso, si r.Read devuelve un error, ese error se devuelve a la persona que llama. De lo contrario, n pasaría a f

Editar:

A menos que me confundan los términos, pienso en or como un operador binario, cuyos operandos tienen que ser del mismo tipo (pero un poco mágicos, si el operando de la izquierda es una lista de cosas, y en ese caso toma el último elemento de dicha lista de cosas). raise sería un operador unario que toma su operando y regresa de la función, usando el valor de ese operando como el valor del último argumento de retorno, y los anteriores tienen valores predeterminados. Luego, podría usar técnicamente raise en una declaración independiente, con el propósito de regresar de una función, también conocida como return ..., err

Este será el caso ideal, pero también estoy de acuerdo con que or raise sea ​​solo una alternativa de sintaxis a =? , siempre que también acepte una declaración simple en lugar de un bloque, de modo que cubren la mayoría de los casos de uso de una manera menos detallada. O también podemos usar una gramática similar a diferir, donde acepta una expresión. Esto cubriría la mayoría de casos como:

f := os.Open("/some/file") or raise(err) errors.Wrap(err, "with context")

y casos complejos:

f := os.Open or raise(err) func() {
     if err == io.EOF {
         […]
     }
  return err
}()

Pensando un poco más en mi propuesta, dejo un poco más sobre los tipos de unión / suma. La sintaxis que propongo es

guard [ ASSIGNMENT || EXPRESSION ] else { [ BLOCK ] }

En el caso de una expresión, la expresión se evalúa y si el resultado no es igual a true para expresiones booleanas o al valor en blanco para otras expresiones, se ejecuta BLOCK. En una asignación, el último valor asignado se evalúa para != true / != nil . Después de una declaración de guardia, cualquier asignación realizada estará dentro del alcance (no crea un nuevo alcance de bloque [¿excepto tal vez para la última variable?]).

En Swift, el BLOQUE para guard declaraciones debe contener uno de return , break , continue o throw . No he decidido si eso me gusta o no. Parece agregar algo de valor porque el lector sabe por la palabra guard lo que seguirá.

¿Alguien sigue a Swift lo suficientemente bien como para decir si esa comunidad considera bien guard ?

Ejemplos:

guard f, err := os.Open("/some/file") else { return errors.Wrap(err, "could not open:") }

guard data, err := ioutil.ReadAll(f) else { return errors.Wrap(err, "could not read:") }

var obj interface{}

guard err = json.Unmarshal(data, &obj) else { return errors.Wrap(err, "could not unmarshal:") }

guard m, _ := obj.(map[string]interface{}) else { return errors.New("unexpected data format") }

guard val, _ := m["key"] else { return errors.New("missing key") }

En mi humilde opinión, todo el mundo está discutiendo una gama demasiado amplia de problemas aquí a la vez, pero el patrón más común en realidad es "devolver el error como está". Entonces, ¿por qué no abordar la mayoría de los problemas con algo como:

code, err ?= fn()

lo que significa que la función debería devolver err! = nil?

para: = operador que podemos introducir?: =

code, err ?:= fn()

La situación con?: = parece ser peor debido al sombreado, ya que el compilador tendrá que pasar la variable "err" al mismo valor de retorno err nombrado.

De hecho, estoy bastante emocionado de que algunas personas se estén enfocando en facilitar la escritura del código correcto en lugar de simplemente acortar el código incorrecto.

Algunas notas:

Un interesante "informe de experiencia" de uno de los diseñadores de Midori en Microsoft sobre los modelos de error.

Creo que algunas ideas de este documento y Swift pueden aplicarse maravillosamente a Go2.

Al presentar una nueva palabra clave throws resguardada, las funciones se pueden definir como:

func Get() []byte throws {
  if (...) {
    raise errors.New("oops")
  }

  return []byte{...}
}

Intentar llamar a esta función desde otra función que no arroje resultados dará como resultado un error de compilación, debido a un error arrojable no controlado.
En cambio, deberíamos poder propagar el error, que todos están de acuerdo en que es un caso común, o manejarlo.

func ScrapeDate() time.Time throws {
  body := Get() // compilation error, unhandled throwable
  body := try Get() // we've been explicit about potential throwable

  // ...
}

Para los casos en los que sabemos que un método no fallará, o en las pruebas, podemos introducir try! similar a swift.

func GetWillNotFail() time.Time {
  body := Get() // compilation error, throwable not handled
  body := try Get() // compilation error, throwable can not be propagated, because `GetWillNotFail` is not annotated with throws
  body := try! Get() // works, but will panic on throws != nil

  // ...
}

Sin embargo, no estoy seguro de estos (similar a Swift):

func main() {
  // 1:
  do {
    fmt.Printf("%v", try ScrapeDate())
  } catch err { // err contains caught throwable
    // ...
  }

  // 2:
  do {
    fmt.Printf("%v", try ScrapeDate())
  } catch err.(type) { // similar to a switch statement
    case error:
      // ...
    case io.EOF
      // ...
  }
}

ps1. múltiples valores de retorno func ReadRune() (ch Rune, size int) throws { ... }
ps2. podemos regresar con return try Get() o return try! Get()
ps3. ahora podemos encadenar llamadas como buffer.NewBuffer(try Get()) o buffer.NewBuffer(try! Get())
ps4. No estoy seguro de las anotaciones (forma fácil de escribir errors.Wrap(err, "context") )
ps5. estas son en realidad excepciones
ps6. La mayor ganancia son los errores de tiempo de compilación para las excepciones ignoradas

Las sugerencias que escribe se describen exactamente en el enlace de Midori con todas las malas
lados de la misma ... Y una consecuencia obvia de "lanzamientos" será "personas
lo odio ". ¿Por qué debería uno escribir" arroja "cada vez durante la mayor parte de
funciones?

Por cierto, su intención de forzar la verificación de errores y no ignorarlos puede ser
aplicado a tipos que no son de error y, en mi humilde opinión, es mejor tenerlo de una manera más
forma generalizada (por ejemplo, gcc __attribute __ ((warn_unused_result))).

En cuanto a la forma del operador, sugeriría una forma corta o
formulario de palabra clave como este:

? = fn () O check fn () - propaga el error a la persona que llama
! = fn () O nofail fn () - pánico en caso de error

El sábado 26 de agosto de 2017 a las 12:15 p.m., nvartolomei [email protected]
escribió:

Algunas notas:

Un relato de experiencia interesante
http://joeduffyblog.com/2016/02/07/the-error-model/ de uno de
diseñadores de Midori en Microsoft sobre los modelos de error.

Creo que algunas ideas de este documento y Swift
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html
se puede aplicar maravillosamente a Go2.

Al introducir una nueva palabra clave de throws reseved, las funciones se pueden definir como:

func Get () [] byte throws {
Si (...) {
generar errores.Nuevo ("oops")
}

return [] byte {...}
}

Intentar llamar a esta función desde otra función que no arroje
dar como resultado un error de compilación, debido a un error arrojable no controlado.
En su lugar, deberíamos poder propagar el error, que todos están de acuerdo en que es un
caso común, o manejarlo.

func ScrapeDate () time.Time arroja {
cuerpo: = Get () // error de compilación, arrojable no manejado
body: = try Get () // hemos sido explícitos sobre el potencial arrojadizo

// ...
}

Para los casos en los que sabemos que un método no fallará, o en las pruebas, podemos
introducir probar! similar a rápido.

func GetWillNotFail () time.Time {
cuerpo: = Get () // error de compilación, arrojable no manejado
body: = try Get () // error de compilación, throwable no se puede propagar, porque GetWillNotFail no está anotado con throws
cuerpo: = prueba! Get () // funciona, pero entrará en pánico en los tiros! = Nil

// ...
}

Sin embargo, no estoy seguro de estos (similar a Swift):

func main () {
// 1:
hacer {
fmt.Printf ("% v", intente ScrapeDate ())
} catch err {// err contiene capturado arrojable
// ...
}

// 2:
hacer {
fmt.Printf ("% v", intente ScrapeDate ())
} catch err. (type) {// similar a una declaración de cambio
error de caso:
// ...
caso io.EOF
// ...
}
}

ps1. múltiples valores de retorno func ReadRune () (ch Rune, size int) throws {
...}
ps2. podemos regresar con return try Get () o return try! Obtener()
ps3. ahora podemos encadenar llamadas como buffer.NewBuffer (intente Get ()) o buffer.NewBuffer (intente!
Obtener())
ps4. No estoy seguro de las anotaciones

-
Estás recibiendo esto porque comentaste.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/21161#issuecomment-325106225 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AICzv9CLN77RmPceCqvjXVE_UZ6o7JGvks5sb-IYgaJpZM4Oi1c-
.

Creo que el operador propuesto por @jba y @bcmills es una idea muy bonita, aunque se lee mejor escrito como "??" en lugar de "=?" OMI.

Mirando este ejemplo:

func doStuff() (int,error) {
    x, err := f() 
    if err != nil {
        return 0, wrapError("f failed", err)
    }

    y, err := g(x)
    if err != nil {
        return 0, wrapError("g failed", err)
    }

    return y, nil
}

func doStuff2() (int,error) {
    x := f()  ?? (err error) { return 0, wrapError("f failed", err) }
    y := g(x) ?? (err error) { return 0, wrapError("g failed", err) }
    return y, nil
}

Creo que doStuff2 es considerablemente más fácil y rápido de leer porque:

  1. desperdicia menos espacio vertical
  2. es fácil de leer rápidamente el camino feliz en el lado izquierdo
  3. es fácil de leer rápidamente las condiciones de error en el lado derecho
  4. no tiene ninguna variable de error que contamine el espacio de nombres local de la función

Para mí, esta propuesta por sí sola parece incompleta y tiene demasiada magia. ¿Cómo se definiría el operador ?? ? ¿"Captura el último valor de retorno si no es cero"? "¿Captura el último valor de error si coincide con el tipo de método?"

Agregar nuevos operadores para manejar valores de retorno basados ​​en su posición y tipo parece un truco.

El 29 de agosto de 2017, 13:03 +0300, Mikael Gustavsson [email protected] , escribió:

Creo que el operador propuesto por @jba y @bcmills es una idea muy bonita, aunque se lee mejor escrito como "??" en lugar de "=?" OMI.
Mirando este ejemplo:
func doStuff () (int, error) {
x, err: = f ()
if err! = nil {
return 0, wrapError ("f falló", err)
}

   y, err := g(x)
   if err != nil {
           return 0, wrapError("g failed", err)
   }

   return y, nil

}

func doStuff2 () (int, error) {
x: = f () ?? (err error) {return 0, wrapError ("f falló", err)}
y: = g (x) ?? (err error) {return 0, wrapError ("g falló", err)}
return y, nil
}
Creo que doStuff2 es considerablemente más fácil y rápido de leer porque:

  1. desperdicia menos espacio vertical
  2. es fácil de leer rápidamente el camino feliz en el lado izquierdo
  3. es fácil de leer rápidamente las condiciones de error en el lado derecho
  4. no tiene ninguna variable de error que contamine el espacio de nombres local de la función

-
Estás recibiendo esto porque comentaste.
Responda a este correo electrónico directamente, véalo en GitHub o silencia el hilo.

@nvartolomei

¿Cómo se definiría el operador ?? ?

Consulte https://github.com/golang/go/issues/21161#issuecomment -319434101 y https://github.com/golang/go/issues/21161#issuecomment -320758279.

Dado que @bcmills recomendó resucitar un hilo inactivo, si vamos a considerar utilizar otros lenguajes, parece que los modificadores de declaraciones ofrecerían una solución razonable a todo esto. Para tomar el ejemplo de @slvmnd ,

func doStuff() (int, err) {
        x, err := f()
        return 0, wrapError("f failed", err)     if err != nil

    y, err := g(x)
        return 0, wrapError("g failed", err)     if err != nil

        return y, nil
}

No es tan conciso como tener la declaración y la verificación de errores en una sola línea, pero se lee razonablemente bien. (Sugeriría no permitir la forma: = de asignación en la expresión if, de lo contrario, los problemas de alcance probablemente confundirían a las personas incluso si son claras en la gramática) Permitir "a menos que" como versión negada de "si" es un poco de azúcar sintáctico, pero funciona bien para leer y valdría la pena considerarlo.

Sin embargo, no recomendaría cribing de Perl aquí. (Basic Plus 2 está bien) De esa manera se encuentran los modificadores de declaraciones en bucle que, aunque a veces son útiles, traen otro conjunto de problemas bastante complejos.

una versión más corta:
volver si err! = nil
entonces también debería ser compatible.

con tal sintaxis surge la pregunta: ¿deberían las declaraciones de no devolución también
apoyado con tales declaraciones "si", como esta:
func (args) si condición

tal vez en lugar de inventar una acción posterior, si vale la pena presentar un single
línea si es?

if err! = nil return
if err! = nil return 0, wrapError ("fallido", err)
si err! = nil do_smth ()

parece mucho más natural que las formas especiales de sintaxis, ¿no? Aunque supongo
introduce mucho dolor al analizar: /

Pero ... son solo pequeños ajustes y no soporte de idioma especial para errores
manipulación / propagación.

El lunes 18 de septiembre de 2017 a las 4:14 p.m., dsugalski [email protected] escribió:

Dado que @bcmills https://github.com/bcmills recomendó resucitar un
hilo inactivo, si vamos a considerar el uso de otros idiomas,
parece que los modificadores de declaraciones ofrecerían una solución razonable para todos
esta. Para tomar el ejemplo de @slvmnd https://github.com/slvmnd , rehecho con
modificadores de declaración:

func doStuff () (int, err) {
x, err: = f ()
return 0, wrapError ("f falló", err) if err! = nil

  y, err := g(x)
    return 0, wrapError("g failed", err)     if err != nil

    return y, nil

}

No es tan conciso como tener la declaración y la verificación de errores en una sola
línea, pero se lee razonablemente bien. (Sugeriría no permitir la: = forma de
asignación en la expresión if, de lo contrario, los problemas de alcance probablemente
confundir a la gente incluso si tienen clara la gramática) Permitir "a menos que" como
versión negada de "if" es un poco de azúcar sintáctico, pero funciona muy bien para
leer y valdría la pena considerarlo.

Sin embargo, no recomendaría cribing de Perl aquí. (Basic Plus 2 es
bien) De esa manera se encuentran los modificadores de declaraciones en bucle que, aunque a veces
útil, trae otro conjunto de cuestiones bastante complejas.

-
Estás recibiendo esto porque comentaste.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/21161#issuecomment-330215402 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AICzv1rfnXeGVRRwaigCyyVK_STj-i83ks5sjmylgaJpZM4Oi1c-
.

Pensando un poco más en la sugerencia de @dsugalski , no tiene la propiedad que @jba y otros han solicitado, es decir, que el código sin error sea visiblemente distinto del código de error. Todavía podría ser una idea interesante si también tiene beneficios significativos para las rutas de código sin error, pero cuanto más lo pienso, menos atractivo parece en comparación con las alternativas propuestas.

No estoy seguro de cuánta distinción visual es razonable esperar del texto puro. En algún momento, parece más apropiado apuntar eso al IDE o la capa de color de código de su editor de texto.

Pero para la distinción visible basada en texto, el estándar de formato que teníamos cuando comencé a usar esto hace un tiempo vergonzosamente largo era que los modificadores de declaraciones IF / UNLESS tenían que estar justificados a la derecha, lo que los hacía destacar lo suficientemente bien. (Aunque se le otorgó un estándar que era más fácil de hacer cumplir y quizás más distintivo visualmente en un terminal VT-220 que en editores con tamaños de ventana más flexibles)

Para mí, al menos, encuentro que el caso del modificador de declaración es fácilmente distinto y se lee mejor que el esquema if-block actual. Este puede no ser el caso para otras personas, por supuesto: leo el código fuente de la misma manera que leo el texto en inglés, por lo que se asigna a un patrón cómodo existente, y no todos lo hacen.

return 0, wrapError("f failed", err) if err != nil se puede escribir if err != nil { return 0, wrapError("f failed", err) }

if err != nil return 0, wrapError("f failed", err) se puede escribir igual.

¿Quizás todo lo que se necesita aquí es que gofmt deje if escritos en una sola línea en una sola línea en lugar de expandirlos a tres líneas?

Hay otra posibilidad que me sorprende. Gran parte de la fricción que experimento cuando trato de escribir código Go desechable rápidamente se debe a que tengo que verificar los errores en cada llamada, por lo que no puedo anidar bien las llamadas.

Por ejemplo, no puedo llamar a http.Client.Do en un nuevo objeto de solicitud sin primero asignar el resultado de http.NewRequest a una variable temporal y luego llamar a Do sobre eso.

Me pregunto si podríamos permitir:

f(y())

para que funcione incluso si y devuelve (T, error) tupla. Cuando y devuelve un error, el compilador podría abortar la evaluación de la expresión y hacer que ese error se devuelva desde f. Si f no devuelve un error, se le podría dar uno.

Entonces podría hacer:

n, err := http.DefaultClient.Do(http.NewRequest("DELETE", "/foo", nil))

y el resultado del error no sería nulo si NewRequest o Do fallaran.

Sin embargo, esto tiene un problema importante: la expresión anterior ya es válida si f acepta dos argumentos o argumentos variados. Además, es probable que las reglas exactas para hacer esto sean bastante complicadas.

Entonces, en general, no creo que me guste (tampoco estoy interesado en ninguna de las otras propuestas en este hilo), pero pensé en descartar la idea para su consideración de todos modos.

@rogpeppe o simplemente puede usar json.NewEncoder

@gbbr Ja sí, mal ejemplo.

Un mejor ejemplo podría ser http.Request. Cambié el comentario para usar eso.

Guau. Muchas ideas empeoran la legibilidad del código.
Estoy bien con el enfoque

if val, err := DoMethod(); err != nil {
   // val is accessible only here
   // some code
}

Solo una cosa es realmente molesta es el alcance de las variables devueltas.
En este caso, debe usar val pero está dentro del alcance de if .
Entonces tienes que usar else pero linter estará en contra (y yo también), y la única forma es

val, err := DoMethod()
if err != nil {
   // some code
}
// some code with val

Sería bueno tener acceso a las variables del bloque if :

if val, err := DoMethod(); err != nil {
   // some code
}
// some code with val

@dmbreaker Para eso es esencialmente la cláusula de protección de Swift. Asigna una variable dentro del ámbito actual si pasa alguna condición. Vea mi comentario anterior .

Estoy totalmente a favor de simplificar el manejo de errores en Go (aunque personalmente no me importa mucho), pero creo que esto agrega un poco de magia a un lenguaje que, por lo demás, es simple y extremadamente fácil de leer.

@gbbr
¿Qué es el 'esto' al que te refieres aquí? Hay bastantes sugerencias diferentes sobre cómo hacer las cosas.

¿Quizás una solución de dos partes?

Defina try como "quitar el valor más a la derecha en la tupla de retorno; si no es el valor cero para su tipo, devuélvalo como el valor más a la derecha de esta función con los otros establecidos en cero". Esto hace que el caso común

 a := try ErrorableFunction(b)

y permite encadenar

 a := try ErrorableFunction(try SomeOther(b, c))

(Opcionalmente, hágalo distinto de cero en lugar de distinto de cero, para mayor eficiencia). Si las funciones con errores devuelven un valor distinto de cero, la función "aborta con un valor". El valor más a la derecha de la función try 'ed debe ser asignable al valor más a la derecha de la función que llama o es un error de verificación de tipo en tiempo de compilación. (Por lo tanto, esto no está codificado para manejar solo error , aunque quizás la comunidad debería desalentar su uso para cualquier otro código "inteligente").

Luego, permita que los retornos de prueba se detecten con una palabra clave similar a aplazar, ya sea:

catch func(e error) {
    // whatever this function returns will be returned instead
}

o, quizás de forma más detallada, pero más en línea con la forma en que Go ya funciona:

defer func() {
    if err := catch(); err != nil {
        set_catch(ErrorWrapper{a, "while posting request to server"})
    }
}()

En el caso catch , el parámetro de la función debe coincidir exactamente con el valor que se devuelve. Si se proporcionan varias funciones, el valor pasará por todas ellas en orden inverso. Por supuesto, puede poner un valor que se resuelva en una función del tipo correcto. En el caso del ejemplo basado en defer , si una defer func llama a set_catch la siguiente función diferida obtendrá ese valor de catch() . (Si es lo suficientemente tonto como para volver a establecerlo en cero en el proceso, obtendrá un valor de retorno confuso. No haga eso). El valor pasado a set_catch debe ser asignable al tipo devuelto. En ambos casos, espero que esto funcione como defer en el sentido de que es una declaración, no una declaración, y solo se aplicará al código después de que se ejecute la declaración.

Tiendo a preferir la solución basada en aplazamiento desde una perspectiva de simplicidad (básicamente no se introducen nuevos conceptos allí, es un segundo tipo de recover() lugar de algo nuevo), pero reconozco que puede tener algunos problemas de rendimiento. Tener una palabra clave de captura separada podría permitir una mayor eficiencia al ser más fácil omitir por completo cuando se produce un retorno normal, y si uno desea obtener la máxima eficiencia, tal vez vincularlos a los alcances para que solo uno pueda estar activo por alcance o función , lo que, creo, tendría un costo casi nulo. (¿Posiblemente el nombre del archivo del código fuente y el número de línea también deberían devolverse desde la función de captura? Es barato en el momento de la compilación hacer eso y evitaría algunas de las razones por las que las personas piden un seguimiento de pila completo en este momento).

Cualquiera de los dos también permitiría que el manejo de errores repetitivos se manejara de manera efectiva en un lugar dentro de una función, y permitiría que el manejo de errores se ofreciera fácilmente como una función de biblioteca, que es en mi humilde opinión uno de los peores aspectos del caso actual, según los comentarios de rsc anteriores; la laboriosidad del manejo de errores tiende a alentar el "error de devolución" en lugar de un manejo correcto. Sé que yo mismo lucho mucho con eso.

@thejerf Parte del punto de Ian con esta propuesta es explorar formas de abordar el texto estándar de errores sin desalentar a las funciones de agregar contexto o manipular los errores que devuelven.

Separar el manejo de errores en try y catch parece que iría en contra de ese objetivo, aunque supongo que eso depende de qué tipo de detalles los programas normalmente querrán agregar.

Como mínimo, me gustaría ver cómo funciona con ejemplos más realistas.

El objetivo de mi propuesta es permitir agregar contexto o manipular los errores, de una manera que considero más correcta desde el punto de vista programático que la mayoría de las propuestas aquí que implican repetir ese contexto una y otra vez, lo que a su vez inhibe el deseo de poner el contexto adicional en .

Para reescribir el ejemplo original,

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

sale como

func Chdir(dir string) error {
    catch func(e error) {
        return &PathError{"chdir", dir, e}
    }

    try syscall.Chdir(dir)
    return nil
}

excepto que este ejemplo es demasiado trivial para, bueno, cualquiera de estas propuestas, en realidad, y yo diría que en este caso dejaríamos la función original en paz.

Personalmente, no considero que la función Chdir original sea un problema en primer lugar. Estoy ajustando esto específicamente para abordar el caso en el que una función se interrumpe por un manejo prolongado de errores repetidos, no para una función de error único. También diría que si tiene una función en la que literalmente está haciendo algo diferente para cada posible caso de uso, probablemente la respuesta correcta sea seguir escribiendo lo que ya tenemos. Sin embargo, sospecho que ese es, con mucho, el caso poco común para la mayoría de las personas, sobre la base de que si ese _ fuera_ el caso común, no habría una queja en primer lugar. El ruido de comprobar el error sólo es significativo precisamente porque la gente quiere hacer "casi lo mismo" una y otra vez en una función.

También sospecho que la mayoría de lo que la gente quiere se cumpliría con

func SomethingBigger(dir string) (interface{}, error) {
     catch func (e error, filename string, lineno int) {
         return PackageSpecificError{e, filename, lineno, dir}
     }

     x := try Something()

     if x == true {
         try SomethingElse()
     } else {
         a, b = try AThirdThing()
     }

     return whatever, nil
}

Si eliminamos el problema de tratar de hacer que una sola declaración if se vea bien con el argumento de que es demasiado pequeña para preocuparse por ella, y eliminamos el problema de una función que realmente está haciendo algo único para cada error, se basa en que A : ese es en realidad un caso bastante raro y B: en ese caso, la sobrecarga estándar en realidad no es tan significativa frente a la complejidad del código de manejo único, tal vez el problema se pueda reducir a algo que tenga una solución.

Yo también realmente quiero ver

func packageSpecificHandler(f string) func (err error, filename string, lineno int) {
    return func (err error, filename string, lineno int) {
        return &PackageSpecificError{"In function " + f, err, filename, lineno}
    }
}

 func SomethingBigger(dir string) (interface{}, error) {
     catch packageSpecificHandler("SomethingBigger")

     ...
 }

o algún equivalente sea posible, para cuando eso funcione.

Y, de todas las propuestas en la página ... ¿no se parece todavía a Go? Se parece más a Go que al Go actual.

Para ser honesto, la mayor parte de mi experiencia profesional en ingeniería ha sido con PHP (lo sé), pero el principal atractivo de Go fue siempre la legibilidad. Si bien disfruto de algunos aspectos de PHP, la pieza que más desprecio son las tonterías "estáticas" "abstractas" "finales" y la aplicación de conceptos demasiado complicados a un fragmento de código que hace una sola cosa.

Ver esta propuesta me dio un flashback inmediato a la sensación de mirar una pieza y tener que hacer una doble toma y realmente "pensar" en lo que esa pieza de código está diciendo / haciendo. No creo que este código sea legible y realmente no se agregue al lenguaje. Mi primer instinto es mirar a la izquierda y creo que esto siempre devuelve nil . Sin embargo, con este cambio ahora tendría que mirar hacia la izquierda y hacia la derecha para determinar el comportamiento del código, lo que significa más tiempo de lectura y más modelo mental.

Sin embargo, esto no significa que no haya lugar para mejoras en el manejo de errores en Go.

Lo siento, no he leído (todavía) este hilo completo (es muy largo) pero veo que la gente arroja una sintaxis alternativa, así que me gustaría compartir mi idea:

a, err := helloWorld(); err? {
  return fmt.Errorf("helloWorld failed with %s", err)
}

Espero no haberme perdido algo arriba que anule esto. Prometo que algún día leeré todos los comentarios :)

El operador tendría que estar permitido solo en el tipo error , creo, para evitar el desorden semántico de conversión de tipos.

Interesante, @buchanae , pero nos

if a, err := helloWorld(); err != nil {
  return fmt.Errorf("helloWorld failed with %s", err)
}

Veo que permitiría a a escapar, mientras que en el estado actual, tiene el alcance de los bloques then y else.

@ object88 Tienes razón, el cambio es sutil, estético y subjetivo. Personalmente, todo lo que quiero de Go 2 sobre este tema es un cambio sutil de legibilidad.

Personalmente, lo encuentro más legible porque la línea no comienza con if y no requiere !=nil . Las variables están en el borde izquierdo donde están (¿la mayoría?) De otras líneas.

Gran punto sobre el alcance de a , no lo había considerado.

Considerando las otras posibilidades de esta gramática, parece que esto es posible.

err := helloWorld(); err? {
  return fmt.Errorf("error: %s", err)
}

y probablemente

helloWorld()? {
  return fmt.Errorf("hello world failed")
}

que es quizás donde se derrumba.

Tal vez devolver un error debería ser parte de cada llamada de función en Go, por lo que podría imaginar:
''
a: = holaMundo (); ¿errar? {
return fmt.Errorf ("helloWorld falló:% s", err)
}

¿Qué hay de tener un manejo de excepciones real? Me refiero a probar, atrapar, finalmente en cambio, como muchos lenguajes modernos.

No, hace que el código sea implícito y poco claro (aunque en realidad es un poco más corto)

El jueves 23 de noviembre de 2017 a las 07:27, Kamyar Miremadi [email protected]
escribió:

¿Qué hay de tener un manejo de excepciones real? Me refiero a intentar, atrapar, finalmente
en cambio, como muchos lenguajes modernos?

-
Estás recibiendo esto porque comentaste.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/21161#issuecomment-346529787 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AICzvyy_kGAlcs6RmL8AKKS5deNRU4_5ks5s5PQVgaJpZM4Oi1c-
.

Volviendo a @mpvl 's WriteToGCS ejemplo arriba-hilo , me gustaría sugerir (una vez más) que el commit / rollback patrón no es lo suficientemente común como para justificar un cambio importante en el tratamiento de errores de Go. No es difícil capturar el patrón en una función ( enlace del patio de recreo ):

func runWithCommit(f, commit func() error, rollback func(error)) (err error) {
    defer func() {
        if r := recover(); r != nil {
            rollback(fmt.Errorf("panic: %v", r))
            panic(r)
        }
    }()
    if err := f(); err != nil {
        rollback(err)
        return err
    }
    return commit()
}

Entonces podemos escribir el ejemplo como

func writeToGCS(ctx context.Context, bucket, dst string, r io.Reader) error {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    return runWithCommit(
        func() error { _, err := io.Copy(w, r); return err },
        func() error { return w.Close() },
        func(err error) { _ = w.CloseWithError(err) })
}

Sugeriría una solución más simple:

func someFunc() error {
    ^err := someAction()
    ....
}

Para múltiples retornos de función múltiple:

func someFunc() error {
    result, ^err := someAction()
    ....
}

Y para múltiples argumentos de retorno:

func someFunc() (result Result, err error) {
    var result Result
    params, ^err := someAction()
    ....
}

^ sign significa retorno si el parámetro no es nil.
Básicamente, "mueva el error hacia arriba en la pila si ocurre"

¿Algún inconveniente de este método?

@gladkikhartem
¿Cómo se modificaría el error antes de que se devuelva?

@urandom
Envolver errores es una acción importante que, en mi opinión, debería hacerse de forma explícita.
Go code tiene que ver con la legibilidad, no con la magia.
Me gustaría mantener el envoltorio de errores más claro

Pero al mismo tiempo, me gustaría deshacerme del código que no contiene mucha información y solo ocupa el espacio.

if err != nil {
    return err
}

Es como un cliché de Go: no quiere leerlo, simplemente quiere saltearlo.

Lo que he visto hasta ahora en esta discusión es una combinación de:

  1. reducir la verbosidad de la sintaxis
  2. mejorar el error agregando contexto

Esto está en línea con la descripción del problema original de @ianlancetaylor que menciona ambos aspectos, sin embargo, en mi opinión, los dos deben discutirse / definirse / experimentarse por separado y posiblemente en diferentes iteraciones para limitar el alcance de los cambios y solo por razones de efectividad (un un cambio más grande en el idioma es más difícil de hacer que uno incremental).

1. Reducción de la verbosidad de la sintaxis

Me gusta la idea de @gladkikhartem , incluso en su forma original que

 result, ^ := someAction()

En el contexto de una función:

func getOddResult() (int, error) {
    result, ^ := someResult()
    if result % 2 == 0 {
          return result + 1, nil
    }
    return result, nil
}

Esta breve sintaxis, o en la forma propuesta por @gladkikhartem con err^ , abordaría la parte de la verbosidad de la sintaxis del problema (1).

2. Contexto de error

Para la segunda parte, agregando más contexto, podríamos incluso olvidarnos por completo de él por ahora y más adelante proponer agregar automáticamente un seguimiento de pila a cada error si se usa un tipo especial contextError . Este nuevo tipo de error nativo podría tener trazas de pila cortas o completas (imagina un GO_CONTEXT_ERROR=full ) y ser compatible con la interfaz error tiempo que ofrece la posibilidad de extraer al menos la función y el nombre de archivo de la pila de llamadas superior entrada.

Cuando se usa un contextError , de alguna manera, Go debe adjuntar el seguimiento de la pila de llamadas exactamente en el punto donde se crea el error.

De nuevo con un ejemplo de función:

func getOddResult() (int, contextError) {
    result, ^ := someResult() // here a 'contextError' is created; if the error received from 'someResult()' is also a `contextError`, the two are nested
    if result % 2 == 0 {
          return result + 1, nil
    }
    return result, nil
}

Solo el tipo cambió de error a contextError , que podría definirse como:

type contextError interface {
    error
    Stack() []StackEntry
    Cause() contextError
}

(observe cómo este Stack() es diferente de https://golang.org/pkg/runtime/debug/#Stack, ya que esperamos tener una versión sin bytes de la pila de llamadas goroutine aquí)

El método Cause() devolvería cero o el contextError como resultado del anidamiento.

Soy muy consciente de las posibles implicaciones de memoria de llevar pilas como esta, por lo que insinué la posibilidad de tener una pila corta predeterminada que contendría solo 1 o algunas entradas más. Un desarrollador normalmente habilitaría los stracktraces completos en las versiones de desarrollo / depuración y dejaría el valor predeterminado (short stacktraces) de lo contrario.

Estado de la técnica:

Solo para pensar.

@gladkikhartem @ gdm85

Creo que ha perdido el sentido de esta propuesta. Según la publicación original de Ian:

Ya es fácil (quizás demasiado fácil) ignorar el error (ver # 20803). Muchas propuestas existentes para el manejo de errores facilitan la devolución del error sin modificar (por ejemplo, # 16225, # 18721, # 21146, # 21155). Pocos facilitan la devolución del error con información adicional.

Devolver errores sin modificar suele ser incorrecto y, por lo general, al menos es inútil. Queremos fomentar un manejo cuidadoso de los errores: abordar solo el caso de uso de "devolución sin modificar" sesgaría los incentivos en la dirección incorrecta.

@bcmills si se

El "retorno sin modificar" podría contrarrestarse como se explicó anteriormente con "retorno sin modificar con stacktrace" de forma predeterminada, y (en un estilo reactivo) agregar un mensaje legible por humanos según sea necesario. No he especificado cómo se podría agregar un mensaje legible por humanos, pero se puede ver cómo funciona el ajuste en pkg/errors para algunas ideas.

"Devolver errores sin modificar es a menudo incorrecto": por lo tanto, propongo una ruta de actualización para el caso de uso perezoso, que es el mismo caso de uso actualmente señalado como perjudicial.

@bcmills
Estoy 100% de acuerdo con # 20803 en que los errores siempre deben manejarse o ignorarse explícitamente (y no tengo idea de por qué esto no se hizo antes ...)
sí, no abordé el punto de la propuesta y no tengo que hacerlo. Me importa la solución real propuesta, no las intenciones detrás de ella, porque las intenciones no coinciden con los resultados. Y cuando veo || tal || cosas que se proponen, me pone realmente triste.

Si incrustar información, como códigos de error y mensajes de error en el error, fuera fácil y transparente, no es necesario que fomente el manejo cuidadoso de los errores, las personas lo harán por sí mismas.
Por ejemplo, simplemente haga de error un alias. Podríamos devolver cualquier tipo de material y usarlo fuera de la función sin emitir contenido. Haría la vida mucho más fácil.

Me encanta que Go me recuerde que debo manejar los errores, pero odio cuando el diseño me anima a hacer algo que es cuestionable.

@ gdm85
Agregar seguimiento de pila a un error automáticamente es una idea terrible, solo mire y rastro de pila de Java.
Cuando corrige los errores usted mismo, es mucho más fácil navegar y comprender qué está fallando. Ese es el objetivo de envolverlo.

@gladkikhartem No estoy de acuerdo con que una forma de "envoltura automática" sea mucho peor para navegar y para ayudar a comprender qué va mal. Tampoco entiendo exactamente a qué te refieres en los seguimientos de pila de Java (supongo que de excepciones? ¿Estéticamente feo? ¿Qué problema específico?), Pero para discutir en una dirección constructiva: ¿cuál podría ser una buena definición de "error cuidadosamente manejado"?

Les pido que mejoren mi comprensión de las mejores prácticas de Go (por más o menos canónicas que sean) y porque siento que esa definición podría ser clave para hacer alguna propuesta que mejore la situación actual.

@gladkikhartem Sé que esta propuesta ya está por todos lados, pero hagamos lo que podamos para mantenerla enfocada en los objetivos que me propuse inicialmente. Como dije al publicar este número, ya hay varias propuestas diferentes que tratan de simplificar if err != nil { return err } , y ese es el lugar para discutir la sintaxis que solo mejora ese caso específico. Gracias.

@ianlancetaylor
Lo siento si moví la discusión fuera de su camino.

Si desea agregar información de contexto a un error, sugeriría usar esta sintaxis:
(y obligar a las personas a usar solo un tipo de error para una función para facilitar la extracción del contexto)

type MyError struct {
    Type int
    Message string
    Context string
    Err error
}

func VeryLongFunc() error {
    var err MyError
    err.Context = "general function context"


   result, ^err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
   }

    // in case we need to make a cleanup after error

   result, ^err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       file.Close()
   }

   // another variant with different symbol and return statement

   result, ?err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       return err
   }

   // using original approach

   result, err.Err := someAction()
   if err != nil {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       return err
   }
}

func main() {
    err := VeryLongFunc()
    if err != nil {
        e := err.(MyError)
        log.Print(e.Error(), " in ", e.Dir)
    }
}

El símbolo ^ se usa para indicar el parámetro de error, así como para diferenciar la definición de la función del manejo de errores para "someAction () {}"
{} podría omitirse si el error se devuelve sin modificar

Añadiendo algunos recursos más para responder a mi propia invitación para definir mejor el "manejo cuidadoso de errores":

Por tedioso que sea el enfoque actual, creo que es menos confuso que las alternativas, aunque ¿las declaraciones de una línea si podrían funcionar? ¿Quizás?

blah, err := doSomething()
if err != nil: return err

...o incluso...

blah, err := doSomething()
if err != nil: return &BlahError{"Something",err}

Es posible que alguien ya haya mencionado esto, pero hay muchas, muchas publicaciones y he leído muchas de ellas, pero no todas. Dicho esto, personalmente creo que sería mejor ser explícito que implícito.

He sido un fanático de la programación orientada al ferrocarril, la idea vino de la declaración with de Elixir.
else bloque e == nil cortocircuitado.

Aquí está mi propuesta con pseudo código por delante:

func Chdir(dir string) (e error) {
    with e == nil {
            e = syscall.Chdir(dir)
            e, val := foo()
            val = val + 1
            // something else
       } else {
           printf("e is not nil")
           return
       }
       return nil
}

@ardhitama ¿No es esto como Try catch, excepto que "With" es como la declaración "Try" y que "Else" es como "Catch"?
¿Por qué no implementar el manejo de excepciones como Java o C #?
ahora mismo en go si un programador no quiere manejar la excepción en esa función, la devuelve como resultado de esa función. Aún así, no hay forma de obligar a los programadores a manejar una excepción si no quieren y muchas veces realmente no es necesario, pero lo que obtenemos aquí son muchas declaraciones if err! = Nil que hacen que el código sea feo y no legible (mucho ruido). ¿No es la razón por la que la declaración Try Catch Finalmente se inventó en primer lugar en otro lenguaje de programación?

Por lo tanto, creo que es mejor si los autores de Go "Intenten" no ser tercos. e introduzca la declaración "Try Catch Finalmente" en las próximas versiones. Gracias.

@KamyarM
No puede introducir el manejo de excepciones en Go, porque no hay excepciones en Go.
Introducir try {} catch {} en Go es como introducir try {} catch {} en C: es totalmente incorrecto .

@ianlancetaylor
¿Qué hay de no cambiar el manejo de errores de Go en absoluto, sino cambiar la herramienta gofmt como esta para el manejo de errores de una sola línea?

err := syscall.Chdir(dir)
    if err != nil {return &PathError{"chdir", dir, err}}
err = syscall.Chdir(dir2)
    if err != nil {return err}

Es compatible con versiones anteriores y puede aplicarlo a sus proyectos actuales.

Las excepciones son las declaraciones goto decoradas, que convierten su pila de llamadas en un gráfico de llamadas y hay una buena razón por la que la mayoría de los proyectos no académicos serios impiden o limitan su uso. Un objeto con estado llama a un método que transfiere el control arbitrariamente hacia arriba en la pila y luego reanuda la ejecución de instrucciones ... suena como una mala idea porque lo es.

@KamyarM En esencia lo es, pero en la práctica no lo es. En mi opinión, porque estamos siendo explícitos aquí y no estamos rompiendo ningún modismo de Go.

¿Por qué?

  1. Las expresiones dentro de la declaración with no pueden declarar una nueva var, por lo tanto, establece explícitamente que la intención es evaluar las vars fuera del bloque.
  2. Las declaraciones dentro de with se comportarán como dentro del bloque try y catch . De hecho, será más lento ya que en cada instrucción siguiente que necesita evaluar las condiciones de with en el peor de los casos.
  3. Por diseño, la intención es para eliminar el exceso if s y no crear gestor de excepciones como el controlador será siempre local (el with 's expresión y else bloque).
  4. No es necesario desenrollar la pila debido a throw

PD. Por favor corrígeme si estoy equivocado.

@ardhitama
KamyarM tiene razón en el sentido de que con una declaración se ve tan feo como try catch y también introduce un nivel de sangría para el flujo de código normal.
Ni siquiera mencionar la idea de la propuesta original de modificar cada error individualmente. Simplemente no va a funcionar elegantemente con try catch , ni con ningún otro método que agrupe declaraciones.

@gladkikhartem
Sí, por eso propongo adoptar una "programación orientada a los ferrocarriles" en su lugar y tratar de no eliminar lo explícito. Es solo otro ángulo para atacar el problema, las otras soluciones quieren resolverlo no permitiendo que el compilador escriba automáticamente if err != nil por usted.

with no solo para el manejo de errores, puede ser útil para cualquier otro flujo de control.

@gladkikhartem
Déjame aclarar que creo que el bloque Try Catch Finally es hermoso. If err!=nil ... es en realidad el código feo.

Go es solo un lenguaje de programación. Hay muchos otros idiomas. Descubrí que muchos en la Comunidad Go lo ven como su religión y no están abiertos a cambiar o admitir los errores. Esto está mal.

@gladkikhartem

Estoy bien si los autores de Go lo llaman Go ++ o Go # o GoJava e introducen The Try Catch Finally allí;)

@KamyarM

Evitar cambios innecesarios es necesario, fundamental, para cualquier esfuerzo de ingeniería. Cuando la gente dice cambio en este contexto, realmente quiere decir _cambio para mejor_, que transmiten de manera efectiva con _argumentos_ que guían una premisa hacia la conclusión deseada.

La apelación de "¡abre tu mente, hombre!" No es convincente. Irónicamente, intenta decir que algo que la mayoría de los programadores ven como antiguo y torpe es _nuevo y mejorado_.

También hay muchas propuestas y discusiones en las que la comunidad de Go analiza errores anteriores. Pero estoy de acuerdo contigo cuando dices que Go es solo un lenguaje de programación. Lo dice en el sitio web de Go y en otros lugares, y he hablado con algunas personas que también lo confirmaron.

Descubrí que muchos en la Comunidad Go lo ven como su religión y no están abiertos a cambiar o admitir los errores.

Go se basa en la investigación académica; las opiniones personales no importan.

Incluso los principales desarrolladores del compilador C # de Microsoft reconocieron públicamente que las _excepciones_ son una mala manera de administrar los errores, al tiempo que valoran el modelo Go / Rust como una mejor alternativa: http://joeduffyblog.com/2016/02/07/the-error-model/

Seguramente, hay espacio para mejorar el modelo de error de Go, pero no adoptando soluciones similares a las excepciones, ya que solo agregan una complejidad descomunal a cambio de unos pocos beneficios cuestionables.

@ Dr-Terrible Gracias por el artículo.

Pero no encontré ningún lugar que mencione a GoLang como lenguaje académico.

Por cierto, para aclarar mi punto, en este ejemplo

func Execute() error {
    err := Operation1()
    if err!=nil{
        return err
    }

    err = Operation2()
    if err!=nil{
        return err
    }

    err = Operation3()
    if err!=nil{
        return err
    }

    err = Operation4()
    return err
}

Es similar a implementar el manejo de excepciones en C # así:

         public void Execute()
        {

            try
            {
                Operation1();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation2();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation3();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation4();
            }
            catch (Exception)
            {
                throw;
            }
        }

¿No es esa una forma terrible de manejo de excepciones en C #? Mi respuesta es sí, ¡no sé la tuya! En Go no tengo otra opción. Es esa terrible elección o autopista. Así es en GO y no tengo elección.

Por cierto, como también se menciona en el artículo que compartió, cualquier lenguaje puede implementar el manejo de errores como Go sin necesidad de ninguna sintaxis adicional, por lo que Go en realidad no implementó ninguna forma revolucionaria de manejo de errores. Simplemente no tiene ninguna forma de manejo de errores, por lo que está limitado a usar la declaración If para el manejo de errores.

Por cierto, sé que GO tiene una Panic, Recover , Defer no recomendada que es similar a Try Catch Finally pero, en mi opinión personal, la sintaxis Try Catch Finally es mucho más limpia y mejor organizada para manejar las excepciones.

@ Dr-Terrible

También por favor mira esto:
https://github.com/manucorporat/try

@KamyarM , no dijo que Go es un lenguaje académico, dijo que se basa en la investigación académica. Tampoco fue el artículo sobre Go, pero investiga el paradigma de manejo de errores empleado por Go.

Si encuentra que manucorporat/try funciona para usted, utilícelo en su código. Pero los costos (rendimiento, complejidad del idioma, etc.) de agregar try/catch al idioma en sí no valen la pena.

@KamyarM
Tu ejemplo no es exacto. Alternativa a

    err := Operation1()
    if err!=nil {
        return err
    }
    err = Operation2()
    if err!=nil{
        return err
    }
    err = Operation3()
    if err!=nil{
        return err
    }
    return Operation4()

estarán

            Operation1();
            Operation2();
            Operation3();
            Operation4();

el manejo de excepciones parece una opción mucho mejor en este ejemplo. En teoría debería ser bueno, pero en la práctica
tiene que responder con un mensaje de error preciso para cada error ocurrido en su punto final.
Toda la aplicación en Go suele tener un 50% de manejo de errores.

         err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

Y si la gente tiene una herramienta tan poderosa como try catch, estoy 100% seguro de que la usarán en exceso a favor de un manejo cuidadoso de los errores.

Es interesante que se mencione la academia, pero Go es una colección de lecciones aprendidas de la experiencia práctica. Si el objetivo es escribir una API incorrecta que devuelva mensajes de error incorrectos, el manejo de excepciones es el camino a seguir.

Sin embargo, no quiero un error invalid HTTP header cuando mi solicitud contiene un JSON request body formato incorrecto, el manejo de excepciones es el botón mágico de disparar y olvidar que logra eso en las API de C ++ y C # que usan ellos.

Para una gran cobertura de API, es imposible proporcionar suficiente contexto de error para lograr un manejo de errores significativo. Esto se debe a que cualquier buena aplicación tiene un 50% de manejo de errores en Go, y debe ser un 90% en un idioma que requiera una transferencia de control no local para manejar errores.

@gladkikhartem

La forma alternativa que mencionaste es la forma correcta de escribir el código en C #. Son solo 4 líneas del código y muestra la ruta de ejecución feliz. No tiene esos if err!=nil ruidos. Si ocurre una excepción, la función que se preocupa por esas excepciones puede manejarla usando Try Catch Finally (Puede ser la misma función en sí misma o la persona que llama o la persona que llama de la persona que llama o la persona que llama de la persona que llama de la persona que llama de la persona que llama ... o simplemente un controlador de eventos que procesa todos los errores no manejados en una aplicación. El programador tiene diferentes opciones).

err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

Parece simple pero eso es complicado. Supongo que podría generar un tipo de error personalizado que lleve el error del sistema, el error del usuario (sin filtrar el estado interno al usuario que podría no tener las mejores intenciones) y el código HTTP.

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

Pero pruébalo

func Chdir(dir string) error {
    return  syscall.Chdir(dir) ? &PathError{"chdir", dir, err}:nil;
}
func Chdir(dir string) error {
    return  syscall.Chdir(dir) ? &PathError{"chdir", dir, err};
}



md5-9bcd2745464e8d9597cba6d80c3dcf40



```go
func Chdir(dir string) error {
    n , _ := syscall.Chdir(dir):
               // something to do
               fmt.Println(n)
}

Todos ellos contienen algún tipo de magia no obvia, que no simplifica las cosas para el lector. En los dos ejemplos anteriores, err convierte en una especie de pseudo palabra clave o variable de aparición espontánea. En los dos últimos ejemplos, no está del todo claro qué se supone que debe hacer el operador : ¿se devolverá automáticamente un error? ¿El RHS del operador es una sola declaración o un bloque?

FWIW, escribiría una función contenedora para que puedas hacer return newPathErr("chdir", dir, syscall.Chdir(dir)) y automáticamente devolvería un error nulo si el tercer parámetro es nulo. :-)

En mi opinión, la mejor propuesta que he visto para lograr los objetivos de "simplificar el manejo de errores en Go" y "devolver el error con información contextual adicional" es de @mrkaspa en # 21732:

a, b, err? := f1()

se expande a esto:

if err != nil {
   return nil, errors.Wrap(err, "failed")
}

y puedo forzarlo a entrar en pánico con esto:

a, b, err! := f1()

se expande a esto:

if err != nil {
   panic(errors.Wrap(err, "failed"))
}

Esto mantendrá la compatibilidad con versiones anteriores y solucionará todos los puntos débiles del manejo de errores en Go

Esto no maneja casos como las funciones bufio que devuelven valores distintos de cero, así como errores, pero creo que está bien hacer un manejo de errores explícito en los casos en los que le interesan los otros valores devueltos. Y, por supuesto, los valores devueltos sin error deberían ser el valor nulo apropiado para ese tipo.

El ? El modificador reducirá el error estándar en la entrega de funciones y el! El modificador hará lo mismo para los lugares donde aserrar se usaría en otros lenguajes, como en algunas funciones principales.

Esta solución tiene la ventaja de ser muy simple y no intentar hacer demasiado, pero creo que cumple los requisitos establecidos en esta declaración de propuesta.

En el caso de que tengas ...

func foo() (int, int, error) {
    a, b, err? := f1()
    return a, b, nil
}
func bar() (int, error) {
    a, b, err? := foo()
    return a+b, nil
}

Si algo sale mal en foo , entonces en el sitio de llamada de bar , el error se envuelve doblemente con el mismo texto, sin agregar ningún significado. Como mínimo, me opondría a la parte errors.Wrap de la sugerencia.

Pero expandiéndonos más, ¿cuál es el resultado esperado de esto?

func baz() (a, b int, err error) {
  a = 1
  b = 2
  a, b, err? = f1()
  return

¿Se reasignan a y b a valores nulos? Si es así, eso es magia, que creo que deberíamos evitar. ¿Realizan los valores previamente asignados? (A mí no me importan los valores devueltos con nombre, pero aún así deben considerarse a los efectos de esta propuesta).

@ dup2X Sí, eliminemos el idioma, debería ser más así

@ object88 es natural esperar que, en caso de error, todo lo demás se anule. Eso es fácil de entender y no tiene magia, prácticamente ya es una convención para el código Go, requiere poco que recordar y no tiene casos especiales. Si permitimos que se retengan los valores, se complican las cosas. Si olvida verificar si hay errores, los valores devueltos podrían usarse por accidente, en cuyo caso podría suceder cualquier cosa. Como llamar a métodos en una estructura parcialmente asignada en lugar de entrar en pánico en cero. Los programadores pueden incluso comenzar a esperar que se devuelvan ciertos valores en caso de error. En mi opinión, sería un desastre y no se ganaría nada bueno con ello.

En cuanto a la envoltura, no creo que el mensaje predeterminado proporcione nada útil. Estaría bien simplemente encadenar los errores. Como cuando las excepciones tienen excepciones internas. Muy útil para depurar errores en el interior de una biblioteca.

Respetuosamente, no estoy de acuerdo, @creker. Tenemos ejemplos de ese escenario en Go stdlib de valores de retorno distintos de nulo incluso en el caso de un error no nulo y, de hecho, son funcionales, como varias funciones en la estructura bufio.Reader . A nosotros, como programadores de Go, se nos anima activamente a comprobar / gestionar todos los errores; se siente más atroz ignorar los errores que obtener valores de retorno que no sean nulos y un error. En el caso que cita, si devuelve un cero y no verifica el error, es posible que aún esté operando con un valor no válido.

Pero dejando eso a un lado, examinemos esto un poco más. ¿Cuál sería la semántica del operador ? ? ¿Solo se puede aplicar a tipos que implementan la interfaz error ? ¿Se puede aplicar a otros tipos o devolver argumentos? Si se puede aplicar a tipos que no implementan el error, ¿se activa mediante algún valor / puntero que no sea nulo? ¿Se puede aplicar el operador ? a más de un valor de retorno o es un error del compilador?

@erwbgy
Si desea devolver un error sin nada útil adjunto, sería mucho más sencillo decirle al compilador que trate todos los errores no tratados como "if err! = Nil return ...", por ejemplo:

func doStuff() error {
    doAnotherStuff() // returns error
}

func doStuff() error {
    res := doAnotherStuff() // returns string, error
}

¿Y no hay necesidad de una locura adicional? símbolo en este caso.

@ object88
Intenté aplicar la mayoría de las propuestas de envoltura de errores que se muestran aquí en código real y me enfrenté a un problema importante: el código se vuelve demasiado denso e ilegible.
Lo que hace es simplemente sacrificar el ancho del código a favor de la altura del código.
Envolver los errores con el habitual if err! = Nil en realidad permite difundir el código para una mejor legibilidad, por lo que no creo que debamos cambiar nada para envolver los errores en absoluto.

@ object88

En el caso de su sitio, si devuelve un cero y no verifica el error, es posible que aún opere con un valor no válido.

Pero eso producirá errores obvios y fáciles de detectar como pánico en cero. Si necesita devolver valores significativos en caso de error, debe hacerlo explícitamente y documentar exactamente qué valor es utilizable en cuyo caso. El solo hecho de devolver elementos aleatorios que se encontraban en las variables tras un error es peligroso y dará lugar a errores sutiles. Una vez más, no se gana nada con eso.

@gladkikhartem el problema con if err! = nil es que la lógica real está completamente perdida y debe buscarla activamente si desea comprender qué hace el código en su ruta exitosa y no le importa todo el manejo de errores . Se vuelve como leer mucho código C donde tienes varias líneas de código real y todo lo demás es solo una verificación de errores. La gente incluso recurre a macros que envuelven todo eso y van al final de la función.

No veo cómo la lógica puede volverse demasiado densa en un código escrito correctamente. Es lógica. Cada línea de su código contiene código real que le interesa, eso es lo que quiere. Lo que no desea es pasar por líneas y líneas de calderas. Use comentarios y divida su código en bloques si eso ayuda. Pero eso suena más como un problema con el código real y no con el idioma.

Esto funciona en el patio de recreo, si no lo formatea:

a, b, err := Frob("one string"); if err != nil { return a, b, fmt.Errorf("couldn't frob: %v", err) }
// continue doing stuff with a and b

Entonces me parece que la propuesta original, y muchas de las otras mencionadas anteriormente, están tratando de encontrar una sintaxis taquigráfica clara para esos 100 caracteres y evitar que gofmt insista en agregar saltos de línea y reformatear el bloque en 3 líneas.

Así que imaginemos que cambiamos gofmt para dejar de insistir en bloques de líneas múltiples, comenzamos con la línea anterior e intentamos encontrar formas de hacerla más corta y clara.

No creo que la parte antes del punto y coma (la asignación) deba cambiarse, por lo que deja 69 caracteres que podríamos cortar. De esos, 49 son la declaración de devolución, los valores a devolver y el ajuste de error, y no veo mucho valor en cambiar la sintaxis de eso (digamos, al hacer que las declaraciones de devolución sean opcionales, lo que confunde a los usuarios).

Así que eso deja la búsqueda de una abreviatura para ; if err != nil { _ } donde el subrayado representa un fragmento de código. Creo que cualquier taquigrafía debería incluir explícitamente err para mayor claridad, incluso si hace que la comparación nula sea algo invisible, por lo que nos quedamos con una taquigrafía para ; if _ != nil { _ } .

Imagine por un momento que usamos un solo carácter. Voy a elegir § como marcador de posición para lo que sea que sea ese personaje. La línea de código sería entonces:

a, b, err := Frob("one string") § err return a, b, fmt.Errorf("couldn't frob: %v", err)

No veo cómo podría hacerlo mucho mejor que eso sin cambiar la sintaxis de asignación existente o la sintaxis de retorno, o sin que suceda magia invisible. (Todavía hay algo de magia, en el sentido de que el hecho de que estemos comparando err con nil no es evidente).

Eso es 88 caracteres, guardando un total de 12 caracteres en una línea de 100 caracteres.

Entonces mi pregunta es: ¿Realmente vale la pena hacerlo?

Editar: Supongo que mi punto es que, cuando la gente mira los bloques if err != nil Go y dice "Ojalá pudiéramos deshacernos de esa basura", el 80-90% de lo que están hablando es _cosas que tienes inherentemente hacer para manejar errores_. La sobrecarga real causada por la sintaxis de Go es mínima.

@lpar , estás siguiendo principalmente la misma lógica que apliqué anteriormente , así que, naturalmente, estoy de acuerdo con tu razonamiento. Pero creo que descarta el atractivo visual de poner todos los errores a la derecha:

a, b := Frob("one string")  § err { return ... }

es más legible por un factor que supera la mera reducción de caracteres.

@lpar , puede guardar aún más caracteres si elimina fmt.Errorf prácticamente inútiles, cambia el retorno a una sintaxis especial e introduce la pila de llamadas a los errores para que tengan un contexto real para ellos y no sean meras cadenas glorificadas. Eso te dejaría con algo como esto

a, b, err? := Frob("one string")

El problema con los errores de Go para mí siempre fue la falta de contexto. Devolver y envolver cadenas no es útil en absoluto para determinar dónde ocurrió realmente el error. Es por eso que github.com/pkg/errors por ejemplo, se convirtió en una necesidad para mí. Con errores como ese, obtengo los beneficios de la simplicidad de manejo de errores de Go y los beneficios de las excepciones que capturan perfectamente el contexto y le permiten encontrar el lugar exacto del error.

E incluso si tomamos su ejemplo tal como está, el hecho de que el manejo de errores esté a la derecha es una mejora significativa en la legibilidad. Ya no tiene que saltarse varias líneas de texto estándar para llegar al significado real del código. Puede decir lo que quiera sobre la importancia del manejo de errores, pero cuando leo el código para entenderlo, no me preocupan los errores. Todo lo que necesito es un camino exitoso. Y cuando necesite errores, los buscaré específicamente. Los errores, por su naturaleza, son casos excepcionales y deberían ocupar el menor espacio posible.

Creo que la pregunta de si fmt.Errorf es "inútil" en comparación con errors.Wrap es ortogonal a este problema, ya que ambos son igualmente detallados. (En aplicaciones reales, tampoco uso, uso otra cosa que también registra el error y la línea de código en la que ocurrió).

Así que supongo que a algunas personas les gusta mucho que el manejo de errores esté a la derecha. Simplemente no estoy tan convencido, incluso si tengo experiencia en Perl y Ruby.

@lpar Utilizo errors.Wrap porque captura automáticamente la pila de llamadas; realmente no necesito todos estos mensajes de error. Me importa más el lugar donde sucedió y, tal vez, qué argumentos se pasaron a la función que produjo el error. Incluso dice que está haciendo algo similar: registrar una línea de código para proporcionar contexto a sus mensajes de error. Dado que podemos pensar en formas de reducir el texto estándar y al mismo tiempo dar más contexto a los errores (esa es prácticamente la propuesta aquí).

En cuanto a los errores a la derecha. Para mí, no se trata simplemente del lugar, sino de reducir la carga cognitiva que se necesita para leer un código plagado de manejo de errores. No compro el argumento de que los errores son tan importantes que quieres que ocupen tanto espacio como lo hacen. De hecho, preferiría que se fueran lo más posible. No son la historia principal.

@creker

Es más probable que esto describa un error trivial del desarrollador que un error en un sistema de producción que genera un error debido a una mala entrada del usuario. Si todo lo que necesita para determinar el error es el número de línea y la ruta del archivo, es probable que haya escrito el código y ya sepa cuál es el problema.

@ ya que es similar a las excepciones. En la mayoría de los casos, la pila de llamadas y el mensaje de excepción son suficientes para precisar el lugar y la causa donde ocurrió el error. En casos más complejos, al menos conoce el lugar donde ocurrió el error. Las excepciones le otorgan este beneficio por defecto. Con Go, tiene que encadenar los errores, básicamente emulando la pila de llamadas, o incluir la pila de llamadas real.

En el código escrito correctamente, la mayor parte del tiempo sabría por el número de línea y la ruta del archivo la causa exacta porque se esperaría el error. De hecho, escribiste un código en preparación para que pudiera suceder. Si sucedió algo inesperado, entonces sí, la pila de llamadas no le daría la causa, pero reduciría significativamente el espacio de búsqueda.

@como

En mi experiencia, los errores de entrada del usuario se manejan casi de inmediato. Los errores de producción problemáticos reales ocurren en lo profundo del código (por ejemplo, un servicio está inactivo, lo que hace que otro servicio arroje errores), y es bastante útil obtener un seguimiento de pila adecuado. Por lo que vale, los seguimientos de la pila de Java son extremadamente útiles cuando se depuran problemas de producción, no los mensajes.

@creker
Los errores son solo valores y forman parte de las entradas y salidas de la función. No pueden ser "inesperados".
Si desea averiguar por qué la función le dio un error, use pruebas, registro, etc.

@gladkikhartem en el mundo real no es tan simple. Sí, espera errores en el sentido de que la firma de la función incluye un error como valor de retorno. Pero lo que miento al esperar es saber exactamente por qué sucedió y qué lo causó, para que realmente sepa qué hacer para solucionarlo o no solucionarlo en absoluto. La entrada incorrecta del usuario suele ser muy fácil de solucionar con solo mirar el mensaje de error. Si usa búferes profocol y algún campo obligatorio no está configurado, se espera y es realmente fácil de solucionar si valida adecuadamente todo lo que recibe en el cable.

En este punto ya no entiendo de qué estamos discutiendo. El seguimiento de la pila o la cadena de mensajes de error son bastante similares si se implementan correctamente. Reducen el espacio de búsqueda y le proporcionan un contexto útil para reproducir y corregir un error. Lo que necesitamos es pensar en formas de simplificar el manejo de errores proporcionando suficiente contexto. De ninguna manera estoy defendiendo que la simplicidad sea más importante que el contexto adecuado.

Ese es el argumento de Java: mueva todo el código de error a otro lugar para no tener que mirarlo. Creo que está equivocado; no solo porque el mecanismo de Java para hacerlo ha fallado en gran medida, sino porque cuando miro el código, cómo se comportará cuando haya un error es tan importante para mí como cómo se comportará cuando todo funcione.

Nadie está haciendo ese argumento. No confundamos lo que se discute aquí con el manejo de excepciones donde todo el manejo de errores está en un solo lugar. Llamarlo "fracasado en gran medida" es solo una opinión, pero no creo que Go vuelva a hablar de eso en ningún caso. El manejo de errores de Go es simplemente diferente y se puede mejorar.

@creker Traté de hacer el mismo punto y pedí que aclarara lo que se considera un mensaje de error significativo / útil.

La verdad es que regalaría cualquier día un texto de mensaje de error de calidad variable (que tiene el sesgo del desarrollador que lo escribe en ese momento y con ese conocimiento) a cambio de la pila de llamadas y los argumentos de la función. Con un texto de mensaje de error ( fmt.Errorf o errors.New ) terminas buscando el texto en el código fuente, mientras lees pilas de llamadas / retrocesos (que aparentemente son odiados y espero que no por razones estéticas) corresponde a buscar directamente por número de archivo / línea ( errors.Wrap y similar).

Dos estilos diferentes, pero el propósito es el mismo: intentar reproducir en tu mente lo que sucedió en tiempo de ejecución en esas condiciones.

Sobre este tema, el número # 19991 quizás esté haciendo un resumen válido para un enfoque del segundo estilo de definir errores significativos.

mueva todo el código de error a otro lugar para que no tenga que mirarlo

@lpar , si está respondiendo a mi punto sobre mover el manejo de errores a la derecha: hay una gran diferencia entre notas al pie / notas al final (Java) y notas al margen (mi propuesta). Las notas al margen requieren solo un pequeño cambio de ojo, sin pérdida de contexto.

@ gdm85

terminas buscando el texto en el código fuente

Exactamente lo que quise decir cuando los seguimientos de pila y los mensajes de error encadenados son similares. Ambos registran un camino que llevó al error. Solo en el caso de mensajes, podría terminar con mensajes completamente inútiles que podrían ser de cualquier parte del programa si no tiene el cuidado suficiente al escribirlos. El único beneficio de los errores encadenados es la capacidad de registrar valores de variables. E incluso eso podría automatizarse en el caso de argumentos de función o incluso variables en general y, al menos para mí, cubriría casi todo lo que necesito de los errores. Aún serían valores, aún puede escribir cambiarlos si lo necesita. Pero en algún momento probablemente los registraría y poder ver el seguimiento de la pila es extremadamente útil.

Solo mira lo que hace Go con el pánico. Obtienes un seguimiento de pila completo de cada goroutine. No recuerdo cuántas veces me ayudó a identificar la causa del error y solucionarlo en poco tiempo. A menudo me asombra lo fácil que es. Fluye perfectamente con todo el lenguaje siendo muy predecible que ni siquiera necesitas un depurador.

Parece haber un estigma en torno a todo lo relacionado con Java y la gente a menudo no presenta ningún argumento. Es malo porque sí. No soy fanático de Java, pero ese tipo de razonamiento no ayuda a nadie.

Nuevamente, los errores no son para que el desarrollador los arregle. Ese es uno de los beneficios del manejo de errores. El método Java les ha enseñado a los desarrolladores que eso es el manejo de errores, no. Pueden existir errores en la capa de aplicación y más allá en una capa de flujo. Los errores en Go se utilizan habitualmente para controlar la estrategia de recuperación que toma un sistema, en tiempo de ejecución, no en tiempo de compilación.

Esto puede ser incomprensible cuando los lenguajes paralizan su control de flujo como resultado de un error al desentrañar la pila y perder la memoria de todo lo que hicieron antes de que ocurriera el error. Los errores son realmente útiles en tiempo de ejecución en Go; No veo por qué deberían llevar cosas como números de línea; el código de ejecución no se preocupa por eso.

@como

Nuevamente, los errores no son para que el desarrollador los corrija.

Eso es total y absolutamente incorrecto. Los errores son exactamente por esa razón. No se limitan a eso, pero es uno de los principales usos. Los errores indican que hay algún problema con el sistema y que debe hacer algo al respecto. Para errores esperados y fáciles, puede intentar recuperarlos como, por ejemplo, el tiempo de espera de TCP. Para algo más serio, lo vuelca en registros y luego depura el problema.

Ese es uno de los beneficios del manejo de errores. El método Java les ha enseñado a los desarrolladores que eso es el manejo de errores, no.

No sé qué te enseñó Java, pero utilizo excepciones por la misma razón: para controlar la estrategia de recuperación que toma el sistema en tiempo de ejecución. Go no tiene nada de especial en términos de manejo de errores.

Esto puede ser incomprensible cuando los idiomas paralizan su control de flujo como resultado de un error al desentrañar la pila y perder la memoria de todo lo que hicieron antes de que ocurriera el error.

Podría ser para alguien, no para mí.

Los errores son realmente útiles en tiempo de ejecución en Go; No veo por qué deberían llevar cosas como números de línea; el código de ejecución no se preocupa por eso.

Si le preocupa corregir errores en su código, los números de línea son la forma de hacerlo. No es Java lo que nos enseñó sobre esto, C tiene __LINE__ y __FUNCTION__ exactamente por esa razón. Desea registrar sus errores y registrar el lugar exacto donde sucedió. Y cuando algo sale mal, al menos tienes algo con lo que empezar. No es un mensaje de error aleatorio causado por un error irrecuperable. Si no necesita ese tipo de información, ignórela. No te hace daño. Pero al menos está ahí y se puede usar cuando sea necesario.

No entiendo por qué la gente aquí sigue cambiando la conversación a excepciones frente a valores de error. Nadie estaba haciendo esa comparación. Lo único que se discutió es que los seguimientos de pila son muy útiles y contienen mucha información de contexto. Si eso es incomprensible, probablemente viva en un universo completamente diferente donde el rastreo no existe.

Eso es total y absolutamente incorrecto.

Pero el sistema de producción al que me refiero todavía se está ejecutando y usa errores para el control de flujo, está escrito en Go y reemplazó una implementación más lenta en un lenguaje que usaba seguimientos de pila para la propagación de errores.

Si eso es incomprensible, probablemente viva en un universo completamente diferente donde el rastreo no existe.

Para encadenar la información de la pila de llamadas para cada función que devuelva un tipo de error, hágalo a su discreción. Los rastros de pila son más lentos y no son adecuados para su uso fuera de proyectos de juguetes por razones de seguridad. Es una falta técnica convertirlos en ciudadanos de Go de primera clase para simplemente ayudar a las estrategias de propagación de errores irreflexivas.

si no necesita ese tipo de información, ignórela. No te hace daño.

La hinchazón del software es la razón por la que los servidores se reescriben en Go. Lo que no ve puede degradar el rendimiento de su canalización.

Preferiría ejemplos de software real que se beneficia de tener esta función en lugar de una lección levemente irrelevante sobre el manejo de los tiempos de espera de TCP y el volcado de registros.

Los seguimientos de pila son más lentos

Dado que los seguimientos de la pila se generan en la ruta del error, a nadie le importa lo lentos que sean. El funcionamiento normal del software ya se interrumpió.

y no apto para su uso fuera de proyectos de juguetes por razones de seguridad.

Hasta ahora, todavía no he visto que un solo sistema de producción apague los seguimientos de pila debido a "razones de seguridad", o en absoluto. Por otro lado, ha sido de gran utilidad poder identificar rápidamente la ruta que tomó el código para producir un error. Y esto es para grandes proyectos, con muchos equipos diferentes trabajando en la base del código, y nadie tiene un conocimiento completo de todo el sistema.

Lo que no ve puede degradar el rendimiento de su canalización.

No, realmente no es así. Como dije antes, los rastros de pila se generan en errores. A menos que su software los encuentre constantemente, el rendimiento no se verá afectado ni un poco.

Dado que los seguimientos de la pila se generan en la ruta del error, a nadie le importa lo lentos que sean. El funcionamiento normal del software ya se interrumpió.

Falso.

  • Pueden ocurrir errores como parte del funcionamiento normal.
  • Los errores se pueden recuperar y el programa puede continuar, por lo que el rendimiento aún está en duda.
  • Disminuir la velocidad de una rutina agota los recursos de otras rutinas que _ están_ operando en el camino feliz.

@ object88 imagina código de producción real. ¿Cuántos errores espera que genere? No pensaría mucho. Al menos en una solicitud debidamente redactada. Si una goroutine está en un bucle ocupado y constantemente arroja errores en cada iteración, hay algo mal en el código. Pero incluso si ese es el caso, dado que la mayoría de las aplicaciones de Go están vinculadas a IO, incluso eso no sería un problema grave.

@como

Pero el sistema de producción al que me refiero todavía se está ejecutando y usa errores para el control de flujo, está escrito en Go y reemplazó una implementación más lenta en un lenguaje que usaba seguimientos de pila para la propagación de errores.

Lo siento, pero esta es una frase sin sentido que no tiene nada que ver con lo que dije. No voy a responder.

Los seguimientos de pila son más lentos

Más lento pero ¿cuánto? ¿Importa? No lo creo. Las aplicaciones Go están vinculadas a IO en general. Perseguir los ciclos de la CPU es una tontería en este caso. Tiene problemas mucho mayores en el tiempo de ejecución de Go que consume CPU. No es un argumento para tirar una característica útil que ayuda a corregir errores.

inadecuado para su uso fuera de proyectos de juguetes por razones de seguridad.

No me voy a molestar en cubrir "razones de seguridad" inexistentes. Pero me gustaría recordarle que, por lo general, los rastreos de aplicaciones se almacenan internamente y solo los desarrolladores tienen acceso a ellos. Y tratar de ocultar los nombres de sus funciones es una pérdida de tiempo de todos modos. No es seguridad. Espero no necesitar dar más detalles sobre eso.

Si insiste en razones de seguridad, me gustaría que pensara en macOS / iOS, por ejemplo. No tienen ningún problema en lanzar pánicos y volcados por caída que contienen pilas de todos los subprocesos y valores de todos los registros de la CPU. No los vea afectados por estas "razones de seguridad".

Es una falta técnica convertirlos en ciudadanos de Go de primera clase para simplemente ayudar a las estrategias de propagación de errores irreflexivas.

¿Podrías ser más subjetivo? "estrategias irreflexivas de propagación de errores", ¿dónde vio eso?

La hinchazón del software es la razón por la que los servidores se reescriben en Go. Lo que no ve puede degradar el rendimiento de su canalización.

De nuevo, ¿por cuánto?

Preferiría ejemplos de software real que se beneficia de tener esta función en lugar de una lección levemente irrelevante sobre el manejo de los tiempos de espera de TCP y el volcado de registros.

En este punto parece que estoy hablando con cualquiera menos con un programador. El rastreo beneficia a todos y cada uno de los programas. Es una técnica común en todos los idiomas y todo tipo de software que ayuda a corregir errores. Puede leer wikipedia si desea obtener más información al respecto.

Tener tantas discusiones improductivas sin consenso significa que no hay una manera elegante de resolver este problema.

@ object88
Los seguimientos de la pila pueden ser lentos si desea realizar un seguimiento de todas las gorutinas, porque Go debe esperar a que se desbloqueen otras gorutinas.
Si solo rastrea la goroutine que está ejecutando actualmente, no es tan lento.

@creker
El rastreo beneficia a todo el software, pero depende de lo que esté rastreando. En la mayoría de los proyectos de Go en los que estuve involucrado, rastrear pilas no fue una gran idea, porque la concurrencia está involucrada. Los datos se mueven hacia adelante y hacia atrás, muchas cosas se comunican entre sí y algunas gorutinas son solo unas pocas líneas de código. Tener un seguimiento de la pila en tal caso no ayuda.
Es por eso que utilizo errores envueltos con información de contexto escrita en el registro para recrear el mismo seguimiento de pila, pero que no está vinculado a la pila de goroutine real, sino a la lógica de la aplicación en sí.
Para poder hacer cat * .log | grep "orderID = xxx" y obtenga el seguimiento de la pila de la secuencia real de acciones que llevaron a un error.
Debido a la naturaleza concurrente de Go, los errores ricos en contexto son más valiosos que los seguimientos de pila.

@gladkikhartem gracias por tomarse el tiempo para escribir un argumento adecuado. Estaba empezando a frustrarme con esa conversación.

Entiendo su argumento y en parte estoy de acuerdo con él. Aún así, me encuentro teniendo que lidiar con pilas de al menos 5 funciones de profundidad. Eso ya es lo suficientemente grande como para poder comprender lo que está sucediendo y dónde debería comenzar a buscar. Pero en una aplicación muy concurrente con muchas rutinas muy pequeñas, los rastros de pila pierden sus beneficios. Con eso estoy de acuerdo.

@creker

imagina el código de producción real. ¿Cuántos errores espera que genere? [...], dado que la mayoría de las aplicaciones de Go están vinculadas a IO, incluso eso no sería un problema grave.

Es bueno que mencione operaciones vinculadas a IO. El método de lectura io.Reader devuelve un error

@urandom

Los seguimientos de pila exponen involuntariamente información valiosa para crear perfiles de un sistema.

  • Nombres de usuario
  • Rutas del sistema de archivos
  • Tipo / versión de base de datos backend
  • Flujo de transacciones
  • Estructura de objeto
  • Algoritmos de cifrado

No sé si la aplicación promedio notaría la sobrecarga de recopilar marcos de pila en tipos de error, pero puedo decirle que para aplicaciones críticas para el rendimiento, muchas funciones pequeñas de Go se insertan manualmente debido a la sobrecarga actual de llamadas a funciones. El rastreo lo empeorará.

Creo que el objetivo de Go es tener un software simple y rápido, y el rastreo sería un paso atrás. Deberíamos poder escribir funciones pequeñas y devolver errores de esas funciones sin una degradación del rendimiento que fomente los tipos de errores no convencionales y la inserción manual.

@creker

Evitaré darte ejemplos que causen más disonancia. Lamento haberte frustrado.

Propondría usar una nueva palabra clave "returnif" que el nombre revela instantáneamente su función. Además, es lo suficientemente flexible como para poder usarse en más casos de uso que en el manejo de errores.

Ejemplo 1 (usando retorno con nombre):

a, err = algo (b)
if err! = nil {
regreso
}

Se convertiría:

a, err = algo (b)
returnif err! = nil

Ejemplo 2 (sin usar devolución con nombre):

a, err: = algo (b)
if err! = nil {
devolver un, err
}

Se convertiría:

a, err: = algo (b)
returnif err! = nil {a, err}

Con respecto a su ejemplo de devolución con nombre, ¿quiere decir ...

a, err = something(b)
returnif err != nil

@ambernardino
¿Por qué no simplemente actualizar la herramienta fmt en su lugar y no tiene que actualizar la sintaxis de un idioma y agregar palabras clave nuevas e inútiles?

a, err := something(b)
if err != nil { return a, err }

o

a, err := something(b)
    if err != nil { return a, err }

@gladkikhartem la idea no es escribir que cada vez que quieras propagar el error, prefiero esto y debería funcionar de la misma manera

a, err? := something(b)

@mrkaspa
La idea es hacer que el código sea más legible . Escribir código no es un problema, leerlo.

@gladkikhartem rust usa ese enfoque y no creo que lo haga menos legible

@gladkikhartem No creo que ? haga menos legible. Yo diría que elimina el ruido por completo. El problema para mí es que con el ruido también elimina la posibilidad de proporcionar un contexto útil. Simplemente no veo dónde podría conectar el mensaje de error habitual o envolver los errores. La pila de llamadas es una solución obvia pero, como ya se mencionó, no funciona para todos.

@mrkaspa
Y creo que lo hace menos legible, ¿qué sigue? ¿Estamos tratando de encontrar la mejor solución o simplemente compartiendo opiniones?

@creker
'?' El personaje agrega carga cognitiva al lector, porque no es tan obvio lo que se devolverá y, por supuesto, la persona debe saber qué está haciendo. Y por supuesto ? signo plantea preguntas en la mente del lector.

Como dije anteriormente, si desea deshacerse de err! = Nil, el compilador podría detectar parámetros de error no utilizados y reenviarlos él mismo.
Y

a, err? := doStuff(a,b)
err? := doAnotherStuff(b,z,d,g)
a, b, err? := doVeryComplexStuff(b)

será más legible

a := doStuff(a,b)
doAnotherStuff(b,z,d,g)
a, b := doVeryComplexStuff(b)

misma magia, solo menos cosas que escribir y menos cosas en las que pensar

@gladkikhartem Bueno, no creo que haya una solución que no requiera que los lectores aprendan algo nuevo. Esa es la consecuencia de cambiar el idioma. Tenemos que hacer una compensación: o vivimos con una sintaxis detallada en tu cara que muestra exactamente lo que se está haciendo en términos primitivos o introducimos una nueva sintaxis que podría ocultar la verbosidad, agregar algo de sintaxis, etc. No hay otra manera. Simplemente rechazar cualquier cosa que agregue algo para que el lector aprenda es contraproducente. También podríamos cerrar todos los problemas de Go2 y terminarlo.

En cuanto a su ejemplo, introduce aún más cosas mágicas y oculta cualquier punto de inyección para introducir una sintaxis que permitiría al desarrollador proporcionar contexto a los errores. Y, lo que es más importante, oculta por completo cualquier información sobre qué llamada de función podría generar un error. Eso huele cada vez más a excepciones. Y si nos tomamos en serio la reincorporación de errores, los seguimientos de pila se vuelven imprescindibles porque esa es la única forma en que puede retener el contexto en ese caso.

La propuesta original ya cubre bastante bien todo eso. Es lo suficientemente detallado y le brinda un buen lugar para envolver el error y proporcionar un contexto útil. Pero uno de los principales problemas es este "err" mágico. Creo que es feo porque no es lo suficientemente mágico ni lo suficientemente detallado. Está en el medio. Lo que podría mejorarlo es introducir más magia.

¿Qué pasa si || produce un nuevo error que envuelve automáticamente el error original? Entonces el ejemplo se vuelve así

func Chdir(dir string) error {
    syscall.Chdir(dir) || &PathError{"chdir", dir}
    return nil
}

err simplemente desaparece y todo el ajuste se maneja implícitamente. Ahora necesitamos alguna forma de acceder a esos errores internos. Agregar otro método como Inner() error a error interfaz no funcionará, creo. Una forma es introducir una función incorporada como unwrap(error) []error . Lo que hace es devolver una porción de todos los errores internos en el orden en que fueron ajustados. De esa manera, puede acceder a cualquier error interno o pasar por encima de ellos.

La implementación de esto es cuestionable dado que error es solo una interfaz y necesita un lugar donde colocar esos errores envueltos para cualquier tipo error .

Para mí, esto cumple todos los requisitos, pero podría ser algo demasiado mágico. Pero, dado que la interfaz de error es bastante especial por definición, acercarla al ciudadano de primera clase podría no ser una mala idea. El manejo de errores es detallado porque es solo un código Go normal, no tiene nada de especial. Eso podría ser bueno en papel y para hacer titulares llamativos, pero los errores son demasiado especiales para justificar ese tratamiento. Necesitan carcasa especial.

¿La propuesta original trata de reducir el número de verificaciones de errores o la duración de cada verificación individual?

Si es lo último, entonces es trivial refutar la necesidad de las propuestas sobre la base de que solo hay una declaración condicional y una declaración de repetición. A algunas personas no les gustan los bucles for, ¿deberíamos proporcionar también una construcción de bucle implícita?

Los cambios de sintaxis propuestos hasta ahora han servido como un experimento mental interesante, pero ninguno de ellos es tan claro, pronunciable o simple como el original. Go no es bash y nada debería ser "mágico" en los errores.

Cada vez leo más estas propuestas, cada vez veo más gente cuyos argumentos no son más que "añade algo nuevo, entonces es malo, ilegible, deja todo como está".

@como la propuesta da un resumen de lo que intenta lograr. Lo que se está haciendo está bastante bien definido.

tan claro, pronunciable o simple como el original

Cualquier propuesta introducirá una nueva sintaxis y ser nuevo para algunas personas suena igual a "ilegible, complicado, etc., etc.". El hecho de que sea nuevo no lo hace menos claro, pronunciable o simple. "||" y "?" los ejemplos son tan claros y simples como la sintaxis existente una vez que sepa lo que hace. ¿O deberíamos empezar a quejarnos de que "->" y "<-" son demasiado mágicos y el lector tiene que saber lo que significan? Reemplácelos con llamadas a métodos.

Go no es bash y nada debería ser "mágico" en los errores.

Eso es completamente infundado y no cuenta como argumento para nada. ¿Qué tiene eso que ver con Bash?

@creker
Sí, estoy totalmente de acuerdo contigo en que el evento introdujo más magia. Mi ejemplo es solo una continuación de? idea del operador de escribir menos cosas.

Estoy de acuerdo en que tenemos que sacrificar algo e introducir algún cambio y, por supuesto, algo de magia. Es solo un equilibrio entre las ventajas de la usabilidad y las desventajas de tal magia.

Original || propuesta es bastante agradable y prácticamente probada, pero el formato es feo en mi opinión, sugiero cambiar el formato a

syscal.Chdir(dir)
    || return &PathError{"chdir", dir}

PD: ¿qué piensas de esa variante de sintaxis mágica?

syscal.Chdir(dir) {
    return &PathError{"chdir", dir}
}

@gladkikhartem ambos se ven bastante bien desde el punto de vista de la legibilidad, pero tengo un mal presentimiento de este último. Introduce este extraño alcance de bloque del que no estoy tan seguro.

Te animo a que no mires la sintaxis de forma aislada, sino en el contexto de una función. Este método tiene un par de bloques de manejo de errores diferentes.

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath, err := p.preparePath()
        if err != nil {
                return nil, err
        }
    fis, err := l.context.ReadDir(absPath)
    if err != nil {
        return nil, err
    } else if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

¿Cómo limpian los cambios propuestos esta función?

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath, err? := p.preparePath()
    fis, err? := l.context.ReadDir(absPath)
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
         if _, ok := err.(*build.NoGoError); ok {
             // There isn't any Go code here.
             return nil, nil
         }
         return nil, err
    }

    return buildPkg, nil
}

La propuesta de @bcmills encaja mejor.

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath := p.preparePath()        =? err { return nil, err }
    fis := l.context.ReadDir(absPath) =? err { return nil, err }
    if len(fis) == 0 {
        return nil, nil
    }
    buildPkg := l.context.Import(".", absPath, 0) =? err {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }
    return buildPkg, nil
}

@ object88

func (l *Loader) processDirectory(p *Package) (p *build.Package, err error) {
        absPath, err := p.preparePath() {
        return nil, fmt.Errorf("prepare path: %v", err)
    }
    fis, err := l.context.ReadDir(absPath) {
        return nil, fmt.Errorf("read dir: %v", err)
    }
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0) {
        err, ok := err.(*build.NoGoError)
                if !ok {
            return nil, fmt.Errorf("buildpkg: %v",err)
        }
        return nil, nil
    }
    return buildPkg, nil
}

Continuando con esto. Quizás podamos ofrecer ejemplos de uso más completos.

@erwbgy , este me parece el mejor, pero no estoy convencido de que el pago sea tan bueno.

  • ¿Cuáles son los valores devueltos sin error? ¿Son siempre el valor cero? Si había _había_ un valor asignado previamente para una devolución con nombre, ¿se anula? Si el valor nombrado se usa para almacenar el resultado de una función errónea, ¿se devuelve?
  • ¿Se puede aplicar el operador ? a un valor sin error? ¿Podría hacer algo como (!ok)? o ok!? (lo cual es un poco extraño, porque está agrupando asignación y operación)? ¿O esta sintaxis solo es válida para error ?

@rodcorsi , este me molesta porque ¿y si la función no fuera ReadDir sino ReadBuildTargetDirectoryForFileInfo o algo tan tonto como eso? O quizás tenga una gran cantidad de argumentos. El manejo de errores para preparePath también saldría de la pantalla. En un dispositivo con un tamaño de pantalla horizontal limitado (o ventana gráfica que no es tan amplia, como Github), es probable que pierda la parte =? . Somos muy buenos en el desplazamiento vertical; no tanto en horizontal.

@gladkikhartem , parece que está vinculado a algún argumento (¿solo el último?) que implementa la interfaz error . Se parece mucho a una declaración de función, y eso simplemente ... _ se siente_ raro. ¿Hay alguna forma de que pueda estar vinculado a un valor de retorno de estilo ok ? En general, solo está comprando 1 línea.

@ object88
el ajuste de palabras resuelve problemas de código realmente amplios. ¿No se usa ampliamente?

@ object88 con respecto a una llamada de función muy larga. Tratemos el problema principal aquí. El problema no es el manejo de errores eliminado de la pantalla. El problema es un nombre de función largo y / o una gran lista de argumentos. Eso debe arreglarse antes de que se pueda argumentar que el manejo de errores está fuera de la pantalla.

Todavía tengo que ver un editor de texto IDE o compatible con código que se configuró para ajustar la palabra de forma predeterminada. Y no he encontrado una manera de hacerlo con Github, aparte de piratear manualmente el CSS después de que se cargue la página.

Y creo que el ancho del código es un factor importante: habla de la _ legibilidad_, que es el ímpetu de esta propuesta. La afirmación es que hay "demasiado código" en torno a los errores. No es que la funcionalidad no esté ahí, o que los errores deban implementarse de alguna otra manera, sino que el código no se lee bien.

@ object88
sí, este código funcionará para cualquier función que devuelva la interfaz de error como último parámetro.

Con respecto al ahorro de líneas, simplemente no puede poner más información en menos líneas. El código debe distribuirse de manera uniforme, no demasiado denso y sin espacio después de cada declaración.
Estoy de acuerdo en que parece una declaración de función, pero al mismo tiempo es muy similar a la existente if ...; err! = nil {declaración, para que la gente no se confunda demasiado.

El ancho del código es un factor importante. ¿Qué pasa si tengo un editor de 80 líneas y 80 líneas de código es una llamada de función y luego tengo || error ? Simplemente no podré identificar que la función devuelve algo, porque lo que leeré será un código go válido sin devoluciones.

Solo para completar, arrojaré un ejemplo con la sintaxis || , mi ajuste automático de errores y la puesta a cero automática de los valores de retorno sin error

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath := p.preparePath() || errors.New("prepare path")
    fis := l.context.ReadDir(absPath) || errors.New("ReadDir")
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

Con respecto a su pregunta sobre otros valores de retorno. En caso de error tendrán valor cero en todos los casos. Ya cubrí por qué creo que eso es importante.

El problema es que su ejemplo no es tan complicado para empezar. Pero todavía muestra lo que representa esta propuesta, al menos para mí. Lo que quiero que resuelva es el modismo más común y más utilizado

err := func()
if err != nil {
    return err
}

Todos podemos estar de acuerdo en que el tipo de código es una parte importante (si no la más importante) del manejo de errores en general. Así que es lógico resolver ese caso. Y si desea hacer algo más relacionado con el error, aplique algo de lógica, hágalo. Ahí es donde debería estar la verbosidad donde hay una lógica real para que el programador la lea y comprenda. Lo que no necesitamos es perder el espacio y el tiempo leyendo un texto estándar sin sentido. Es una parte insensata pero esencial del código Go.

Con respecto a la charla anterior sobre la devolución implícita de valores cero. Si necesita devolver un valor significativo en caso de error, hágalo. Nuevamente, la verbosidad es buena aquí y ayuda a comprender el código. No hay nada de malo en deshacerse del azúcar sintáctico si necesita hacer algo más complicado. Y || es lo suficientemente flexible para resolver ambos casos. Puede omitir los valores que no sean de error y se pondrán a cero implícitamente. O puede especificarlos explícitamente si es necesario. Recuerdo que incluso hay una propuesta separada para esto que también involucra casos en los que desea devolver el error y poner a cero todo lo demás.

@ object88

La afirmación es que hay "demasiado código" en torno a los errores.

No es un código cualquiera. El problema principal es que hay demasiadas plantillas sin sentido en torno a los errores y casos muy comunes de manejo de errores. La verbosidad es importante cuando hay algo valioso para leer. No hay nada de valor en if err == nil then return err excepto que desea volver a generar el error. Para una lógica tan primitiva se necesita mucho espacio. Y cuanto más tiene lógica, llamadas a la biblioteca, envoltorios, etc., que todo podría muy bien devolver un error, cuanto más este texto estándar comience a dominar cosas importantes: la lógica real de su código. Y esa lógica puede contener alguna lógica importante para el manejo de errores. Pero se pierde en esta naturaleza repetitiva de la mayor parte de la repetición a su alrededor. Y eso se puede resolver y otros lenguajes modernos con los que compite Go intentan resolverlo. Debido a que el manejo de errores es tan importante, no es solo un código normal.

@creker
Estoy de acuerdo en que si err != nil return err es demasiado repetitivo, lo que tememos es que si creamos una manera fácil de simplemente reenviar el error en la pila, estadísticamente los programadores, especialmente los junior, usarán el método más fácil, en lugar de hacerlo. lo que es apropiado en una determinada situación.
Es la misma idea con el manejo de errores de Go: te obliga a hacer algo decente.
Entonces, en esta propuesta queremos alentar a otros a manejar y corregir los errores de manera cuidadosa.

Yo diría que deberíamos hacer que el manejo de errores simple parezca feo y largo de implementar, pero el manejo elegante de errores con seguimiento o seguimiento de pila se ve agradable y fácil de hacer.

@gladkikhartem Siempre encontré tonto este argumento sobre los editores antiguos. ¿A quién le importa eso y por qué el idioma debería sufrir esto? Es 2018, casi todo el mundo tiene una gran pantalla y un editor decente. La minoría muy pequeña no debería influir en los demás. Debería ser al revés: la minoría debería afrontarlo por sí misma. Desplácese, ajuste de palabras, cualquier cosa.

@gladkikhartem Go ya tiene ese problema y no creo que podamos hacer nada al respecto. Los desarrolladores siempre serán perezosos hasta que los fuerces con una compilación fallida o pánico en tiempo de ejecución que, por cierto, Go no está haciendo.

Lo que Go realmente hace es no forzar nada. La noción de que Go te obliga a manejar los errores es engañosa y siempre lo fue. Los autores de Go te obligan a hacer eso en sus publicaciones de blog y conferencias. El idioma real te deja hacer lo que quieras. Y el principal problema aquí es lo que Go elige de forma predeterminada: de forma predeterminada, el error se ignora silenciosamente. Incluso hay una propuesta para cambiar eso. Si Go trataba de obligarte a hacer algo decente, entonces debería hacer lo siguiente. Devuelve un error en tiempo de compilación o entra en pánico en tiempo de ejecución si el error devuelto no se maneja correctamente. Por lo que tengo entendido, Rust hace esto: los errores entran en pánico de forma predeterminada. Eso es lo que yo llamo forzar a hacer lo correcto.

Lo que realmente me obligó a manejar errores en Go es mi conciencia de desarrollador, nada más. Pero siempre es tentador ceder. En este momento, si no voy a leer explícitamente la firma de la función, nadie me dirá nada de que devuelve un error. Hay un ejemplo real. Durante mucho tiempo no tuve idea de que fmt.Println devolvía un error. No tengo ningún uso para su valor de retorno, solo quiero imprimir cosas. Así que no tengo ningún incentivo para mirar lo que devuelve. Ese es el mismo problema que tiene C. Los errores son valores y puede ignorarlos todo lo que quiera hasta que su código se rompa en tiempo de ejecución y no sabrá nada al respecto porque no hay bloqueo con pánico útil como, por ejemplo, con excepciones no controladas.

@gladkikhartem por lo que tengo entendido sobre esta propuesta, no es para animar a los desarrolladores a que envuelvan los errores con cuidado. Se trata de animar a quienes presenten propuestas a que no se olviden de cubrir eso. Porque a menudo las personas encuentran soluciones que simplemente vuelven a lanzar el error y olvidan que realmente desea darle más contexto y solo luego volver a lanzarlo.

Estoy escribiendo esta propuesta principalmente para alentar a las personas que desean simplificar el manejo de errores de Go a pensar en formas de facilitar el contexto de los errores, no solo para devolver el error sin modificar.

@creker
Mi editor tiene un ancho de 100 caracteres, porque tengo explorador de archivos, consola git, etc., en nuestro equipo nadie escribe un código de más de 100 caracteres, es una tontería (con algunas excepciones)

Go no está forzando el manejo de errores, los linters sí lo hacen. (¿Quizás deberíamos escribir un linter para esto?)

Bien, si no podemos encontrar una solución y todos entienden la propuesta a su manera, ¿por qué no especificar algunos requisitos para lo que necesitamos? Primero, estar de acuerdo en los requisitos y luego desarrollar la solución sería una tarea mucho más sencilla.

Por ejemplo:

  1. La sintaxis de la nueva propuesta debe tener una declaración de retorno en el texto; de lo contrario, no es obvio para el lector lo que está sucediendo. ( de acuerdo en desacuerdo )
  2. La nueva propuesta debe admitir funciones que devuelvan múltiples valores (de acuerdo / en desacuerdo)
  3. La nueva propuesta debería ocupar menos espacio (1 línea, 2 líneas, en desacuerdo)
  4. La nueva propuesta debe poder manejar expresiones muy largas (de acuerdo / en desacuerdo)
  5. La nueva propuesta debe permitir múltiples declaraciones en caso de error (de acuerdo / en desacuerdo)
  6. .....

@creker , aproximadamente el 75% de mi desarrollo se realiza en una computadora portátil de 15 "en VSCode. Maximizo mi espacio horizontal, pero todavía hay un límite, especialmente si estoy editando lado a lado. Apostaría eso entre los estudiantes , hay muchas más computadoras portátiles que de escritorio. No quisiera limitar la accesibilidad del lenguaje porque anticipamos que todos tendrán monitores de gran formato.

Y desafortunadamente, no importa qué tan grande sea la pantalla que tenga, github aún limita la ventana gráfica.

@gladkikhartem

La pereza de los principiantes es aplicable aquí, pero el uso liberal de errors.New en algunos de estos ejemplos también demuestra una falta de comprensión del lenguaje. Los errores no deben asignarse en valores de retorno a menos que sean dinámicos, y si esos errores se colocarían en una variable de alcance de paquete comparable, la sintaxis sería más corta en la página y también aceptable en el código de producción. Aquellos que sufren más con el "estándar" de manejo de errores de Go, toman la mayoría de los atajos y no tienen la experiencia suficiente para manejar los errores correctamente.

No es obvio qué constituye simplifying error handling , pero el precedente es que less runes != simple . Creo que hay algunos calificadores de simplicidad que pueden medir una construcción de una manera cuantificable:

  • El número de formas en que se expresa la construcción.
  • La similitud de ese constructo con otro constructo, y la cohesión entre esos constructos
  • El número de operaciones lógicas resumidas por la construcción.
  • La similitud del constructo con el lenguaje natural (es decir, ausencia de negación, etc.)

Por ejemplo, la propuesta original aumenta el número de formas de propagar errores de 2 a 3. Es similar al OR lógico, pero tiene una semántica diferente. Resume un retorno condicional de baja complejidad (en comparación con, por ejemplo, copy o append , o >> ). El nuevo método es menos natural que el anterior, y si se habla en voz alta probablemente sería abs, err := path(foo) || return err -> if theres an error, it's returning err en cuyo caso sería un misterio por qué es posible usar las barras verticales si puede escribirlo de la misma manera que se dice en voz alta en una revisión de código.

@como
Totalmente de acuerdo en que less runes != simple .
Por simple me refiero a legible y comprensible.
Para que cualquiera que no esté familiarizado con Go debería leerlo y entender lo que hace.
Debería ser como una broma, no tienes que explicarlo.

El manejo de errores actual en realidad es comprensible, pero no completamente legible si tiene demasiado if err != nil return.

@ object88 está bien. Dije más en general porque este argumento surge con bastante frecuencia. Imaginemos una ridícula pantalla de terminal antigua que podría usarse para escribir Go. ¿Qué tipo de argumento es ese? ¿Dónde está el límite de su ridiculez? Si nos lo tomamos en serio, entonces deberíamos observar hechos concretos: cuál es el tamaño y la resolución de pantalla más populares. Y solo de eso podemos sacar algo. Pero el argumento generalmente solo imagina un tamaño de pantalla que nadie usa, pero hay una pequeña posibilidad de que alguien pueda hacerlo.

@gladkikhartem no, los

Estoy de acuerdo, deberíamos formular mejor lo que queremos porque la propuesta no cubre completamente todos los aspectos.

@como

El nuevo método es menos natural que el anterior, y si se habla en voz alta probablemente sería abs, err: = path (foo) || return err -> si hay un error, devuelve err, en cuyo caso sería un misterio por qué es posible usar las barras verticales si puede escribirlo de la misma manera que se dice en voz alta en una revisión de código.

El nuevo método es menos natural solo por una razón: no es parte del lenguaje en este momento. No hay otra razón. Imagínese Go ya con esa sintaxis; sería natural porque está familiarizado con ella. Al igual que está familiarizado con -> , select , go y otras cosas que no están presentes en otros idiomas. ¿Por qué es posible utilizar barras verticales en lugar de retorno? Respondo con una pregunta. ¿Por qué hay una forma de agregar segmentos en una llamada cuando puedes hacer lo mismo con el bucle? ¿Por qué hay una forma de copiar cosas de la interfaz del lector a la del escritor en una llamada cuando puedes hacer lo mismo con el bucle? etc etc etc Porque desea que su código sea más compacto y más legible. Estás haciendo estos argumentos cuando Go ya los contradice con numerosos ejemplos. Nuevamente, seamos más abiertos y no eliminemos nada solo porque es nuevo y no está ya en el idioma. No lograremos nada con eso. Hay un problema, mucha gente está pidiendo una solución, solucionémoslo. Go no es un idioma ideal sagrado que será profanado por cualquier cosa que se le agregue.

¿Por qué hay una forma de agregar segmentos en una llamada cuando puedes hacer lo mismo con el bucle?

Escribir una verificación de error de declaración if es trivial, me interesaría ver su implementación de append .

¿Por qué hay una forma de copiar cosas de la interfaz del lector a la del escritor en una llamada cuando puedes hacer lo mismo con el bucle?

Un lector y un escritor abstraen las fuentes y destinos de una operación de copia, la estrategia de almacenamiento en búfer y, a veces, incluso los valores centinela en el bucle. No se puede expresar esa abstracción con un bucle y un corte.

Estás haciendo estos argumentos cuando Go ya los contradice con numerosos ejemplos.

No creo que ese sea el caso, al menos no con esos ejemplos.

Nuevamente, seamos más abiertos y no eliminemos nada solo porque es nuevo y no está ya en el idioma.

Dado que Go tiene una garantía de compatibilidad, debería analizar más las nuevas funciones, ya que tendrá que lidiar con ellas para siempre si son terribles. Lo que nadie ha hecho aquí hasta ahora es crear una prueba de concepto real y utilizarla con un pequeño equipo de desarrollo.

Si observa el historial de algunas propuestas (p. Ej., Genéricos), verá que después de hacer eso, a menudo se da cuenta: "wow, esto en realidad no es una buena solución, no hagamos ningún cambio todavía". La alternativa es un lenguaje lleno de sugerencias y no es una manera fácil de desalojarlas retroactivamente.

Acerca de la pantalla ancha frente a la delgada, otra cosa a considerar es la multitarea .

Es posible que tenga varias ventanas una al lado de la otra para realizar un seguimiento ocasional de otra cosa mientras junta un poco de código, en lugar de simplemente mirar al editor, cambiando completamente los contextos a otra ventana para buscar una función, tal vez StackOverflow, y volver al editor.

@como
Estoy totalmente de acuerdo en que la mayoría de las funciones propuestas no son prácticas y estoy empezando a pensar que || y ? cosas podrían ser el caso.

@creker
copy () y append () no son tareas triviales de implementar

Tengo linters en CI / CD y literalmente me obligan a manejar todos los errores. No son parte del lenguaje, pero no me importa, solo necesito resultados.
(y, por cierto, tengo una opinión firme, si alguien no usa linters en Go, simplemente ...)

Sobre el tamaño de la pantalla, ni siquiera es gracioso, en serio. Detenga esta discusión irrelevante. Su pantalla puede ser tan ancha como desee; siempre tendrá la probabilidad de que || return &PathError{Err:err} parte del código no sea visible. Simplemente busque en Google la palabra "ide" y vea qué tipo de espacio está disponible para el código.

Y, por favor, lea atentamente el texto de los demás, no dije que Go le obliga a manejar todos los errores.

Es la misma idea con el manejo de errores de Go: te obliga a hacer algo decente.

@gladkikhartem Go no fuerza nada en términos de manejo de errores, ese es el problema. Algo decente o no, no importa, eso es solo picardías. Aunque para mí significa manejar todos los errores en todos los casos, excepto quizás cosas como fmt.Println .

si alguien no usa linters en Go, simplemente

Tal vez sea. Pero si algo no es realmente forzado, no volará. Algunos lo usarán, otros no.

Sobre el tamaño de la pantalla, ni siquiera es gracioso, en serio. Detenga esta discusión irrelevante.

No soy yo quien comenzó a lanzar números aleatorios que de alguna manera deberían afectar la toma de decisiones. Declaro claramente que entiendo el problema pero que debe ser objetivo. No "Tengo un IDE de 80 símbolos de ancho, Go debería tener en cuenta eso e ignorar a todos los demás".

Si hablamos del tamaño de mi pantalla. El código de Visual Studio me da 270 símbolos de espacio horizontal. No voy a defender que sea normal ocupar tanto espacio. Pero mi código puede superar fácilmente los 120 símbolos cuando se tienen en cuenta las estructuras con comentarios y los tipos de campos con nombres particularmente largos. Si tuviera que usar la sintaxis || entonces encajaría fácilmente en 100-120 en caso de una llamada de función de 3-5 argumentos y un error envuelto con un mensaje personalizado.

Ether way si se implementara algo como || entonces gofmt probablemente no debería obligarlo a escribirlo en una línea. En algunos casos, es muy posible que ocupe demasiado espacio.

@erwbgy , este me parece el mejor, pero no estoy convencido de que el pago sea tan bueno.

@ object88 El pago para mí es que elimina el texto estándar para el manejo simple de errores y no intenta hacer demasiado. Solo hace:

val, err := func()
if err != nil {
    return nil, errors.WithStack(err)
}

más simple:

val, err? := func()

Nada impide que se realice un manejo de errores más complejo de la forma actual.

¿Cuáles son los valores devueltos sin error? ¿Son siempre el valor cero? Si había un valor asignado previamente para una devolución con nombre, ¿se anula? Si el valor nombrado se usa para almacenar el resultado de una función errónea, ¿se devuelve?

Todos los demás parámetros de retorno son valores nulos apropiados. Para los parámetros con nombre, esperaría que mantuvieran cualquier valor asignado previamente, ya que se garantiza que ya se les asignará algún valor.

Puede el ? ¿Se aplica el operador a un valor sin error? ¿Podría hacer algo como (! Ok)? ¿¡O bien !? (lo cual es un poco extraño, porque está agrupando asignación y operación)? ¿O esta sintaxis solo sirve para errores?

No, no creo que tenga sentido usar esta sintaxis para cualquier otra cosa que no sean valores de error.

Creo que las funciones "obligatorias" van a proliferar debido a la desesperación por un código más legible.

sqlx

db.MustExec(schema)

plantilla html

var t = template.Must(template.New("name").Parse("html"))

Propongo el operador de pánico (no estoy seguro de si debería llamarlo 'operador')

a,  😱 := someFunc(b)

igual que, pero tal vez más inmediato que

a, err := someFunc(b)
if err != nil {
  panic(err)
}

😱 es probablemente demasiado difícil de escribir, podríamos usar algo como!, O !!, o

a,  !! := someFunc(b)
!! = maybeReturnsError()

Quizás !! pánico y! devoluciones

Es hora de mis 2 centavos. ¿Por qué no podemos simplemente usar debug.PrintStack() de la biblioteca estándar para los seguimientos de pila? La idea es imprimir el seguimiento de la pila solo en el nivel más profundo, donde ocurrió el error.

¿Por qué no podemos simplemente usar debug.PrintStack() de la biblioteca estándar para los seguimientos de pila?

Los errores pueden transitar por muchas pilas. Pueden enviarse a través de canales, almacenarse en variables, etc. A menudo es más útil conocer esos puntos de transición que conocer el fragmento donde se generó el error por primera vez.

Además, el seguimiento de la pila a menudo incluye funciones auxiliares internas (no exportadas). Eso es útil cuando intenta depurar un bloqueo inesperado, pero no es útil para los errores que ocurren en el curso del funcionamiento normal.

¿Cuál es el enfoque más fácil de usar para los principiantes completos en programación?

Me encuentro de una versión más simple. Solo necesita uno if !err
Nada especial, intuitivo, sin puntuación adicional, código mucho más pequeño

ir
absPath, err: = p.preparePath ()
devuelve nulo, err si err

err: = doSomethingWith (absPath) if! err
doSomethingElse () if! err

doSomethingRegardlessOfErr ()

// Manejar err en un solo lugar; si es necesario; como una captura sin sangría
if err {
devuelve "error sin contaminación de código", err
}
''

err := doSomethingWith(absPath) if !err
doSomethingElse() if !err

Bienvenido de nuevo, buenas condiciones de publicación de MUMPS ;-)

Gracias pero no gracias.

@dmajkic Esto no ayuda en nada a "devolver el error con información contextual adicional".

@erwbgy el título de este número es _propuesta: Ir 2: simplificar el manejo de errores con || err sufijo_ mi comentario estaba en ese contexto. Lo siento si intervine en la discusión anterior.

@cznic Sí. Las condiciones posteriores no son Go-way, pero las condiciones previas también parecen contaminadas:

if !err; err := doSomethingWith(absPath)
if !err; doSomethingElse()

@dmajkic Hay más en la propuesta que solo el título: ianlancetaylor describe tres formas de manejar los errores y señala específicamente que pocas propuestas facilitan la devolución del error con información adicional.

@erwbgy Pasé por todos los problemas especificados por @ianlancetaylor Todos se try() ) o usar caracteres especiales no alfanuméricos. Personalmente, no me gusta eso, ¡ya que el código está sobrecargado! "# $% & Tiende a parecer ofensivo, como palabrotas.

Estoy de acuerdo, y siento, lo que dicen las primeras líneas de este problema: demasiado código Go se utiliza para el manejo de errores. La sugerencia que hice está en línea con ese sentimiento, con una sugerencia muy cercana a lo que Go siente ahora, sin necesidad de palabras clave o caracteres clave adicionales.

¿Qué tal un aplazamiento condicional?

func something() (int, error) {
    var error err
    var oth err

    defer err != nil {
        return 0, mycustomerror("More Info", err)
    }
    defer oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }

    _, err = a()
    _, err = b()
    _, err = c()
    _, oth = d()
    _, err = e()

    return 2, nil
}


func something() (int, error) {
    var error err
    var oth err

    _, err = a()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = b()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = c()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, oth = d()
    if oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }
    _, err = e()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }

    return 2, nil
}

Eso cambiaría significativamente el significado de defer : es solo algo que se ejecuta al final del alcance, no algo que hace que un alcance se cierre antes.

Si introducen el Try Catch en este idioma, todos estos problemas se resolverán de una manera muy sencilla.

Deberían introducir algo como esto. Si el valor del error se establece en un valor distinto de cero, puede interrumpir el flujo de trabajo actual y activar automáticamente la sección de captura y luego la sección finalmente y las bibliotecas actuales también pueden funcionar sin cambios. ¡Problema resuelto!

try (var err error){
     i, err:=DoSomething1()
     i, err=DoSomething2()
     i, err=DoSomething3()
} catch (err error){
   HandleError(err)
   // return err  // similar to throw err
} finally{
  // Do something
}

image

@sbinet Esto es mejor que nada, pero si simplemente usan el mismo paradigma try-catch con el que todos están familiarizados, es mucho mejor.

@KamyarM Parece que está sugiriendo agregar un mecanismo para lanzar una excepción cada vez que una variable se establece en un valor distinto de cero. Ese no es el "paradigma con el que todo el mundo está familiarizado". No conozco ningún idioma que funcione de esa manera.

Parece similar a Swift, que también tiene "excepciones" que no funcionan como excepciones.

Diferentes lenguajes han demostrado que try catch es realmente una solución de segunda clase, mientras que supongo que Go no podrá resolver eso como con una mónada Maybe y así sucesivamente.

@ianlancetaylor Acabo de

Las excepciones de Java son un desastre, así que tengo que estar firmemente en desacuerdo contigo aquí @KamyarM. El hecho de que algo sea familiar no significa que sea una buena elección.

Lo que quiero decir.

@KamyarM Gracias por la aclaración. Consideramos y rechazamos explícitamente las excepciones. Los errores no son excepcionales; ocurren por todo tipo de razones completamente normales. https://blog.golang.org/errors-are-values

Excepcional o no, pero resuelven el problema de la hinchazón del código debido al código repetitivo de manejo de errores. El mismo problema paralizó Objective-C, que funciona casi exactamente como Go. Los errores son solo valores de tipo NSError, no tienen nada de especial. Y tiene el mismo problema con muchos ifs y errores de envoltura. Por eso Swift cambió las cosas. Terminaron con una combinación de dos: funciona como excepciones, lo que significa que finaliza la ejecución y debería detectar la excepción. Pero no desenrolla la pila y funciona como un retorno normal. Entonces, el argumento técnico en contra del uso de excepciones para el flujo de control no se aplica allí; estas "excepciones" son tan rápidas como el retorno normal. Es más un azúcar sintáctico. Pero Swift tiene un problema único con ellos. Muchas de las API de Cocoa son asincrónicas (devoluciones de llamada y GCD) y simplemente no son compatibles con ese tipo de manejo de errores; las excepciones son inútiles sin algo como await. Pero casi todo el código de Go es sincrónico y estas "excepciones" podrían funcionar.

@urandom
Las excepciones en Java no son malas. El problema está en los malos programadores que no saben cómo usarlo.

Si su idioma tiene características terribles, alguien eventualmente usará esa característica. Si su idioma no tiene tal característica, hay un 0% de posibilidades. Es matemática simple.

@como no estoy de acuerdo contigo en que try-catch es una característica terrible. Es una característica muy útil y nos hace la vida mucho más fácil y esa es la razón por la que estamos comentando aquí, por lo que es posible que el equipo de Google GoLang agregue una funcionalidad similar. Personalmente, odio esos códigos de manejo de errores if-elses en GoLang y no me gusta mucho ese concepto de aplazamiento-pánico-recuperación (es similar a try-catch pero no tan organizado como lo es con los bloques Try-Catch-Finalmente) . Agrega tanto ruido al código que hace que el código sea ilegible en muchos casos.

La funcionalidad para manejar errores sin repetición ya existe en el idioma. Agregar más funciones para saciar a los principiantes que provienen de lenguajes basados ​​en excepciones no parece una buena idea.

¿Y quién viene de C / C ++, Objective-C, donde tenemos exactamente el mismo problema con el texto estándar? Y es frustrante ver que un lenguaje moderno como Go sufre exactamente los mismos problemas. Es por eso que todo este bombo en torno a los errores como valores se siente tan falso y tonto: ya se ha hecho durante años, decenas de años. Parece que Go no aprendió nada de esa experiencia. Especialmente mirando a Swift / Rust, que en realidad está tratando de encontrar una mejor manera. Vaya a un acuerdo con una solución existente como Java / C # resuelto con excepciones, pero al menos esos son lenguajes mucho más antiguos.

@KamyarM ¿Alguna vez ha utilizado la programación orientada a ferrocarriles? ¿El haz?

No elogiaría tanto las excepciones si las usara, en mi humilde opinión.

@ShalokShalom No mucho. ¿Pero no es solo una máquina de estado? ¿En caso de falla hacer esto y en caso de éxito en aquello? Bueno, creo que no todos los tipos de errores deberían manejarse como excepciones. Cuando solo se necesita una validación de entrada de usuario, uno puede simplemente devolver un valor booleano con el detalle de los errores de validación. Las excepciones deben limitarse a E / S o acceso a la red o entradas de funciones incorrectas y en el caso de que un error sea realmente crítico y desee detener la ruta de ejecución feliz a toda costa.

Una de las razones por las que algunas personas dicen que Try-Catch no es bueno es por su rendimiento. Probablemente se deba al uso de una tabla de mapa de controladores para cada lugar en el que puede ocurrir una excepción. Leí en alguna parte que incluso las excepciones son más rápidas (costo cero cuando no ocurren excepciones, pero tienen un costo mucho mayor cuando realmente ocurren) comparándolo con la verificación If Error (siempre se verifica independientemente de que haya error o no). Aparte de eso, no creo que haya ningún problema con la sintaxis de Try-Catch. Es solo la forma en que lo implementa el compilador lo que lo hace diferente, no su sintaxis.

Las personas que vienen de C / C ++ elogian exactamente a Go por NO tener excepciones y
por tomar una decisión sabia, resistir a quienes afirman que es "moderno" y
agradeciendo a Dios por el flujo de trabajo legible (especialmente después de C ++).

El martes 17 de abril de 2018 a las 03:46, Antonenko Artem [email protected]
escribió:

¿Y quién viene de C / C ++, Objective-C donde tenemos el mismo
¿Problema exacto con el texto estándar? Y frustrante ver un lenguaje moderno
como Go sufren exactamente los mismos problemas. Es por eso que todo este bombo
en torno a los errores, ya que los valores se sienten tan falsos y tontos, ya se ha hecho
durante años, decenas de años. Se siente como si Go no aprendiera nada de eso.
experiencia. Especialmente mirando a Swift / Rust que en realidad está tratando de encontrar
una mejor manera. Resolver con una solución existente como Java / C # resuelto con
excepciones, pero al menos esos son lenguajes mucho más antiguos.

-
Estás recibiendo esto porque comentaste.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/21161#issuecomment-381793840 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AICzv9w608ea2fwPq_wNpTDBnKMAdAKTks5tpTtsgaJpZM4Oi1c-
.

@kirillx Nunca dije que quiero excepciones como en C ++. Por favor, lea mi comentario de nuevo. ¿Y qué tiene eso que ver con C, donde el manejo de errores es aún más horrible? No solo tiene toneladas de texto repetitivo, sino que también carece de valores diferidos y de retorno múltiples, lo que lo obliga a devolver valores utilizando argumentos de puntero y usar goto para organizar su lógica de limpieza. Go usa el mismo concepto de errores, pero resuelve algunos de los problemas con valores de retorno múltiples y diferidos. Pero la repetición sigue ahí. Otros lenguajes modernos tampoco quieren excepciones, pero tampoco quieren conformarse con el estilo C debido a su verbosidad. Por eso tenemos esta propuesta y tanto interés por este problema.

Las personas que abogan por las excepciones deben leer este artículo: https://ckwop.me.uk/Why-Exceptions-Suck.html

La razón por la que las excepciones de estilo Java / C ++ son inherentemente malas no tiene nada que ver con el rendimiento de implementaciones particulares. Las excepciones son malas porque son de BASIC "en error goto", con los gotos invisibles en el contexto donde pueden tener efecto. Las excepciones ocultan el manejo de errores donde puede olvidarlo fácilmente. Se suponía que las excepciones comprobadas de Java resolverían ese problema, pero en la práctica no lo hicieron porque la gente simplemente detectaba y comía las excepciones o volcaba los rastros de pila en todas partes.

Escribo Java la mayoría de las semanas y, enfáticamente, no quiero ver excepciones de estilo Java en Go, por muy alto rendimiento que tengan.

@lpar no es todos los bucles for, mientras que los bucles, si elses, cambian de caja, se rompe y continúa como cosas de GoTo. ¿Qué queda entonces de un lenguaje de programación?

Mientras, para y si / de lo contrario no implican un flujo de ejecución que salta de manera invisible a otro lugar sin ningún marcador que indique que lo hará.

¿Qué es diferente si alguien simplemente pasa el error a la persona que llamó anteriormente en GoLang y esa persona simplemente devuelve eso a la persona que llamó anteriormente y así sucesivamente (aparte de mucho ruido de código)? ¿Cuántos códigos debemos buscar y recorrer para ver quién se encargará del error? Lo mismo ocurre con try-catch.

¿Qué puede detener al programador? A veces, la función realmente no necesita manejar un error. solo queremos pasar el error a la interfaz de usuario para que un usuario o el administrador del sistema pueda resolverlo o encontrar una solución.

Si una función no quiere manejar una excepción, simplemente no usa el bloque try-catch para que el llamador anterior pueda manejarlo. No creo que la sintaxis tenga ningún problema. También es mucho más limpio. Sin embargo, el rendimiento y la forma en que se implementa en un idioma es diferente.

Como puede ver a continuación, necesitamos agregar 4 líneas de código para no manejar un error:

func myFunc1() error{
  // ...
  if (err){
      return err
  }
  return nil
}

Si desea devolver los errores a la persona que llama para que los maneje, está bien. El punto es que es visible que lo está haciendo, en el punto en el que se le devolvió el error.

Considerar:

x, err := lib.SomeFunc(100, 4)
if err != nil {
  // A
}
// B

Al mirar el código, sabe que puede ocurrir un error al llamar a la función. Usted sabe que si ocurre el error, el flujo de código terminará en el punto A. Sabe que el único otro lugar donde el flujo de código terminará es el punto B. También hay un contrato implícito que si err es nulo, x es un valor válido, cero o de otro modo.

Contraste con Java:

x = SomeFunc(100, 4)

Al mirar el código, no tiene idea de si podría ocurrir un error cuando se llama a la función. Si ocurre un error y se expresa como una excepción, entonces ocurre un goto , y podría terminar en algún lugar en la parte inferior del código circundante ... o si no se detecta la excepción, podría terminar en algún lugar al final de una parte completamente diferente de su código. O podría terminar en el código de otra persona. De hecho, dado que el manejador de excepciones predeterminado puede ser reemplazado, potencialmente podría terminar literalmente en cualquier lugar, basado en algo hecho por el código de otra persona.

Además, no hay un contrato implícito de que x sea válido; es común que las funciones devuelvan un valor nulo para indicar errores o valores faltantes.

Con Java, estos problemas pueden ocurrir con cada llamada, no solo con código incorrecto, es algo de lo que debe preocuparse con _todo_ el código Java. Es por eso que los entornos de desarrollo de Java tienen ayuda emergente para mostrarle si la función a la que está apuntando podría causar una excepción o no, y qué excepciones podría causar. Es por eso que Java agregó excepciones marcadas, de modo que para errores comunes debe tener al menos alguna advertencia de que la llamada a la función podría generar una excepción y desviar el flujo del programa. Mientras tanto, los nulos devueltos y la naturaleza no verificada de NullPointerException son un problema tal que agregaron la clase Optional a Java 8 para intentar mejorarlo, aunque el costo es tener que envolver explícitamente el devuelve el valor de cada función que devuelve un objeto.

Mi experiencia es que NullPointerException de un valor nulo inesperado que me han entregado es la forma más común en la que mi código Java termina fallando, y por lo general termino con un gran rastreo que es casi completamente inútil, y un mensaje de error que no indica la causa porque se generó lejos del código defectuoso. En Go, honestamente, no he encontrado que el pánico nulo de desreferencia sea un problema significativo, a pesar de que tengo mucha menos experiencia con Go. Eso, para mí, indica que Java debería aprender de Go, y no al revés.

No creo que la sintaxis tenga ningún problema.

No creo que nadie esté diciendo que la sintaxis sea el problema con las excepciones de estilo Java.

@lpar , ¿Por qué

¿Los pánicos solo se pueden recuperar y los lanzamientos se pueden atrapar? ¿Correcto?

Acabo de recordar una diferencia, con el pánico puede entrar en pánico un objeto de error o un objeto de cadena o puede ser cualquier otro tipo de objetos (corríjame si me equivoco) pero con throw puede lanzar un objeto de tipo Exception o una subclase de excepción solamente.

¿Por qué cero pánicos de desreferencia en Go es mejor que NullPointerException en Java?

Porque lo primero casi nunca ocurre en mi experiencia, mientras que lo segundo sucede todo el tiempo, por las razones que expliqué.

@lpar Bueno, no he programado con Java recientemente y supongo que eso es algo nuevo (últimos 5 años) pero C # tiene un operador de navegación seguro para evitar referencias nulas para crear excepciones, pero ¿Qué tiene Go? No estoy seguro, pero supongo que no tiene nada para manejar esas situaciones. Entonces, si desea evitar el pánico, aún debe agregar esas feas declaraciones anidadas if-not-nil-else al código.

Por lo general, no es necesario verificar los valores de retorno para ver si son nulos en Go, siempre que verifique el valor de retorno del error. Así que no hay declaraciones if anidadas feas.

La desreferencia nula fue un mal ejemplo. Si no lo detecta, Go y Java funcionan exactamente igual: se produce un bloqueo con el seguimiento de la pila. ¿Cómo puede ser inútil el seguimiento de la pila? No lo hago ahora. Sabes el lugar exacto donde sucedió. Para mí, tanto en C # como en Go, generalmente es trivial corregir ese tipo de bloqueo porque, en mi experiencia, la desreferencia nula se debe a un simple error del programador. En este caso particular, no hay nada que aprender de nadie.

@lpar

Porque lo primero casi nunca ocurre en mi experiencia, mientras que lo segundo sucede todo el tiempo, por las razones que expliqué.

Eso es accidental y no vi ninguna razón en su comentario de que Java de alguna manera sea peor en nil / null que Go. Observé numerosos bloqueos nulos de desreferencia en el código Go. Son exactamente lo mismo que la desreferencia nula en C # / Java. Es posible que esté utilizando más tipos de valores en Go, lo que ayuda (C # también los tiene) pero no cambia nada.

En cuanto a las excepciones, veamos Swift. Tiene una palabra clave throws para funciones que podrían generar un error. Funcionar sin él no se puede lanzar. En cuanto a la implementación, funciona como return: probablemente algún registro esté reservado para devolver el error y cada vez que lanza la función, se devuelve normalmente pero lleva consigo un valor de error. Entonces, el problema de los errores inesperados se resolvió. Sabes exactamente qué función podría producirse, sabes el lugar exacto donde podría suceder. Los errores son valores y no requieren desenrollar la pila. Simplemente se devuelven hasta que lo atrapas.

O algo similar a Rust, donde tiene un tipo de resultado especial que lleva un resultado y un error. Los errores se pueden propagar sin declaraciones condicionales explícitas. Más un montón de bondad de coincidencia de patrones, pero eso probablemente no sea para Go.

Ambos lenguajes toman ambas soluciones (C y Java) y las combinan en algo mejor. Propagación de errores de excepciones + valores de error y flujo de código obvio de C + sin código repetitivo feo que no hace nada útil. Así que creo que es prudente observar estas implementaciones en particular y no rechazarlas por completo solo porque se parecen a excepciones de alguna manera. Hay una razón por la que las excepciones se utilizan en tantos idiomas porque tienen un lado positivo. De lo contrario, los idiomas los ignorarían. Especialmente después de C ++.

¿Cómo puede ser inútil el seguimiento de la pila? No lo hago ahora.

Dije "casi completamente inútil". Como en, solo necesito una línea de información, pero tiene docenas de líneas.

Eso es accidental y no vi ninguna razón en su comentario de que Java de alguna manera sea peor en nil / null que Go.

Entonces no estás escuchando. Vuelve atrás y lee la parte sobre contratos implícitos.

Los errores se pueden propagar sin declaraciones condicionales explícitas.

Y ese es exactamente el problema: los errores se propagan y el flujo de control cambia sin nada explícito que indique que va a suceder. Aparentemente, no cree que sea un problema, pero otros no están de acuerdo.

No sé si las excepciones implementadas por Rust o Swift tienen los mismos problemas que Java, se lo dejaré a alguien con experiencia en los lenguajes en cuestión.

Y ese es exactamente el problema: los errores se propagan y el flujo de control cambia sin nada explícito que indique que va a suceder.

Esto me suena a verdad. Si desarrollo algún paquete que consume otro paquete, y ese paquete arroja una excepción, ahora _I_ también tengo que ser consciente de eso, independientemente de si quiero usar esa función. Ésta es una faceta poco común entre las características del lenguaje propuestas; la mayoría son cosas en las que un programador puede optar, o simplemente no usar a su discreción. Las excepciones, por su propia intención, cruzan todo tipo de fronteras, esperadas o no.

Dije "casi completamente inútil". Como en, solo necesito una línea de información, pero tiene docenas de líneas.

¿Y los trazos de Go enormes con cientos de rutinas de gor son de alguna manera más útiles? No entiendo a dónde vas con esto. Java y Go son exactamente iguales aquí. Y ocasionalmente le resulta útil observar la pila completa para comprender cómo terminó su código donde se bloqueó. Las trazas de C # y Go me ayudaron varias veces con eso.

Entonces no estás escuchando. Vuelve atrás y lee la parte sobre contratos implícitos.

Lo leí, nada cambió. En mi experiencia, no es un problema. Para eso es la documentación en ambos idiomas ( net.ParseIP por ejemplo). Si olvida verificar si su valor es nulo / nulo o no, tiene exactamente el mismo problema en ambos idiomas. En la mayoría de los casos, Go devolverá un error y C # lanzará una excepción, por lo que ni siquiera tendrá que preocuparse por nil. Una buena API no solo te devuelve un valor nulo sin generar una excepción o algo que indique lo que está mal. En otros casos, lo verifica explícitamente. Los tipos más comunes de errores con nulo en mi experiencia son cuando tiene búferes de protocolo donde cada campo es un puntero / objeto o tiene una lógica interna donde los campos de clase / estructura pueden ser nulos según el estado interno y se olvida de verificarlo antes acceso. Ese es el patrón más común para mí y nada en Go alivia significativamente este problema. Puedo nombrar dos cosas que ayudan un poco: valores vacíos útiles y tipos de valores. Pero se trata más de la facilidad de programación porque no es necesario que construyas todas las variables antes de usarlas.

Y ese es exactamente el problema: los errores se propagan y el flujo de control cambia sin nada explícito que indique que va a suceder. Aparentemente, no cree que sea un problema, pero otros no están de acuerdo.

Eso es un problema, nunca dije lo contrario, pero la gente aquí está tan obsesionada con las excepciones de Java / C # / C ++ que ignoran cualquier cosa que se les parezca un poco. ¿Exactamente por qué Swift requiere que marque funciones con throws para que pueda ver exactamente lo que debe esperar de una función y dónde se puede romper el flujo de control y en Rust que usa? para propagar explícitamente un error con varios métodos auxiliares para darle más contexto. Ambos usan el mismo concepto de errores como valores, pero lo envuelven en azúcar sintáctico para reducir la repetición.

¿Y los trazos de Go enormes con cientos de rutinas de gor son de alguna manera más útiles?

Con Go, puede lidiar con los errores registrándolos junto con la ubicación en el punto en que se detectan. No hay retroceso a menos que elija agregar uno. Solo una vez tuve que hacer eso.

En mi experiencia, no es un problema.

Bueno, mi experiencia es diferente, y creo que las experiencias de la mayoría de las personas son diferentes, y como prueba de ello, cito el hecho de que Java 8 agregó tipos opcionales.

Este hilo aquí discutió muchas de las fortalezas y debilidades de Go y su sistema de manejo de errores, incluida una discusión sobre excepciones o no, recomiendo leerlo:

https://elixirforum.com/t/discussing-go-split-thread/13006/2

Mis 2 centavos para el manejo de errores (lo siento si tal idea se mencionó anteriormente).

Queremos volver a lanzar errores en la mayoría de los casos. Esto conduce a tales fragmentos:

a, err := fn()
if err != nil {
    return err
}
use(a)
return nil

Volvamos a generar el error no nulo automáticamente si no se asignó a una variable (sin sintaxis adicional). El código anterior se convertirá en:

a := fn()
use(a)

// or just

use(fn())

El compilador guardará err en la variable implícita (invisible), verificará nil y procederá (si err == nil) o lo devolverá (si err! = Nil) y devolverá nil al final de la función si no se produjeron errores durante la ejecución de la función como de costumbre, pero de forma automática e implícita.

Si err debe manejarse, debe asignarse a una variable explícita y usarse:

a, err := fn()
if err != nil {
    doSomething(err)
} else {
    use(a)
}
return nil

El error se puede eliminar de esta manera:

a, _ := fn()
use(a)

En casos raros (fantásticos) con más de un error devuelto, el manejo explícito de errores será obligatorio (como ahora):

err1, err2 := fn2()
if err1 != nil || err2 != nil {
    return err1, err2
}
return nil, nil

Ese también es mi argumento: queremos volver a arrojar errores en la mayoría de los casos, que suele ser el caso predeterminado. Y quizás darle algo de contexto. Con excepciones, el contexto se agrega automáticamente mediante seguimientos de pila. Con errores como en Go lo hacemos a mano agregando mensaje de error. ¿Por qué no hacerlo más simple? Y eso es exactamente lo que otros lenguajes están tratando de hacer mientras lo equilibran con el tema de la claridad.

Así que estoy de acuerdo con "Relanzar el error no nulo automáticamente si no se asignó a una variable (sin sintaxis adicional)", pero la última parte me molesta. Ahí es donde está la raíz del problema con las excepciones y por qué, creo, la gente está tan en contra de hablar sobre cualquier tema ligeramente relacionado con ellas. Cambian el flujo de control sin ninguna sintaxis adicional. Eso es malo.

Si observa Swift, por ejemplo, este código no se compilará

func a() throws {}
func b() throws {
  a()
}

a puede arrojar un error, por lo que debe escribir try a() para propagar un error. Si elimina throws de b , no se compilará ni siquiera con try a() . Tienes que manejar el error dentro de b . Esa es una forma mucho mejor de manejar errores que resuelve tanto el problema del flujo de control poco claro de las excepciones como la verbosidad de los errores de Objective-C. Este último es casi exactamente como errores en Go y lo que Swift debe reemplazar. Lo que no me gusta es try, catch cosas que también usa Swift. Preferiría dejar errores como parte de un valor de retorno.

Entonces, lo que propondría es tener la sintaxis adicional. De modo que el sitio de la llamada se diga por sí mismo que es un lugar potencial donde el flujo de control podría cambiar. Lo que también propondría es que no escribir esta sintaxis adicional produciría un error de compilación. Eso, a diferencia de cómo funciona Go ahora, lo obligaría a manejar el error. Podría incluir la capacidad de silenciar el error con algo como _ porque en algunos casos sería muy frustrante manejar cada pequeño error. Como printf . No me importa si falla al registrar algo. Go ya tiene estas molestas importaciones. Pero eso se resolvió con herramientas al menos.

Hay dos alternativas para el error de tiempo de compilación que puedo pensar en este momento. Al igual que Go ahora, deje que el error se ignore en silencio. No me gusta eso y ese siempre fue mi problema con el manejo de errores de Go. No fuerza nada, el comportamiento predeterminado es ignorar silenciosamente el error. Eso es malo, no es así como se escriben programas robustos y fáciles de depurar. Tuve demasiados casos en Objective-C cuando era vago o no tenía tiempo e ignoré el error solo para recibir un error en ese mismo código, pero sin ninguna información de diagnóstico de por qué sucedió. Al menos registrarlo me permitiría resolver el problema allí mismo en muchos casos.

La desventaja es que las personas pueden comenzar a ignorar los errores, coloque try, catch(...) todas partes, por así decirlo. Esa es una posibilidad pero, al mismo tiempo, con los errores ignorados por defecto, es aún más fácil hacerlo. Creo que el argumento sobre las excepciones no se aplica aquí. Con excepciones, lo que algunas personas están tratando de lograr es una ilusión de que su programa es más estable. El hecho de que una excepción no controlada bloquee el programa es el problema aquí.

Otra alternativa sería entrar en pánico. Pero eso es frustrante y trae recuerdos de excepciones. Eso definitivamente llevaría a la gente a realizar una codificación "defensiva" para que su programa no se bloquee. Para mí, el lenguaje moderno debería hacer tantas cosas como sea posible en tiempo de compilación y dejar la menor cantidad posible de decisiones en tiempo de ejecución. Donde el pánico podría ser apropiado es en la parte superior de la pila de llamadas. Por ejemplo, no manejar un error en la función principal produciría automáticamente un pánico. ¿Esto también se aplica a las gorutinas? Probablemente no debería.

¿Por qué considerar compromisos?

@ nick-korsakov la propuesta original (este número) quiere agregar más contexto a los errores:

Ya es fácil (quizás demasiado fácil) ignorar el error (ver # 20803). Muchas propuestas existentes para el manejo de errores facilitan la devolución del error sin modificar (por ejemplo, # 16225, # 18721, # 21146, # 21155). Pocos facilitan la devolución del error con información adicional.

Vea también este comentario .

En este comentario sugiero que para avanzar en esta discusión (en lugar de ejecutar en bucles) deberíamos definir mejor los objetivos, por ejemplo, qué es un mensaje de error manejado cuidadosamente. Todo es bastante interesante de leer, pero parece afectado por un problema de memoria de peces de colores de tres segundos (no muy concentrado / avanzando, repitiendo agradables cambios de sintaxis creativos y argumentos sobre excepciones / pánicos, etc.).

Otro cobertizo para bicicletas:

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    try err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    try errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    try errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

try significa "devolver si no es el valor vacío". En esto, supongo que errors.Errorf devolverá nil cuando err sea nil. Creo que se trata de tantos ahorros como podemos esperar si nos apegamos al objetivo de un envoltorio fácil.

Los tipos de escáner en la biblioteca estándar almacenan el estado de error dentro de una estructura cuyos métodos pueden verificar responsablemente la existencia de un error antes de continuar.

type Scanner struct{
    err error
}
func (s *Scanner) Scan() bool{
   if s.err != nil{
       return false
   }
   // scanning logic
}
func (s *Scanner) Err() error{ return s.err }

Al usar tipos para almacenar el estado de error, es posible mantener el código que usa ese tipo libre de verificaciones de errores redundantes.

Tampoco requiere cambios de sintaxis creativos y extraños ni transferencias de control inesperadas en el idioma.

También tengo que sugerir algo como try / catch, donde err se define dentro de try {}, y si err se establece en un valor distinto de nil, el flujo se interrumpe desde try {} para err los bloques del controlador (si hay alguno).

Internamente no hay excepciones, pero todo debería estar más cerca
a la sintaxis que hace if err != nil break comprobaciones después de cada línea donde se puede asignar err.
P.ej:

...
try(err) {
   err = doSomethig()
   err, value := doSomethingElse()
   doSomethingObliviousToErr()
   err = thirdErrorProneThing()
} 
catch(err SomeErrorType) {
   handleSomeSpecificErr(err)
}
catch(err Error) {
  panic(err)
}

Sé que se parece a C ++, pero también es bien conocido y más limpio que el manual if err != nil {...} después de cada línea.

@como

El tipo de escáner funciona porque está haciendo todo el trabajo, por lo tanto, puede permitirse realizar un seguimiento de su propio error en el camino. No nos engañemos pensando que esta es una solución universal, por favor.

@carlmjohnson

Si queremos un manejo de línea para un error simple, podríamos cambiar la sintaxis para permitir que la declaración de retorno sea el comienzo de un bloque de una línea.
Permitiría a la gente escribir:

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    if err != nil return err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    if err != nil return errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    if err != nil return errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

Creo que la especificación debería cambiarse a algo como (esto podría ser bastante ingenuo :))

Block = "{" StatementList "}" | "return" Expression .

No creo que el retorno de carcasa especial sea realmente mejor que simplemente cambiar gofmt para simplificar si err marca una línea en lugar de tres.

@urandom

Error que se fusiona más allá de un tipo encuadrable y sus acciones no deben fomentarse. Para mí, indica una falta de esfuerzo para ajustar o agregar un contexto de error entre los errores que se originan en diferentes acciones no relacionadas.

El enfoque del escáner es una de las peores cosas que leí en el contexto de todo este mantra de "los errores son valores":

  1. Es inútil en casi todos los casos de uso que requieren una gran cantidad de errores en el manejo de la placa de caldera. Las funciones que llaman a varios paquetes externos no se beneficiarán de ella.
  2. Es un concepto complicado y desconocido. Presentarlo solo confundirá a los futuros lectores y hará que su código sea más complicado de lo necesario para que pueda solucionar las deficiencias en el diseño del lenguaje.
  3. Oculta la lógica e intenta ser similar a las excepciones tomando lo peor de ella (flujo de control complejo) sin obtener ningún beneficio.
  4. En algunos casos, desperdiciará recursos informáticos. Cada llamada tendrá que perder tiempo en verificaciones de errores inútiles que sucedieron hace mucho tiempo.
  5. Oculta el lugar exacto donde ocurrió el error. Imagine un caso en el que analiza o serializa algún formato de archivo. Tendría una cadena de llamadas de lectura / escritura. Imagina que el primero falla. ¿Cómo dirías dónde ocurrió exactamente el error? ¿Qué campo estaba analizando o serializando? "IO error", "timeout": estos errores serían inútiles en este caso. Puede proporcionar contexto a cada lectura / escritura (nombre de campo, por ejemplo). Pero en este punto es mejor simplemente renunciar a todo el enfoque, ya que está trabajando en su contra.

En algunos casos, desperdiciará recursos informáticos.

¿Puntos de referencia? ¿Qué es exactamente un "recurso informático"?

Oculta el lugar exacto donde ocurrió el error.

No, no es así, porque los errores que no son nulos no se sobrescriben

Las funciones que llaman a varios paquetes externos no se beneficiarán de ella.
Es un concepto complicado y desconocido.
El enfoque del escáner es una de las peores cosas que leí en el contexto de todo este "errores son valores"

Mi impresión es que no comprende el enfoque. Es lógicamente equivalente a la verificación de errores regular en un tipo autónomo, le sugiero que estudie el ejemplo de cerca para que tal vez sea lo peor que entienda en lugar de lo peor que haya _leído_.

Lo siento, voy a agregar mi propia propuesta a la pila. He leído la mayor parte de lo que hay aquí, y aunque me gustan algunas de las propuestas, siento que están tratando de hacer demasiado. El problema es el repetitivo del error. Mi propuesta es simplemente eliminar ese texto estándar en el nivel de sintaxis y dejar las formas en que se transmiten los errores.

Propuesta

Reduzca el texto repetitivo al habilitar el uso del token _! como azúcar sintáctico para causar pánico cuando se le asigna un error no sea nulo

val, err := something.MayError()
if err != nil {
    panic(err)
}

podría convertirse

val, _! := something.MayError()

y

if err := something.MayError(); err != nil {
    panic(err)
}

podría convertirse

_! = something.MayError()

Por supuesto, el símbolo en particular está sujeto a debate. También consideré _^ , _* , @ y otros. Elegí _! como la sugerencia de facto porque pensé que sería la más familiar de un vistazo.

Sintácticamente, _! (o el token elegido) sería un símbolo de tipo error disponible en el ámbito en el que se usa. Comienza como nil , y cada vez que se asigna, se realiza una verificación nil . Si se establece en un valor de error no sea nulo, se inicia un pánico. Debido a que _! (o, nuevamente, el token elegido) no sería un identificador sintácticamente válido en go, la colisión de nombres no sería una preocupación. Esta variable etérea solo se introduciría en los ámbitos donde se usa, similar a los valores de retorno con nombre. Si se necesita un identificador sintácticamente válido, quizás se podría usar un marcador de posición que se volvería a escribir en un nombre único en el momento de la compilación.

Justificación

Una de las críticas más comunes que veo es la verbosidad del manejo de errores. Los errores en los límites de la API no son algo malo. Sin embargo, tener que llevar los errores a los límites de la API puede ser una molestia, especialmente para los algoritmos profundamente recursivos. Para sortear la propagación de errores de verbosidad añadida que introduce el código recursivo, se pueden utilizar los pánicos. Siento que esta es una técnica bastante utilizada. Lo he usado en mi propio código, y lo he visto usar en la naturaleza, incluso en el analizador de go. A veces, ha realizado la validación en otra parte de su programa y espera que el error sea nulo. Si se recibiera un error no nulo, esto violaría su invariante. Cuando se viola una invariante, es aceptable entrar en pánico. En el código de inicialización complejo, a veces tiene sentido convertir los errores en pánico y recuperarlos para que se devuelvan a algún lugar con más conocimiento del contexto. En todos estos escenarios, existe la oportunidad de reducir la repetición de errores.

Me doy cuenta de que la filosofía de go es evitar el pánico en la medida de lo posible. No son una herramienta para la propagación de errores a través de los límites de la API. Sin embargo, son una característica del lenguaje y tienen casos de uso legítimos, como los descritos anteriormente. Los pánicos son una forma fantástica de simplificar la propagación de errores en el código privado, y una simplificación de la sintaxis sería de gran ayuda para hacer que el código sea más limpio y, posiblemente, más claro. Siento que es más fácil reconocer _! (o @ , o `_ ^, etc ...) de un vistazo que la forma" if-error-panic ". Un token puede reducir drásticamente la cantidad de código que se debe escribir / leer para transmitir / comprender:

  1. podría haber un error
  2. si hay un error, no lo esperamos
  3. si hay un error, probablemente se esté manejando hacia arriba en la cadena

Al igual que con cualquier característica de sintaxis, existe la posibilidad de abuso. En este caso, la comunidad de go ya tiene un conjunto de mejores prácticas para lidiar con los pánicos. Dado que esta adición de sintaxis es azúcar sintáctica para el pánico, ese conjunto de mejores prácticas se puede aplicar a su uso.

Además de la simplificación de los casos de uso aceptables para el pánico, esto también facilita la creación rápida de prototipos en marcha. Si tengo una idea que quiero anotar en el código, y solo quiero que los errores bloqueen el programa mientras jugueteo, podría hacer uso de esta adición de sintaxis en lugar de la forma "if-error-panic". Si puedo expresarme en menos líneas en las primeras etapas de desarrollo, me permite convertir mis ideas en código más rápido. Una vez que tengo una idea completa en el código, regreso y refactorizo ​​mi código para devolver errores en los límites apropiados. No dejaría pánicos libres en el código de producción, pero pueden ser una poderosa herramienta de desarrollo.

Los pánicos son solo excepciones con otro nombre, y una cosa que me encanta de Go es que las excepciones son excepcionales. No quiero fomentar más excepciones dándoles azúcar sintáctica.

@carlmjohnson Una de dos cosas tiene que ser verdad:

  1. Los pánicos son parte del lenguaje con casos de uso legítimos, o
  2. Los pánicos no tienen casos de uso legítimos y, por lo tanto, deben eliminarse del lenguaje.

Sospecho que la respuesta es 1.
También estoy en desacuerdo con que "los pánicos son solo excepciones con otro nombre". Creo que ese tipo de agitación de la mano impide una discusión real. Existen diferencias clave entre pánicos y excepciones, como se ve en la mayoría de los otros idiomas.

Entiendo la reacción instintiva de "los pánicos son malos", pero los sentimientos personales sobre el uso del pánico no cambian el hecho de que los pánicos se usan y, de hecho, son útiles. El compilador go usa pánicos para salir de procesos profundamente recursivos tanto en el analizador como en la fase de verificación de tipos (la última vez que miré).
Usarlos para propagar errores a través de código profundamente recursivo parece no solo ser un uso aceptable, sino un uso respaldado por los desarrolladores de go.

Los pánicos comunican algo específico:

algo salió mal aquí que aquí no estaba preparado para manejar

Siempre habrá lugares en el código donde eso sea cierto. Especialmente al principio del desarrollo. Go se ha modificado para mejorar la experiencia de refactorización antes: la adición de alias de tipo. Ser capaz de propagar errores no deseados con pánico hasta que pueda desarrollar si y cómo manejarlos en un nivel más cercano a la fuente puede hacer que la escritura y la refactorización progresiva del código sean mucho menos detalladas.

Siento que la mayoría de las propuestas aquí proponen grandes cambios en el lenguaje. Este es el enfoque más transparente que se me ocurrió. Permite que todo el modelo cognitivo actual de manejo de errores en go permanezca intacto, mientras que permite la reducción de sintaxis para un caso específico, pero común. Actualmente, las mejores prácticas dictan que "el código Go no debe entrar en pánico a través de los límites de la API". Si tengo métodos públicos en un paquete, deberían devolver errores si algo sale mal, excepto en raras ocasiones en las que el error es irrecuperable (violaciones invariantes, por ejemplo). Esta adición al lenguaje no reemplazaría esa mejor práctica. Esta es simplemente una forma de reducir el texto repetitivo en el código interno y hacer que las ideas de bosquejos sean más claras. Sin duda, hace que el código sea más fácil de leer linealmente.

var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

es mucho más legible que

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

Realmente no existe una diferencia fundamental entre un pánico en Go y una excepción en Java o Python, etc., aparte de la sintaxis y la falta de una jerarquía de objetos (lo cual tiene sentido porque Go no tiene herencia). Cómo funcionan y cómo se utilizan es el mismo.

Por supuesto, el pánico tiene un lugar legítimo en el idioma. Los pánicos son para manejar errores que solo deberían ocurrir debido a errores del programador que de otra manera serían irrecuperables. Por ejemplo, si divide por cero en un contexto entero, no hay un valor de retorno posible y es su propia culpa no verificar primero el cero, por lo que entra en pánico. De manera similar, si lee un segmento fuera de los límites, intente usar nil como valor, etc. Esas cosas son causadas por un error del programador, no por una condición anticipada, como que la red esté inactiva o un archivo con permisos incorrectos, por lo que simplemente entran en pánico y volar la pila. Go proporciona algunas funciones auxiliares que causan pánico, como la plantilla, debido a que se anticipa que se usarán con cadenas codificadas donde cualquier error tendría que ser causado por un error del programador. La falta de memoria no es una falla del programador per se, pero también es irrecuperable y puede ocurrir en cualquier lugar, por lo que no es un error sino un pánico.

La gente a veces también usa los pánicos como una forma de cortocircuitar la pila, pero eso generalmente está mal visto por razones de legibilidad y rendimiento, y no veo ninguna posibilidad de que Go cambie para fomentar su uso.

Vaya pánico y las excepciones no comprobadas de Java son prácticamente idénticas y existen por las mismas razones y para manejar los mismos casos de uso. No anime a la gente a usar el pánico para otros casos porque esos casos tienen los mismos problemas que las excepciones en otros idiomas.

La gente a veces también usa los pánicos como una forma de cortocircuitar la pila, pero eso generalmente está mal visto por razones de legibilidad y rendimiento.

En primer lugar, el problema de la legibilidad es algo que este cambio de sintaxis aborda directamente:

// clearly, linearly shows that these steps must occur in order,
// and any errors returned cause a panic, because this piece of
// code isn't responsible for reporting or handling possible failures:
// - IO Error: either network or disk read/write failed
// - External service error: some unexpected response from the external service
// - etc...
// It's not this code's responsibility to be aware of or handle those scenarios.
// That's perhaps the parent process's job.
var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

vs

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

Dejando de lado la legibilidad por el momento, la otra razón dada es el rendimiento.
Sí, es cierto que el uso de declaraciones de pánico y aplazamiento incurre en una penalización de rendimiento, pero en muchos casos esta diferencia es insignificante para la operación que se realiza. Las E / S de disco y de red, en promedio, tomarán mucho más tiempo que cualquier magia de pila potencial para administrar aplazamientos / pánicos.

Escucho que este punto se repite mucho cuando se habla de pánico, y creo que es falso decir que los pánicos son una degradación del rendimiento. Ciertamente PUEDEN serlo, pero no tienen por qué serlo. Como muchas otras cosas en un idioma. Si está entrando en pánico dentro de un bucle estrecho donde el impacto de la interpretación realmente importaría, no debería diferir también dentro de ese bucle. De hecho, cualquier función que elija entrar en pánico generalmente no debería detectar su propio pánico. De manera similar, una función go escrita hoy no devolvería tanto un error como un pánico. Eso es confuso, tonto y no es la mejor práctica. Quizás así es como podemos estar acostumbrados a ver las excepciones que se usan en Java, Python, Javascript, etc., pero no es así como los pánicos se usan generalmente en el código go, y no creo que agregar un operador específicamente para el caso de propagar un error subir la pila de llamadas a través del pánico va a cambiar la forma en que la gente usa el pánico. De todos modos, están usando el pánico. El objetivo de esta extensión de sintaxis es reconocer el hecho de que los desarrolladores usan el pánico, y tiene usos que son perfectamente legítimos, y reducir la repetición a su alrededor.

¿Puede darme algunos ejemplos de código problemático que cree que habilitaría esta función de sintaxis, que actualmente no son posibles / contra las mejores prácticas? Si alguien está comunicando errores a los usuarios de su código a través de pánico / recuperación, eso está actualmente mal visto y obviamente seguirá siéndolo, incluso si se agregara una sintaxis como esta. Si puede, responda lo siguiente:

  1. ¿Qué abusos imagina que surgirían de una extensión de sintaxis como esta?
  2. ¿Qué transmite var1, err := trySomeTask1(); if err != nil { panic(err) } que var1, _! := trySomeTask1() no? ¿Por qué?

Me parece que el meollo de su argumento es que "los pánicos son malos y no deberíamos usarlos".
No puedo descomprimir y discutir las razones detrás de eso si no se comparten.

Esas cosas son causadas por un error del programador, no por una condición anticipada, como que la red esté inactiva o un archivo con permisos incorrectos, por lo que entran en pánico y hacen estallar la pila.

A mí, como a la mayoría de las ardillas, me gusta la idea de errores como valores. Creo que ayuda a comunicar claramente qué partes de una API garantizan un resultado frente a cuáles pueden fallar, sin tener que mirar la documentación.

Permite cosas como recopilar errores y aumentar los errores con más información. Todo eso es muy importante en los límites de la API, donde su código se cruza con el código de usuario. Sin embargo, dentro de esos límites de API, a menudo no es necesario hacer todo eso con sus errores. Especialmente si está esperando la ruta feliz y tiene otro código responsable de manejar el error si esa ruta falla.

Hay ocasiones en las que no es tarea de su código manejar un error.
Si estoy escribiendo una biblioteca, no me importa si la pila de red está inactiva, eso está fuera de mi control como desarrollador de bibliotecas. Devolveré esos errores al código de usuario.

Incluso en mi propio código, hay ocasiones en las que escribo un fragmento de código cuyo único trabajo es devolver los errores a una función principal.

Por ejemplo, digamos que http.HandlerFunc lee un archivo del disco como respuesta; esto casi siempre funcionará y, si falla, es probable que el programa no esté escrito correctamente (error del programador) o que haya un problema con el sistema de archivos. fuera del ámbito de responsabilidad del programa. Una vez que http.HandlerFunc entra en pánico, se cierra y algún controlador base captará ese pánico y escribirá un 500 en el cliente. Si en algún momento en el futuro quisiera manejar ese error de manera diferente, puedo reemplazar _! con err y hacer lo que quiera con el valor del error. La cuestión es que, durante la vida del programa, es probable que no necesite hacer eso. Si me encuentro con problemas como ese, el controlador no es la parte del código responsable de manejar ese error.

Puedo, y normalmente lo hago, escribir if err != nil { panic(err) } o if err != nil { return ..., err } en mis controladores para cosas como fallas de E / S, fallas de red, etc. Cuando necesito verificar un error, aún puedo hacerlo. La mayor parte del tiempo, sin embargo, solo escribo if err != nil { panic(err) } .

O, otro ejemplo, si estoy buscando de forma recursiva un trie (digamos en una implementación de enrutador http), declararé una función func (root *Node) Find(path string) (found Value, err error) . Esa función aplazará una función para recuperar los pánicos generados al bajar del árbol. ¿Qué pasa si el programa crea intentos con formato incorrecto? ¿Qué pasa si algún IO falla porque el programa no se está ejecutando como un usuario con los permisos correctos? Estos problemas no son el problema de mi algoritmo de búsqueda de prueba, a menos que lo haga explícitamente más tarde, pero son posibles errores que puedo encontrar. Devolverlos hasta el final de la pila conduce a una gran cantidad de verbosidad adicional, incluida la retención de lo que idealmente serán varios valores de error nulos en la pila. En cambio, puedo optar por generar un error en pánico hasta esa función de API pública y devolvérselo al usuario. Por el momento, esto todavía incurre en esa verbosidad adicional, pero no tiene por qué ser así.

Otras propuestas están discutiendo cómo tratar un valor de retorno como especial. Esencialmente es el mismo pensamiento, pero en lugar de usar características ya integradas en el lenguaje, buscan modificar el comportamiento del lenguaje para ciertos casos. En términos de facilidad de implementación, este tipo de propuesta (azúcar sintáctica para algo ya soportado) va a ser la más sencilla.

Editar para agregar:
No estoy casado con la propuesta que hice tal como está escrita, pero sí creo que es importante mirar el problema del manejo de errores desde un nuevo ángulo. Nadie está sugiriendo algo tan novedoso, y quiero ver si podemos replantear nuestra comprensión del tema. El problema es que hay demasiados lugares donde los errores se manejan explícitamente cuando no es necesario, y los desarrolladores desean una forma de propagarlos en la pila sin código repetitivo adicional. Resulta que Go ya tiene esa función, pero no tiene una buena sintaxis. Esta es una discusión sobre cómo envolver la funcionalidad existente en una sintaxis menos detallada para hacer que el idioma sea más ergonómico sin cambiar el comportamiento. ¿No es eso una victoria, si podemos lograrlo?

@mccolljr Gracias, pero uno de los objetivos de esta propuesta es animar a las personas a desarrollar nuevas formas de manejar los tres casos de manejo de errores: ignorar el error, devolver el error sin modificar, devolver el error con información contextual adicional. Su propuesta de pánico no aborda el tercer caso. Es importante.

@mccolljr Creo que los límites de la API son mucho más comunes de lo que parece. No veo las llamadas dentro de la API como el caso común. En todo caso, podría ser al revés (algunos datos serían interesantes aquí). Así que no estoy seguro de que desarrollar una sintaxis especial para llamadas dentro de la API sea la dirección correcta. Además, usar errores return ed, en lugar de errores panic ed, dentro de una API suele ser una buena forma de hacerlo (especialmente si ideamos un plan para este problema). panic errores

No creo que agregar un operador específicamente para el caso de propagar un error en la pila de llamadas a través del pánico vaya a cambiar la forma en que las personas usan el pánico.

Creo que te equivocas. La gente buscará su operador de taquigrafía porque es muy conveniente, y luego terminarán usando el pánico mucho más que antes.

Si los pánicos son útiles a veces o raramente, y si son útiles a través o dentro de los límites de la API, son pistas falsas. Hay muchas acciones que se pueden realizar ante un error. Estamos buscando una forma de acortar el código de manejo de errores sin privilegiar una acción sobre las demás.

pero en muchos casos esta diferencia es insignificante para la operación que se realiza

Si bien es cierto, creo que es un camino peligroso. Pareciendo insignificante al principio, se acumularía y eventualmente causaría cuellos de botella más adelante cuando ya es tarde. Creo que deberíamos tener en cuenta el rendimiento desde el principio y tratar de encontrar mejores soluciones. Swift y Rust ya mencionados tienen propagación de errores, pero lo implementan, básicamente, como retornos simples envueltos en azúcar sintáctico. Sí, es fácil reutilizar la solución existente, pero preferiría dejar todo como está que simplificar y alentar a la gente a usar pánicos escondidos detrás de un azúcar sintáctico desconocido que intenta ocultar el hecho de que son, básicamente, excepciones.

Pareciendo insignificante al principio, se acumularía y eventualmente causaría cuellos de botella más adelante cuando ya es tarde.

No, gracias. Los cuellos de botella de desempeño imaginarios son cuellos de botella de desempeño geométricamente insignificantes.

No, gracias. Los cuellos de botella de desempeño imaginarios son cuellos de botella de desempeño geométricamente insignificantes.

Deje sus sentimientos personales fuera de este tema. Obviamente tienes algún problema conmigo y no quieres traer nada útil, así que simplemente ignora mis comentarios y deja un voto negativo como hiciste con casi todos los comentarios antes. No es necesario que sigas publicando estas respuestas sin sentido.

No tengo ningún problema contigo, solo estás haciendo afirmaciones sobre los cuellos de botella en el rendimiento sin ningún dato que respalde eso y lo señalo con palabras y pulgares.

Amigos, mantengan la conversación respetuosa y centrada en el tema. Este problema trata sobre el manejo de errores de Go.

https://golang.org/conduct

Me gustaría volver a visitar la parte "devolver el error / con contexto adicional", ya que supongo que ignorar el error ya está cubierto por el _ ya existente.

Propongo una palabra clave de dos palabras que puede ir seguida de una cadena (opcionalmente). La razón por la que es una palabra clave de dos palabras es doble. Primero, a diferencia de un operador que es intrínsecamente críptico, es más fácil comprender lo que hace sin demasiado conocimiento previo. Elegí "o burbuja", porque espero que la palabra or sin un error asignado signifique para el usuario que el error se está manejando aquí, si no es nulo. Algunos usuarios ya asociarán el or con el manejo de un valor falso de otros lenguajes (perl, python), y leer data := Foo() or ... podría inconscientemente decirles que data se puede utilizar si el or se alcanza parte del estado de cuenta. En segundo lugar, la palabra clave bubble aunque es relativamente corta, podría significar para el usuario que algo está subiendo (la pila). La palabra up también podría ser adecuada, aunque no estoy seguro de si todo or up es lo suficientemente comprensible. Finalmente, todo es una palabra clave, en primer lugar porque es más legible, y en segundo lugar porque ese comportamiento no puede ser escrito por una función en sí (es posible que pueda llamar al pánico para escapar de la función en la que se encuentra, pero luego puede ' No te detengas, alguien más tendrá que recuperarse).

Lo siguiente es solo para la propagación de errores, por lo tanto, solo se puede usar en funciones que devuelven un error y los valores cero de cualquier otro argumento de retorno:

Para devolver un error sin modificarlo de ninguna manera:

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble

    return data;
}

Para devolver un error con un mensaje adicional:

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble fmt.Sprintf("reading file %s", path)

    modified := modifyData(data) or bubble "modifying the data"

    return data;
}

Y finalmente, introduzca un mecanismo de adaptador global para la modificación personalizada de errores:

// Default Bubble Processor
errors.BubbleProcessor(func(msg string, err error) error {
    return fmt.Errorf("%s: %v", msg, err)
})

// Some program might register the following:
errors.BubbleProcessor(func(msg string, err error) error {
    return errors.WithMessage(err, msg)
})

Finalmente, para los pocos lugares donde es necesario un manejo realmente complejo, la forma detallada ya existente es ya la mejor forma.

Interesante. Tener un manejador de burbujas global les da a las personas que quieren trazas de pila un lugar para realizar la llamada para una traza, lo cual es un buen beneficio de ese método. OTOH, si tiene la firma func(string, error) error , eso significa que el burbujeo debe realizarse con el tipo de error incorporado y no con ningún otro tipo, como un tipo concreto que implemente error .

Además, la existencia de or bubble sugiere la posibilidad de or die o or panic . No estoy seguro de si eso es una característica o un error.

a diferencia de un operador que es intrínsecamente críptico, es más fácil comprender lo que hace sin demasiado conocimiento previo

Eso puede ser bueno cuando lo encuentre por primera vez. Pero leerlo y escribirlo una y otra vez, parece demasiado detallado y requiere demasiado espacio para transmitir una cosa bastante simple: un error no manejado con burbujas en la pila. Los operadores son crípticos al principio, pero son concisos y contrastan bien con el resto del código. Separan claramente la lógica principal del manejo de errores porque de hecho es un separador. Tener tantas palabras en una línea dañará la legibilidad en mi opinión. Al menos combínalos en orbubble o suelta uno de ellos. No veo el sentido de tener dos palabras clave allí. Convierte Go en un idioma hablado y sabemos cómo va eso (VB, por ejemplo)

No soy muy fanático de un adaptador global. Si mi paquete establece un procesador personalizado y el suyo también establece un procesador personalizado, ¿quién gana?

@ object88
Creo que es similar al registrador predeterminado. Solo configura la salida una vez (en su programa), y eso afecta a todos los paquetes que usa.

El manejo de errores es muy diferente al registro; uno describe la salida informativa del programa, el otro gestiona el flujo del programa. Si configuro el adaptador para hacer una cosa en mi paquete, que necesito administrar adecuadamente el flujo lógico, y otro paquete o programa lo altera, estás en un mal lugar.

Por favor, trae de vuelta el Try Catch Finalmente y no necesitamos más peleas. Hace felices a todos. No hay nada de malo en tomar prestadas características y sintaxis de otros lenguajes de programación. Java lo hizo y C # lo hizo también y ambos son lenguajes de programación realmente exitosos. GO comunidad (o autores) por favor estén abiertos a cambios cuando sea necesario.

@KamyarM , respetuosamente no estoy de acuerdo; try / catch _no_ hace felices a todos. Incluso si quisiera implementar eso en su código, una excepción lanzada significa que todos los que usan su código deben manejar las excepciones. Ese no es un cambio de idioma que se pueda localizar en su código.

@ object88
En realidad, me parece que un procesador de burbujas describe la salida de error informativa del programa, por lo que no es tan diferente de un registrador. E imagino que desea una representación de error única en toda su aplicación y que no varíe de un paquete a otro.

Aunque tal vez puedas dar un ejemplo breve, tal vez me falte algo.

Muchas gracias por tus pulgares hacia abajo. Ese es exactamente el tema del que estoy hablando. La comunidad de GO no está abierta a cambios y lo siento y realmente no me gusta eso.

Esto probablemente no esté relacionado con este caso, pero estaba buscando otro día para Go equivalente del operador ternario de C ++ y me encontré con este enfoque alternativo:

v: = map [bool] int {true: first_expression, false: second_expression} [condición]
en lugar de simplemente
v = condición? first_expression: second_expression;

¿Cuál de las 2 formas prefieren, chicos? ¿Un código ilegible arriba (Go My Way) con probablemente muchos problemas de rendimiento o la segunda sintaxis simple en C ++ (Highway)? Prefiero a los de la carretera. Yo no se ustedes.

Entonces, para resumir, traiga nuevas sintaxis, póngalas prestadas de otros lenguajes de programación. No hay nada de malo en ello.

Atentamente,

La comunidad de GO no está abierta a cambios y lo siento y realmente no me gusta eso.

Creo que esto caracteriza erróneamente la actitud subyacente a lo que está experimentando. Sí, la comunidad produce mucho rechazo cuando alguien propone try / catch o?:. Pero la razón no es que seamos resistentes a nuevas ideas. Casi todos tenemos experiencia en el uso de lenguajes con estas características. Estamos bastante familiarizados con ellos y algunos de nosotros los hemos usado a diario durante años. Nuestra resistencia se basa en el hecho de que estas son _ ideas viejas_, no nuevas. Ya abrazamos un cambio: un cambio lejos de try / catch y un cambio lejos de usar?:. A lo que nos resistimos es a cambiar _back_ para usar estas cosas que ya usamos y que no nos gustaron.

En realidad, me parece que un procesador de burbujas describe la salida de error informativa del programa, por lo que no es tan diferente de un registrador. E imagino que desea una representación de error única en toda su aplicación y que no varíe de un paquete a otro.

¿Qué pasaría si alguien quisiera usar el burbujeo para pasar rastros de pila y luego usarlo para tomar una decisión? Por ejemplo, si el error se origina en una operación de archivo, falla, pero si se origina en la red, espere y vuelva a intentarlo. Podría ver la construcción de algo de lógica para esto en un controlador de errores, pero si solo hay un controlador de errores por tiempo de ejecución, eso sería una receta para el conflicto.

@urandom , tal vez este sea un ejemplo trivial, pero digamos que mi adaptador devuelve otra estructura que implementa error , que espero consumir en otra parte de mi código. Si aparece otro adaptador y reemplaza mi adaptador, entonces mi código deja de funcionar correctamente.

@KamyarM El lenguaje y sus modismos van de la mano. Cuando consideramos cambios en el manejo de errores, no solo estamos hablando de cambiar la sintaxis, sino (potencialmente) también la estructura misma del código.

Try-catch-finalmente sería un cambio de este tipo muy invasivo: cambiaría fundamentalmente la forma en que se estructuran los programas de Go. Por el contrario, la mayoría de las otras propuestas que ve aquí son locales para cada función: los errores siguen siendo valores devueltos explícitamente, el flujo de control evita saltos no locales, etc.

Para usar su ejemplo de un operador ternario: sí, puede falsificar uno hoy usando un mapa, pero espero que no lo encuentre en el código de producción. No sigue los modismos. En cambio, normalmente verá algo más como:

    var v int
    if condition {
        v = first_expression
    } else {
        v = second_expression
    }

No es que no queramos tomar prestada la sintaxis, es que tenemos que considerar cómo encajaría con el resto del lenguaje y el resto del código que ya existe hoy.

@KamyarM Yo uso Go y Java, y enfáticamente _no_ quiero que Go copie el manejo de excepciones de Java. Si quieres Java, usa Java. Y lleve la discusión de los operadores ternarios a un tema apropiado, por ejemplo, # 23248.

@lpar Entonces, si trabajo para una empresa y, por alguna razón desconocida, eligieron GoLang como su lenguaje de programación, ¡simplemente necesito renunciar a mi trabajo y solicitar uno de Java! ¡Vamos hombre!

@bcmills Puede contar el código que sugirió allí. Creo que son 6 líneas de código en lugar de una y probablemente obtengas un par de puntos de complejidad ciclomática de código por eso (Ustedes usan Linter, ¿verdad?).

@carlmjohnson y @bcmills Cualquier sintaxis que sea antigua y madura no significa que sea mala. En realidad, creo que la sintaxis if else es mucho más antigua que la sintaxis del operador ternario.

Es bueno que hayas traído este modismo de GO. Creo que ese es solo uno de los problemas de este lenguaje. Siempre que hay una solicitud de cambios, alguien dice oh no, eso está en contra del idioma Go. Lo veo solo como una excusa para resistir los cambios y bloquear cualquier idea nueva.

@KamyarM, por favor sea cortés. Si desea leer más sobre algunas ideas detrás de mantener el lenguaje pequeño, le recomiendo https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html.

Además, un comentario general, no relacionado con la discusión reciente de try / catch.

Ha habido muchas propuestas en este hilo. Hablando por mí mismo, todavía no siento que tenga una comprensión sólida del problema (s) por resolver. Me encantaría saber más sobre ellos.

También estaría emocionado si alguien quisiera asumir la nada envidiable pero importante tarea de mantener una lista organizada y resumida de los problemas que se han discutido.

@josharian Estaba hablando con franqueza allí. Quería mostrar los problemas exactos en el idioma o la comunidad. Considérelo como más crítica. GoLang está abierto a las críticas, ¿verdad?

@KamyarM Si trabajara para una empresa que eligió Rust por su lenguaje de programación, ¿iría al Rust Github y comenzaría a exigir administración de memoria recolectada de basura y punteros de estilo C ++ para no tener que lidiar con el verificador de préstamos?

La razón por la que los programadores de Go no quieren excepciones al estilo de Java no tiene nada que ver con la falta de familiaridad con ellas. Encontré excepciones por primera vez en 1988 a través de Lisp, y estoy seguro de que hay otras personas en este hilo que las encontraron incluso antes; la idea se remonta a principios de la década de 1970.

Lo mismo es aún más cierto para las expresiones ternarias. Lea sobre la historia de Go: Ken Thompson, uno de los creadores de Go, implementó el operador ternario en el lenguaje B (predecesor de C) en Bell Labs en 1969. Creo que es seguro decir que estaba al tanto de sus beneficios y trampas al considerar si incluirlo en Go.

Go está abierto a críticas, pero requerimos que la discusión en los foros de Go sea cortés. Ser franco no es lo mismo que ser descortés. Consulte la sección "Valores de Gopher" de https://golang.org/conduct. Gracias.

@lpar Sí, si Rust tiene un foro así, lo haría ;-) En serio, lo haría. Porque quiero que se escuche mi voz.

@ianlancetaylor ¿
Vamos, hombre, solo estamos hablando del lenguaje de programación Go. No se trata de una religión o política ni nada por el estilo.
Fui franco. Quería que se escuchara mi voz. Creo que por eso existe este foro. Para que se escuchen las voces. Puede que no le guste mi sugerencia o mi crítica. Está bien. Pero supongo que debe dejarme hablar y discutir, de lo contrario, todos podemos concluir que todo es perfecto y que no hay ningún problema y, por lo tanto, no hay necesidad de más discusiones.

@josharian Gracias por el artículo,

Bueno, miré hacia atrás en mis comentarios para ver si había algo malo allí. ¡Lo único que podría haber insultado (todavía llamo a eso crítica por cierto) es el lenguaje de programación GoLang Idioms! ¡Jaja!

Para volver a nuestro tema, si escuchan mi voz, por favor Go Autores considere traer de vuelta los bloques Try catch. Deje que el programador decida usarlo en el lugar correcto o no (ya tiene algo similar, me refiero al pánico aplazar la recuperación, ¿por qué no probar Catch, que es más familiar para los programadores?).
Sugerí una solución para el manejo actual de Go Error para compatibilidad con versiones anteriores. No digo que sea la mejor opción, pero creo que es viable.

Me retiraré de discutir más sobre este tema.

Gracias por la oportunidad.

@KamyarM Estás confundiendo nuestras solicitudes de que seas educado con nuestro desacuerdo con tus argumentos. Cuando la gente no está de acuerdo contigo, estás respondiendo en términos personales con comentarios como "Muchas gracias por tus pulgares hacia abajo. Ese es exactamente el problema del que estoy hablando. La comunidad de GO no está abierta a cambios y lo siento y realmente no lo hago". Me gusta eso ".

Una vez más: sea cortés. Cíñete a los argumentos técnicos. Evite los argumentos ad hominem que atacan a las personas en lugar de a las ideas. Si eres sincero, no entiendes lo que quiero decir, estoy dispuesto a discutirlo sin conexión; envíeme un correo electrónico. Gracias.

Lanzaré mi 2c, y espero que no esté literalmente repitiendo algo en los otros N cien comentarios (o interviniendo en la discusión de la propuesta de urandom).

Me gusta la idea original que se publicó, pero con dos ajustes principales:

  • Desprendimiento sintáctico de bicicletas: Creo firmemente que cualquier cosa que tenga un flujo de control implícito debería ser un operador por sí solo, en lugar de una sobrecarga de un operador existente. Lanzaré ?! , pero estoy contento con cualquier cosa que no se confunda fácilmente con un operador existente en Go.

  • El RHS de este operador debe tomar una función, en lugar de una expresión con un valor inyectado arbitrariamente. Esto permitiría a los desarrolladores escribir un código de manejo de errores bastante conciso, sin dejar de tener clara su intención y ser flexibles con lo que pueden hacer, p. Ej.

func returnErrorf(s string, args ...interface{}) func(error) error {
  return func(err error) error {
    return errors.New(fmt.Sprintf(s, args...) + ": " + err.Error())
  }
}

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) ?! returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() ?! returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() ?! func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

El RHS nunca debe evaluarse si no ocurre un error, por lo que este código no asignará ningún cierre o lo que sea en el camino feliz.

También es bastante sencillo "sobrecargar" este patrón para que funcione en casos más interesantes. Tengo tres ejemplos en mente.

Primero, podríamos hacer que return sea ​​condicional si el RHS es un func(error) (error, bool) , así (si permitimos esto, creo que deberíamos usar un operador distinto de los retornos incondicionales. use ?? , pero mi declaración "No me importa mientras sea distinta" todavía se aplica):

func maybeReturnError(err error) (error, bool) {
  if err == io.EOF {
    return nil, false
  }
  return err, true
}

func id(err error) error { return err }

func ignoreError(err error) (error, bool) { return nil, false }

func foo(n int) error {
  // Does nothing
  id(io.EOF) ?? ignoreError
  // Still does nothing
  id(io.EOF) ?? maybeReturnError
  // Returns the given error
  id(errors.New("oh no")) ?? maybeReturnError
  return nil
}

Alternativamente, podríamos aceptar funciones RHS que tengan tipos de retorno que coincidan con los de la función externa, así:

func foo(r io.Reader) ([]int, error) {
  returnError := func(err error) ([]int, error) { return []int{0}, err }
  // returns `[]int{0}, err` on a Read failure
  n := r.Read(make([]byte, 4)) ?! returnError
  return []int{n}, nil
}

Y finalmente, si realmente queremos, podemos generalizar esto para que funcione con más que solo errores cambiando el tipo de argumento:

func returnOpFailed(name string) func(bool) error {
  return func(_ bool) error {
    return errors.New(name + " failed")
  }
}

func returnErrOpFailed(name string) func(error) error {
  return func(err error) error {
    return errors.New(name + " failed: " + err.Error())
  }
}

func foo(c chan int, readInt func() (int, error), d map[int]string) (string, error) {
  n := <-c ?! returnOpFailed("receiving from channel")
  m := readInt() ?! returnErrOpFailed("reading an int")
  result := d[n + m] ?! returnOpFailed("looking up the number")
  return result, nil
}

... Lo que personalmente me resultaría muy útil cuando tengo que hacer algo terrible, como decodificar manualmente un map[string]interface{} .

Para ser claros, principalmente muestro las extensiones como ejemplos. No estoy seguro de cuál de ellos (si alguno) logra un buen equilibrio entre simplicidad, claridad y utilidad general.

Me gustaría volver a visitar la parte "devolver el error / con contexto adicional", ya que supongo que ignorar el error ya está cubierto por el _ ya existente.

Propongo una palabra clave de dos palabras que puede ir seguida de una cadena (opcionalmente).

@urandom, la primera parte de su propuesta está de acuerdo, siempre se puede comenzar con eso y dejar el BubbleProcessor para una segunda revisión. Las preocupaciones planteadas por @ object88 son válidas en mi opinión; Recientemente he visto consejos como "no debes sobrescribir el cliente / transporte predeterminado de http ", este se convertiría en otro de esos.

Ha habido muchas propuestas en este hilo. Hablando por mí mismo, todavía no siento que tenga una comprensión sólida del problema (s) por resolver. Me encantaría saber más sobre ellos.

También estaría emocionado si alguien quisiera asumir la nada envidiable pero importante tarea de mantener una lista organizada y resumida de los problemas que se han discutido.

¿Podrías ser tú @josharian si @ianlancetaylor te nombra? : blush: No sé cómo se están planificando / discutiendo otros temas, pero tal vez esta discusión solo se esté utilizando como un "buzón de sugerencias".

@KamyarM

@bcmills Puede contar el código que sugirió allí. Creo que son 6 líneas de código en lugar de una y probablemente obtengas un par de puntos de complejidad ciclomática de código por eso (Ustedes usan Linter, ¿verdad?).

Ocultar la complejidad ciclomática hace que sea más difícil de ver, pero no la elimina (¿recuerdas strlen ?). Al igual que hacer que el manejo de errores sea "abreviado" hace que la semántica del manejo de errores sea más fácil de ignorar, pero más difícil de ver.

Cualquier enunciado o expresión en la fuente que desvíe el control de flujo debe ser obvio y conciso, pero si se trata de una decisión entre obvio o conciso, en este caso se debe preferir lo obvio.

Es bueno que hayas traído este modismo de GO. Creo que ese es solo uno de los problemas de este lenguaje. Siempre que hay una solicitud de cambios, alguien dice oh no, eso está en contra del idioma Go. Lo veo solo como una excusa para resistir los cambios y bloquear cualquier idea nueva.

Hay una diferencia entre lo nuevo y lo beneficioso. ¿Cree que porque tiene una idea, la mera existencia de ella merece aprobación? Como ejercicio, mire el rastreador de problemas e intente imaginar Go hoy si todas las ideas fueran aprobadas independientemente de lo que pensara la comunidad.

Quizás crea que su idea es mejor que las demás. Ahí es donde entra la discusión. En lugar de degenerar la conversación para hablar sobre cómo todo el sistema está roto debido a modismos, aborde las críticas directamente, punto por punto, o encuentre un término medio entre usted y sus compañeros.

@ gdm85
Agregué el procesador para algún tipo de personalización por parte del error devuelto. Y aunque creo que es un poco como usar el registrador predeterminado, ya que puede salirse con la suya usándolo durante la mayor parte del tiempo, dije que estoy abierto a sugerencias. Y para que conste, no creo que el registrador predeterminado y el cliente http predeterminado estén ni siquiera remotamente en la misma categoría.

También me gusta la propuesta de @gburgessiv , aunque no soy un gran admirador del operador críptico en sí (tal vez al menos elija ? como en Rust, aunque sigo pensando que es críptico). ¿Se vería esto más legible?

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) or returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() or returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() or func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

Y con suerte, su propuesta también incluirá una implementación predeterminada de una función similar a su returnErrorf en algún lugar del paquete errors . Quizás errors.Returnf() .

@KamyarM
Ya expresó su opinión aquí y no recibió ningún comentario o reacción que simpatizara con la causa de la excepción. No veo qué logrará repetir lo mismo, tbh, además de interrumpir las otras discusiones. Y si ese es tu objetivo, simplemente no está bien.

@josharian , intentaré resumir la discusión brevemente. Será parcial, ya que tengo una propuesta en la mezcla, e incompleta, ya que no estoy en condiciones de releer todo el hilo.

El problema que estamos tratando de abordar es el desorden visual causado por el manejo de errores de Go. Aquí hay un buen ejemplo ( fuente ):

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {
    if err := createFolderIfNotExist(to); err != nil {
        return nil, err
    }
    if err := clearFolder(to); err != nil {
        return nil, err
    }
    if err := cloneRepo(to, from); err != nil {
        return nil, err
    }
    dirs, err := getContentFolders(to)
    if err != nil {
        return nil, err
    }
    return dirs, nil
}

Varios comentaristas en este hilo no creen que esto necesite ser arreglado; están felices de que el manejo de errores sea intrusivo, porque lidiar con los errores es tan importante como manejar el caso sin errores. Para ellos, ninguna de las propuestas aquí valen la pena.

Las propuestas que intentan simplificar un código como este se dividen en algunos grupos.

Algunos proponen una forma de manejo de excepciones. Dado que Go podría haber elegido el manejo de excepciones al principio y eligió no hacerlo, parece poco probable que se acepten.

Muchas de las propuestas aquí eligen una acción predeterminada, como regresar de la función (la propuesta original) o entrar en pánico, y sugieren un poco de sintaxis que hace que esa acción sea fácil de expresar. En mi opinión, todas esas propuestas fracasan, porque privilegian una acción a expensas de otras. Regularmente utilizo devoluciones, t.Fatal y log.Fatal para manejar errores, a veces todos en el mismo día.

Otras propuestas no proporcionan forma de aumentar o ajustar el error original, o hacer que sea mucho más difícil de ajustar que no. Estos también son inadecuados, ya que el ajuste es la única forma de agregar contexto a los errores, y si hacemos que sea demasiado fácil de omitir, se hará con menos frecuencia que ahora.

La mayoría de las propuestas restantes agregan algo de azúcar y, a veces, un poco de magia para simplificar las cosas sin restringir las posibles acciones o la capacidad de envolver. Las propuestas de my @bcmills agregan una cantidad mínima de azúcar y cero magia para aumentar ligeramente la legibilidad, y también para evitar un tipo de error desagradable .

Algunas otras propuestas agregan algún tipo de flujo de control no local restringido, como una sección de manejo de errores al principio o al final de una función.

Por último, pero no menos importante, @mpvl reconoce que el manejo de errores puede volverse muy complicado en presencia de pánico. Sugiere un cambio más radical en el manejo de errores de Go para mejorar la precisión y la legibilidad. Tiene un argumento convincente, pero al final creo que sus casos no requieren cambios drásticos y pueden manejarse con los mecanismos existentes .

Disculpas a cualquiera cuyas ideas no estén representadas aquí.

Tengo la sensación de que alguien me va a preguntar sobre la diferencia entre el azúcar y la magia. (Me lo pregunto yo mismo).

Sugar es un poco de sintaxis que acorta el código sin alterar fundamentalmente las reglas del lenguaje. El operador de asignación corta := es azúcar. También lo es el operador ternario de C ?: .

La magia es una alteración más violenta del lenguaje, como introducir una variable en un ámbito sin declararla o realizar una transferencia de control no local.

La línea es definitivamente borrosa.

Gracias por hacer eso, @jba. Muy útil. Solo para destacar los aspectos más destacados, los problemas identificados hasta ahora son:

el desorden visual causado por el manejo de errores de Go

y

el manejo de errores puede volverse muy complicado en presencia de pánico

Si hay otros problemas fundamentalmente diferentes (no soluciones) que @jba y yo nos hemos perdido, por favor,

@josharian ¿Desea considerar los problemas de alcance (https://github.com/golang/go/issues/21161#issuecomment-319277657) como una variante del problema de "desorden visual", o un problema separado?

@bcmills me parece distinto, ya que se trata de problemas sutiles de corrección, a diferencia de la estética / ergonomía (o como mucho, problemas de corrección que involucran el volumen del código). ¡Gracias! ¿Quieres editar mi comentario y agregar una sinopsis de una línea?

Tengo la sensación de que alguien me va a preguntar sobre la diferencia entre el azúcar y la magia. (Me lo pregunto yo mismo).

Utilizo esta definición de magia: mirando un poco de código fuente, si puedes averiguar qué se supone que debe hacer mediante alguna variante del siguiente algoritmo:

  1. Busque todos los identificadores, palabras clave y construcciones gramaticales presentes en la línea o en la función.
  2. Para las construcciones gramaticales y las palabras clave, consulte la documentación oficial del idioma.
  3. Para los identificadores, debe haber un mecanismo claro para ubicarlos usando la información en el código que está mirando, usando los alcances en los que se encuentra actualmente el código, según lo definido por el idioma, desde el cual puede obtener la definición del identificador, lo que sea ​​preciso en tiempo de ejecución.

Si este algoritmo _de manera confiable_ produce una comprensión correcta de lo que va a hacer el código, no es mágico. Si no es así, entonces hay algo de magia en él. La forma recursiva que tenga que aplicarlo mientras intenta seguir las referencias de la documentación y las definiciones de identificadores hasta otras definiciones de identificadores afecta la _complejidad_, pero no la _magia_, de las construcciones / código en cuestión.

Algunos ejemplos de magia incluyen: Identificadores sin una ruta clara de regreso a su origen porque los importó sin un espacio de nombres (importaciones de puntos en Go, especialmente si tiene más de uno). Cualquier habilidad que pueda tener un lenguaje para definir de forma no local lo que resolverá algún operador, como en lenguajes dinámicos donde el código puede redefinir completamente una referencia de función de forma no local, o redefinir lo que hace el lenguaje para identificadores inexistentes. Objetos construidos por esquemas cargados desde una base de datos en tiempo de ejecución, por lo que en el momento del código uno está más o menos ciegamente esperando que estén allí.

Lo bueno de esto es que elimina casi toda la subjetividad de la cuestión.

Volviendo al tema en cuestión, parece que ya hay un montón de propuestas hechas, y las probabilidades de que alguien más resuelva esto con otra propuesta que haga que todos digan "¡Sí! ¡Eso es!" acercarse a cero.

Me parece que tal vez la conversación debería ir en la dirección de categorizar las diversas dimensiones de las propuestas hechas aquí y tener una idea de la priorización. En especial, me gustaría ver esto con miras a revelar requisitos contradictorios que la gente de aquí aplica vagamente.

Por ejemplo, he visto algunas quejas sobre la adición de saltos adicionales en el flujo de control. Pero para mí, en el lenguaje de la propuesta muy original, valoro no tener que agregar || &PathError{"chdir", dir, err} ocho veces dentro de una función si son comunes. (Sé que Go no es tan alérgico al código repetido como otros lenguajes, pero aún así, el código repetido tiene un riesgo muy alto de errores de divergencia). Pero prácticamente por definición, si existe un mecanismo para descartar dicho manejo de errores, el código no puede fluir de arriba a abajo, de izquierda a derecha, sin saltos. ¿Qué se considera generalmente más importante? Sospecho que un examen cuidadoso de los requisitos que la gente está colocando implícitamente en el código revelaría otros requisitos mutuamente contradictorios.

Pero solo en general, siento que si la comunidad pudiera estar de acuerdo con los requisitos después de todo este análisis, la solución correcta podría muy bien salirse de ellos claramente, o al menos, el conjunto de soluciones correcto estará tan obviamente limitado que el problema se vuelve manejable.

(También quiero señalar que al tratarse de una propuesta, el comportamiento actual debería en general ser sometido al mismo análisis que las nuevas propuestas. El objetivo es la mejora significativa, no la perfección; rechazando dos o tres mejoras significativas porque ninguna de ellas son perfectos es un camino a la parálisis. Todas las propuestas son compatibles con versiones anteriores de todos modos, por lo que en aquellos casos en los que el enfoque actual ya es el mejor de todos modos (en mi humilde opinión, el caso en el que cada error se maneja legítimamente de manera diferente, lo que en mi experiencia es raro pero sucede) , el enfoque actual seguirá estando disponible).

He estado pensando en esto desde la segunda vez que escribí if err! = Nil en una función, me parece que una solución bastante simple sería permitir un retorno condicional que se parece a la primera parte del ternario con el entendimiento de si la condición falla, no regresamos.

No estoy seguro de qué tan bien funcionaría esto en términos de análisis / compilación, pero parece que debería ser bastante fácil de interpretar como una declaración if donde el '?' se ve sin romper la compatibilidad donde no se ve, así que pensé en tirarlo como una opción.

Además, habría otros usos para esto más allá del manejo de errores.

entonces podrías hacer algo como esto:

func example1() error {
    err := doSomething()
    return err != nil ? err
    //more code
}

func example2() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, err
    //more code
}

También podríamos hacer cosas como esta cuando tengamos algún código de limpieza, asumiendo que handleErr devolvió un error:

func example3() error {
    err := doSomething()
    return err !=nil ? handleErr(err)
    //more code
}

func example4() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, handleErr(err)
    //more code
}

Quizás luego también se deduzca que podría reducir esto a una sola línea si quisiera:

func example5() error {
    return err := doSomething(); err !=nil ? handleErr(err)
    //more code
}

func example6() (*Mything, error) {
    return err := doSomething(); err !=nil ? nil, handleErr(err)
    //more code
}

El ejemplo de recuperación anterior de @jba podría verse así:

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return err := createFolderIfNotExist(to); err != nil ? nil, err
    return err := clearFolder(to); err != nil ? nil, err
    return err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

Estaría interesado en las reacciones a esta sugerencia, tal vez no sea una gran victoria al salvar el texto estándar, pero se mantiene bastante explícito y, con suerte, solo requiere un pequeño cambio compatible con versiones anteriores (tal vez algunas suposiciones enormemente inexactas en ese frente).

¿Quizás podría separar esto con una devolución por separado? palabra clave que puede agregar claridad y simplificar la vida en términos de no tener que preocuparse por la compatibilidad con return (pensando en todas las herramientas), estas podrían ser reescritas internamente como declaraciones if / return, dándonos esto:

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return? err := createFolderIfNotExist(to); err != nil ? nil, err
    return? err := clearFolder(to); err != nil ? nil, err
    return? err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

No parece haber mucha diferencia entre

return err != nil ? err

y

 if err != nil { return err }

Además, a veces es posible que desee hacer algo diferente a return, como llamar panic o log.Fatal .

He estado dando vueltas en esto desde que presenté una propuesta la semana pasada, y he llegado a la conclusión de que estoy de acuerdo con

Los requisitos establecidos con más frecuencia son que, al final del día, Go debe poder manejar 4 casos de manejo de errores:

  1. Ignorando el error
  2. Devolviendo el error sin modificar
  3. Devolviendo el error con contexto agregado
  4. Entrar en pánico (o matar el programa)

Las propuestas parecen caer en una de tres categorías:

  1. Vuelva a un estilo try-catch-finally de manejo de errores.
  2. Agregue nueva sintaxis / incorporaciones para manejar los 4 casos enumerados anteriormente
  3. Afirmar que go maneja algunos casos bastante bien y proponer sintaxis / incorporaciones para ayudar con los otros casos.

Las críticas a las propuestas dadas parecen dividirse entre preocupaciones sobre la legibilidad del código, saltos no obvios, adición implícita de variables a los alcances y concisión. Personalmente, creo que ha habido mucha opinión personal en las críticas a las propuestas. No digo que eso sea algo malo, pero me parece que no hay realmente un criterio objetivo para calificar las propuestas.

Probablemente no soy la persona para tratar de crear esa lista de criterios, pero creo que sería muy útil si alguien los juntara. He tratado de esbozar mi comprensión del debate hasta ahora como un punto de partida para desglosar 1. lo que hemos visto, 2. lo que está mal, 3. por qué esas cosas están mal y 4. lo que haríamos me gusta ver en su lugar. Creo que capturé una cantidad decente de los primeros 3 elementos, pero tengo problemas para encontrar una respuesta para el elemento 4 sin recurrir a "lo que Go tiene actualmente".

@jba tiene otro buen comentario de resumen arriba, para más contexto. Dice mucho de lo que he dicho aquí, en diferentes palabras.

@ianlancetaylor , o cualquier otra persona más involucrada con el proyecto que yo, ¿se sentiría cómodo agregando un conjunto de criterios "formales" (todo en un comentario, organizado y algo completo pero de ninguna manera vinculante) que deben cumplirse? ¿Quizás si discutimos esos criterios y concretamos 4-6 puntos que las propuestas deben cumplir, podamos reiniciar la discusión con algo más de contexto?

No creo que pueda escribir un conjunto de criterios formales y completos. Lo mejor que puedo hacer es una lista incompleta de las cosas que importan y que solo deben ignorarse si hay un beneficio significativo al hacerlo.

  • Buen soporte para 1) ignorar un error; 2) devolver un error sin modificar; 3) envolver un error con contexto adicional.
  • Si bien el código de manejo de errores debe ser claro, no debe dominar la función. Debería ser fácil de leer el código de manejo sin errores.
  • El código Go 1 existente debería seguir funcionando, o al menos debe ser posible traducir mecánicamente Go 1 al nuevo enfoque con total fiabilidad.
  • El nuevo enfoque debería alentar a los programadores a manejar los errores correctamente. Idealmente, debería ser fácil hacer lo correcto, sea lo que sea lo correcto en cualquier situación.
  • Cualquier enfoque nuevo debe ser más corto y / o menos repetitivo que el enfoque actual, sin dejar de ser claro.
  • El idioma funciona hoy en día y cada cambio tiene un costo. El beneficio del cambio debe valer claramente el costo. No debería ser solo un lavado, debería ser claramente mejor.

Espero recopilar las notas en algún momento aquí, pero quiero abordar lo que, en mi humilde opinión, es otro gran obstáculo en esta discusión, la trivialidad de los ejemplos.

Extraje esto de un proyecto mío real y lo borré para su lanzamiento externo. (Creo que stdlib no es la mejor fuente, ya que le faltan problemas de registro, entre otros ..)

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    if err != nil {
        return nil, err
    }

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    if err != nil {
        return nil, err
    }

    session, err := communicationProtocol.FinalProtocol(conn)
    if err != nil {
        return nil, err
    }
    client.session = session

    return client, nil
}

(No cambiemos demasiado el código en bicicleta. No puedo detenerlo, por supuesto, pero recuerde que este no es mi código real y ha sido destrozado un poco. Y no puedo evitar que publique su propio código de muestra).

Observaciones:

  1. Este no es un código perfecto; Devuelvo muchos errores simples porque es muy fácil, exactamente el tipo de código con el que tenemos problemas. Las propuestas deben puntuarse tanto por su concisión como por la facilidad con la que demuestran corregir este código.
  2. La cláusula if forwardPort == 0 _deliberately_ continúa a través de errores, y sí, este es el comportamiento real, no algo que agregué para este ejemplo.
  3. Este código devuelve un cliente conectado válido O devuelve un error y no hay fugas de recursos, por lo que el manejo de .Close () (solo si la función falla) es deliberado. Tenga en cuenta también que los errores de Cerrar desaparecen, como es bastante típico en Go real.
  4. El número de puerto está restringido en otros lugares, por lo que url.Parse no puede fallar (por examen).

No diría que esto demuestra todos los posibles comportamientos de error, pero cubre una amplia gama. (A menudo defiendo Go on HN y demás señalando que cuando mi código termina de cocinarse, a menudo ocurre en mis servidores de red que tengo _todos tipos_ de comportamientos erróneos; examinando mi propio código de producción, desde 1/3 para completar la mitad de los errores hizo algo más que simplemente devolverse).

También (re) publicaré mi propia propuesta (actualizada) aplicada a este código (a menos que alguien me convenza de que tienen algo aún mejor antes de esa fecha), pero en aras de no monopolizar la conversación, voy a esperar al menos durante el fin de semana. (Esto es menos texto de lo que parece porque es una gran parte de la fuente, pero aún así ...)

El uso de try donde try es solo un atajo para if! = Nil return reduce el código en 6 líneas de 59, que es aproximadamente un 10%.

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    try err

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    try err

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    try err

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    try err

    session, err := communicationProtocol.FinalProtocol(conn)
    try err

    client.session = session

    return client, nil
}

En particular, en varios lugares quise escribir try x() pero no pude porque necesitaba que se estableciera err para que los diferidos funcionen correctamente.

Uno mas. Si tenemos try como algo que puede suceder en las líneas de asignación, se reduce a 47 líneas.

func NewClient(...) (*Client, error) {
    try listener, err := net.Listen("tcp4", listenAddr)

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    try conn, err := ConnectionManager{}.connect(server, tlsConfig)

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    try session, err := communicationProtocol.FinalProtocol(conn)

    client.session = session

    return client, nil
}
import "github.com/pkg/errors"

func Func3() (T1, T2, error) {...}

type PathError {
    err Error
    x   T3
    y   T4
}

type MiscError {
    x   T5
    y   T6
    err Error
}


func Foo() (T1, T2, error) {
    // Old school
    a, b, err := Func(3)
    if err != nil {
        return nil
    }

    // Simplest form.
    // If last unhandled arg's type is same 
    // as last param of func,
    // then use anon variable,
    // check and return
    a, b := Func3()
    /*    
    a, b, err := Func3()
    if err != nil {
         return T1{}, T2{}, err
    }
    */

    // Simple wrapper
    // If wrappers 1st param TypeOf Error - then pass last and only unhandled arg from Func3() there
    a, b, errors.WithStack() := Func3() 
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithStack(err)
    }
    */

    // Bit more complex wrapper
    a, b, errors.WithMessage("unable to get a and b") := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithMessage(err, "unable to get a and b")
    }
    */

    // More complex wrapper
    // If wrappers 1nd param TypeOf is not Error - then pass last and only unhandled arg from Func3() as last
    a, b, fmt.Errorf("at %v Func3() return error %v", time.Now()) := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, fmt.Errorf("at %v Func3() return error %v", time.Now(), err)
    }
    */

    // Wrapping with error types
    a, b, &PathError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &PathError{err, x, y}
    }
    */
    a, b, &MiscError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &MiscError{x, y, err}
    }
    */

    return a, b, nil
}

Ligeramente mágico (siéntase libre de -1) pero admite traducción mecánica

Así es como se vería mi propuesta (algo actualizada):

func NewClient(...) (*Client, error) {
    defer annotateError("client couldn't be created")

    listener := pop net.Listen("tcp4", listenAddr)
    defer closeOnErr(listener)
    conn := pop ConnectionManager{}.connect(server, tlsConfig)
    defer closeOnErr(conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    pop toServer.Send(&client.serverConfig)
    pop toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    session := pop communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

func closeOnErr(c io.Closer) {
    if err := erroring(); err != nil {
        closeErr := c.Close()
        if err != nil {
            seterr(multierror.Append(err, closeErr))
        }
    }
}

func annotateError(annotation string) {
    if err := erroring(); err != nil {
        log.Printf("%s: %v", annotation, err)
        seterr(errwrap.Wrapf(annotation +": {{err}}", err))
    }
}

Definiciones:

pop es legal para expresiones de función donde el valor más a la derecha es un error. Se define como "si el error no es nulo, regrese de la función con los valores cero para todos los demás valores en los resultados y ese error; de lo contrario, arroje un conjunto de valores sin el error". pop no tiene interacción privilegiada con erroring() ; una devolución normal de un error seguirá siendo visible para erroring() . Esto también significa que puede devolver valores distintos de cero para los otros valores devueltos y seguir utilizando el manejo de errores diferidos. La metáfora está sacando el elemento más a la derecha de la "lista" de valores de retorno.

erroring() se define como subir la pila hasta la función diferida que se está ejecutando, y luego el elemento de pila anterior (la función en la que se está ejecutando el aplazamiento, NewClient en este caso), para acceder al valor del error devuelto actualmente en curso. Si la función no tiene dicho parámetro, entra en pánico o devuelve nulo (lo que tenga más sentido). Este valor de error no necesita provenir de pop ; es cualquier cosa que devuelva un error de la función de destino.

seterr(error) permite cambiar el valor de error que se devuelve. Entonces será el error visto por cualquier llamada erroring() futura, que como se muestra aquí permite el mismo encadenamiento basado en aplazamiento que se puede hacer ahora.

Estoy usando el envoltorio hashicorp y multierror aquí; inserte sus propios paquetes inteligentes como desee.

Incluso con la función adicional definida, la suma es más corta. Espero amortizar las dos funciones en usos adicionales, por lo que solo deberían contar parcialmente.

Observe que simplemente dejo el manejo de forwardPort solo, en lugar de intentar atascar un poco más de sintaxis a su alrededor. Como caso excepcional, está bien que esto sea más detallado.

En mi humilde opinión, lo más interesante de esta propuesta solo se puede ver si se imagina tratando de escribir esto con excepciones convencionales. Termina anidando bastante profundamente, y manejar la _colección_ de los errores que pueden ocurrir es bastante tedioso con el manejo de excepciones. (Al igual que en el código Go real, los errores .Close tienden a ignorarse, los errores que ocurren en los propios controladores de excepciones tienden a ignorarse en el código basado en excepciones).

Esto amplía los patrones de Go existentes como defer y el uso de errores como valores para facilitar patrones correctos de manejo de errores que en algunos casos son difíciles de expresar con Go actual o con excepciones, no requiere cirugía radical al tiempo de ejecución (no lo creo), y tampoco, en realidad, _requiere_ un Go 2.0.

Las desventajas incluyen reclamar erroring , pop y seterr como palabras clave, incurriendo en la sobrecarga de defer para estas funcionalidades, el hecho de que el manejo de errores factorizados salta en algunos a las funciones de manejo, y que no hace nada para "forzar" el manejo correcto. Aunque no estoy seguro de que lo último sea posible, ya que según el requisito (correcto) de ser compatible con versiones anteriores, siempre puede hacer lo actual.

Discusión muy interesante aquí.

Me gustaría mantener la variable de error en el lado izquierdo para que no se introduzcan variables que aparezcan mágicamente. Al igual que la propuesta original, me gustaría que los errores se manejen en la misma línea. No usaría el operador || ya que me parece "demasiado booleano" y de alguna manera oculta el "retorno".

Entonces lo haría más legible usando la palabra clave extendida "retorno?". En C #, el signo de interrogación se usa en algunos lugares para hacer atajos. P.ej. en lugar de escribir:

if(foo != null)
{ foo.Bar(); }

puedes simplemente escribir:
foo?.Bar();

Entonces para Go 2 me gustaría proponer esta solución:

func foobar() error {
    return fmt.Errorf("Some error happened")
}

// Implicitly return err (there must be exactly one error variable on the left side)
err := foobar() return?
// Explicitly return err
err := foobar() return? err
// Return extended error info
err := foobar() return? &PathError{"chdir", dir, err}
// Return tuple
err := foobar() return? -1, err
// Return result of function (e. g. for logging the error)
err := foobar() return? handleError(err)
// This doesn't compile as you ignore the error intentionally
foobar() return?

Solo un pensamiento:

foo, err: = myFunc ()
err! = nil? retorno de envoltura (err)

O

si err! = nil? retorno de envoltura (err)

Si está dispuesto a poner algunos tirantes alrededor de eso, ¡no es necesario que cambiemos nada!

if err != nil { return wrap(err) }

Puede tener _todo_ el manejo personalizado que desee (o ninguno), ha guardado dos líneas de código del caso típico, es 100% compatible con versiones anteriores (porque no hay cambios en el idioma), es más compacto y es fácil. Golpea muchos de los puntos impulsores de gofmt ?

Escribí esto antes de leer la sugerencia de carlmjohnson, que es similar ...

Solo un # antes de un error.

Pero en una aplicación del mundo real, aún tendría que escribir el if err != nil { ... } normal para que pueda registrar errores, esto hace que el manejo de errores minimalista sea inútil, a menos que pueda agregar un middleware de retorno a través de anotaciones llamadas after , que se ejecuta después de que las funciones devuelvan ... (como defer pero con argumentos).

@after(func (data string, err error) {
  if err != nil {
    log.Error("error", data, " - ", err)
  }
})
func alo() (string, error) {
  // this is the equivalent of
  // data, err := db.Find()
  // if err != nil { 
  //   return "", err 
  // }
  str, #err := db.Find()

  // ...

  #err = db.Create()

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  // this is the equivalent of
  // data, data2, err, errNotFound := db.Find()
  // if err != nil { 
  //   return nil, nil, err, nil
  // } else if errNotFound != nil {
  //   return nil, nil, nil, errNotFound
  // }
  data, data2, #err, #errNotFound := db.Find()

  // ...

  return data, data2, nil, nil
}

más limpio que:

func alo() (string, error) {
  str, err := db.Find()
  if err != nil {
    log.Error("error on find in database", err)
    return "", err
  }

  // ...

  if err := db.Create(); err != nil {
    log.Error("error on create", err)
    return "", err
  }

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  data, data2, err, errNotFound := db.Find()
  if err != nil { 
    return nil, nil, err, nil
  } else if errNotFound != nil {
    return nil, nil, nil, errNotFound
  }

  // ...

  return data, data2, nil, nil
}

¿Qué tal una declaración Swift como guard , excepto que en lugar de guard...else es guard...return :

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

Me gusta lo explícito del manejo de errores de Go. El único problema, en mi humilde opinión, es cuánto espacio ocupa. Sugeriría 2 ajustes:

  1. Permitir el uso de elementos nulos en un contexto booleano, donde nil es equivalente a false , no nil a true
  2. Admite operadores de declaraciones condicionales de una sola línea, como && y ||

Entonces

file, err := os.Open("fails.txt")
if err != nil {
    return &FooError{"Couldn't foo fails.txt", err}
}

puede llegar a ser

file, err := os.Open("fails.txt")
if err {
    return &FooError{"Couldn't foo fails.txt", err}
}

o incluso más corto

file, err := os.Open("fails.txt")
err && return &FooError{"Couldn't foo fails.txt", err}

y podemos hacer

i,ok := v.(int)
ok || return fmt.Errorf("not a number")

o quizás

i,ok := v.(int)
ok && s *= i

Si la sobrecarga de && y || crea demasiada ambigüedad, tal vez se puedan seleccionar otros caracteres (no más de 2), por ejemplo, ? y # o ?? y ## , o ?? y !! , lo que sea. El objetivo es admitir una declaración condicional de una sola línea con un mínimo de caracteres "ruidosos" (no se necesitan paréntesis, llaves, etc.). Los operadores && y || son buenos porque este uso tiene precedentes en otros idiomas.

Esta no es una propuesta para admitir expresiones condicionales complejas de una sola línea, solo declaraciones condicionales de una sola línea.

Además, esta no es una propuesta para apoyar una gama completa de "veracidad" en algunos otros idiomas. Estos condicionales solo admitirían nil / non-nil o booleanos.

Para estos operadores condicionales, incluso puede ser adecuado restringir a variables individuales y no admitir expresiones. Cualquier cosa más compleja, o con una cláusula else se manejaría con construcciones estándar if ... .

¿Por qué no inventar una rueda y usar la forma conocida try..catch como @mattn dijo antes? )

try {
    a := foo() // func foo(string, error)
    b := bar() // func bar(string, error)
} catch (err) {
    // handle error
}

Parece que no hay razón para distinguir la fuente del error detectado, porque si realmente lo necesita, siempre puede usar la forma anterior de if err != nil sin try..catch .

Además, no estoy realmente seguro de eso, pero ¿se puede agregar la capacidad de "lanzar" un error si no se maneja?

func foo() (string, error) {
    f := bar() // similar to if err != nil { return "", err }
}

func baz() string {
    // Compilation error.
    // bar's error must be handled because baz() does not return error.
    return bar()
}

@gobwas desde el punto de vista de la legibilidad, es muy importante comprender completamente el flujo de control. Mirando su ejemplo, no se sabe qué línea podría causar un salto al bloque de captura. Es como una declaración goto oculta. No es de extrañar que los lenguajes modernos intenten ser explícitos al respecto y requieran que el programador marque explícitamente los lugares donde el flujo de control podría divergir debido a un error. Muy parecido a return o goto pero con una sintaxis mucho mejor.

@creker sí, estoy totalmente de acuerdo contigo. Estaba pensando en el control de flujo en el ejemplo anterior, pero no me di cuenta de cómo hacerlo de forma simple.

Tal vez algo como:

try {
    a ::= foo() // func foo(string, error)
    b ::= bar() // func bar(string, error)
} catch (err) {
    // handle error
}

¿O otras sugerencias anteriores como try a := foo() ..?

@gobwas

Cuando aterrizo en el bloque de captura, ¿cómo sé qué función en el bloque de prueba causó el error?

@urandom, si necesitas saberlo, probablemente quieras hacer if err != nil sin try..catch .

@ robert-wallis: mencioné la declaración de guardia de Swift anteriormente en el hilo, pero la página es tan grande que Github ya no la carga de forma predeterminada. : -PI todavía creo que es una buena idea y, en general, apoyo la búsqueda de ejemplos positivos / negativos en otros lenguajes.

@pdk

permitir el uso de elementos nulos en un contexto booleano, donde nil es equivalente a falso, no nil a verdadero

Veo que esto conduce a muchos errores al usar el paquete de banderas donde la gente escribirá if myflag { ... } pero querrán escribir if *myflag { ... } y el compilador no lo detectará.

try / catch es solo más corto que if / else cuando intentas varias cosas seguidas, que es algo que más o menos todos están de acuerdo en que es malo debido a los problemas del flujo de control, etc.

FWIW, el try / catch de Swift al menos resuelve el problema visual de no saber qué declaraciones pueden arrojar:

do {
    let dragon = try summonDefaultDragon() 
    try dragon.breathFire()
} catch DragonError.dragonIsMissing {
    // ...
} catch DragonError.halatosis {
    // ...
}

@ robert-wallis, tienes un ejemplo:

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

En el primer uso de guard , se parece muchísimo a if err != nil { return &FooError{"Couldn't foo fails.txt", err}} , así que no estoy seguro de si eso es una gran ganancia.

En el segundo uso, no está claro de inmediato de dónde viene err . Casi parece que es lo que devuelve os.Open , lo cual supongo que no fue su intención. ¿Sería esto más exacto?

guard err = os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

En cuyo caso, parece que ...

if err = os.Remove("fails.txt"); err != nil { return &FooError{"Couldn't remove fails.txt", err}}

Pero todavía tiene menos desorden visual. if err = , ; err != nil { - incluso si se trata de una sola línea, todavía están sucediendo demasiadas cosas para algo tan simple

Estuvo de acuerdo en que hay menos desorden. ¿Pero apreciablemente menos para justificar la adición al lenguaje? No estoy seguro de estar de acuerdo con eso.

Creo que la legibilidad de los bloques try-catch en Java / C # / ... es muy buena, ya que puede seguir la secuencia de "ruta feliz" sin ninguna interrupción por el manejo de errores. La desventaja es que básicamente tienes un mecanismo de goto oculto.

En Go, empiezo a insertar líneas vacías después del controlador de errores para hacer más visible la continuación de la lógica del "camino feliz". Entonces, de esta muestra de golang.org (9 líneas)

record := new(Record)
err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}
err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

a menudo hago eso (11 líneas)

record := new(Record)

err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}

err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

Ahora volvamos a la propuesta, como ya publiqué algo como esto, estaría bien (3 líneas)

record := new(Record)
err := datastore.Get(c, key, record) return? &appError{err, "Record not found", 404}
err := viewTemplate.Execute(w, record) return? &appError{err, "Can't display record", 500}

Ahora veo claramente el camino feliz. Mis ojos todavía son conscientes de que en el lado derecho hay un código para el manejo de errores, pero solo necesito "analizarlo visualmente" cuando sea realmente necesario.

Una pregunta para todos: ¿debería compilarse este código?

func foobar() error {
    return fmt.Errorf("Some error")
}
func main() {
    foobar()
}

En mi humilde opinión, el usuario debería verse obligado a decir que ignora intencionalmente el error con:

func main() {
    _ := foobar()
}

Agregando un mini informe de experiencia relacionado con el punto 3 de @ianlancetaylor publicación original, devuelva el error con información contextual adicional .

Al desarrollar una biblioteca flac para Go, queríamos agregar información contextual a los errores usando el paquete @davecheney pkg / errors (https://github.com/mewkiz/flac/issues/22). Más específicamente, ajustamos los errores devueltos mediante

Dado que el error está anotado, es necesario crear un nuevo tipo subyacente para almacenar esta información adicional, en el caso de errores.WithStack, el tipo es errors.withStack .

type withStack struct {
    error
    *stack
}

Ahora, para recuperar el error original, la convención es usar errores . io.EOF .

Un usuario de la biblioteca puede escribir algo como https://github.com/mewkiz/flac/blob/0884ed715ef801ce2ce0c262d1e674fdda6c3d94/cmd/flac2wav/flac2wav.go#L78 usando errors.Cause para verificar el error original valor:

frame, err := stream.ParseNext()
if err != nil {
    if errors.Cause(err) == io.EOF {
        break
    }
    return errors.WithStack(err)
}

Esto funciona bien en casi todos los casos.

Sin embargo, al refactorizar nuestro manejo de errores para hacer un uso consistente de pkg / errors para obtener información de contexto adicional, nos encontramos con un problema bastante serio. Para validar el relleno de ceros, hemos implementado un io.Reader que simplemente verifica si los bytes leídos son cero y, de lo contrario, informa un error. El problema es que después de haber realizado una refactorización automática para agregar información contextual a nuestros errores, de repente nuestros casos de prueba comenzaron a fallar .

El problema era que el tipo subyacente del error devuelto por zeros.Read ahora es errors.withStack, en lugar de io.EOF. Por lo tanto, posteriormente causó problemas cuando usamos ese lector en combinación con io.Copy , que verifica io.EOF específicamente, y no sabe usar errors.Cause para "desenvolver" un error anotado con Información contextual. Como no podemos actualizar la biblioteca estándar, la solución fue devolver el error sin información anotada (https://github.com/mewkiz/flac/commit/6805a34d854d57b12f72fd74304ac296fd0c07be).

Si bien perder la información anotada para las interfaces que devuelven valores concretos es una pérdida, es posible vivir con ello.

La conclusión de nuestra experiencia ha sido que tuvimos suerte, ya que nuestros casos de prueba detectaron esto. El compilador no produjo ningún error, ya que el tipo zeros aún implementa la interfaz io.Reader . Tampoco pensamos que íbamos a encontrar un problema, ya que la anotación de error agregada era una reescritura generada por la máquina, simplemente agregar información contextual a los errores no debería afectar el comportamiento del programa en un estado normal.

Pero lo hizo, y por esta razón, deseamos contribuir con nuestro informe de experiencia para su consideración; al pensar en cómo integrar la adición de información contextual en el manejo de errores para Go 2, de modo que la comparación de errores (como se usa en los contratos de interfaz) aún se mantenga sin problemas.

Amable,
Robin

@mewmew , mantengamos este problema sobre los aspectos del flujo de control del manejo de errores. La mejor forma de envolver y desenvolver los errores debe discutirse en otra parte, ya que controlar el flujo es en gran medida ortogonal.

No estoy familiarizado con su código base, y me doy cuenta de que dijo que era una refactorización automatizada, pero ¿por qué necesitaba incluir información contextual con EOF? Aunque el sistema de tipos lo trata como un error, EOF es más un valor de señal, no un error real. En una implementación io.Reader en particular, es un valor esperado la mayor parte del tiempo. ¿No habría sido una mejor solución envolver solo el error si no fuera io.EOF ?

Sí, propongo que dejemos las cosas como están. Tenía la impresión de que el sistema de errores de Go se diseñó deliberadamente de esta manera para disuadir a los desarrolladores de que no acumularan errores en la pila de llamadas. Que los errores deben resolverse donde ocurren y saber cuándo es más apropiado usar el pánico cuando no se puede.

Quiero decir, ¿no es esencialmente intentar-atrapar-lanzar el mismo comportamiento de pánico () y recuperar () de todos modos?

Suspiro, si realmente vamos a empezar a intentar ir por este camino. ¿Por qué no podemos hacer algo como

_, ? := foo()
x?, err? := bar()

o tal vez incluso algo como

_, err := foo(); return err?
x, err := bar(); return x? || err?
x, y, err := baz(); return (x? && y?) || err?

donde el ? se convierte en un alias abreviado para if var! = nil {return var}.

Incluso podemos definir otra interfaz incorporada especial que se satisfaga con el método

func ?() bool //looks funky but avoids breakage.

que podemos usar para anular esencialmente el comportamiento predeterminado del operador condicional nuevo y mejorado.

@mortdeus

Creo que estoy de acuerdo.
Si el problema es tener una buena forma de presentar el camino feliz, ¿un complemento para un IDE podría plegar / desplegar cada instancia de if err != nil { return [...] } con un atajo?

Siento que cada parte ahora es importante. err != nil es importante. return ... es importante.
Es un poco complicado escribirlo, pero hay que escribirlo. ¿Y realmente ralentiza a la gente? Lo que lleva tiempo es pensar en el error y qué devolver, no escribirlo.

Me interesaría mucho más una propuesta que permita limitar el alcance de la variable err .

Creo que mi idea condicional es la forma más ordenada de resolver este problema. Solo pensé en algunas otras cosas que harían que esta función valiera la pena para incluirla en Go. Voy a escribir mi idea en una propuesta separada.

No veo cómo podría funcionar esto:

x, y, err: = baz (); retorno ( x? && y? ) || err?

donde el ? se convierte en un alias abreviado para if var == nil {return var}.

x, y, err: = baz (); retorno ( if x == nil{ return x} && if y== nil{ return y} ) || if err == nil{ return err}

x, y, err: = baz (); return (x? && y?) || ¿errar?

se convierte en

x, y, err: = baz ();
if ((x! = nil && y! = nil) || err! = nil)) {
devuelve x, y, err
}

cuando veas x? && y? || ¿errar? debería estar pensando "¿son válidas xey? ¿Qué hay de err?"

si no, la función de retorno no se ejecuta. Acabo de redactar una nueva propuesta sobre esta idea que lleva la idea un poco más allá con un nuevo tipo de interfaz incorporado especial

Sugiero que Go agregue el manejo de errores predeterminado en la versión 2 de Go.

Si el usuario no maneja el error, el compilador devuelve err si no es nil, entonces si el usuario escribe:

func Func() error {
    func1()
    func2()
    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

compilar, transformarlo en:

func Func() error {
    err := func1()
    if err != nil {
        return err
    }

    err = func2()
    if err != nil {
        return err
    }

    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

Si el usuario maneja el error o lo ignora usando _, el compilador no hará nada:

_ = func1()

o

err := func1()

para múltiples valores de retorno, es similar:

func Func() (*YYY, error) {
    ch, x := func1()
    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

el compilador se transformará en:

func Func() (*YYY, error) {
    ch, x, err := func1()
    if err != nil {
        return nil, err
    }

    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

Si la firma de Func () no devuelve el error pero llama a las funciones que devuelven el error, el compilador informará Error: "Por favor, maneje su error en Func ()"
Entonces el usuario puede simplemente registrar el error en Func ()

Y si el usuario quiere ajustar algo de información al error:

func Func() (*YYY, error) {
    ch, x := func1() ? Wrap(err, "xxxxx", ch, "abc", ...)
    return yyy, nil
}

o

func Func() (*YYY, error) {
    ch, x := func1() ? errors.New("another error")
    return yyy, nil
}

El beneficio es

  1. el programa simplemente falla en el punto donde ocurre el error, el usuario no puede ignorar el error implícitamente.
  2. puede reducir notablemente las líneas de código.

no es tan fácil porque Go puede tener múltiples valores de retorno y el lenguaje no debería asignar lo que esencialmente equivale a valores predeterminados para los argumentos de retorno sin que el desarrollador sepa explícitamente lo que está sucediendo.

Creo que poner el manejo de errores en la sintaxis de la asignación no resuelve la raíz del problema, que es "el manejo de errores es repetitivo".

El uso de if (err != nil) { return nil } (o similar) después de muchas líneas de código (cuando tenga sentido hacerlo) va en contra del principio DRY (no te repitas). Creo que por eso no nos gusta esto.

También hay problemas con try ... catch . No es necesario que maneje explícitamente el error en la misma función donde ocurre. Creo que esa es una razón notable por la que no nos gusta try...catch .

No creo que estos sean mutuamente excluyentes; podemos tener una especie de try...catch sin un throws .

Otra cosa que personalmente no me gusta de try...catch es la necesidad arbitraria de la palabra clave try . No hay ninguna razón por la que no puedas catch después de cualquier limitador de alcance, en lo que respecta a la gramática de trabajo. (que alguien lo diga si me equivoco en esto)

Esto es lo que propongo:

  • usando ? como marcador de posición para un error devuelto, donde _ se usaría para ignorarlo
  • en lugar de catch como en mi ejemplo a continuación, se podría usar error? su lugar para una compatibilidad total con versiones anteriores

^ Si mi suposición de que estos son compatibles con versiones anteriores es incorrecta, llámelo.

func example() {
    {
        // The following line will invoke the catch block
        data, ? := foopkg.buyIt()
        // The next two lines handle an error differently
        otherData, err := foopkg.useIt()
        if err != nil {
            // Here we eliminated deeper indentation
            otherData, ? = foopkg.breakIt()
        }
        if data == "" || otherData == "" {
        }
    } catch (err) {
        return errors.Label("fix it", err)
        // Aside: why not add Label() to the error package?
    }
}

Pensé en un argumento en contra de esto: si lo escribe de esta manera, cambiar ese bloque catch podría tener efectos no deseados en el código en ámbitos más profundos. Este es el mismo problema que tenemos con try...catch .

Creo que si solo puede hacer esto en el alcance de una sola función, el riesgo es manejable, posiblemente el mismo que el riesgo actual de olvidarse de cambiar una línea de código de manejo de errores cuando tiene la intención de cambiar muchos de ellos. Veo esto como la misma diferencia entre las consecuencias de la reutilización del código y las consecuencias de no seguir DRY (es decir, no hay almuerzo gratis, como dicen)

Editar: Olvidé especificar un comportamiento importante para mi ejemplo. En el caso de que ? se use en un alcance sin ningún catch , creo que esto debería ser un error del compilador (en lugar de provocar pánico, que ciertamente fue lo primero que pensé)

Edición 2: idea loca: tal vez el bloque catch simplemente no afectaría el flujo de control ... literalmente sería como copiar y pegar el código dentro de catch { ... } en la línea después de que se produce el error ? ed (bueno, no del todo, todavía tendría su propio alcance). Parece extraño ya que ninguno de nosotros está acostumbrado, así que catch definitivamente no debería ser la palabra clave si se hace de esta manera, pero de lo contrario ... ¿por qué no?

@mewmew , mantengamos este problema sobre los aspectos del flujo de control del manejo de errores. La mejor forma de envolver y desenvolver los errores debe discutirse en otra parte, ya que controlar el flujo es en gran medida ortogonal.

Ok, mantengamos este hilo para controlar el flujo. Lo agregué simplemente porque era un problema relacionado con el uso concreto del punto 3 y devuelve el error con información contextual adicional .

@jba ¿Conoce algún problema específicamente dedicado a envolver / desenvolver información contextual en busca de errores?

No estoy familiarizado con su código base, y me doy cuenta de que dijo que era una refactorización automatizada, pero ¿por qué necesitaba incluir información contextual con EOF? Aunque el sistema de tipos lo trata como un error, EOF es más un valor de señal, no un error real. En una implementación de io.Reader en particular, es un valor esperado la mayor parte del tiempo. ¿No habría sido una mejor solución envolver el error solo si no fuera io.EOF?

@DeedleFake Puedo elaborar un poco, pero para mantenerme en el tema lo haré en el número antes mencionado dedicado a envolver / desenvolver la información contextual en busca de errores.

Cuanto más leo todas las propuestas (incluida la mía), menos creo que realmente tenemos un problema con el manejo de errores en marcha.

Lo que me gustaría es alguna aplicación para no ignorar accidentalmente un valor de retorno de error, pero hacer cumplir al menos
_ := returnsError()

Sé que hay herramientas para encontrar estos problemas, pero un soporte de primer nivel del lenguaje podría detectar algunos errores. No manejar un error en absoluto es como tener una variable sin usar para mí, que ya es un error. También ayudaría con la refactorización, cuando introduce un tipo de retorno de error en una función, ya que está obligado a manejarlo en todos los lugares.

El problema principal que la mayoría de la gente intenta resolver aquí parece ser la "cantidad de escritura" o el "número de líneas". Estoy de acuerdo con cualquier sintaxis que reduzca el número de líneas, pero eso es principalmente un problema gofmt. Solo permite "osciloscopios de una sola línea" en línea y estamos bien.

Otra sugerencia para guardar algo de escritura es la comprobación implícita de nil como con booleanos:

err := returnsError()
if err { return err }

o incluso

if err := returnsError(); err { return err }

Eso funcionaría con todos los tipos de causas de puntero.

Mi sensación es que todo lo que reduce la llamada a la función + el manejo de errores en una sola línea conducirá a un código menos legible y una sintaxis más compleja.

código menos legible y sintaxis más compleja.

Ya tenemos un código menos legible debido al detallado manejo de errores. Agregar el truco de la API del escáner ya mencionado, que se supone que oculta esa verbosidad, lo empeora aún más. Agregar una sintaxis más compleja podría ayudar con la legibilidad, para eso es el azúcar sintáctico al final. De lo contrario, no tiene sentido esta discusión. El patrón de generar un error y devolver un valor cero para todo lo demás es lo suficientemente común como para justificar un cambio de idioma, en mi opinión.

El patrón de generar un error y devolver un valor cero para todo

19642 haría esto más fácil.


Además, gracias @mewmew por el informe de experiencia. Definitivamente está relacionado con este hilo, en la medida en que se relaciona con peligros en tipos particulares de diseños de manejo de errores. Me encantaría ver más de estos.

No siento que expliqué muy bien mi idea, así que creé una esencia (y revisé muchas de las deficiencias que acabo de notar)

https://gist.github.com/KernelDeimos/384aabd36e1789efe8cbce3c17ffa390

Hay más de una idea en esta esencia, así que espero que puedan discutirse por separado.

Dejando de lado por un momento la idea de que la propuesta aquí tiene que ser explícitamente sobre el manejo de errores, ¿qué pasaría si Go introdujera algo como una declaración collect ?

Una declaración collect tendría el formato collect [IDENT] [BLOCK STMT] , donde ident debe ser una variable dentro del alcance de un tipo nil -able. Dentro de una instrucción collect , una variable especial _! está disponible como un alias para la variable que se recopila. _! no se puede usar en ningún otro lugar que no sea una asignación, al igual que _ . Siempre que _! se asigna a, una implícita nil se realiza cheque, y si _! no es nulo, el bloque cesa la ejecución y continúa con el resto del código.

En teoría, esto se vería así:

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    collect err {
        intermediate1, _! := Step1()
        intermediate2, _! := Step2(intermediate1, "something")
        // assign to result from the outer scope
        result, _! = Step3(intermediate2, 12)
    }
    return result, err
}

que es equivalente a

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    {
        var intermediate1 SomeType
        intermediate1, err = Step1()
        if err != nil { goto collectEnd }

        var intermediate2 SomeOtherType
        intermediate2, err = Step2(intermediate1, "something")
        if err != nil { goto collectEnd }

        result, err = Step3(intermediate2, 12)
        // if err != nil { goto collectEnd }, but since we're at the end already we can omit this
    }

collectEnd:
    return result, err
}

Algunas otras cosas agradables que una característica de sintaxis como esta permitiría:

// try several approaches for acquiring a value
func GetSomething() (s *Something) {
    collect s {
        _! = fetchOrNil1()
        _! = fetchOrNil2()
        _! = new(Something)
    }
    return s
}

Se requieren nuevas funciones de sintaxis:

  1. palabra clave collect
  2. ident especial _! (he jugado con esto en el analizador, no es difícil hacer esta coincidencia como una identificación sin romper nada más)

La razón por la que sugiero algo como esto es porque el argumento "el manejo de errores es demasiado repetitivo" puede reducirse a "las comprobaciones nulas son demasiado repetitivas". Go ya tiene muchas funciones de manejo de errores que funcionan tal cual. Puede ignorar un error con _ (o simplemente no capturar valores devueltos), puede devolver un error sin modificar con if err != nil { return err } , o agregar contexto y regresar con if err != nil { return wrap(err) } . Ninguno de esos métodos, por sí solo, es demasiado repetitivo. La repetitividad ( obviamente ) proviene de tener que repetir estas o declaraciones de sintaxis similares en todo el código. Creo que introducir una forma de ejecutar declaraciones hasta que se encuentre un valor que no sea nulo es una buena manera de mantener el mismo manejo de errores, pero reducir la cantidad de repetición necesaria para hacerlo.

  • Buen soporte para 1) ignorar un error; 3) envolver un error con contexto adicional.

comprobar, ya que permanece igual (en su mayoría)

  • Si bien el código de manejo de errores debe ser claro, no debe dominar la función.

verifique, ya que el código de manejo de errores ahora puede ir en un lugar si es necesario, mientras que la esencia de la función puede ocurrir de una manera linealmente legible

  • El código Go 1 existente debería seguir funcionando, o al menos debe ser posible traducir mecánicamente Go 1 al nuevo enfoque con total fiabilidad.

comprobar, esto es una adición y no una alteración

  • El nuevo enfoque debería alentar a los programadores a manejar los errores correctamente.

verifique, creo, ya que los mecanismos para el manejo de errores no son diferentes; solo tendríamos una sintaxis para "recopilar" el primer valor no nulo de una serie de ejecuciones y asignaciones, que se puede usar para limitar el número de lugares en los que tenemos que escribir nuestro código de manejo de errores en una función

  • Cualquier enfoque nuevo debe ser más corto y / o menos repetitivo que el enfoque actual, sin dejar de ser claro.

No estoy seguro de que esto se aplique aquí, ya que la función sugerida se aplica a algo más que al manejo de errores. Creo que puede acortar y aclarar el código que puede generar errores, sin saturar los controles nulos y las devoluciones anticipadas.

  • El lenguaje funciona hoy en día y cada cambio tiene un costo. No debería ser solo un lavado, debería ser claramente mejor.

De acuerdo, por lo que parece que un cambio cuyo alcance se extienda más allá del simple manejo de errores puede ser apropiado. Creo que el problema subyacente es que los cheques de nil se vuelven repetitivos y detallados, y da la casualidad de que error es un tipo nil -able.

@KernelDeimos Básicamente, se nos ocurrió lo mismo. Sin embargo, di un paso más y expliqué por qué la forma x, ? := doSomething() no funciona tan bien en la práctica. Aunque es bueno ver que no soy la única persona que está pensando en agregar el? operador en el idioma de una manera interesante.

https://github.com/golang/go/issues/25582

¿No es esto básicamente una trampa ?

Aquí hay una bola de saliva:

func NewClient(...) (*Client, error) {
    trap(err error) {
        return nil, err
    }

    listener, err? := net.Listen("tcp4", listenAddr)
    trap(_ error) {
        listener.Close()
    }

    conn, err? := ConnectionManager{}.connect(server, tlsConfig)
    trap(_ error) {
        conn.Close()
    }

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?

    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?

    session, err? := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

59 líneas → 44

trap significa "ejecutar este código en orden de pila si una variable marcada con ? del tipo especificado no es el valor cero". Es como defer pero puede afectar el flujo de control.

Me gusta la idea trap , pero la sintaxis me molesta un poco. ¿Y si fuera un tipo de declaración? Por ejemplo, trap err error {} declara un trap llamado err de tipo error que, cuando se asigna a, ejecuta el código dado. El código ni siquiera tiene que regresar; solo está permitido hacerlo. Esto también rompe la dependencia de que nil sea ​​especial.

Editar: expandiendo y dando un ejemplo ahora que no estoy en un teléfono.

func Example(r io.Reader) error {
  trap err error {
    if err != nil {
      return err
    }
  }

  n, err? := io.Copy(ioutil.Discard, r)
  fmt.Printf("Read %v bytes.\n", n)
}

Básicamente, un trap funciona como un var , excepto que siempre que se le asigna con el operador ? adjunto, se ejecuta el bloque de código. El operador ? también evita que sea sombreado cuando se usa con := . Se permite volver a declarar un trap en el mismo ámbito, a diferencia de un var , pero debe ser del mismo tipo que el existente; esto le permite cambiar el bloque de código asociado. Debido a que el bloque que se está ejecutando no necesariamente regresa, también le permite tener rutas separadas para cosas específicas, como verificar si err == io.EOF .

Lo que me gusta de este enfoque es que parece similar al ejemplo errWriter de Los errores son valores , pero en una configuración algo más genérica que no requiere la declaración de un nuevo tipo.

@carlmjohnson ¿a quién le respondías?
Independientemente, este concepto trap parece ser solo una forma diferente de escribir una declaración defer , ¿no? El código, tal como está escrito, sería esencialmente el mismo si panic 'd en un error no nulo y luego usara un cierre diferido para establecer valores de retorno con nombre y realizar la limpieza. Creo que esto tiene los mismos problemas que mi propuesta anterior de usar _! para entrar en pánico automáticamente, ya que coloca un método de manejo de errores sobre otro. FWIW También sentí que el código, tal como estaba escrito, era mucho más difícil de razonar que el original. ¿Podría imitarse este concepto trap con go hoy, aunque sea menos claro que tener una sintaxis para él? Siento que podría y sería if err != nil { panic (err) } y defer capturar y manejar eso.

Parece similar al concepto del bloque collect que sugerí anteriormente, que personalmente creo que proporciona una forma más limpia de expresar la misma idea ("si este valor no es nulo, quiero capturarlo y hacer algo con eso"). A Go le gusta ser lineal y explícito. trap parece una nueva sintaxis para panic / defer pero con un flujo de control menos claro.

@mccolljr , parecía ser una respuesta para mí . De su publicación infiero que no vio mi propuesta (ahora en los "elementos ocultos", aunque no tan arriba), porque en realidad utilicé una declaración de aplazamiento en mi propuesta, ampliada con un recover - como función para el manejo de errores.

También observaría que la reescritura de la "trampa" eliminó muchas de las funcionalidades que tenía mi propuesta (surgen errores muy diferentes) y, además, no me queda claro cómo descartar el manejo de errores con las declaraciones de trampa. Gran parte de esa reducción de mi propuesta viene en forma de eliminar la corrección del manejo de errores y, creo, volver a hacer que sea más fácil devolver errores directamente que hacer cualquier otra cosa.

La capacidad de continuar el flujo está permitida por el ejemplo trap modificado que di arriba . Lo edité más tarde, así que no sé si lo viste o no. Es muy similar a un collect , pero creo que le da un poco más de control sobre él. Dependiendo de cómo funcionen las reglas de alcance, podría ser un poco espaguetis, pero creo que sería posible encontrar un buen equilibrio.

@thejerf Ah, eso tiene más sentido. No me di cuenta de que era una respuesta a tu propuesta. Sin embargo, no tengo claro cuál sería la diferencia entre erroring() y recover() , aparte del hecho de que recover responde a panic . Parece que estaríamos haciendo implícitamente algún tipo de pánico cuando se necesita devolver un error. Aplazar también es una operación algo costosa, por lo que no estoy seguro de cómo me siento al usarlo en todas las funciones que puedan generar errores.

@DeedleFake Lo mismo ocurre con trap , porque la forma en que yo lo veo trap es esencialmente una macro que inserta código cuando se usa el operador ? que presenta su propio conjunto de preocupaciones y consideraciones, o se implementa como goto ... que, ¿qué pasa si el usuario no regresa en el bloque trap , o es solo un defer sintácticamente diferente? Además, ¿qué pasa si declaro varios bloques de trampa en una función? eso está permitido? Si es así, ¿cuál es ejecutado? Eso agrega complejidad a la implementación. A Go le gusta tener opiniones y eso me gusta. Creo que collect o una construcción lineal similar está más alineada con la ideología de Go que trap , que, como se me señaló después de mi primera propuesta, parece ser un try-catch construir en traje.

¿Qué pasa si el usuario no regresa en el bloque de trampa?

Si trap no regresa o modifica de otra manera el flujo de control ( goto , continue , break , etc.), el flujo de control regresa a donde el código bloque fue 'llamado' desde. El bloque en sí funcionaría de manera similar a llamar a un cierre, con la excepción de que tiene acceso a los mecanismos de control de flujo. Los mecanismos funcionarían en el lugar desde donde se declara el bloque, no en el lugar desde donde se llama, por lo que

for {
  trap err error {
    break
  }

  err? = errors.New("Example")
}

trabajaría.

Además, ¿qué pasa si declaro varios bloques de trampa en una función? eso está permitido? Si es así, ¿cuál es ejecutado?

Sí, eso está permitido. Los bloques reciben el nombre de la trampa, por lo que es bastante sencillo averiguar cuál debe llamarse. Por ejemplo, en

trap err error {
  // Block 1.
}

trap n int {
  // Block 2.
}

n? = 3

se llama al bloque 2. La gran pregunta en ese caso probablemente sería qué sucede en el caso de n?, err? = 3, errors.New("Example") , que probablemente requeriría que se especificara el orden de las asignaciones, como se planteó en # 25609.

Creo que recopilar o una construcción lineal similar está más alineada con la ideología de Go que con la trampa, que, como se me señaló después de mi primera propuesta, parece ser una construcción de intentar atrapar disfrazado.

Creo que tanto collect como trap son esencialmente try-catch s al revés. Un try-catch estándar es una política de falla por defecto que requiere que usted verifique o explota. Este es un sistema exitoso por defecto que le permite especificar una ruta de falla, esencialmente.

Una cosa que complica todo el asunto es el hecho de que los errores no se tratan inherentemente como fallas, y algunos errores, como io.EOF , no especifican realmente fallas en absoluto. Creo que es por eso que los sistemas que no están vinculados a errores específicamente, como collect o trap , son el camino a seguir.

"Ah, eso tiene más sentido. No me di cuenta de que era una respuesta a tu propuesta. Sin embargo, no tengo claro cuál sería la diferencia entre error () y recuperar (), aparte del hecho de que recuperar responde a pánico."

No tener una gran diferencia es el punto. Estoy tratando de minimizar la cantidad de nuevos conceptos creados mientras obtengo la mayor potencia posible de ellos. Considero que aprovechar la funcionalidad existente es una característica, no un error.

Uno de los puntos de mi propuesta es explorar más allá de "¿qué pasa si arreglamos este fragmento recurrente de tres líneas donde return err y lo reemplazamos con un ? " para "cómo afecta el resto del lenguaje? ¿Qué nuevos patrones habilita? ¿Qué nuevas 'mejores prácticas' crea? ¿Qué viejas 'mejores prácticas' dejan de ser mejores prácticas? " No estoy diciendo que terminé ese trabajo. E incluso si se juzga que la idea en realidad tiene demasiado poder para el gusto de Go (ya que Go no es un lenguaje que maximice el poder, e incluso con la opción de diseño de limitarlo a escribir error , sigue siendo probablemente el más poderoso propuesta hecha en este hilo, a la que me refiero tanto en el sentido bueno como en el malo de "poderoso"), creo que podríamos estar explorando las preguntas de lo que los nuevos constructos harán a los programas en su conjunto, en lugar de lo que funcionará con funciones de ejemplo de siete líneas, por lo que traté de llevar los ejemplos al menos a las ~ 50-100 líneas del rango de "código real". Todo parece igual en 5 líneas, lo que incluye el manejo de errores de Go 1.0, que es quizás parte de la razón por la que todos sabemos por nuestras propias experiencias que hay un problema real aquí, pero la conversación simplemente da vueltas en círculos si hablamos de en una escala demasiado pequeña hasta que algunas personas comiencen a convencerse de que, después de todo, tal vez no haya ningún problema. (¡Confíe en sus experiencias reales de codificación, no en las muestras de 5 líneas!)

"Parece que estaríamos entrando implícitamente en algún tipo de pánico cuando es necesario devolver un error".

No está implícito. Es explícito. Utiliza el operador pop cuando hace lo que desea. Cuando no hace lo que quieres, no lo usas. Lo que hace es lo suficientemente simple como para capturarlo en una sola oración simple, aunque la especificación probablemente tomaría un párrafo completo, ya que así es como funcionan esas cosas. No hay implícito. Además, no es un pánico porque solo se desenrolla un nivel de la pila, exactamente como un retorno; es tanto un pánico como un regreso, que no lo es en absoluto.

Tampoco me importa si deletreas pop como? o lo que sea. Personalmente, creo que una palabra se parece un poco más a Go, ya que Go no es actualmente un lenguaje rico en símbolos, pero no puedo negar que un símbolo tiene la ventaja de que no entra en conflicto con ningún código fuente existente. Estoy interesado en la semántica y lo que podemos construir sobre ellos y qué comportamientos la nueva semántica les brinda a los programadores, tanto nuevos como experimentados, más que la ortografía.

"El aplazamiento también es una operación algo costosa, por lo que no estoy seguro de cómo me siento al usarlo en todas las funciones que podrían generar errores".

Ya lo reconocí. Aunque sugeriría que, en general, no es tan caro y no me siento tan mal por decir que para fines de optimización, si tiene una función activa, escríbala de la manera actual. No es explícitamente mi objetivo tratar de modificar el 100% de todas las funciones de manejo de errores, sino hacer que el 80% de ellas sean mucho más simples y correctas y dejar que el 20% de los casos (probablemente más como 98/2, honestamente) permanezcan como están son. La gran mayoría del código Go no es sensible al uso de diferir, que es, después de todo, la razón por la que defer existe en primer lugar.

De hecho, puede modificar trivialmente la propuesta para no usar aplazar y usar alguna palabra clave como trap como una declaración que se ejecuta solo una vez, independientemente de dónde aparezca, en lugar de la forma en que aplazar es en realidad una declaración que empuja un handler en la pila de funciones diferidas. Elegí deliberadamente reutilizar defer para evitar agregar nuevos conceptos al lenguaje ... incluso comprender las trampas que podrían resultar de los aplazamientos en bucles que muerden inesperadamente a las personas. Pero sigue siendo solo el concepto defer que hay que entender.

Solo para dejar en claro que agregar una nueva palabra clave al idioma es un cambio radical.

package main

import (
    "fmt"
)

func return(i int)int{
    return i
}

func main() {
    return(1)
}

resultados en

prog.go:7:6: syntax error: unexpected select, expecting name or (

Lo que significa que si intentamos agregar try , trap , assert , cualquier palabra clave en el idioma, corremos el riesgo de romper una tonelada de código. Código que puede mantenerse por más tiempo.

Es por eso que inicialmente propuse agregar un operador especial ? go que se pueda aplicar a las variables en el contexto de las declaraciones. El carácter ? partir de ahora está designado como carácter ilegal para nombres de variables. Lo que significa que actualmente no está en uso en ningún código Go actual y, por lo tanto, podemos introducirlo sin incurrir en cambios importantes.

Ahora, el problema de usarlo en el lado izquierdo de una asignación es que no toma en consideración que Go permite múltiples argumentos de retorno.

Por ejemplo, considere esta función

func getCoord() x int, y int, z int, err error{
    x, err = getX()
    if err != nil{
        return 
    }

    y, err = getY()
    if err != nil{
        return 
    }

    z, err = getZ()
        if err != nil{
        return 
    }
    return
}

si usamos? o intente en las lhs de asignación para deshacerse de los bloques if err! = nil, ¿asumimos automáticamente que los errores significan que todos los demás valores ahora son basura? ¿Y si lo hiciéramos así?

func GetCoord() (x, y, z int, err error) {
    err = try GetX(&x) // or err? = GetX(&x) 
    err = try GetY(&y) // or err? = GetY(&x) 
    err = try GetZ(&z) // or err? = GetZ(&x) 
}

¿Qué suposiciones hacemos aquí? ¿Que no debería ser perjudicial simplemente asumir que está bien tirar el valor? ¿Qué pasa si el error está destinado a ser más una advertencia y el valor x está bien? ¿Qué pasa si la única función que arroja el error es la llamada a GetZ () y los valores x, y son realmente buenos? ¿Presumimos devolverlos? ¿Qué pasa si no usamos argumentos de retorno con nombre? ¿Qué pasa si los argumentos de retorno son tipos de referencia como un mapa o un canal? ¿Debemos suponer que es seguro devolver nulo a la persona que llama?

TLDR; agregando? o try a asignaciones en un esfuerzo por eliminar

if err != nil{
    return err
}

introduce demasiada confusión que ventajas.

Y agregar algo como la sugerencia trap introduce la posibilidad de rotura.

Es por eso que en mi propuesta que hice en un número aparte. Permití la posibilidad de declarar un func ?() bool en cualquier tipo para que cuando llames di

x, err := doSomething; return x, err?    

puede hacer que el efecto secundario de la trampa suceda de una manera que se aplique a cualquier tipo.

¿Y aplicando el? trabajar solo en declaraciones como las que mostré permite la programabilidad de las declaraciones. En mi propuesta, sugiero permitir una declaración de cambio especial que le permita a alguien cambiar los casos que son la palabra clave +?

switch {
    case select?:
    //side effect/trap code specific to select
    case return?:
    //side effect/trap code specific to returns
    case for?: 
    //side effect/trap code specific to for? 

    //etc...
}  

Si estamos usando? en un tipo que no tiene un explícito? función declarada o un tipo incorporado, entonces el comportamiento predeterminado de verificar si var == nil || El valor cero {ejecutar la declaración} es la supuesta intención.

Idk, no soy un experto en diseño de lenguajes de programación, pero ¿no es así?

Por ejemplo, la función os.Chdir está actualmente

func Chdir(dir string) error {
  if e := syscall.Chdir(dir); e != nil {
      return &PathError{"chdir", dir, e}
  }
  return nil
}

Según esta propuesta, podría redactarse como

func Chdir(dir string) error {
  syscall.Chdir(dir) || &PathError{"chdir", dir, err}
  return nil
}

esencialmente lo mismo que las funciones de flecha de JavaScript o como Dart lo define "sintaxis de flecha gruesa"

p.ej

func Chdir(dir string) error {
    syscall.Chdir(dir) => &PathError{"chdir", dir, err}
    return nil
}

de la gira de dardos .

Para las funciones que contienen solo una expresión, puede usar una sintaxis abreviada:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
La sintaxis => expr es una abreviatura de {return expr; }. La notación => a veces se denomina sintaxis de flecha gruesa.

@mortdeus , el lado izquierdo de la flecha de Dart es una firma de función, mientras que syscall.Chdir(dir) es una expresión. Parecen más o menos ajenos.

@mortdeus Olvidé aclarar antes, pero la idea que comenté aquí no se parece mucho a la propuesta que etiquetó. Me gusta la idea de ? como marcador de posición, así que lo copié, pero mi idea enfatizaba la reutilización de un solo bloque de código para manejar los errores y evitar algunos de los problemas conocidos con try...catch . Tuve mucho cuidado de pensar en algo de lo que no se había hablado antes para poder contribuir con una nueva idea.

¿Qué tal una nueva declaración condicional return (o returnIf )?

return(bool expression) ...

es decir.

err := syscall.Chdir(dir)
return(err != nil) &PathError{"chdir", dir, e}

a, b, err := Foo()    // signature: func Foo() (string, string, error)
return(err != nil) "", "", err

O simplemente deje que fmt formatee las funciones de solo retorno de una línea en una línea en lugar de tres:

err := syscall.Chdir(dir)
if err != nil { return &PathError{"chdir", dir, e} }

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil { return "", "", err }

Se me ocurre que puedo obtener todo lo que quiero con solo la adición de algún operador que regrese temprano si el error más a la derecha no es nil, si lo combino con parámetros con nombre:

func NewClient(...) (c *Client, err error) {
    defer annotateError(&err, "client couldn't be created")

    listener := net.Listen("tcp4", listenAddr)?
    defer closeOnErr(&err, listener)
    conn := ConnectionManager{}.connect(server, tlsConfig)?
    defer closeOnErr(&err, conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?
    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?
    session := communicationProtocol.FinalProtocol(conn)?
    client.session = session

    return client, nil
}

func closeOnErr(err *error, c io.Closer) {
    if *err != nil {
        closeErr := c.Close()
        if err != nil {
            *err = multierror.Append(*err, closeErr)
        }
    }
}

func annotateError(err *error, annotation string) {
    if *err != nil {
        log.Printf("%s: %v", annotation, *err)
        *err = errwrap.Wrapf(annotation +": {{err}}", err)
    }
}

Actualmente, mi comprensión del consenso de Go va en contra de los parámetros con nombre, pero si cambian las posibilidades de los parámetros de nombre, también puede cambiar el consenso. Por supuesto, si el consenso es lo suficientemente fuerte en su contra, existe la opción de incluir accesos.

Este enfoque me da lo que estoy buscando (lo que facilita el manejo de errores sistemáticamente en la parte superior de una función, la capacidad de factorizar dicho código, también la reducción del recuento de líneas) con prácticamente cualquiera de las otras propuestas aquí también, incluido el original . E incluso si la comunidad de Go decide que no le gusta, no me tiene que preocupar, porque está en mi código función por función y no hay desajuste de impedancia en ninguna dirección.

Aunque expresaría una preferencia por una propuesta que permita que una función de la firma func GetInt() (x int, err error) se use en el código con OtherFunc(GetInt()?, "...") (o cualquiera que sea el resultado final) a una que no se pueda componer en una expresión. Si bien es una molestia menor para la cláusula de manejo de errores simples repetitivos y constantes, la cantidad de mi código que descomprime una función de arity 2 solo para que pueda tener el primer resultado sigue siendo molestamente sustancial y realmente no agrega nada a la claridad del código resultante.

@thejerf , siento que hay muchos comportamientos extraños aquí. Estás llamando a net.Listen , que devuelve un error, pero no está asignado. Y luego aplazas, pasando err . ¿Cada defer anula al último, de modo que nunca se invoca annotateError ? ¿O se apilan, de modo que si se devuelve un error de, digamos, toServer.Send , entonces closeOnErr se llama dos veces y luego annotateError ? ¿Se llama a closeOnErr solo si la llamada anterior tiene una firma coincidente? ¿Y este caso?

conn := ConnectionManager{}.connect(server, tlsConfig)?
fmt.Printf("Attempted to connect to server %#v", server)
defer closeOnErr(&err, conn)

Leer el código también confunde cosas, como por qué no puedo simplemente decir

client.session = communicationProtocol.FinalProtocol(conn)?

Presumiblemente, ¿porque FinalProtocol devuelve un error? Pero eso está oculto al lector.

Finalmente, ¿qué sucede cuando quiero informar de un error y recuperarme dentro de una función? ¿Parece que su ejemplo evitaría ese caso?

_Apéndice_

OK, creo que cuando quieres recuperarte de un error, con tu ejemplo, lo asignas, como en esta línea:

env, err := environment.GetRuntimeEnvironment()

Eso está bien porque err está sombreado, pero luego si cambié ...

forwardPort, err = env.PortToForward()
if err != nil {
    log.Printf("env couldn't provide forward port: %v", err)
}

para sólo

forwardPort = env.PortToForward()

Entonces su error diferido manejado no lo detectará, porque está utilizando el alcance creado err . ¿O me estoy perdiendo algo?

Creo que una adición a la sintaxis que denota que una función puede fallar es un buen comienzo. Propongo algo en esta línea:

func (r Reader) Read(b []byte) (n int) fails {
    if somethingFailed {
        fail errors.New("something failed")
    }

    return 0
}

Si una función falla (al usar la palabra clave fail lugar de return , devuelve el valor cero para cada parámetro de retorno.

func (c EmailClient) SendEmail(to, content string) fails {
    if !c.connected() {
        fail errors.New("could not connect")
    }

    // You can handle it and execution will continue if you don't fail or return
    n := r.Read(b) handle (err) {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    n := r.Read(b)
}

Este enfoque tendrá la ventaja de permitir que funcione el código actual, al tiempo que habilita un nuevo mecanismo para un mejor manejo de errores (al menos en la sintaxis).

Comentarios sobre las palabras clave:

  • fails puede que no sea la mejor opción, pero es la mejor que se me ocurre actualmente. Pensé en usar err (o errs ), pero la forma en que se usan actualmente puede hacer que sea una mala elección debido a las expectativas actuales ( err es probablemente un nombre de variable, y se puede suponer que errs es un segmento o matriz o errores).
  • handle podría ser un poco engañoso. Quería usar recover , pero se usa para panic s ...

editar: Se modificó la invocación r. Lee para que coincida con io.Reader.Read() .

Parte del motivo de esa sugerencia es que el enfoque actual en Go no ayuda a las herramientas a comprender si un error devuelto denota una función que falla o está devolviendo un valor de error como parte de su función (por ejemplo, github.com/pkg/errors ).

Creo que habilitar funciones para expresar fallas explícitamente es el primer paso para mejorar el manejo de errores.

@ibrasho , ¿en qué se diferencia tu ejemplo de ...

func (c EmailClient) SendEmail(to, content string) error {
    // ...

    // You can handle it and execution will continue if you don't fail or return
    _, _, err := r.Read()
        if err != nil {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    _, _, err := r.Read()
}

... si damos advertencias del compilador o lint para instancias no manejadas de error ? No se requiere cambio de idioma.

Dos cosas:

  • Creo que la sintaxis propuesta se lee y se ve mejor. 😁
  • Mi versión requiere dar a las funciones la capacidad de indicar que fallan explícitamente. Esto es algo que falta en Go actualmente y que podría permitir que las herramientas hagan más. Siempre podemos tratar una función que devuelve un error como fallando, pero eso es una suposición. ¿Qué pasa si la función devuelve 2 error valores?

Mi sugerencia tenía algo que eliminé, que era la propagación automática:

func (c EmailClient) SendEmail(to, content string) fails {
    n := r.Read(b)

    // Would automaticaly propgate the error, so it will be equivlent to this:
    // n := r.Read(b) handle (err) {
    //  fail err
    // }
}

Lo eliminé porque creo que el manejo de errores debería ser explícito.

editar: Se modificó la invocación r. Lee para que coincida con io.Reader.Read() .

Entonces, ¿esta sería una firma o un prototipo válido?

func (r *MyFileReader) Read(b []byte) (n int, err error) fails

(Dado que una implementación io.Reader asigna io.EOF cuando no hay nada más para leer y algún otro error por condiciones de falla).

Si. Pero nadie debería esperar que err denote una falla en la función que hace su trabajo. El error de lectura debe pasarse al bloque de mango. Los errores son valores en Go, y el que devuelve esta función no debería tener un significado especial además de ser algo que devuelve esta función (por alguna extraña razón).

Estaba proponiendo que una falla dará como resultado que los valores de retorno se devuelvan como valores cero. El Reader.Read actual ya hizo algunas promesas que podrían no ser posibles con este nuevo enfoque.

Cuando Read encuentra un error o una condición de fin de archivo después de leer con éxito n> 0 bytes, devuelve el número de bytes leídos. Puede devolver el error (no nulo) de la misma llamada o devolver el error (y n == 0) de una llamada posterior. Un ejemplo de este caso general es que un lector que devuelve un número de bytes distinto de cero al final del flujo de entrada puede devolver err == EOF o err == nil. La siguiente lectura debería devolver 0, EOF.

Las personas que llaman siempre deben procesar los n> 0 bytes devueltos antes de considerar el error err. Hacerlo maneja correctamente los errores de E / S que ocurren después de leer algunos bytes y también los dos comportamientos EOF permitidos.

Se desaconseja que las implementaciones de Read devuelvan un recuento de bytes cero con un error nulo, excepto cuando len (p) == 0. Las personas que llaman deben tratar un retorno de 0 y nil como una indicación de que no pasó nada; en particular, no indica EOF.

No todo este comportamiento es posible en el enfoque propuesto actualmente. En el contrato de interfaz de lectura actual, veo algunas deficiencias, como cómo manejar las lecturas parciales.

En general, ¿cómo debería comportarse una función cuando se realiza parcialmente cuando falla? Honestamente, no pensé en esto todavía.

El caso de io.EOF es simple:

func DoSomething(r io.Reader) fails {
    // I'm using rerr so that I don't shadow the err returned from the function
    n, err := r.Read(b) handle (rerr) {
        if rerr != io.EOF {
            fail err
        }
        // Else do nothing?
    }
}

@thejerf , siento que hay muchos comportamientos extraños aquí. Estás llamando a net.Listen, que devuelve un error, pero no está asignado.

Estoy usando el operador común ? propuesto por varias personas para indicar que se devuelve el error con valores cero para los otros valores, si el error no es nulo. Prefiero un poco una palabra corta a un operador, ya que no creo que Go sea un lenguaje pesado para los operadores, pero si ? fuera al idioma, todavía contaría mis ganancias en lugar de estampar mis pies. un bufido.

¿Cada nuevo aplazamiento anula el último, de modo que nunca se invoca annotateError? ¿O se apilan, de modo que si se devuelve un error de, digamos, toServer.Send, se llama dos veces a closeOnErr y luego se llama a annotateError?

Funciona como lo hace ahora: https://play.golang.org/p/F0xgP4h5Vxf Esperaba algunos comentarios negativos para esa publicación, a lo que mi respuesta planeada iba a ser señalar que esto es _ya_ cómo funciona el aplazamiento y tú simplemente reduzca el comportamiento actual de Go, pero no obtuvo ninguno. Pobre de mí. Como también muestra ese fragmento, el sombreado no es un problema, o al menos, no es más un problema de lo que ya es. (Esto no lo arreglaría ni lo empeoraría particularmente).

Creo que un aspecto que puede ser confuso es que ya es el caso en Go actual que un parámetro con nombre terminará siendo "lo que sea que se devuelva realmente", por lo que puede hacer lo que hice y tomar un puntero y pasar eso a una función diferida y manipularla, independientemente de si devuelve directamente un valor como return errors.New(...) , que intuitivamente puede parecer una "nueva variable" que no es la variable nombrada, pero de hecho, Go terminará con ella asignada a la variable nombrada en el momento en que se ejecuta el aplazamiento. Es fácil ignorar este detalle particular de Go actual en este momento. Sugiero que, si bien puede resultar confuso ahora, si trabajara incluso en una base de código que usara este modismo (es decir, no estoy diciendo que esto tenga que convertirse en "Practique las mejores prácticas", solo unas pocas exposiciones serían suficientes) Lo resolvería bastante rápido. Porque, solo para decirlo una vez más para ser muy claro, así es como funciona Go, no un cambio propuesto.

Aquí hay una propuesta que creo que no se ha sugerido antes. Usando un ejemplo:

 r, !handleError := something()

El significado de esto es el mismo que esto:

 r, _xyzzy := something()
 if ok, R := handleError(_xyzzy); !ok { return R }

(donde _xyzzy es una nueva variable cuyo alcance se extiende solo a estas dos líneas de código, y R pueden ser valores múltiples).

Las ventajas de esta propuesta no son específicas de los errores, no trata los valores cero de manera especial y es fácil especificar de manera concisa cómo envolver los errores dentro de cualquier bloque de código en particular. Los cambios de sintaxis son pequeños. Dada la sencilla traducción, es fácil entender cómo funciona esta característica.

Las desventajas son que introduce un retorno implícito, no se puede escribir un controlador genérico que simplemente devuelva el error (ya que sus valores de retorno deben basarse en la función desde donde se llama), y que el valor que se pasa al controlador es no disponible en el código de llamada.

Así es como puede usarlo:

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

O puede usarlo en una prueba para llamar a t.Fatal si su código de inicio falla:

func TestSomething(t *testing.T) {
  must := func(err error) bool { t.Fatalf("init code failed: %s", err); return true }
  !must := setupTest()
  !must := clearDatabase()
  ...
}

Sugeriría cambiar la firma de la función a solo func(error) error . Simplifica el caso de la mayoría, y si necesita analizar más el error, simplemente use el mecanismo actual.

Pregunta de sintaxis: ¿Puede definir la función en línea?

func Read(filename string) error {
    f, !func(err error) error {
        if err != nil { return true, fmt.Errorf("... %s", err) }
        return false, nil
    } := OpenFile(filename)
    /...

Me siento cómodo con "no hagas eso", pero la sintaxis probablemente debería permitirle reducir el recuento de casos especiales. Esto también permitiría:

func failed(s string) func(error) error {
    return func(err error) {
       // returns a decorated error with the given string
   }
}

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

Lo que me parece una de las mejores propuestas para ese tipo de cosas, al menos en términos de incluir ese material de manera concisa. Este es otro caso en el que, en mi humilde opinión, está emitiendo mejores errores en este punto de lo que tiende el código basado en excepciones, que a menudo no logra obtener información tan detallada sobre la naturaleza del error porque es muy fácil dejar que las excepciones se propaguen.

También sugeriría, por razones de rendimiento, que las funciones de error de explosión se definan como no invocadas en valores cero. Eso mantiene su impacto en el rendimiento bastante mínimo; en el caso que acabo de mostrar, si la lectura tiene éxito normalmente, no es más costosa que una implementación de lectura actual que ya es if ing en cada error y falla la cláusula if . Si llamamos a una función en nil todo el tiempo, se volverá muy costoso siempre que no se pueda insertar, lo que terminará siendo una cantidad no trivial del tiempo. (Si se está produciendo un error de forma activa, probablemente podamos justificar y permitirnos una llamada de función bajo casi cualquier circunstancia (si no puede, recurra al método actual), pero realmente no queremos eso para los que no son errores). también significa que las funciones bang pueden asumir un valor distinto de cero en su implementación, lo que las simplifica también.

@thejerf camino agradable pero feliz está muy alterado.
Hace muchos mensajes se sugirió tener un poco de Ruby como "o" sintax - f := OpenFile(filename) or failed("couldn't open file") .

Preocupación adicional: ¿es eso para cualquier tipo de parámetros o solo para errores? si es solo para errores, entonces el tipo error debe tener un significado especial para el compilador.

@thejerf camino agradable pero feliz está muy alterado.

Recomendaría distinguir entre el camino probablemente común de la propuesta original donde se parece a la sugerencia original de Paulhankin:

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

tal vez incluso con herr factorizado en alguna parte, y mis exploraciones de lo que se necesitaría para especificarlo completamente, que es una necesidad para esta conversación, y mis propias reflexiones sobre cómo podría usarlo en mi código personal, que es meramente una exploración de qué más está habilitado y proporcionado por una sugerencia. Ya dije literalmente que insertar una función es probablemente una mala idea después de todo, pero la gramática probablemente debería permitirle mantener la gramática simple. Ya puedo escribir una función Go que tome tres funciones e incorporarlas todas directamente en la llamada. Eso no significa que Go esté roto o que Go necesite hacer algo para evitarlo; significa que no debería hacer eso si valoro la claridad del código. Me gusta que Go ofrece claridad de código, pero todavía hay cierto grado de responsabilidad irreductible en los desarrolladores para mantener el código claro.

Si me vas a decir que el "camino feliz" para

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

está arrugado y es difícil de leer, pero el camino feliz es fácil de leer con

func Read(filename string) error {
  f, err := OpenFile(filename)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  b, err := ReadBytes(f)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  err = ProcessBytes(b)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  return nil
}

luego presento la única forma posible en la que el segundo es más fácil de leer es que ya estás acostumbrado a leerlo y tus ojos están entrenados para saltar exactamente el camino correcto para ver el camino feliz. Puedo adivinar eso, porque los míos también lo son. Pero una vez que esté acostumbrado a una sintaxis alternativa, también estará capacitado para eso. Debe analizar la sintaxis basándose en cómo se sentirá cuando ya esté acostumbrado, no en cómo se sienta ahora.

También señalaría que las nuevas líneas agregadas en el segundo ejemplo representan lo que sucede con mi código real. No son sólo "líneas" de código lo que el actual manejo de errores de Go tiende a agregar al código, sino que también agrega muchos "párrafos" a lo que de otra manera debería ser una función muy simple. Quiero abrir el archivo, leer algunos bytes y procesarlos. No quiero

Abra un archivo.

Y luego lea algunos bytes, si está bien.

Y luego procéselos, si está bien.

Siento que hay muchos votos negativos que equivalen a "esto no es a lo que estoy acostumbrado", en lugar de un análisis real de cómo se desarrollarán en el código real, una vez que esté acostumbrado a ellos y los use con fluidez. .

No me gusta la idea de ocultar la declaración de devolución, prefiero:

f := OpenFile(filename) or return failed("couldn't open file")
....
func failed(msg string, err error) error { ... } 

En este caso or es un operador de reenvío condicional cero ,
reenviando la última devolución si no es cero.
Hay una propuesta similar en C # usando el operador ?>

f := OpenFile(filename) ?> return failed("couldn't open file")

@thejerf "camino feliz" en su caso precedido por una llamada a fallado (...), que puede ser muy largo. también suena como yoda: rofl:

Los caracteres

Por favor, no lo haga más complejo de lo que es ahora. Realmente mover los mismos códigos en una línea (en lugar de 3 o más) no es realmente una solución. Personalmente, no creo que ninguna de estas propuestas sea tan viable. Chicos, las matemáticas son muy simples. Adopte la idea de "Try-catch" o mantenga las cosas como están ahora, lo que significa muchos "si entonces" y ruidos de código y no es realmente adecuado para usar en patrones OO como Fluent Interface.

Muchas gracias por todas sus donaciones y tal vez unas pocas donaciones ;-) (es broma)

@KamyarM En mi opinión , "utilizar la alternativa más conocida o no hacer ningún cambio" no es una declaración muy productiva. Detiene la innovación y facilita los argumentos circulares.

@KernelDeimos Estoy de acuerdo con usted, pero veo muchos comentarios en este hilo que esencialmente defendía lo antiguo con mover exactamente 4 5 líneas en una sola línea, lo cual no lo veo como una solución real y también muchos en Go Community rechazan RELIGIOSAMENTE el uso Try-Catch que cierra las puertas a cualquier otra opinión. Personalmente, creo que aquellos que inventaron este concepto de prueba y captura realmente lo pensaron y, aunque puede tener algunos defectos, esos defectos son causados ​​por malos hábitos de programación y no hay forma de obligar a los programadores a escribir buenos códigos, incluso si elimina o limita todo lo bueno o algunos pueden decir malas características que puede tener un lenguaje de programación.
Propuse algo como esto antes y esto no es exactamente java o C # try-catch y creo que puede admitir el manejo de errores y bibliotecas actuales, y uso uno de los ejemplos de arriba. Entonces, básicamente, el compilador verifica los errores después de cada línea y salta al bloque de captura si el valor de error se establece en no nil:

try (var err error){ 
    f, err := OpenFile(filename)
    b, err := ReadBytes(f)
    err = ProcessBytes(b)
    return nil
} catch (err error){ //Required
   return err
} finally{ // Optional
    // Do something else like close the file or connection to DB. Not necessary in this example since we  return earlier.
}

@KamyarM
En su ejemplo, ¿cómo sé (en el momento de escribir el código) qué método devolvió el error? ¿Cómo cumplo con la tercera forma de manejar el error ("devolver el error con información contextual adicional")?

@urandom
una forma es usar el interruptor Go y encontrar el tipo de excepción en la captura. Digamos que tiene una excepción pathError que sabe que es causada por OpenFile () de esa manera. Otra forma que no es muy diferente de la actual if err! = Nil error handling en GoLang es esta:

try (var err error){ 
    f, err := OpenFile(filename)
} catch (err error){ //Required
   return err
}
try (var err error){ 
    b, err := ReadBytes(f)
catch (err error){ //Required
   return err
}
try (var err error){ 
    err = ProcessBytes(b)
catch (err error){ //Required
   return err
}
return nil

Así que tienes opciones de esta manera, pero no estás limitado. Si realmente desea saber exactamente qué línea causó el problema, colocó cada línea en un try catch de la misma manera que ahora escribe muchos if-then-elses. Si el error no es importante para usted y desea pasarlo al método de la persona que llama, que en los ejemplos discutidos en este hilo en realidad se trata de eso, creo que mi código propuesto simplemente hace el trabajo.

@KamyarM Veo de dónde vienes ahora. Yo diría que si hay tanta gente en contra de try ... catch, veo eso como evidencia de que try ... catch no es perfecto y tiene fallas. Es una solución fácil a un problema, pero si Go2 puede hacer que el manejo de errores sea mejor que lo que hemos visto en otros idiomas, creo que sería realmente genial.

Creo que es posible tomar lo bueno de try ... catch sin tomar lo malo de try ... catch, que propuse antes. Estoy de acuerdo en que convertir tres líneas en 1 o 2 no resuelve nada.

El problema fundamental, como yo lo veo, es que el código de manejo de errores en una función se repite si parte de la lógica es "regresar al llamador". Si desea cambiar la lógica en cualquier momento, debe cambiar cada instancia de if err != nil { return nil } .

Dicho esto, me gusta mucho la idea de try...catch siempre que las funciones no puedan throw nada implícitamente.

Otra cosa que creo que sería útil es si la lógica en catch {} requiere una palabra clave break para romper el flujo de control. A veces, desea manejar un error sin interrumpir el flujo de control. (por ejemplo: "para cada elemento de estos datos, haga algo, agregue un error que no sea nulo a una lista y continúe")

@KernelDeimos Estoy completamente de acuerdo con eso. He visto la situación exacta así. Debe capturar los errores tanto como pueda antes de descifrar los códigos. Si algo como Canales se pudiera usar en esas situaciones en GoLang, eso fue bueno, entonces podría enviar todos los errores a ese canal que espera la captura y luego la captura podría manejarlos uno por uno.

Prefiero mezclar "o regresar" con # 19642, # 21498 que usar try..catch (aplazar / pánico / recuperar ya existe; lanzar dentro de la misma función es como tener múltiples declaraciones goto y se vuelve complicado con el cambio de tipo adicional dentro de catch; permite olvidar el manejo de errores al tener try..catch en lo alto de la pila (o complicar significativamente el compilador si el alcance try..catch dentro de una sola función)

@egorse
Parece que la sintaxis try-catch que sugiere @KamyarM es algo de azúcar sintáctico para manejar las variables de retorno de error, no una introducción a las excepciones. Si bien prefiero una sintaxis de tipo "or return" por varias razones, parece una sugerencia legítima.

Dicho esto, @KamyarM , ¿por qué try tiene una parte de definición variable? Está definiendo una variable err , pero está sombreada por las otras variables err dentro del bloque mismo. ¿Cual es su propósito?

Creo que es para decirle qué variable vigilar, lo que le permite desacoplarse del tipo error . Probablemente requiera un cambio en las reglas de sombreado, a menos que solo necesite que las personas tengan mucho cuidado con él. Sin embargo, no estoy seguro de la declaración en el bloque catch .

@egorse Exactamente lo que @DeedleFake mencionó es el propósito. Significa que try block tiene un ojo en ese objeto. También limita su alcance. Es algo similar a la instrucción using en C #. En C #, los objetos que se definen mediante el uso de palabras clave se eliminan automáticamente una vez que se ejecuta ese bloque y el alcance de esos objetos se limita al bloque "Uso".
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement

Usar la captura es necesario porque queremos forzar al programador a decidir cómo manejar el error correctamente. En C # y Java, la captura también es obligatoria. en C # si no desea manejar una excepción, no use try-catch en esa función en absoluto. Cuando ocurre una excepción, cualquier método en la jerarquía de llamadas puede manejar la excepción o incluso volver a lanzarla (o envolverla en otra excepción) nuevamente. No creo que puedas hacer lo mismo en Java. En Java, un método que puede generar una excepción debe declararlo en la firma de la función.

Quiero enfatizar que este bloque try-catch no es el exacto. Usé estas palabras clave porque esto es similar en lo que quiere lograr y también esto es con lo que muchos programadores están familiarizados y se les enseña en la mayoría de los cursos de conceptos de programación.

Podría haber una asignación _return on error_, que funciona solo si hay un _ parámetro de retorno de error con nombre_, como en:

func process(someInput string) (someOutput string, err error) {
    err ?= otherAction()
    return
}

Si err no es nil entonces regrese.

Creo que esta discusión de agregar un azúcar try a Rust sería esclarecedora para los participantes en esta discusión.

FWIW, un viejo pensamiento sobre la simplificación del manejo de errores (disculpas si esto es una tontería):

El identificador de aumento , denotado por el símbolo de intercalación ^ , puede usarse como uno de los operandos en el lado izquierdo de una asignación. Para los propósitos de la asignación, el identificador de aumento es un alias para el último valor de retorno de la función contenedora, ya sea que el valor tenga un nombre o no. Una vez completada la asignación, la función prueba el último valor devuelto con el valor cero de su tipo (nulo, 0, falso, ""). Si se considera cero, la función continúa ejecutándose; de ​​lo contrario, regresa.

El propósito principal del identificador de aumento es propagar de manera concisa los errores de las funciones llamadas al llamador en un contexto dado sin ocultar el hecho de que esto está ocurriendo.

Como ejemplo, considere el siguiente código:

func Alpha() (string, error) {

    b, ^ := beta()
    g, ^ := gamma()
    return b + g, nil
}

Esto es aproximadamente equivalente a:

func Alpha() (ret1 string, ret2 error) {

    b, ret2 := beta()
    if ret2 != nil {
        return
    }

    g, ret2 := gamma()
    if ret2 != nil {
        return
    }

    return b + g, nil
}

El programa tiene un formato incorrecto si:

  • el identificador de aumento se usa más de una vez en una asignación
  • la función no devuelve un valor
  • el tipo del último valor devuelto no tiene una prueba significativa y eficiente para cero

Esta sugerencia es similar a otras en que no aborda el problema de brindar mayor información contextual, por lo que vale.

@gboyle Es por eso que el último valor de retorno de la OMI debe ser nombrado y de tipo error . Esto tiene dos implicaciones importantes:

1 - también se nombran otros valores de retorno, por lo tanto
2 - ya tienen valores cero significativos.

@ object88 Como nos context , esto necesita alguna acción del equipo central, como definir un tipo error incorporado (solo un Go error normal) con algunos atributos comunes (mensaje, pila de llamadas, etc., etc.).

AFAIK, no hay muchas construcciones de lenguaje contextual en Go. Además de go y defer no hay otros e incluso estos dos son muy explícitos y claros (bot en sintaxis - y para el ojo - y semántica).

¿Y algo como esto?

(copié un código real en el que estoy trabajando):

func (g *Generator) GenerateDevices(w io.Writer) error {
    var err error
    catch err {
        _, err = io.WriteString(w, "package cc\n\nconst (") // if err != nil { goto Caught }
        for _, bd := range g.zwClasses.BasicDevices {
            _, err = w.Write([]byte{'\t'}) // if err != nil { goto Caught }
            _, err = io.WriteString(w, toGoName(bd.Name)) // if err != nil { goto Caught }
            _, err = io.WriteString(w, " BasicDeviceType = ") // if err != nil { goto Caught }
            _, err = io.WriteString(w, bd.Key) // if err != nil { goto Caught }
            _, err = w.Write([]byte{'\n'}) // if err != nil { goto Caught }
        }
        _, err = io.WriteString(w, ")\n\nvar BasicDeviceTypeNames = map[BasicDeviceType]string{\n") // if err != nil { goto Caught }
       // ...snip
    }
    // Caught:
    return err
}

Cuando err no es nulo, llega al final de la instrucción "catch". Puede utilizar "catch" para agrupar llamadas similares que normalmente devuelven el mismo tipo de error. Incluso si las llamadas no están relacionadas, puede verificar los tipos de error posteriormente y ajustarlo adecuadamente.

@lukescott lea esta publicación de blog de @robpike https://blog.golang.org/errors-are-values

@davecheney La idea de captura (sin el intento) se mantiene en el espíritu de ese sentimiento. Trata el error como un valor. Simplemente se rompe (dentro de la misma función) cuando el valor ya no es nulo. No bloquea el programa de ninguna manera.

@lukescott , puedes usar la técnica de Rob hoy, no necesitas cambiar el idioma.

Existe una gran diferencia entre excepciones y errores:

  • se esperan errores (podemos escribir una prueba para ellos),
  • no se esperan excepciones (de ahí la "excepción"),

Muchos idiomas tratan a ambos como excepciones.

Entre los genéricos y un mejor manejo de errores, elegiría un mejor manejo de errores ya que la mayor parte del desorden de código en Go proviene del manejo de errores. Si bien se puede decir que este tipo de verbosidad es buena y está a favor de la simplicidad, en mi opinión también oscurece el _ camino feliz_ de un flujo de trabajo al nivel de ser ambiguo.

Me gustaría basarme un poco en la propuesta de

Primero, en lugar de ! , se introduce or operador

El método de lectura se verá así:

func Read(filename string) error {
  f := OpenFile(filename) or return errors.Contextf("opening file %s", filename)
  b := ReadBytes(f) or return errors.Contextf("reading file %s", filename)
  ProcessBytes(b) or return errors.Context("processing data")
  return nil
}

Supongo que el paquete de errores proporciona funciones de conveniencia como las siguientes:

func Noop() func(error) error {
   return func(err error) {
       return err   
   }
}


func Context(msg string) func(error) error {
    return func(err error) {
        return fmt.Errorf("%s: %v", msg, err)
    }
}
...

Esto parece perfectamente legible, al tiempo que cubre todos los puntos necesarios, y tampoco parece demasiado extraño, debido a la familiaridad de la declaración de devolución.

@urandom En esta declaración f := OpenFile(filename) or return errors.Contextf("opening file %s", filename) ¿cómo se puede saber la razón? Por ejemplo, ¿es la falta de permiso de lectura o el archivo no existe?

@ dc0d
Bueno, incluso en el ejemplo anterior, se incluye el error original, ya que el mensaje dado por el usuario es solo contexto agregado. Como se indicó, y derivado de la propuesta original, or return espera una función que reciba un único parámetro del tipo desplazado. Esto es clave y permite no solo funciones de utilidad que se adaptarán a una gran cantidad de personas, sino que también puede escribir las suyas propias si necesita un manejo realmente personalizado de valores específicos.

@urandom IMO oculta demasiado.

Mis 2 centavos aquí, me gustaría proponer una regla simple:

"parámetro de error de resultado implícito para funciones"

Para cualquier función, un parámetro de error está implícito al final de la lista de parámetros de resultado.
si no se define explícitamente.

Supongamos que tenemos una función definida de la siguiente manera por el bien de la discusión:

func f () (int) {}
que es idéntico a: func f () (int, error) {}
de acuerdo con nuestra regla de error de resultado implícito.

para la asignación, puede agregar, ignorar o detectar el error de la siguiente manera:

1) burbujear

x: = f ()

si f devuelve un error, la función actual volverá inmediatamente con el error
(¿o crear una nueva pila de errores?)
si la función actual es principal, el programa se detendrá.

Es equivalente al siguiente fragmento de código:

x, err: = f ()
if err! = nil {
volver ..., err
}

2) ignorar

x, _: = f ()

un identificador en blanco al final de la lista de expresiones de asignación para señalar explícitamente el descarte del error.

3) atrapar

x, err: = f ()

err debe manejarse como de costumbre.

Creo que este cambio de convención de código idiomático solo debería requerir cambios mínimos en el compilador
o un preprocesador debería hacer el trabajo.

@ dc0d ¿Puede dar un ejemplo de lo que esconde y cómo?

@urandom Es el motivo por el que f := OpenFile(filename) or return errors.Contextf("opening file %s", filename) . El error original devuelto por OpenFile() - que podría ser algo como la falta de permiso de lectura o la ausencia del archivo y no solo "hay algo mal con el nombre del archivo".

@ dc0d
Estoy en desacuerdo. Es tan claro como tratar con http.Handlers, donde con los últimos los pasas a un mux, y de repente obtienes una solicitud y un escritor de respuesta. Y la gente está acostumbrada a este tipo de comportamiento. ¿Cómo sabe la gente lo que hace la declaración go ? Obviamente, no está claro en el primer encuentro, pero es bastante omnipresente y está en el idioma.

No creo que debamos estar en contra de ninguna propuesta sobre la base de que es nueva y nadie tiene idea de cómo funciona, porque eso es cierto para la mayoría de ellos.

@urandom Ahora eso tiene un poco más de sentido (incluido el ejemplo de http.Handler ).

Y estamos discutiendo cosas. No hablo en contra ni a favor de ninguna idea concreta. Pero apoyo la simplicidad y ser explícito y al mismo tiempo transmitir un poco de cordura sobre la experiencia del desarrollador.

@ dc0d

que podría ser algo como la falta de permiso de lectura o la ausencia del archivo

En ese caso, no solo volvería a lanzar el error, sino que verificaría su contenido real. Para mí, esta edición se trata de cubrir el caso más popular. Es decir, volver a lanzar el error con contexto adicional. Solo en casos más raros convierte el error en algún tipo concreto y comprueba lo que realmente dice. Y por eso, la sintaxis actual de manejo de errores está perfectamente bien y no irá a ninguna parte, incluso si una de las propuestas aquí fuera aceptada.

@creker Los errores no son excepciones (algún comentario anterior mío). Los errores son valores, por lo que no es posible lanzarlos o relanzarlos. Para escenarios de prueba / captura, Go tiene pánico / recuperación.

@ dc0d No estoy hablando de excepciones. Al volver a lanzar me refiero a devolver el error a la persona que llama. El or return errors.Contextf("opening file %s", filename) básicamente envuelve y vuelve a generar un error.

@creker Gracias por la explicación. También agrega algunas llamadas de función adicionales que afectan al programador, lo que a su vez puede no producir el comportamiento deseado en algunas situaciones.

@ dc0d es un detalle de implementación y podría cambiar en el futuro. Y en realidad podría cambiar, la preferencia no cooperativa está en proceso en este momento.

@creker
Creo que puede cubrir incluso más casos que solo devolver un error modificado:

func retryReadErrHandler(filename string, count int) func(error) error {
     return func(err error) error {
          if os.IsTimeout(err) {
               count++
               return Read(filename, count)
          }
          if os.IsPermission(err) {
               log.Fatal("Permission")
          }

          return fmt.Errorf("opening file %s: %v", filename, err)
      }
}

func Read(filename string, count int) error {
  if count > 3 {
    return errors.New("max retries")
  }

  f := OpenFile(filename) or return retryReadErrHandler(filename, count)

  ...
}

@ dc0d
El compilador probablemente incluirá las llamadas a funciones adicionales.

@urandom que parece muy interesante. Un poco mágico con un argumento implícito, pero este podría ser lo suficientemente general y conciso como para cubrir todo. Solo en casos muy raros tendría que recurrir a if err != nil regulares

@urandom , tu ejemplo me confunde. ¿Por qué retryReadErrHandler devuelve una función?

@ object88
Esa es la idea detrás del operador or return . Espera una función a la que llamará en el caso de un último argumento devuelto distinto de cero desde el lado izquierdo. En este sentido, actúa exactamente igual que un http.Handler, dejando la lógica real de cómo manejar el argumento y su retorno (o la solicitud y su respuesta, en el caso de un controlador), a la devolución de llamada. Y para usar sus propios datos personalizados en la devolución de llamada, crea una función contenedora que recibe esos datos como parámetros y devuelve lo que se espera.

O en términos más familiares, es similar a lo que solemos hacer con los controladores:
ir
func nodesHandler (repositorio Repo) http.Handler {
return http.HandlerFunc (func (w http.ResponseWriter, r * http.Request) {
datos, _: = json.Marshal (repo.GetNodes ())
w.Write (datos)
})
}

@urandom , puede evitar algo de magia dejando el LHS igual que hoy y cambiando or ... return a returnif (cond) :

func Read(filename string) error {
   f, err := OpenFile(filename) returnif(err != nil) errors.Contextf(err, "opening file %s", filename)
   b, err := ReadBytes(f) returnif(err != nil) errors.Contextf(err, "reading file %s", filename)
   err = ProcessBytes(b) returnif(err != nil) errors.Context(err, "processing data")
   return nil
}

Esto mejora la generalidad y transparencia de los valores de error a la izquierda y la condición de activación a la derecha.

Cuanto más veo estas propuestas diferentes, más me inclino a querer un cambio exclusivo. El lenguaje ya tiene el poder, hagámoslo más escaneable. @billyh , no returnif(cond) ... es solo una forma de reescribir if cond { return ...} . ¿Por qué no podemos simplemente escribir lo último? Ya sabemos lo que significa.

x, err := foo()
if err != nil { return fmt.Errorf(..., err) }

o incluso

if x, err := foo(); err != nil { return fmt.Errorf(..., err) }

o

x, err := foo(); if err != nil { return fmt.Errorf(..., err) }

No hay nuevas palabras clave mágicas, sintaxis u operadores.

(Podría ayudar si también arreglamos # 377 para agregar algo de flexibilidad al uso de := ).

@ randall77 Yo también me inclino cada vez más por eso.

@ randall77 ¿En qué punto se ajustará la línea de ese bloque?

La solución anterior es más agradable en comparación con las alternativas propuestas aquí, pero no estoy convencido de que sea mejor que no tomar ninguna medida. Gofmt debería ser lo más determinista posible.

@as , no lo he pensado completamente, pero tal vez "si el cuerpo de una declaración if contiene una sola declaración return , entonces la declaración if tiene el formato una sola línea ".

Tal vez deba haber una restricción adicional en la condición if , como que debe ser una variable booleana o un operador binario de dos variables o constantes.

@billyh
No veo la necesidad de hacerlo más detallado, ya que no veo nada confuso de ese poco de magia en el or . Supongo que, a diferencia de @as , mucha gente tampoco encuentra nada confuso en la forma en que trabajamos con los controladores http.

@ randall77
Lo que sugieres está más en línea como una sugerencia de estilo de código, y ahí es donde hay muchas opiniones. Puede que no funcione bien en la comunidad en su conjunto que de repente haya 2 estilos de formateo de declaraciones if.

Sin mencionar que frases como esas son mucho más difíciles de leer. if != ; { } es demasiado incluso en varias líneas, de ahí esta propuesta. El patrón es fijo para casi todos los casos y se puede convertir en azúcar sintáctico que es fácil de leer y comprender.

El problema que tengo con la mayoría de estas sugerencias es que no está claro qué está pasando. En la publicación de apertura, se sugiere reutilizar || para devolver un error. No me queda claro que se esté produciendo un regreso allí. Creo que si se va a inventar una nueva sintaxis, debe alinearse con las expectativas de la mayoría de la gente. Cuando veo || no espero una devolución, ni siquiera una interrupción en la ejecución. Es discordante para mí.

Me gusta el sentimiento de "los errores son valores" de Go, pero también estoy de acuerdo en que if err := expression; err != nil { return err } es demasiado detallado, principalmente porque se espera que casi todas las llamadas devuelvan un error. Esto significa que tendrá muchos de estos, y es fácil estropearlo, dependiendo de dónde se declare (o se ensombrezca) err. Ha sucedido con nuestro código.

Dado que Go no usa try / catch y usa panic / defer para circunstancias "excepcionales", es posible que tengamos la oportunidad de reutilizar las palabras clave try y / o catch para acortar el manejo de errores sin bloquear el programa.

Aquí hay un pensamiento que tuve:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
}

La idea es que usted prefija err en el LHS con la palabra clave try . Si err no es nulo, se produce una devolución inmediatamente. No tiene que usar una captura aquí, a menos que la devolución no esté completamente satisfecha. Esto se alinea más con las expectativas de la gente de "intentar romper la ejecución", pero en lugar de bloquear el programa, simplemente regresa.

Si la devolución no está completamente satisfecha (verificación de tiempo de compilación) o si queremos ajustar el error, podríamos usar catch como una etiqueta especial de solo avance como esta:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
catch:
    return &CustomError{"some detail", err}
}

Esto también le permite verificar e ignorar ciertos errores:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, err = io.WriteString(w, "bar")
        if err == io.EOF {
            err = nil
        } else {
            goto catch
        }
    return
catch:
    return &CustomError{"some detail", err}
}

Quizás incluso podría hacer que try especifique la etiqueta:

func WriteFooBar(w io.Writer) (err error) {
    _, try(handle1) err = io.WriteString(w, "foo")
    _, try(handle2) err = w.Write([]byte{','})
    _, try(handle3) err = io.WriteString(w, "bar")
    return
handle1:
    return &CustomError1{"...", err}
handle2:
    return &CustomError2{"...", err}
handle3:
    return &CustomError3{"...", err}
}

Me doy cuenta de que mis ejemplos de código apestan (foo / bar, ack). Pero espero haber ilustrado lo que quiero decir con ir con o en contra de las expectativas existentes. También estaría totalmente de acuerdo con mantener los errores como están en Go 1. Pero si se inventa una nueva sintaxis, es necesario pensar detenidamente en cómo esa sintaxis ya se percibe, no solo en Go. Es difícil inventar una nueva sintaxis sin que ya signifique algo, por lo que a menudo es mejor ir con las expectativas existentes en lugar de en contra de ellas.

¿Quizás algún tipo de encadenamiento como cómo se pueden encadenar métodos pero por errores? No estoy realmente seguro de cómo se vería o si funcionaría, solo una idea descabellada.
Puede hacer una especie de cadena ahora para reducir el número de comprobaciones de error manteniendo algún tipo de valor de error dentro de una estructura y extrayéndolo al final de la cadena.

Es una situación muy curiosa porque, si bien hay un poco de repetición, no estoy muy seguro de cómo simplificarlo más sin dejar de tener sentido.

El código de muestra de @thejerf se ve así con la propuesta de @lukescott :

func NewClient(...) (*Client, error) {
    listener, try err := net.Listen("tcp4", listenAddr)
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    return nil, err
}

Va de 59 líneas a 47.

Esta es la misma longitud, pero creo que es un poco más claro que usar defer :

func NewClient(...) (*Client, error) {
    var openedListener, openedConn bool
    listener, try err := net.Listen("tcp4", listenAddr)
    openedListener = true

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    openedConn = true

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    if openedConn {
        conn.Close()
    }
    if openedListener {
        listener.Close()
    }
    return nil, err
}

Ese ejemplo probablemente sería más fácil de seguir con deferifnotnil o algo así.
Pero eso se remonta a toda una línea si se trata de muchas de estas sugerencias.

Habiendo jugado un poco con el código de ejemplo, ahora estoy en contra de la variante try(label) name . Creo que si tiene varias cosas elegantes que hacer, simplemente use el sistema actual de if err != nil { ... } . Si está haciendo básicamente lo mismo, como configurar un mensaje de error personalizado, puede hacer esto:

func WriteFooBar(w io.Writer) (err error) {
    msg := "thing1 went wrong"
    _, try err = io.WriteString(w, "foo")
    msg = "thing2 went wrong"
    _, try err = w.Write([]byte{','})
    msg = "thing3 went wrong"
    _, try err = io.WriteString(w, "bar")
    return nil

catch:
    return &CustomError{msg, err}
}

Si alguien ha usado Ruby, esto se parece mucho a su sintaxis rescue , que creo que se lee razonablemente bien.

Una cosa que se podría hacer es hacer que nil un valor falso y que otros valores se evalúen como verdaderos, por lo que terminará con:

err := doSomething()
if err { return err }

Pero no estoy seguro de que realmente funcione y solo elimina algunos caracteres.
He escrito muchas cosas, pero no creo que alguna vez haya escrito != nil .

Haciendo que las interfaces sean verdaderas / falsey se ha mencionado antes, y dije que haría que los errores con banderas fueran más comunes:

verbose := flag.Bool("v", false, "verbose logging")
flag.Parse()
if verbose { ... } // should be *verbose!

@carlmjohnson , en el ejemplo que proporcionó anteriormente, hay mensajes de error intercalados con código de ruta feliz, que es un poco extraño para mí. Si necesita formatear esas cadenas, entonces está haciendo mucho trabajo adicional independientemente de si algo sale mal o no:

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    msg := fmt.Sprintf("While writing %s, thing1 went wrong", f.foo)
    _, try err = io.WriteString(w, f.foo)
    msg = fmt.Sprintf("While writing %s, thing2 went wrong", f.separator)
    _, try err = w.Write(f.separator)
    msg = fmt.Sprintf("While writing %s, thing3 went wrong", f.bar)
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    return &CustomError{msg, err}
}

@ object88 , creo que el análisis SSA debería poder determinar si ciertas asignaciones no se utilizan y reorganizarlas para que no sucedan si no son necesarias (¿demasiado optimista?). Si eso es cierto, esto debería ser eficiente:

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    var format string, args []interface{}

    msg = "While writing %s, thing1 went wrong", 
    args = []interface{f.foo}
    _, try err = io.WriteString(w, f.foo)

    format = "While writing %s, thing2 went wrong"
    args = []interface{f.separator}
    _, try err = w.Write(f.separator)

    format = "While writing %s, thing3 went wrong"
    args = []interface{f.bar}
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    msg := fmt.Sprintf(format, args...)
    return &CustomError{msg, err}
}

¿Sería esto legal?

func Foo() error {
catch:
    try _ = doThing()
    return nil
}

Creo que debería repetirse hasta que doThing() devuelva cero, pero podría estar convencido de lo contrario.

@carlmjohnson

Habiendo jugado un poco con el código de ejemplo, ahora estoy en contra de la variante del nombre try (etiqueta).

Sí, no estaba seguro de la sintaxis. No me gusta porque hace que try parezca una llamada de función. Pero pude ver el valor de especificar una etiqueta diferente.

¿Sería esto legal?

Yo diría que sí porque el intento debería ser solo hacia adelante. Si quisieras hacer eso, diría que debes hacerlo así:

func Foo() error {
tryAgain:
    if err := doThing(); err != nil {
        goto tryAgain
    }
    return nil
}

O así:

func Foo() error {
    for doThing() != nil {}
    return nil
}

@Azareal

Una cosa que se podría hacer es convertir nil en un valor falso y que otros valores se evalúen como verdaderos, por lo que terminará con: err := doSomething() if err { return err }

Creo que vale la pena acortarlo. Sin embargo, no creo que deba aplicarse a cero en todas las situaciones. Quizás podría haber una nueva interfaz como esta:

interface Truthy {
  True() bool
}

Entonces, cualquier valor que implemente esta interfaz se puede usar como propuso.

Esto funcionaría siempre que el error implemente la interfaz:

err := doSomething()
if err { return err }

Pero esto no funcionaría:

err := doSomething()
if err == true { return err } // err is not true

Soy realmente nuevo en golang, pero ¿cómo piensas sobre la introducción del delegador condicional como se muestra a continuación?

func someFunc() error {

    errorHandler := delegator(arg1 Arg1, err error) error if err != nil {
        // ...
        return err // or modifiedErr
    }

    ret, err := doSomething()
    delegate errorHandler(ret, err)

    ret, err := doAnotherThing()
    delegate errorHandler(ret, err)

    return nil
}

delegador funciona como cosas pero

  • Su return significa return from its caller context . (el tipo de retorno debe ser el mismo que el de la persona que llama)
  • Opcionalmente, toma if antes de { , en el ejemplo anterior es if err != nil .
  • Debe ser delegado por la persona que llama con delegate palabra clave

Es posible que pueda omitir delegate para delegar, pero creo que dificulta la lectura del flujo de la función.

Y quizás sea útil no solo para el manejo de errores, aunque no estoy seguro ahora.

Es hermoso agregar cheque , pero puede hacer más antes de regresar:

result, err := openFile(f);
if err != nil {
        log.Println(..., err)
    return 0, err 
}

se convierte en

result, err := openFile(f);
check err

`` Ir
resultado, err: = openFile (f);
check err {
log.Println (..., err)
}

```Go
reslt, _ := check openFile(f)
// If err is not nil direct return, does not execute the next step.

`` Ir
resultado, err: = check openFile (f) {
log.Println (..., err)
}

It also attempts simplifying the error handling (#26712):
```Go
result, err := openFile(f);
check !err {
    // err is an interface with value nil or holds a nil pointer
    // it is unusable
    result.C...()
}

También intenta simplificar (algunos consideran tedioso) el manejo de errores (# 21161). Se convertiría en:

result, err := openFile(f);
check err {
   // handle error and return
    log.Println(..., err)
}

Por supuesto, puede usar try y otras palabras clave en lugar de check , si está más claro.

reslt, _ := try openFile(f)
// If err is not nil direct return, does not execute the next step.

`` Ir
resultado, err: = openFile (f);
intente err {
// manejar el error y regresar
log.Println (..., err)
}

Reference:

A plain idea, with support for error decoration, but requiring a more drastic language change (obviously not for go1.10) is the introduction of a new check keyword.

It would have two forms: check A and check A, B.

Both A and B need to be error. The second form would only be used when error-decorating; people that do not need or wish to decorate their errors will use the simpler form.

1st form (check A)
check A evaluates A. If nil, it does nothing. If not nil, check acts like a return {<zero>}*, A.

Examples

If a function just returns an error, it can be used inline with check, so
```Go
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

se convierte en

check UpdateDB()

Para una función con múltiples valores de retorno, deberá asignar, como lo hacemos ahora.

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

se convierte en

a, b, err := Foo()
check err

// use a and b

2da forma (marque A, B)
comprobar A, B evalúa A. Si es nulo, no hace nada. Si no es nulo, el cheque actúa como una devolución {}*, B.

Esto es para necesidades de decoración de errores. Seguimos comprobando A, pero es B lo que se utiliza en la devolución implícita.

Ejemplo

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

se convierte en

a, err := Foo()
check err, &BarError{"Bar", err}

Notas
Es un error de compilación

use la declaración de verificación en cosas que no se evalúan como error
use check en una función con valores de retorno que no tienen el formato {type} *, error
La verificación de forma de dos expr A, B está en cortocircuito. B no se evalúa si A es nulo.

Notas sobre practicidad
Hay soporte para decorar errores, pero usted paga por la sintaxis A, B del cheque más torpe solo cuando realmente necesita decorar errores.

Para el texto estándar de if err != nil { return nil, nil, err } (que es muy común), la comprobación de err es tan breve como podría ser sin sacrificar la claridad (consulte la nota sobre la sintaxis a continuación).

Notas sobre la sintaxis
Yo diría que este tipo de sintaxis (comprobar ..., al principio de la línea, similar a un retorno) es una buena manera de eliminar el código repetitivo de verificación de errores sin ocultar la interrupción del flujo de control que introducen los retornos implícitos.

Una desventaja de ideas como la||ycapturaarriba, o el a, b = foo ()? propuesto en otro hilo, es que ocultan la modificación del flujo de control de una manera que hace que el flujo sea más difícil de seguir; el primero con ||maquinaria adjunta al final de una línea de apariencia simple, esta última con un pequeño símbolo que puede aparecer en todas partes, incluso en el medio y al final de una línea de código de apariencia simple, posiblemente varias veces.

Una declaración de verificación siempre será de nivel superior en el bloque actual, teniendo la misma prominencia de otras declaraciones que modifican el flujo de control (por ejemplo, una devolución anticipada).

He aquí otro pensamiento.

Imagine una declaración again que define una macro con una etiqueta. La declaración de declaración que etiqueta se puede expandir nuevamente mediante sustitución textual más adelante en la función (que recuerda a const / iota, con matices de goto: -]).

Por ejemplo:

func(foo int) (int, error) {
    err := f(foo)
again check:
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    check
    x, err := h()
    check
    return x, nil
}

sería exactamente equivalente a:

func(foo int) (int, error) {
    err := f(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    x, err := h()
    if err != nil {
        return 0, errors.Wrap(err)
    }
    return x, nil
}

Tenga en cuenta que la expansión de la macro no tiene argumentos; esto significa que debería haber menos confusión sobre el hecho de que es una macro, porque al compilador no le gustan los símbolos por sí mismos .

Al igual que la instrucción goto, el alcance de la etiqueta está dentro de la función actual.

Idea interesante. Me gustó la idea de la etiqueta de captura, pero no creo que encajara bien con los alcances de Go (con Go actual, no se puede goto una etiqueta con nuevas variables definidas en su alcance). La idea again soluciona ese problema porque la etiqueta viene antes de que se introduzcan nuevos ámbitos.

Aquí está el megaejemplo nuevamente:

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
    catch {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, try err = net.Listen("tcp4", listenAddr)

    conn, try err = ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

Aquí hay una versión más cercana a la propuesta de Rog (no me gusta tanto):

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
again:
    if err != nil {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, err = net.Listen("tcp4", listenAddr)
    check

    conn, err = ConnectionManager{}.connect(server, tlsConfig)
    check

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    check

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    check

    session, err := communicationProtocol.FinalProtocol(conn)
    check
    client.session = session

    return client, nil
}

@carlmjohnson Para que conste, eso no es exactamente lo que estoy sugiriendo. El identificador "comprobar" no es especial; debe declararse colocándolo después de la palabra clave "nuevamente".

Además, sugeriría que el ejemplo anterior no ilustra muy bien su uso: no hay nada en la declaración etiquetada de nuevo que no se pueda hacer tan bien en una declaración diferida. En el ejemplo try / catch, ese código no puede (por ejemplo) envolver el error con información sobre la ubicación de origen de la devolución del error. Tampoco funcionará AFAICS si agrega un "intento" dentro de una de esas declaraciones if (por ejemplo, para verificar el error devuelto por GetRuntimeEnvironment), porque el "err" al que se refiere la declaración catch está en un alcance diferente al de ese declarado dentro del bloque.

Creo que mi único problema con una palabra clave check es que todas las salidas a una función deben ser return (o al menos tener _alguna_ connotación "Voy a dejar la función"). _Podría_ obtener become (para TCO), al menos become tiene algún tipo de "Nos estamos convirtiendo en una función diferente" ... pero la palabra "comprobar" realmente no suena como será una salida para la función.

El punto de salida de una función es extremadamente importante y no estoy seguro de si check realmente tiene esa sensación de "punto de salida". Aparte de eso, me gusta mucho la idea de lo que hace check , permite un manejo de errores mucho más compacto, pero aún permite manejar cada error de manera diferente, o envolver el error como desee.

¿Puedo agregar una sugerencia también?
¿Qué pasa con algo como esto:

func Open(filename string) os.File onerror (string, error) {
       f, e := os.Open(filename)
       if e != nil { 
              fail "some reason", e // instead of return keyword to go on the onerror 
       }
      return f
}

f := Open(somefile) onerror reason, e {
      log.Prinln(reason)
      // try to recover from error and reasign 'f' on success
      nf = os.Create(somefile) onerror err {
             panic(err)
      }
      return nf // return here must return whatever Open returns
}

La asignación de error puede tener cualquier forma, incluso algo estúpido como

f := Open(name) =: e

O devolver un conjunto diferente de valores en caso de error, no solo errores, y también sería bueno un bloque try catch.

try {
    f := Open("file1") // can fail here
    defer f.Close()
    f1 := Open("file2") // can fail here
    defer f1.Close()
    // do something with the files
} onerror err {
     log.Println(err)
}

@cthackers Personalmente creo que es muy bueno que los errores en Go no tengan un tratamiento especial. Son simplemente valores y creo que deberían seguir así.

Además, try-catch (y construcciones similares) es simplemente una mala construcción que fomenta las malas prácticas. Cada error debe ser manejado por separado, no manejado por algún manejador de errores "catch all".

https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md
esto es demasiado complicado.

mi idea: |err| significa error de verificación: if err! = nil {}

// common util func
func parseInt(s string) (i int64, err error){
    return strconv.ParseInt(s, 10, 64)
}

// expression check err 1 : check and use err variable
func parseAndSum(a string ,b string) (int64,error) {
    sum := parseInt(a) + parseInt(b)  |err| return 0,err
    return sum,nil
} 

// expression check err 2 : unuse variable 
func parseAndSum(a string , b string) (int64,error) {
    a,err := parseInt(a) |_| return 0, fmt.Errorf("parseInt error: %s", a)
    b,err := parseInt(b) |_| { println(b); return 0,fmt.Errorf("parseInt error: %s", b);}
    return a+b,nil
} 

// block check err 
func parseAndSum(a string , b string) (  int64,  error) {
    {
      a := parseInt(a)  
      b := parseInt(b)  
      return a+b,nil
    }|err| return 0,err
} 

@ chen56 y todos los futuros comentaristas: consulte https://go.googlesource.com/proposal/+/master/design/go2draft.md .

Sospecho que esto hace obsoleto este hilo ahora y no tiene mucho sentido continuar aquí. La página de comentarios de Wiki es donde probablemente deberían ir las cosas en el futuro.

El megaejemplo usando la propuesta Go 2:

func NewClient(...) (*Client, error) {
    var (
        listener net.Listener
        conn     net.Conn
    )
    handle err {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener = check net.Listen("tcp4", listenAddr)

    conn = check ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    check toServer.Send(&client.serverConfig)

    check toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session := check communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

Creo que esto es lo más limpio que podemos esperar. El bloque handle tiene las buenas cualidades de la etiqueta again o la palabra clave rescue Ruby. Las únicas preguntas que quedan en mi mente son si debo usar puntuación o una palabra clave (creo que palabra clave) y si permitir el error sin devolverlo.

Estoy tratando de entender la propuesta: parece que solo hay un bloque de control por función, en lugar de la capacidad de crear diferentes respuestas a diferentes errores a lo largo de los procesos de ejecución de la función. Esto parece una verdadera debilidad.

También me pregunto si estamos pasando por alto la necesidad crítica de desarrollar arneses de prueba en nuestros sistemas. Teniendo en cuenta cómo vamos a ejercitar las rutas de error durante las pruebas debería ser parte de la discusión, pero yo tampoco veo eso,

@sdwarwick No creo que este sea el mejor lugar para discutir el borrador del diseño descrito en https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md . Un mejor enfoque es agregar un enlace a un artículo en la página wiki en https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback .

Dicho esto, ese borrador de diseño permite múltiples bloques de manijas en una función.

Este tema comenzó como una propuesta específica. No vamos a adoptar esa propuesta. Ha habido una gran discusión sobre este tema, y ​​espero que la gente saque las buenas ideas en propuestas separadas y en la discusión sobre el borrador de diseño reciente. Voy a cerrar este problema. Gracias por toda la discusión.

Si hablar en el conjunto de estos ejemplos:

r, err := os.Open(src)
    if err != nil {
        return err
    }

Que me gustaría escribir en una línea aproximadamente así:

r, err := os.Open(src) try ("blah-blah: %v", err)

En lugar de "intentar", ponga una palabra bonita y adecuada.

Con tal sintaxis, el error volvería y el resto serían algunos valores predeterminados dependiendo del tipo. Si necesito regresar junto con un error y algo más específico, en lugar de predeterminado, entonces nadie cancela la opción clásica más multilínea.

Incluso más brevemente (sin agregar algún tipo de manejo de errores):

r, err := os.Open(src) try

)
PD Disculpe mi inglés))

Mi variante:

func CopyFile(src, dst string) string, error {
    r := check os.Open(src) // return nil, error
    defer r.Close()

    // if error: run 1 defer and retun error message
    w := check os.Create(dst) // return nil, error
    defer w.Close()

    // if error: run 2, 1 defer and retun error message
    if check io.Copy(w, r) // return nil, error

}

func StartCopyFile() error {
  res := check CopyFile("1.txt", "2.txt")

  return nil
}

func main() {
  err := StartCopyFile()
  if err!= nil{
    fmt.printLn(err)
  }
}

Hola,

Tengo una idea simple, que se basa vagamente en cómo funciona el manejo de errores en el shell, al igual que la propuesta inicial. En el shell, los errores se comunican mediante valores de retorno que no son iguales a cero. El valor de retorno del último comando / llamada se almacena en $? en la cáscara. Además del nombre de variable dado por el usuario, podríamos almacenar automáticamente el valor de error de la última llamada en una variable predefinida y hacer que se pueda verificar mediante una sintaxis predefinida. He elegido ? como sintaxis para hacer referencia al último valor de error, que se ha devuelto desde una llamada de función en el ámbito actual. He elegido ! como una abreviatura de si? ! = nulo {}. ¿La elección para? está influenciado por el caparazón, sino también porque parece tener sentido. Si ocurre un error, naturalmente está interesado en lo que sucedió. Esto plantea una pregunta. ? es el signo común de una pregunta planteada y, por lo tanto, lo usamos para hacer referencia al último valor de error que se generó en el mismo alcance.
! se utiliza como abreviatura de si? ! = nil, porque significa que se debe prestar atención en caso de que algo salga mal. ! significa: si algo salió mal, haz esto. ? hace referencia al último valor de error. Como de costumbre, el valor de? es igual a cero si no hubo error.

val, err := someFunc(param)
! { return &SpecialError("someFunc", param, ?) }

Para hacer la sintaxis más atractiva, permitiría colocar el! línea directamente detrás de la llamada, además de omitir las llaves.
Con esta propuesta también podría manejar errores sin usar un identificador definido por el programador.

Esto estaría permitido:

val, _ := someFunc(param)
! return &SpecialError("someFunc", param, ?)

Esto estaría permitido

val, _ := someFunc(param) ! return &SpecialError("someFunc", param, ?)

Según esta propuesta, no tiene que regresar de la función cuando ocurre un error
y en su lugar puede intentar recuperarse del error.

val, _ := someFunc(param)
! {
val, _ := someFunc(paramAlternative)
  !{ return &SpecialError("someFunc alternative try failed too", paramAlternative, ?) }}

¡Bajo esta propuesta podrías usar! en un bucle for para múltiples reintentos como este.

val, _ := someFunc(param)
for i :=0; ! && i <5; i++ {
  // Sleep or make a change or both
  val, _ := someFunc(param)
} ! { return &SpecialError("someFunc", param, ? }

¡Soy consciente de eso! se utiliza principalmente para la negación de expresiones, por lo que la sintaxis propuesta podría causar confusión en los no iniciados. ¡La idea es esa! por sí mismo se expande a? ! = nil cuando se usa en una expresión condicional en un caso como lo demuestra el ejemplo superior, donde no se adjunta a ninguna expresión específica. La línea superior para no se puede compilar con el go actual, porque no tiene ningún sentido sin contexto. ¡Bajo esta propuesta! por sí mismo es verdadero, cuando se ha producido un error en la llamada de función más reciente, que puede devolver un error.

La declaración de retorno para devolver el error se mantiene, porque como otros comentaron aquí, es deseable ver de un vistazo dónde regresa su función. Puede utilizar esta sintaxis en un escenario en el que un error no requiera que abandone la función.

Esta propuesta es más simple que algunas otras propuestas, ya que no hay ningún esfuerzo para crear una variante de la sintaxis similar al bloque try and catch conocida en otros lenguajes. Mantiene la filosofía actual de go de manejar los errores directamente donde ocurren y hace que sea más conciso hacerlo.

@tobimensch, por favor, publique nuevas sugerencias en un wiki de comentarios sobre manejo de errores de Go 2 . Es posible que se pasen por alto las publicaciones sobre este tema cerrado.

Si no lo ha visto, le recomendamos que lea el Borrador de diseño de manejo de errores de Go 2 .

Y es posible que le interesen los requisitos a tener en cuenta para la gestión de errores de Go 2 .

Puede que sea un poco tarde para señalarlo, pero cualquier cosa que se sienta como magia de JavaScript me molesta. Estoy hablando del operador || que de alguna manera debería funcionar mágicamente con un error intedface. No me gusta

¿Fue útil esta página
0 / 5 - 0 calificaciones