Go: Propuesta: una función integrada de comprobación de errores de Go, "probar"

Creado en 5 jun. 2019  ·  808Comentarios  ·  Fuente: golang/go

Propuesta: una función integrada de comprobación de errores de Go, try

Esta propuesta ha sido cerrada .

Antes de comentar, lea el documento de diseño detallado y vea el resumen de discusión del 6 de junio , el resumen del 10 de junio y, lo que es más importante, los consejos para mantenerse enfocado . Es posible que su pregunta o sugerencia ya haya sido respondida o realizada. Gracias.

Proponemos una nueva función integrada llamada try , diseñada específicamente para eliminar las declaraciones repetitivas if típicamente asociadas con el manejo de errores en Go. No se sugieren otros cambios de idioma. Recomendamos el uso de la instrucción defer existente y las funciones de biblioteca estándar para ayudar a aumentar o envolver los errores. Este enfoque mínimo aborda los escenarios más comunes y agrega muy poca complejidad al lenguaje. El try incorporado es fácil de explicar, sencillo de implementar, ortogonal a otras construcciones del lenguaje y totalmente compatible con versiones anteriores. También deja abierta la posibilidad de ampliar el mecanismo, en caso de que deseemos hacerlo en el futuro.

[El texto a continuación se ha editado para reflejar el documento de diseño con mayor precisión.]

La función incorporada try toma una sola expresión como argumento. La expresión debe evaluarse en n+1 valores (donde n puede ser cero) donde el último valor debe ser del tipo error . Devuelve los primeros n valores (si los hay) si el argumento de error (final) es nulo; de lo contrario, vuelve de la función adjunta con ese error. Por ejemplo, código como

f, err := os.Open(filename)
if err != nil {
    return …, err  // zero values for other results, if any
}

se puede simplificar a

f := try(os.Open(filename))

try solo se puede usar en una función que a su vez devuelve un resultado error , y ese resultado debe ser el último parámetro de resultado de la función que lo encierra.

Esta propuesta reduce el borrador del diseño original presentado en la GopherCon del año pasado a su esencia. Si se desea aumentar o ajustar el error, hay dos enfoques: apéguese a la declaración comprobada if o, alternativamente, "declare" un controlador de errores con una declaración defer :

defer func() {
    if err != nil { // no error may have occurred - check for it
        err = … // wrap/augment error
    }
}()

Aquí, err es el nombre del resultado de error de la función envolvente. En la práctica, las funciones auxiliares adecuadas reducirán la declaración de un controlador de errores a una sola línea. Por ejemplo

defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)

(donde fmt.HandleErrorf decora *err ) se lee bien y se puede implementar sin necesidad de nuevas funciones de lenguaje.

El principal inconveniente de este enfoque es que el parámetro de resultado del error debe nombrarse, lo que posiblemente genere API menos bonitas. En última instancia, se trata de una cuestión de estilo, y creemos que nos adaptaremos a esperar el nuevo estilo, tanto como nos adaptamos a no tener punto y coma.

En resumen, try puede parecer inusual al principio, pero es simplemente azúcar sintáctico hecho a medida para una tarea específica, manejo de errores con menos repeticiones y para manejar esa tarea lo suficientemente bien. Como tal, encaja muy bien en la filosofía de Go. try no está diseñado para abordar _todas_ las situaciones de manejo de errores; está diseñado para manejar bien el caso _más común_, para mantener el diseño simple y claro.

Créditos

Esta propuesta está fuertemente influenciada por los comentarios que hemos recibido hasta ahora. Específicamente, toma prestadas ideas de:

documento de diseño detallado

https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

tryhard herramienta para explorar el impacto de try

https://github.com/griesemer/tryhard

Go2 LanguageChange Proposal error-handling

Comentario más útil

Hola a todos,

Nuestro objetivo con propuestas como esta es tener una discusión en toda la comunidad sobre las implicaciones, las compensaciones y cómo proceder, y luego usar esa discusión para ayudar a decidir el camino a seguir.

En base a la abrumadora respuesta de la comunidad y la extensa discusión aquí, marcamos esta propuesta como rechazada antes de lo previsto .

En cuanto a los comentarios técnicos, esta discusión ha identificado de manera útil algunas consideraciones importantes que pasamos por alto, sobre todo las implicaciones para agregar impresiones de depuración y analizar la cobertura del código.

Más importante aún, hemos escuchado claramente a muchas personas que argumentaron que esta propuesta no estaba enfocada en un problema que valiera la pena. Todavía creemos que el manejo de errores en Go no es perfecto y se puede mejorar significativamente, pero está claro que nosotros, como comunidad, debemos hablar más sobre qué aspectos específicos del manejo de errores son problemas que debemos abordar.

En cuanto a discutir el problema a resolver, tratamos de exponer nuestra visión del problema en agosto pasado en la " Resumen del problema de manejo de errores de Go 2 ", pero en retrospectiva no llamamos suficiente atención a esa parte y no alentamos lo suficiente. discusión sobre si el problema específico era el correcto. La propuesta try puede ser una buena solución al problema descrito allí, pero para muchos de ustedes simplemente no es un problema que resolver. En el futuro, debemos hacer un mejor trabajo llamando la atención sobre estas primeras declaraciones de problemas y asegurándonos de que haya un acuerdo generalizado sobre el problema que debe resolverse.

(También es posible que la declaración del problema de manejo de errores se haya eclipsado por completo al publicar un borrador de diseño genérico el mismo día).

En el tema más amplio de qué mejorar en el manejo de errores de Go, nos encantaría ver informes de experiencia sobre qué aspectos del manejo de errores en Go son más problemáticos para usted en sus propias bases de código y entornos de trabajo y cuánto impacto tendría una buena solución. tener en su propio desarrollo. Si escribe un informe de este tipo, publique un enlace en la página Go2ErrorHandlingFeedback .

Gracias a todos los que participaron en esta discusión, aquí y en otros lugares. Como Russ Cox ha señalado anteriormente, las discusiones de toda la comunidad como esta son de código abierto en su máxima expresión . Realmente apreciamos la ayuda de todos para examinar esta propuesta específica y, de manera más general, para analizar las mejores formas de mejorar el estado del manejo de errores en Go.

Robert Griesemer, por el Comité de Revisión de Propuestas.

Todos 808 comentarios

Estoy de acuerdo en que esta es la mejor manera de avanzar: solucionar el problema más común con un diseño simple.

No quiero hacer un cobertizo (siéntete libre de posponer esta conversación), pero Rust fue allí y finalmente se decidió por el operador de postfijo ? en lugar de una función integrada, para una mayor legibilidad.

La propuesta de gophercon cita ? en las ideas consideradas y da tres razones por las que se descartó: la primera ("las transferencias de flujo de control son, por regla general, acompañadas de palabras clave") y la tercera ("los controladores se definen de forma más natural con una palabra clave, por lo que los cheques también deberían) ya no se aplican. El segundo es estilístico: dice que, incluso si el operador postfijo funciona mejor para encadenar, aún puede leerse peor en algunos casos como:

check io.Copy(w, check newReader(foo))

en vez de:

io.Copy(w, newReader(foo)?)?

pero ahora tendríamos:

try(io.Copy(w, try(newReader(foo))))

que creo que es claramente el peor de los tres, ya que ni siquiera es obvio cuál es la función principal que se llama.

Entonces, la esencia de mi comentario es que las tres razones citadas en la propuesta de gophercon para no usar ? no se aplican a esta propuesta try ; ? es conciso, muy legible, no oscurece la estructura de la declaración (con su jerarquía de llamada de función interna) y se puede encadenar. Elimina aún más el desorden de la vista, sin oscurecer el flujo de control más de lo que ya lo hace el try() propuesto.

Para aclarar:

Lo hace

func f() (n int, err error) {
  n = 7
  try(errors.New("x"))
  // ...
}

devolver (0, "x") o (7, "x")? Yo asumiría lo último.

¿Se debe nombrar el retorno de error en el caso de que no haya decoración o manejo (como en una función auxiliar interna)? Supongo que no.

Su ejemplo devuelve 7, errors.New("x") . Esto debería quedar claro en el documento completo que se enviará pronto (https://golang.org/cl/180557).

No es necesario nombrar el parámetro de resultado de error para usar try . Solo necesita nombrarse si la función necesita hacer referencia a ella en una función diferida o en otro lugar.

Estoy realmente descontento con una _función_ incorporada que afecta el flujo de control de la persona que llama. Aprecio la imposibilidad de agregar nuevas palabras clave en Go 1, pero solucionar ese problema con funciones mágicas integradas me parece incorrecto. El sombreado de otras funciones integradas no tiene resultados tan impredecibles como el cambio de flujo de control.

No me gusta la apariencia de postfix ? , pero creo que aún supera a try() .

Editar: Bueno, logré olvidar por completo que el pánico existe y no es una palabra clave.

La propuesta detallada ya está aquí (en espera de mejoras de formato, próximamente) y se espera que responda muchas preguntas.

@dominikh La propuesta detallada analiza esto en detalle, pero tenga en cuenta que panic y recover son dos funciones integradas que también afectan el flujo de control.

Una aclaración/sugerencia de mejora:

if the last argument supplied to try, of type error, is not nil, the enclosing function’s error result variable (...) is set to that non-nil error value before the enclosing function returns

¿Podría esto decir is set to that non-nil error value and the enclosing function returns ? (s/antes/y)

En la primera lectura, before the enclosing function returns parecía que _eventualmente_ establecería el valor de error en algún momento en el futuro justo antes de que regresara la función, posiblemente en una línea posterior. La interpretación correcta es que try puede hacer que la función actual regrese. Ese es un comportamiento sorprendente para el lenguaje actual, por lo que un texto más claro sería bienvenido.

Creo que esto es solo azúcar, y un pequeño número de oponentes vocales se burlaron de golang sobre el uso repetido de escribir if err != nil ... y alguien se lo tomó en serio. No creo que sea un problema. Las únicas cosas que faltan son estos dos incorporados:

https://github.com/purpleidea/mgmt/blob/a235b760dc3047a0d66bb0b9d63c25bc746ed274/util/errwrap/errwrap.go#L26

No estoy seguro de por qué alguien escribiría una función como esta, pero cuál sería el resultado previsto para

try(foobar())

Si foobar devuelve (error, error)

Me retracto de mis preocupaciones anteriores sobre el flujo de control y ya no sugiero usar ? . Pido disculpas por la respuesta instintiva (aunque me gustaría señalar que esto no habría sucedido si el problema se hubiera presentado _después_ de que la propuesta completa estuviera disponible).

No estoy de acuerdo con la necesidad de un manejo de errores simplificado, pero estoy seguro de que es una batalla perdida. try como se establece en la propuesta parece ser la forma menos mala de hacerlo.

@webermaster Solo el último resultado error es especial para la expresión pasada a try , como se describe en el documento de la propuesta.

Al igual que @dominikh , tampoco estoy de acuerdo con la necesidad de un manejo de errores simplificado.

Mueve la complejidad vertical a la complejidad horizontal, lo que rara vez es una buena idea.

Sin embargo, si tuviera que elegir absolutamente entre simplificar las propuestas de manejo de errores, esta sería mi propuesta preferida.

Sería útil si esto pudiera ir acompañado (en alguna etapa de aceptación) por una herramienta para transformar el código Go para usar try en algún subconjunto de funciones que devuelven errores donde dicha transformación se puede realizar fácilmente sin cambiando la semántica. Se me ocurren tres beneficios:

  • Al evaluar esta propuesta, permitiría a las personas tener una idea rápida de cómo se podría usar try en su base de código.
  • Si try llega a una versión futura de Go, es probable que la gente quiera cambiar su código para usarlo. Tener una herramienta para automatizar los casos fáciles ayudará mucho.
  • Tener una forma de transformar rápidamente una gran base de código para usar try facilitará el examen de los efectos de la implementación a escala. (La corrección, el rendimiento y el tamaño del código, por ejemplo). Sin embargo, la implementación puede ser lo suficientemente simple como para hacer que esto sea una consideración insignificante.

Solo me gustaría expresar que creo que un simple try(foo()) que realmente sale de la función de llamada nos quita la señal visual de que el flujo de la función puede cambiar según el resultado.

Siento que puedo trabajar con try si me acostumbro lo suficiente, pero también siento que necesitaremos soporte IDE adicional (o algo similar) para resaltar try para reconocer de manera eficiente el flujo implícito en las revisiones de código /sesiones de depuración

Lo que más me preocupa es la necesidad de tener valores de retorno con nombre solo para que la declaración diferida sea feliz.

Creo que el problema general de manejo de errores del que se queja la comunidad es una combinación del modelo de if err != nil Y agregar contexto a los errores. Las preguntas frecuentes establecen claramente que este último se omite intencionalmente como un problema separado, pero siento que esto se convierte en una solución incompleta, pero estaré dispuesto a darle una oportunidad después de pensar en estas 2 cosas:

  1. Declare err al comienzo de la función.
    ¿Esto funciona? Recuerdo problemas con resultados diferidos y sin nombre. Si no es así, la propuesta debe considerar esto.
func sample() (string, error) {
  var err error
  defer fmt.HandleErrorf(&err, "whatever")
  s := try(f())
  return s, nil
}
  1. Asigne valores como lo hicimos en el pasado, pero use una función auxiliar wrapf que tiene el modelo if err != nil .
func sample() (string, error) {
  s, err := f()
  try(wrapf(err, "whatever"))
  return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
  if err != nil {
    // err = wrapped error
  }
  return err
}

Si cualquiera funciona, puedo lidiar con eso.

func sample() (string, error) {
  var err error
  defer fmt.HandleErrorf(&err, "whatever")
  s := try(f())
  return s, nil
}

Esto no funcionará. El aplazamiento actualizará la variable local err , que no está relacionada con el valor devuelto.

func sample() (string, error) {
  s, err := f()
  try(wrapf(err, "whatever"))
  return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
  if err != nil {
    // err = wrapped error
  }
  return err
}

Eso debería funcionar. Sin embargo, llamará a wrapf incluso en un error nulo.
Esto también (continuará) funcionando, y en mi opinión es mucho más claro:

func sample() (string, error) {
  s, err := f()
  if err != nil {
      return "", wrap(err)
  }
  return s, nil
}

Nadie te obligará a usar try .

No estoy seguro de por qué alguien escribiría una función como esta, pero cuál sería el resultado previsto para

try(foobar())

Si foobar devuelve (error, error)

¿Por qué devolvería más de un error de una función? Si está devolviendo más de un error de la función, tal vez la función deba dividirse en dos en primer lugar, cada una devolviendo solo un error.

¿Podrías elaborar con un ejemplo?

@cespare : Debería ser posible que alguien escriba un go fix que reescriba el código existente adecuado para try modo que use try . Puede ser útil tener una idea de cómo se podría simplificar el código existente. No esperamos cambios significativos en el tamaño o el rendimiento del código, ya que try es solo azúcar sintáctico, reemplazando un patrón común por una pieza más corta de código fuente que produce esencialmente el mismo código de salida. Tenga en cuenta también que el código que usa try estará obligado a usar una versión de Go que sea al menos la versión en la que se introdujo try .

@lestrrat : Estuvo de acuerdo en que uno tendrá que aprender que try puede cambiar el flujo de control. Sospechamos que los IDE podrían resaltar eso con bastante facilidad.

@Goodwine : Como @ randall77 ya señaló, su primera sugerencia no funcionará. Una opción en la que hemos pensado (pero no discutida en el documento) es la posibilidad de tener alguna variable predeclarada que denote el resultado error (si está presente en primer lugar). Eso eliminaría la necesidad de nombrar ese resultado solo para que pueda usarse en defer . Pero eso sería aún más mágico; no parece justificado. El problema de nombrar el resultado de la devolución es esencialmente cosmético, y donde más importa es en las API generadas automáticamente atendidas por go doc y sus amigos. Sería fácil abordar esto en esas herramientas (consulte también las preguntas frecuentes del documento de diseño detallado sobre este tema).

@nictuku : con respecto a su sugerencia de aclaración (s/before/and/): creo que el código inmediatamente antes del párrafo al que se refiere deja en claro qué sucede exactamente, pero veo su punto, s/before/and/ may aclarar la prosa. Haré el cambio.

Ver CL 180637 .

La verdad es que me gusta mucho esta propuesta. Sin embargo, tengo una crítica. El punto de salida de las funciones en Go siempre ha estado marcado por return . Los pánicos también son puntos de salida, sin embargo, esos son errores catastróficos que normalmente no deben encontrarse nunca.

Hacer un punto de salida de una función que no es un return , y está destinado a ser un lugar común, puede conducir a un código mucho menos legible. Escuché sobre esto en una charla y es difícil no ver la belleza de cómo está estructurado este código:

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    defer r.Close()

    w, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    if _, err := io.Copy(w, r); err != nil {
        w.Close()
        os.Remove(dst)
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    if err := w.Close(); err != nil {
        os.Remove(dst)
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
}

Este código puede parecer un gran lío, y fue _destinado_ por el borrador de manejo de errores, pero comparémoslo con lo mismo con try .

func CopyFile(src, dst string) error {
    defer func() {
        err = fmt.Errorf("copy %s %s: %v", src, dst, err)
    }()
    r, err := try(os.Open(src))
    defer r.Close()

    w, err := try(os.Create(dst))

    defer w.Close()
    defer os.Remove(dst)
    try(io.Copy(w, r))
    try(w.Close())

    return nil
}

Puede ver esto a primera vista y pensar que se ve mejor, porque hay mucho menos código repetido. Sin embargo, fue muy fácil detectar todos los puntos que devolvió la función en el primer ejemplo. Todos tenían sangría y comenzaban con return , seguidos de un espacio. Esto se debe al hecho de que todas las devoluciones condicionales _deben_ estar dentro de bloques condicionales, por lo que se sangran según los estándares gofmt . return también es, como se indicó anteriormente, la única forma de salir de una función sin decir que ocurrió un error catastrófico. En el segundo ejemplo, solo hay un único return , por lo que parece que lo único que la función _ever_ debería devolver es nil . Las últimas dos llamadas try son fáciles de ver, pero las dos primeras son un poco más difíciles, y lo serían aún más si estuvieran anidadas en algún lugar, es decir, algo como proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1])))) .

Regresar de una función parece haber sido algo "sagrado", por lo que personalmente creo que todos los puntos de salida de una función deben estar marcados con return .

Alguien ya implementó esto hace 5 años. Si estás interesado, puedes
prueba esta característica

https://noticias.ycombinator.com/item?id=20101417

Implementé try() en Go hace cinco años con un preprocesador AST y lo usé en proyectos reales, fue bastante bueno: https://github.com/lunixbochs/og

Aquí hay algunos ejemplos de mí utilizándolo en funciones de verificación de errores: https://github.com/lunixbochs/poxd/blob/master/tls.go#L13

Agradezco el esfuerzo que se hizo en esto. Creo que es la solución más go-ey que he visto hasta ahora. Pero creo que introduce un montón de trabajo al depurar. Desenvolver el intento y agregar un bloque if cada vez que depuro y volver a envolverlo cuando termino es tedioso. Y también tengo algo de vergüenza por la variable de error mágico que debo considerar. Nunca me ha molestado la comprobación explícita de errores, así que tal vez no sea la persona adecuada para preguntar. Siempre me pareció "listo para depurar".

@griesemer
Mi problema con el uso propuesto de defer como una forma de manejar el ajuste de errores es que el comportamiento del fragmento que mostré (repetido a continuación) no es AFAICT muy común, y debido a que es muy raro, puedo imaginar a las personas escribiendo esto pensando que funciona cuando no lo hace

Como ... un principiante no sabría esto, si tiene un error debido a esto, no dirá "por supuesto, necesito un retorno con nombre", se estresaría porque debería funcionar y no lo hace.

var err error
defer fmt.HandleErrorf(err);

try ya es demasiado mágico, por lo que también puede ir hasta el final y agregar ese valor de error implícito. Piensa en los principiantes, no en aquellos que conocen todos los matices del Go. Si no está lo suficientemente claro, no creo que sea la solución correcta.

O... No sugiera usar diferir de esta manera, intente otra forma que sea más segura pero aún legible.

@deanveloper Es cierto que esta propuesta (y, de hecho, cualquier propuesta que intente hacer lo mismo) eliminará las declaraciones return explícitamente visibles del código fuente; después de todo, ese es el objetivo de la propuesta, ¿no es así? Para eliminar el modelo de declaraciones if y returns que son todas iguales. Si desea conservar los return , no use try .

Estamos acostumbrados a reconocer inmediatamente las sentencias return (y las panic ) porque así es como se expresa este tipo de flujo de control en Go (y muchos otros lenguajes). No parece descabellado que también reconozcamos try como un flujo de control cambiante después de acostumbrarnos, tal como lo hacemos con return . No tengo ninguna duda de que un buen soporte IDE ayudará con esto también.

Tengo dos preocupaciones:

  • las devoluciones con nombre han sido muy confusas, y esto las alienta con un caso de uso nuevo e importante
  • esto desalentará agregar contexto a los errores

En mi experiencia, agregar contexto a los errores inmediatamente después de cada sitio de llamada es fundamental para tener un código que se pueda depurar fácilmente. Y las devoluciones con nombre han causado confusión para casi todos los desarrolladores de Go que conozco en algún momento.

Una preocupación estilística menor es que es desafortunado cuántas líneas de código ahora se incluirán en try(actualThing()) . Puedo imaginar ver la mayoría de las líneas en un código base envuelto en try() . Eso se siente desafortunado.

Creo que estas preocupaciones se abordarían con un ajuste:

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

check() se comportaría de manera muy similar a try() , pero eliminaría el comportamiento de pasar los valores de retorno de la función de forma genérica y, en su lugar, proporcionaría la capacidad de agregar contexto. Todavía desencadenaría un regreso.

Esto conservaría muchas de las ventajas de try() :

  • es un incorporado
  • sigue el flujo de control existente WRT para diferir
  • se alinea bien con la práctica existente de agregar contexto a los errores
  • se alinea con las propuestas y bibliotecas actuales para corregir errores, como errors.Wrap(err, "context message")
  • da como resultado un sitio de llamada limpio: no hay texto estándar en la línea a, b, err := myFunc()
  • Todavía es posible describir errores con defer fmt.HandleError(&err, "msg") , pero no es necesario alentarlo.
  • la firma de check es un poco más simple, porque no necesita devolver un número arbitrario de argumentos de la función que está envolviendo.

@ s4n-gt Gracias por este enlace. Yo no era consciente de ello.

@Goodwine Punto tomado. La razón por la que no se proporciona soporte de manejo de errores más directo se analiza en detalle en el documento de diseño. También es un hecho que en el transcurso de un año más o menos (desde los diseños preliminares publicados en el Gophercon del año pasado) no ha surgido ninguna solución satisfactoria para el manejo explícito de errores. Es por eso que esta propuesta omite esto a propósito (y en su lugar sugiere usar un defer ). Esta propuesta aún deja la puerta abierta para futuras mejoras en ese sentido.

La propuesta menciona cambiar las pruebas de paquetes para permitir que las pruebas y los puntos de referencia devuelvan un error. Aunque no sería "un cambio de biblioteca modesto", también podríamos considerar aceptar func main() error . Sería mucho más agradable escribir pequeños guiones. La semántica sería equivalente a:

func main() {
  if err := newmain(); err != nil {
    println(err.Error())
    os.Exit(1)
  }
}

Una última crítica. No es realmente una crítica a la propuesta en sí, sino una crítica a una respuesta común al contraargumento de la "función que controla el flujo".

La respuesta a "No me gusta que una función controle el flujo" es que "¡ panic también controla el flujo del programa!". Sin embargo, hay algunas razones por las que está bien que panic haga esto que no se aplican a try .

  1. panic es amigable para los programadores principiantes porque lo que hace es intuitivo, continúa desenvolviendo la pila. Ni siquiera debería tener que buscar cómo funciona panic para entender lo que hace. Los programadores principiantes ni siquiera necesitan preocuparse por recover , ya que los principiantes no suelen construir mecanismos de recuperación de pánico, especialmente porque casi siempre son menos favorables que simplemente evitar el pánico en primer lugar.

  2. panic es un nombre fácil de ver. Trae preocupación, y necesita hacerlo. Si uno ve panic en un código base, debería pensar inmediatamente en cómo _evitar_ el pánico, incluso si es trivial.

  3. Aprovechando el último punto, panic no se puede anidar en una llamada, lo que lo hace aún más fácil de ver.

Está bien que panic controle el flujo del programa porque es extremadamente fácil de detectar y es intuitivo en cuanto a lo que hace.

La función try no satisface ninguno de estos puntos.

  1. Uno no puede adivinar lo que hace try sin consultar la documentación correspondiente. Muchos idiomas usan la palabra clave de diferentes maneras, lo que dificulta entender lo que significaría en Go.

  2. try no me llama la atención, especialmente cuando es una función. _Especialmente_ cuando el resaltado de sintaxis lo resaltará como una función. _ESPECIALMENTE_ después de desarrollar en un lenguaje como Java, donde try se considera un modelo innecesario (debido a las excepciones comprobadas).

  3. try se puede usar en un argumento para una llamada de función, según mi ejemplo en mi comentario anterior proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1])))) . Esto hace que sea aún más difícil de detectar.

Mis ojos ignoran las funciones try , incluso cuando las estoy buscando específicamente. Mis ojos los verán, pero inmediatamente pasarán a las llamadas os.FindProcess o strconv.Atoi . try es una devolución condicional. El flujo de control Y los retornos se sostienen sobre pedestales en Go. Todo el flujo de control dentro de una función está sangrado y todos los retornos comienzan con return . La combinación de estos dos conceptos en una llamada de función fácil de perder se siente un poco fuera de lugar.


Sin embargo, este comentario y el último son mis únicas críticas reales a la idea. Creo que puede parecer que no me gusta esta propuesta, pero sigo pensando que es una victoria general para Go. Esta solución todavía se siente más parecida a Go que las otras soluciones. Si se agregara esto, estaría feliz, sin embargo, creo que aún se puede mejorar, pero no estoy seguro de cómo.

@buchanae interesante. Sin embargo, tal como está escrito, mueve el formato de estilo fmt de un paquete al lenguaje mismo, lo que abre una lata de gusanos.

Sin embargo, tal como está escrito, mueve el formato de estilo fmt de un paquete al lenguaje mismo, lo que abre una lata de gusanos.

Buen punto. Un ejemplo más simple:

a, b, err := myFunc()
check(err, "calling myFunc")

@buchanae Hemos considerado hacer que el manejo explícito de errores esté más directamente relacionado con try ; consulte el documento de diseño detallado, específicamente la sección sobre iteraciones de diseño. Su sugerencia específica de check solo permitiría aumentar los errores a través de algo como una API similar a fmt.Errorf (como parte de check ), si entiendo correctamente. En general, la gente puede querer hacer todo tipo de cosas con errores, no solo crear uno nuevo que se refiera al original a través de su cadena de error.

Nuevamente, esta propuesta no intenta resolver todas las situaciones de manejo de errores. Sospecho que en la mayoría de los casos try tiene sentido para el código que ahora se ve básicamente así:

a, b, c, ... err := try(someFunctionCall())
if err != nil {
   return ..., err
}

Hay una gran cantidad de código que se parece a esto. Y no todos los fragmentos de código que se ven así necesitan más manejo de errores. Y donde defer no es correcto, todavía se puede usar una instrucción if .

No sigo esta línea:

defer fmt.HandleErrorf(&err, “foobar”)

Deja caer el error de entrada en el piso, lo cual es inusual. ¿Está destinado a ser usado algo más como esto?

defer fmt.HandleErrorf(&err, “foobar: %v”, err)

La duplicación de err es un poco tartamuda. En realidad, esto no se relaciona directamente con la propuesta, solo es un comentario adicional sobre el documento.

Comparto las dos preocupaciones planteadas por @buchanae , re: devoluciones nombradas y errores contextuales.

Considero que named devuelve un poco problemático tal como es; Creo que solo son realmente beneficiosos como documentación. Apoyarse en ellos más fuertemente es una preocupación. Sin embargo, siento ser tan vago. Pensaré más en esto y proporcionaré algunos pensamientos más concretos.

Creo que existe una preocupación real de que las personas se esfuercen por estructurar su código para que se pueda usar try y, por lo tanto, evitar agregar contexto a los errores. Este es un momento particularmente extraño para presentar esto, dado que ahora estamos brindando mejores formas de agregar contexto a los errores a través de las funciones oficiales de ajuste de errores.

Creo que try según lo propuesto hace que algunos códigos sean mucho más agradables. Aquí hay una función que elegí más o menos al azar de la base de código de mi proyecto actual, con algunos de los nombres cambiados. Estoy particularmente impresionado por cómo funciona try cuando se asigna a campos de estructura. (¿Eso supone que mi lectura de la propuesta es correcta y que esto funciona?)

El código existente:

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        err := dbfile.RunMigrations(db, dbMigrations)
        if err != nil {
                return nil, err
        }
        t := &Thing{
                thingy: thingy,
        }
        t.scanner, err = newScanner(thingy, db, client)
        if err != nil {
                return nil, err
        }
        t.initOtherThing()
        return t, nil
}

Con try :

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        try(dbfile.RunMigrations(db, dbMigrations))
        t := &Thing{
                thingy:  thingy,
                scanner: try(newScanner(thingy, db, client)),
        }
        t.initOtherThing()
        return t, nil
}

Sin pérdida de legibilidad, excepto quizás que es menos obvio que newScanner podría fallar. Pero entonces, en un mundo con try Go, los programadores serían más sensibles a su presencia.

@josharian Con respecto a main que devuelve un error : me parece que su pequeña función auxiliar es todo lo que se necesita para obtener el mismo efecto. No estoy seguro de que se justifique cambiar la firma de main .

Con respecto al ejemplo de "foobar": es solo un mal ejemplo. Probablemente debería cambiarlo. Gracias por sacar el tema.

defer fmt.HandleErrorf(&err, “foobar: %v”, err)

En realidad, eso no puede ser correcto, porque err se evaluará demasiado pronto. Hay un par de formas de evitar esto, pero ninguna tan limpia como el HandleErrorf original (creo defectuoso). Creo que sería bueno tener uno o dos ejemplos prácticos más realistas de una función auxiliar.

EDITAR: este error de evaluación temprana está presente en un ejemplo
cerca del final del documento:

defer fmt.HandleErrorf(&err, "copy %s %s: %v", src, dst, err)

@adg Sí, try se puede usar como lo está usando en su ejemplo. Dejo que sus comentarios sobre: ​​las devoluciones nombradas permanezcan como están.

la gente puede querer hacer todo tipo de cosas con errores, no solo crear uno nuevo que se refiera al original a través de su cadena de error.

try no intenta manejar todos los tipos de cosas que la gente quiere hacer con los errores, solo aquellos que podemos encontrar una manera práctica de simplificar significativamente. Creo que mi ejemplo check sigue la misma línea.

En mi experiencia, la forma más común de código de manejo de errores es el código que esencialmente agrega un seguimiento de pila, a veces con contexto agregado. Descubrí que el seguimiento de la pila es muy importante para la depuración, donde sigo un mensaje de error a través del código.

Pero, ¿tal vez otras propuestas agregarán seguimientos de pila a todos los errores? He perdido la pista.

En el ejemplo que dio @adg , hay dos fallas potenciales pero no hay contexto. Si newScanner y RunMigrations no brindan mensajes que le indiquen cuál salió mal, entonces no sabe qué hacer.

En el ejemplo que dio @adg , hay dos fallas potenciales pero no hay contexto. Si newScanner y RunMigrations no brindan mensajes que le den una pista sobre cuál salió mal, entonces tiene que adivinar.

Así es, y esa es la elección de diseño que hicimos en este código en particular. Envolvemos muchos errores en otras partes del código.

Comparto la preocupación como @deanveloper y otros de que podría dificultar la depuración. Es cierto que podemos optar por no usarlo, pero los estilos de las dependencias de terceros no están bajo nuestro control.
Si el punto principal es if err := ... { return err } menos repetitivo, me pregunto si sería suficiente un "retorno condicional", como https://github.com/golang/go/issues/27794 propuesto.

        return nil, err if f, err := os.Open(...)
        return nil, err if _, err := os.Write(...)

Creo que ? encajaría mejor que try , y tener que perseguir siempre el defer por error también sería complicado.

Esto también cierra las puertas para tener excepciones usando try/catch para siempre.

Esto también cierra las puertas para tener excepciones usando try/catch para siempre.

Estoy _más_ que bien con esto.

Estoy de acuerdo con algunas de las preocupaciones planteadas anteriormente con respecto a agregar contexto a un error. Poco a poco estoy tratando de pasar de solo devolver un error a decorarlo siempre con un contexto y luego devolverlo. Con esta propuesta, tendré que cambiar por completo mi función para usar parámetros de retorno con nombre (lo cual me parece extraño porque apenas uso retornos desnudos).

Como dice @griesemer :

Nuevamente, esta propuesta no intenta resolver todas las situaciones de manejo de errores. Sospecho que en la mayoría de los casos, intentar tiene sentido para el código que ahora se ve básicamente así:
a, b, c, ... err := try(someFunctionCall())
si yerra != nil {
volver..., err
}
Hay una gran cantidad de código que se parece a esto. Y no todos los fragmentos de código que se ven así necesitan más manejo de errores. Y donde defer no es correcto, aún se puede usar una declaración if.

Sí, pero ¿no debería el buen código idiomático siempre envolver/decorar sus errores? Creo que es por eso que estamos introduciendo mecanismos refinados de manejo de errores para agregar errores de ajuste/contexto en stdlib. Como veo, esta propuesta solo parece considerar el caso de uso más básico.

Además, esta propuesta aborda solo el caso de envolver/decorar múltiples sitios de retorno de error posibles en un _lugar único_, utilizando parámetros con nombre con una llamada diferida.

Pero no hace nada en el caso de que se necesite agregar diferentes contextos a diferentes errores en una sola función. Por ejemplo, es muy esencial decorar los errores de la base de datos para obtener más información sobre su procedencia (suponiendo que no haya rastros de pila)

Este es un ejemplo de un código real que tengo:

func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    if err != nil {
        return err
    }
    var res int64
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("insert table: %w", err)
    }

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("insert table2: %w", err)
    }
    return tx.Commit()
}

Según la propuesta:

Si se desea aumentar o ajustar el error, hay dos enfoques: seguir con la declaración if probada y verdadera o, alternativamente, "declarar" un controlador de errores con una declaración diferida:

Creo que esto caerá en la categoría de "seguir con la declaración if probada y verdadera". Espero que la propuesta se pueda mejorar para abordar esto también.

Sugiero encarecidamente que el equipo de Go priorice los genéricos , ya que es donde Go escucha la mayoría de las críticas, y espere a que se solucionen los errores. La técnica de hoy no es tan dolorosa (aunque go fmt debería dejarlo reposar en una línea).

El concepto try() tiene todos los problemas de check de check/handle:

  1. No se lee como Go. La gente quiere sintaxis de asignación, sin la subsiguiente prueba nula, ya que se parece a Go. Trece respuestas separadas para verificar/manejar sugirieron esto; ver _Temas recurrentes_ aquí:
    https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -temas

    f, #      := os.Open(...) // return on error
    f, #panic := os.Open(...) // panic on error
    f, #hname := os.Open(...) // invoke named handler on error
    // # is any available symbol or unambiguous pair
    
  2. El anidamiento de llamadas a funciones que devuelven errores oscurece el orden de las operaciones y dificulta la depuración. El estado de cosas cuando ocurre un error, y por lo tanto la secuencia de llamadas, debería ser claro, pero aquí no lo es:
    try(step4(try(step1()), try(step3(try(step2())))))
    Ahora recuerda que el lenguaje prohíbe:
    f(t ? a : b) y f(a++)

  3. Sería trivial devolver errores sin contexto. Una razón clave de verificar/manejar fue fomentar la contextualización.

  4. Está vinculado al tipo error y al último valor devuelto. Si necesitamos inspeccionar otros valores/tipos de devolución para un estado excepcional, volvemos a: if errno := f(); errno != 0 { ... }

  5. No ofrece múltiples caminos. El código que llama a las API de almacenamiento o redes maneja dichos errores de manera diferente a los que se deben a una entrada incorrecta o a un estado interno inesperado. Mi código hace uno de estos con mucha más frecuencia que return err :

    • registro.Fatal()
    • panic() para errores que nunca deberían surgir
    • registra un mensaje y vuelve a intentarlo

@gopherbot agrega Go2, Cambio de idioma

¿Qué tal usar solo ? para desenvolver el resultado como rust

La razón por la que somos escépticos acerca de llamar a try() puede ser dos enlaces implícitos. No podemos ver el enlace para el error de valor devuelto y los argumentos para try(). Para probar (), podemos hacer una regla que debemos usar try () con la función de argumento que tiene un error en los valores de retorno. Pero el enlace a los valores devueltos no lo es. Así que estoy pensando que se requiere más expresión para que los usuarios entiendan lo que hace este código.

func doSomething() (int, %error) {
  f := try(foo())
  ...
}
  • No podemos usar try() si doSomething no tiene %error en los valores de retorno.
  • No podemos usar try() si foo() no tiene un error en el último de los valores devueltos.

Es difícil agregar nuevos requisitos/características a la sintaxis existente.

Para ser honesto, creo que foo() también debería tener %error.

Agregar 1 regla más

  • %error solo puede ser uno en la lista de valores de retorno de una función.

En el documento de diseño detallado noté que en una iteración anterior se sugirió pasar un controlador de errores a la función incorporada de prueba. Me gusta esto:

handler := func(err error) error {
        return fmt.Errorf("foo failed: %v", err)  // wrap error
}

f := try(os.Open(filename), handler)  

o incluso mejor, así:

f := try(os.Open(filename), func(err error) error {
        return fmt.Errorf("foo failed: %v", err)  // wrap error
})  

Aunque, como dice el documento, esto plantea varias preguntas, creo que esta propuesta sería mucho más deseable y útil si hubiera mantenido esta posibilidad de especificar opcionalmente dicha función de controlador de errores o cierre.

En segundo lugar, no me importa que una función incorporada pueda hacer que la función regrese, pero, para resumir un poco, el nombre 'intentar' es demasiado corto para sugerir que puede causar un regreso. Así que me parece mejor un nombre más largo, como attempt .

EDITAR: en tercer lugar, idealmente, el lenguaje go debería obtener genéricos primero, donde un caso de uso importante sería la capacidad de implementar esta función de prueba como un genérico, para que la eliminación de bicicletas pueda terminar y todos puedan obtener el manejo de errores que prefieran.

Las noticias de hackers tienen algo de razón: try no se comporta como una función normal (puede regresar), por lo que no es bueno darle una sintaxis similar a la de una función. Una sintaxis return o defer sería más apropiada:

func CopyFile(src, dst string) (err error) {
        r := try os.Open(src)
        defer r.Close()

        w := try os.Create(dst)
        defer func() {
                w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        try io.Copy(w, r)
        try w.Close()
        return nil
}

@sheerun, el contraargumento común a esto es que panic también es una función incorporada que altera el flujo de control. Personalmente no estoy de acuerdo con eso, sin embargo, es correcto.

  1. Haciéndonos eco de @deanveloper arriba , así como los comentarios similares de otros, me temo que estamos subestimando los costos de agregar una palabra clave nueva, algo sutil y, especialmente cuando está integrada en otras llamadas a funciones, que se pasa por alto fácilmente y que administra el control de la pila de llamadas. flujo. panic(...) es una excepción relativamente clara (juego de palabras no intencionado) a la regla de que return es la única forma de salir de una función. No creo que debamos usar su existencia como justificación para agregar un tercero.
  2. Esta propuesta canonizaría la devolución de un error no envuelto como el comportamiento predeterminado y relegaría los errores de envuelto como algo que tiene que aceptar, con una ceremonia adicional. Pero, en mi experiencia, eso es precisamente lo contrario a las buenas prácticas. Espero que una propuesta en este espacio facilite, o al menos no dificulte, agregar información contextual a los errores en el sitio del error.

tal vez podamos agregar una variante con función de aumento opcional algo así como tryf con esta semántica:

func tryf(t1 T1, t1 T2, … tn Tn, te error, fn func(error) error) (T1, T2, … Tn)

traduce esto

x1, x2, … xn = tryf(f(), func(err error) { return fmt.Errorf("foobar: %q", err) })

dentro de esto

t1, … tn, te := f()
if te != nil {
    if fn != nil {
        te = fn(te)
    }
    err = te
    return
}

dado que esta es una opción explícita (en lugar de usar try ), podemos encontrar respuestas razonables a las preguntas en la versión anterior de este diseño. por ejemplo, si la función de aumento es nula, no haga nada y simplemente devuelva el error original.

Me preocupa que try reemplace el manejo tradicional de errores y que, como resultado, haga que anotar las rutas de error sea más difícil.

El código que maneja los errores mediante el registro de mensajes y la actualización de los contadores de telemetría será considerado como defectuoso o inadecuado tanto por los linters como por los desarrolladores que esperan try todo.

a, b, err := doWork()
if err != nil {
  updateCounters()
  writeLogs()
  return err
}

Go es un lenguaje extremadamente social con modismos comunes impuestos por herramientas (fmt, lint, etc.). Tenga en cuenta las ramificaciones sociales de esta idea: habrá una tendencia a querer usarla en todas partes.

@politician , lo siento, pero la palabra que buscas no es _social_ sino _opinionario_. Go es un lenguaje de programación obstinado. Por lo demás, en general estoy de acuerdo con lo que dices.

Las herramientas de la comunidad @beoran como Godep y los diversos linters demuestran que Go es obstinado y social, y muchos de los dramas con el lenguaje se derivan de esa combinación. Con suerte, ambos podemos estar de acuerdo en que try no debería ser el próximo drama.

@politician Gracias por aclarar, no lo había entendido así. Ciertamente puedo estar de acuerdo en que debemos tratar de evitar el drama.

Estoy confundido al respecto.

Del blog: Los errores son valores , desde mi perspectiva, está diseñado para ser valorado, no para ser ignorado.

Y sí creo lo que dijo Rop Pike: "Los valores se pueden programar, y dado que los errores son valores, los errores se pueden programar".

No deberíamos considerar error como exception , es como importar complejidad no solo para pensar sino también para codificar si lo hacemos.

"Use el lenguaje para simplificar el manejo de errores". --Rob Pike

Y más, podemos revisar esta diapositiva

image

Una situación en la que encuentro la verificación de errores a través if particularmente incómoda es al cerrar archivos (por ejemplo, en NFS). Supongo que, actualmente, estamos destinados a escribir lo siguiente, si es posible que se produzcan devoluciones de error de .Close() .

r, err := os.Open(src)
if err != nil {
    return err
}
defer func() {
    // maybe check whether a previous error occured?
    return r.Close()
}()

¿Podría defer try(r.Close()) ser una buena forma de tener una sintaxis manejable para solucionar este tipo de errores? Al menos, tendría sentido ajustar el ejemplo de CopyFile() en la propuesta de alguna manera, para no ignorar los errores de r.Close() y w.Close() .

@seehuhn Su ejemplo no se compilará porque la función diferida no tiene un tipo de devolución.

func doWork() (err error) {
  r, err := os.Open(src)
  if err != nil {
    return err
  }
  defer func() {
    err = r.Close()  // overwrite the return value
  }()
}

Funcionará como esperas. La clave es el valor de retorno nombrado.

Me gusta la propuesta, pero creo que el ejemplo de @seehuhn también debería abordarse:

defer try(w.Close())

devolvería el error de Close() solo si el error aún no estaba configurado.
Este patrón se usa tan a menudo...

Estoy de acuerdo con las preocupaciones sobre agregar contexto a los errores. Lo veo como una de las mejores prácticas que mantiene los mensajes de error mucho más amigables (y claros) y facilita el proceso de depuración.

Lo primero que pensé fue reemplazar el fmt.HandleErrorf con una función tryf , que prefija el error con contexto adicional.

func tryf(t1 T1, t1 T2, … tn Tn, te error, ts string) (T1, T2, … Tn)

Por ejemplo (de un código real que tengo):

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil {
        return nil, errors.WithMessage(err, "load config dir")
    }
    b := bytes.NewBuffer(nil)
    if err = templates.ExecuteTemplate(b, "main", c); err != nil {
        return nil, errors.WithMessage(err, "execute main template")
    }
    buf, err := format.Source(b.Bytes())
    if err != nil {
        return nil, errors.WithMessage(err, "format main template")
    }
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    if err := ioutil.WriteFile(target, buf, 0644); err != nil {
        return nil, errors.WithMessagef(err, "write file %s", target)
    }
    // ...
}

Se puede cambiar a algo como:

func (c *Config) Build() error {
    pkgPath := tryf(c.load(), "load config dir")
    b := bytes.NewBuffer(nil)
    tryf(emplates.ExecuteTemplate(b, "main", c), "execute main template")
    buf := tryf(format.Source(b.Bytes()), "format main template")
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    tryf(ioutil.WriteFile(target, buf, 0644), fmt.Sprintf("write file %s", target))
    // ...
}

O, si tomo el ejemplo de @agnivade :

func (p *pgStore) DoWork() (err error) {
    tx := tryf(p.handle.Begin(), "begin transaction")
        defer func() {
        if err != nil {
            tx.Rollback()
        }
    }()
    var res int64
    tryf(tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res), "insert table")
    _, = tryf(tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res), "insert table2")
    return tryf(tx.Commit(), "commit transaction")
}

Sin embargo, @josharian planteó un buen punto que me hace dudar sobre esta solución:

Sin embargo, tal como está escrito, mueve el formato de estilo fmt de un paquete al lenguaje mismo, lo que abre una lata de gusanos.

Estoy totalmente de acuerdo con esta propuesta y puedo ver sus beneficios a través de varios ejemplos.

Mi única preocupación con la propuesta es el nombre de try , siento que sus connotaciones con otros idiomas pueden distorsionar las percepciones de los desarrolladores sobre cuál es su propósito cuando proviene de otros idiomas. Java viene a buscar aquí.

Para mí, preferiría que la función integrada se llamara pass . Siento que esto da una mejor representación de lo que está sucediendo. Después de todo, no está manejando el error, sino devolviéndolo para que lo maneje la persona que llama. try da la impresión de que se ha manejado el error.

Es un pulgar hacia abajo de mi parte, principalmente porque el problema que pretende abordar ("las declaraciones repetitivas if típicamente asociadas con el manejo de errores") simplemente no es un problema para mí. Si todas las comprobaciones de errores fueran simplemente if err != nil { return err } entonces podría ver algún valor en agregar azúcar sintáctico para eso (aunque Go es un lenguaje relativamente libre de azúcar por inclinación).

De hecho, lo que quiero hacer en caso de un error no nulo varía considerablemente de una situación a otra. Tal vez quiera t.Fatal(err) . Tal vez quiera agregar un mensaje de decoración return fmt.Sprintf("oh no: %v", err) . Tal vez solo registro el error y continúo. Tal vez configuré un indicador de error en mi objeto SafeWriter y continué, verificando el indicador al final de alguna secuencia de operaciones. Quizá deba tomar otras medidas. Ninguno de estos se puede automatizar con try . Entonces, si el argumento para try es que eliminará todos los bloques if err != nil , ese argumento no se sostiene.

¿Eliminará _algunos_ de ellos? Por supuesto. ¿Es una propuesta atractiva para mí? Meh. Realmente no estoy preocupado. Para mí, if err != nil es solo parte de Go, como las llaves, o defer . Entiendo que parece detallado y repetitivo para las personas que son nuevas en Go, pero las personas que son nuevas en Go no son las más indicadas para realizar cambios drásticos en el idioma, por un montón de razones.

Tradicionalmente, el estándar para cambios significativos en Go ha sido que el cambio propuesto debe resolver un problema que es (A) significativo, (B) afecta a muchas personas y (C) está bien resuelto por la propuesta. No estoy convencido de ninguno de estos tres criterios. Estoy muy contento con el manejo de errores de Go tal como está.

Para hacer eco de @peterbourgon y @deanveloper , una de mis cosas favoritas de Go es que el flujo de código es claro y panic() no se trata como un mecanismo de control de flujo estándar como lo es en Python.

En cuanto al debate sobre panic, panic() casi siempre aparece solo en una línea porque no tiene ningún valor. No puedes fmt.Println(panic("oops")) . Esto aumenta enormemente su visibilidad y lo hace mucho menos comparable a try() de lo que la gente cree.

Si va a haber otra construcción de control de flujo para funciones, preferiría _mucho_ que sea una declaración garantizada para ser el elemento más a la izquierda en una línea.

Uno de los ejemplos en la propuesta me aclara el problema:

func printSum(a, b string) error {
        fmt.Println(
                "result:",
                try(strconv.Atoi(a)) + try(strconv.Atoi(b)),
        )
        return nil
}

El flujo de control realmente se vuelve menos obvio y muy oscuro.

Esto también va en contra de la intención inicial de Rob Pike de que todos los errores deben manejarse explícitamente.

Si bien una reacción a esto puede ser "entonces no lo use", el problema es que otras bibliotecas lo usarán, y depurarlos, leerlos y usarlos se vuelve más problemático. Esto motivará a mi empresa a nunca adoptar go 2 y comenzar a usar solo bibliotecas que no usan try . Si no estoy solo con esto, podría conducir a una división a-la python 2/3.

Además, la denominación de try implicará automáticamente que eventualmente catch aparecerá en la sintaxis, y volveremos a ser Java.

Entonces, por todo esto, estoy _fuertemente_ en contra de esta propuesta.

No me gusta el nombre try . Implica un _intento_ de hacer algo con un alto riesgo de fallar (puedo tener un sesgo cultural en contra de _intentar_ ya que no soy un hablante nativo de inglés), mientras que en su lugar se usaría try en caso de que esperemos fallas raras (motivación para querer reducir la verbosidad del manejo de errores) y son optimistas. Además try en esta propuesta de hecho _capta_ un error para devolverlo antes de tiempo. Me gusta la sugerencia de pass de @HiImJC.

Además del nombre, me resulta incómodo tener una declaración similar a return ahora oculta en medio de las expresiones. Esto rompe el estilo Go flow. Hará que las revisiones de código sean más difíciles.

En general, encuentro que esta propuesta solo beneficiará al programador perezoso que ahora tiene un arma para un código más corto y aún menos razones para hacer el esfuerzo de envolver errores. Como también dificultará las revisiones (retorno en medio de la expresión), creo que esta propuesta va en contra del objetivo de "programación a escala" de Go.

Una de mis cosas favoritas sobre Go que generalmente digo cuando describo el idioma es que solo hay una forma de hacer las cosas, para la mayoría de las cosas. Esta propuesta va un poco en contra de ese principio al ofrecer múltiples formas de hacer lo mismo. Personalmente, creo que esto no es necesario y que quitaría, en lugar de agregar, simplicidad y legibilidad al lenguaje.

Me gusta esta propuesta en general. La interacción con defer parece suficiente para proporcionar una forma ergonómica de devolver un error al tiempo que agrega contexto adicional. Aunque sería bueno abordar el inconveniente que @josharian señaló sobre cómo incluir el error original en el mensaje de error envuelto.

Lo que falta es una forma ergonómica de interactuar con la(s) propuesta(s) de inspección de errores sobre la mesa. Creo que las API deben ser muy deliberadas sobre qué tipos de errores devuelven, y el valor predeterminado probablemente debería ser "los errores devueltos no son inspeccionables de ninguna manera". Entonces debería ser fácil ir a un estado donde los errores sean inspeccionables de manera precisa, como lo documenta la firma de la función ("Informa un error de tipo X en la circunstancia A y un error de tipo Y en la circunstancia B").

Desafortunadamente, a partir de ahora, esta propuesta hace que la opción más ergonómica sea la más indeseable (para mí); pasando ciegamente a través de tipos de error arbitrarios. Creo que esto no es deseable porque fomenta no pensar en los tipos de errores que devuelve y cómo los consumirán los usuarios de su API. La conveniencia adicional de esta propuesta es ciertamente agradable, pero me temo que fomentará el mal comportamiento porque la conveniencia percibida superará el valor percibido de pensar detenidamente qué información de error proporciona (o filtra).

Una curita sería si los errores devueltos por try se convirtieran en errores que no son "desenvolvibles". Desafortunadamente, esto también tiene desventajas bastante graves, ya que hace que cualquier defer no pueda inspeccionar los errores en sí. Además, evita el uso en el que try en realidad devolverá un error de tipo deseable (es decir, casos de uso en los que try se usa con cuidado en lugar de descuidarlo).

Otra solución sería reutilizar la idea (descartada) de tener un segundo argumento opcional para try para definir o incluir en la lista blanca los tipos de error que pueden devolverse desde ese sitio. Esto es un poco problemático porque tenemos dos formas diferentes de definir un "tipo de error", ya sea por valor ( io.EOF etc.) o por tipo ( *os.PathError , *exec.ExitError ). Es fácil especificar tipos de error que son valores como argumentos de una función, pero es más difícil especificar tipos. No estoy seguro de cómo manejar eso, pero lanzando la idea por ahí.

El problema que señaló @josharian se puede evitar retrasando la evaluación de err:

defer func() { fmt.HandleErrorf(&err, "oops: %v", err) }()

No se ve muy bien, pero debería funcionar. Sin embargo, preferiría que esto se solucione agregando un nuevo verbo/indicador de formato para los punteros de error, o tal vez para los punteros en general, que imprima el valor desreferenciado como %v simple. A los efectos del ejemplo, llamémoslo %*v :

defer fmt.HandleErrorf(&err, "oops: %*v", &err)

Dejando de lado el inconveniente, creo que esta propuesta parece prometedora, pero parece crucial para mantener bajo control la ergonomía de agregar contexto a los errores.

Editar:

Otro enfoque es envolver el puntero de error en una estructura que implementa Stringer :

type wraperr struct{ err *error }
func (w wraperr) String() string { return (*w.err).Error() }

...

defer handleErrorf(&err, "oops: %v", wraperr{&err})

Un par de cosas desde mi perspectiva. ¿Por qué nos preocupamos tanto por ahorrar algunas líneas de código? Considero esto en la misma línea que las funciones pequeñas consideradas dañinas .

Además, encuentro que tal propuesta eliminaría la responsabilidad de manejar correctamente el error a alguna "magia" de la que me preocupa que se abuse y fomente la pereza, lo que resultará en errores y código de mala calidad.

La propuesta, como se indicó, también tiene una serie de comportamientos poco claros, por lo que esto ya es problemático que ~3 líneas adicionales _explícitas_ que son más claras.

Actualmente usamos el patrón de aplazamiento con moderación en casa. Hay un artículo aquí que tuvo una recepción mixta similar cuando lo escribimos: https://bet365techblog.com/better-error-handling-in-go

Sin embargo, nuestro uso de él fue en previsión del progreso de la propuesta check / handle .

Verificar/manejar fue un enfoque mucho más completo para hacer que el manejo de errores fuera más conciso. Su bloque handle retuvo el mismo ámbito de función que el que se definió, mientras que cualquier instrucción defer son contextos nuevos con una cantidad, por grande que sea, de sobrecarga. Esto parecía estar más en consonancia con las expresiones idiomáticas de go, en el sentido de que si deseaba el comportamiento de "simplemente devolver el error cuando suceda", podría declararlo explícitamente como handle { return err } .

Defer obviamente depende de que la referencia de error también se mantenga, pero hemos visto que surgen problemas al sombrear la referencia de error con vars de ámbito de bloque. Por lo tanto, no es lo suficientemente infalible como para ser considerado la forma estándar de manejar errores en marcha.

try , en este caso, no parece resolver demasiado y comparto el mismo temor que otros de que simplemente conduciría a implementaciones perezosas, o que usarían en exceso el patrón de aplazamiento.

Si el manejo de errores basado en aplazamiento va a ser una cosa, entonces probablemente se debería agregar algo como esto al paquete de errores:

        f := try(os.Create(filename))
        defer errors.Deferred(&err, f.Close)

Ignorar los errores de las declaraciones de cierre diferidas es un problema bastante común. Debería haber una herramienta estándar para ayudar con eso.

Una función integrada que devuelve es más difícil de vender que una palabra clave que hace lo mismo.
Me gustaría más si fuera una palabra clave como lo es en Zig[1].

  1. https://ziglang.org/documentation/master/#try

Las funciones integradas, cuya firma de tipo no se puede expresar usando el sistema de tipos del lenguaje, y cuyo comportamiento confunde lo que normalmente es una función, simplemente parece una escotilla de escape que se puede usar repetidamente para evitar la evolución real del lenguaje.

Estamos acostumbrados a reconocer inmediatamente declaraciones de retorno (y pánico) porque así es como se expresa este tipo de flujo de control en Go (y muchos otros lenguajes). No parece descabellado que también reconozcamos try como cambiar el flujo de control después de acostumbrarnos, al igual que lo hacemos con return. No tengo ninguna duda de que un buen soporte IDE ayudará con esto también.

Creo que es bastante exagerado. En código gofmt'ed, un retorno siempre coincide con /^\t*return / ; es un patrón muy trivial para detectar a simple vista, sin ninguna ayuda. try , por otro lado, puede ocurrir en cualquier parte del código, anidado arbitrariamente en profundidad en las llamadas a funciones. Ninguna cantidad de capacitación nos permitirá detectar de inmediato todo el flujo de control en una función sin la ayuda de herramientas.

Además, una función que dependa de un "buen soporte IDE" estará en desventaja en todos los entornos donde no haya un buen soporte IDE. Las herramientas de revisión de código me vienen a la mente de inmediato: ¿Gerrit me resaltará todos los intentos? ¿Qué pasa con las personas que optan por no usar IDE o resaltado de código elegante por varias razones? ¿Acme comenzará a resaltar try ?

Una función de idioma debe ser fácil de entender por sí misma, no depender del soporte del editor.

@kungfusheep Me gusta ese artículo. Solo ocuparse de envolver en un aplazamiento ya aumenta bastante la legibilidad sin try .

Estoy en el campamento que no siente que los errores en Go sean realmente un problema. Aun así, if err != nil { return err } puede ser bastante tartamudo en algunas funciones. He escrito funciones que necesitaban una comprobación de errores después de casi todas las declaraciones y ninguna necesitaba un manejo especial que no fuera el ajuste y devolución. A veces simplemente no hay ninguna estructura de búfer inteligente que mejore las cosas. A veces es solo un paso crítico diferente tras otro y simplemente necesita hacer un cortocircuito si algo salió mal.

Aunque try sin duda haría que el código fuera mucho más fácil de leer y agradable al mismo tiempo que es totalmente compatible con versiones anteriores, estoy de acuerdo en que try no es una característica crítica que debe tener, por lo que si la gente tiene demasiado miedo de tal vez sea mejor no tenerlo.

Sin embargo, la semántica es bastante clara. Cada vez que vea try , está siguiendo el camino feliz o regresa. Realmente no puedo ser más simple que eso.

Esto parece una macro encapsulada especial.

@dominikh try siempre coincide con /try\(/ así que no sé cuál es realmente tu punto. Es igual de buscable y todos los editores de los que he oído hablar tienen una función de búsqueda.

@qrpnxz Creo que el punto que estaba tratando de hacer no es que no puedas buscarlo programáticamente, sino que es más difícil buscarlo con tus ojos. La expresión regular era solo una analogía, con énfasis en /^\t* , lo que significa que todos los retornos se destacan claramente por estar al comienzo de una línea (ignorando los espacios en blanco iniciales).

Pensándolo más, debería haber un par de funciones auxiliares comunes. Quizás deberían estar en un paquete llamado "diferido".

Dirigiéndose a la propuesta de un check con formato para evitar nombrar la devolución, puede hacerlo con una función que verifique que no haya nada, así

func Format(err error, message string, args ...interface{}) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf(...)
}

Esto se puede usar sin un retorno con nombre como este:

func foo(s string) (int, error) {
    n, err := strconv.Atoi(s)
    try(deferred.Format(err, "bad string %q", s))
    return n, nil
}

El fmt.HandleError propuesto podría colocarse en el paquete diferido en su lugar y mi función de ayuda "errors.Defer" podría llamarse deferred.Exec y podría haber un exec condicional para que los procedimientos se ejecuten solo si el error no es nulo.

Al juntarlo, obtienes algo como

func CopyFile(src, dst string) (err error) {
    defer deferred.Annotate(&err, "copy %s %s", src, dst)

    r := try(os.Open(src))
    defer deferred.Exec(&err, r.Close)

    w := try(os.Create(dst))
    defer deferred.Exec(&err, r.Close)

    defer deferred.Cond(&err, func(){ os.Remove(dst) })
    try(io.Copy(w, r))

    return nil
}

Otro ejemplo:

func (p *pgStore) DoWork() (err error) {
    tx := try(p.handle.Begin())

    defer deferred.Cond(&err, func(){ tx.Rollback() })

    var res int64 
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    try(deferred.Format(err, "insert table")

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    try(deferred.Format(err, "insert table2"))

    return tx.Commit()
}

Esta propuesta nos lleva de tener if err != nil todos lados, a tener try todos lados. Desplaza el problema propuesto y no lo resuelve.

Aunque, diría que, para empezar, el mecanismo actual de manejo de errores no es un problema. Solo necesitamos mejorar las herramientas y la investigación a su alrededor.

Además, diría que if err != nil es en realidad más legible que try porque no abarrota la línea del lenguaje de lógica empresarial, sino que se encuentra justo debajo:

file := try(os.OpenFile("thing")) // less readable than, 

file, err := os.OpenFile("thing")
if err != nil {

}

Y si Go iba a ser más mágico en su manejo de errores, ¿por qué no simplemente poseerlo por completo? Por ejemplo, Go puede llamar implícitamente al try incorporado si un usuario no asigna un error. Por ejemplo:

func getString() (string, error) { ... }

func caller() {
  defer func() {
    if err != nil { ... } // whether `err` must be defined or not is not shown in this example. 
  }

  // would call try internally, because a user is not 
  // assigning an error value. Also, it can add a compile error
  // for "defined and not used err value" if the user does not 
  // handle the error. 
  str := getString()
}

Para mí, eso resolvería el problema de la redundancia a costa de la magia y la legibilidad potencial.

Por lo tanto, propongo que realmente resolvamos el 'problema' como en el ejemplo anterior o mantengamos el manejo de errores actual, pero en lugar de cambiar el idioma para resolver la redundancia y el ajuste, no cambiamos el idioma pero mejoramos las herramientas y la verificación . de código para mejorar la experiencia.

Por ejemplo, en VSCode hay un fragmento llamado iferr si lo escribes y presionas enter, se expande a una declaración completa de manejo de errores... por lo tanto, escribirlo nunca me parece aburrido, y leerlo más adelante es mejor .

@josharian

Aunque no sería "un cambio de biblioteca modesto", también podríamos considerar aceptar el error func main().

El problema con eso es que no todas las plataformas tienen una semántica clara sobre lo que eso significa. Su reescritura funciona bien en los programas Go "tradicionales" que se ejecutan en un sistema operativo completo, pero tan pronto como escribe microcontrolador-firmware o incluso solo WebAssembly, no está muy claro qué significaría os.Exit(1) . Actualmente, os.Exit es una llamada de biblioteca, por lo que las implementaciones de Go son gratuitas solo para no proporcionarlo. Sin embargo, la forma de main es una preocupación del idioma.


Una pregunta sobre la propuesta que probablemente se responda mejor con "no": ¿Cómo interactúa try con argumentos variados? Es el primer caso de una función variadic (ish) que no tiene su variadic-nes en el último argumento. ¿Está permitido?

var e []error
try(e...)

Dejando de lado por qué alguna vez harías eso. Sospecho que la respuesta es "no" (de lo contrario, el seguimiento es "¿qué sucede si la longitud del segmento expandido es 0?").

  • Varias de las mejores características de go son que las funciones integradas actuales aseguran un flujo de control claro, el manejo de errores es explícito y alentado, y los desarrolladores están fuertemente disuadidos de escribir código "mágico". La propuesta try no es coherente con estos principios básicos, ya que promoverá la taquigrafía a costa de la legibilidad del flujo de control.
  • Si se adopta esta propuesta, quizás considere hacer que el try sea una declaración incorporada en lugar de una función . Entonces es más consistente con otras declaraciones de flujo de control como if . Además, la eliminación de los paréntesis anidados mejora marginalmente la legibilidad.
  • Una vez más, si se adopta la propuesta, quizás se implemente sin usar defer o similar. Ya no se puede implementar de forma pura (como lo señalaron otros), por lo que también puede usar una implementación más eficiente bajo el capó.

Veo dos problemas con esto:

  1. Pone MUCHO código anidado dentro de funciones. Eso agrega mucha carga cognitiva adicional, tratando de analizar el código en tu cabeza.
  1. Nos da lugares donde el código puede salir desde el medio de una declaración.

Número 2, creo que es mucho peor. Todos los ejemplos aquí son llamadas simples que devuelven un error, pero lo que es mucho más insidioso es esto:

func doit(abc string) error {
    a := fmt.Sprintf("value of something: %s\n", try(getValue(abc)))
    log.Println(a)
    return nil
}

Este código puede salir en medio de ese sprintf, y será SÚPER fácil pasar por alto ese hecho.

Mi voto es no. Esto no mejorará el código go. No hará que sea más fácil de leer. No lo hará más robusto.

Lo he dicho antes, y esta propuesta lo ejemplifica: siento que el 90% de las quejas sobre Go son "No quiero escribir una declaración if o un bucle". Esto elimina algunas declaraciones if muy simples, pero agrega carga cognitiva y hace que sea fácil pasar por alto los puntos de salida de una función.

Solo quiero señalar que no podría usar esto en la página principal y podría ser confuso para los nuevos usuarios o al enseñar. Obviamente, esto se aplica a cualquier función que no devuelva un error, pero creo que main es especial ya que aparece en muchos ejemplos.

func main() {
    f := try(os.Open("foo.txt"))
    defer f.Close()
}

Tampoco estoy seguro de que hacer try panic en main sea aceptable.

Además, no sería particularmente útil en las pruebas ( func TestFoo(t* testing.T) ), lo cual es desafortunado :(

El problema que tengo con esto es que asume que siempre desea devolver el error cuando sucede. Cuando tal vez quiera agregar contexto al error y devolverlo o tal vez solo quiera comportarse de manera diferente cuando ocurre un error. Tal vez eso depende del tipo de error devuelto.

Preferiría algo parecido a un intento/captura que podría verse como

Suponiendo que foo() se define como

func foo() (int, error) {}

Entonces podrías hacer

n := try(foo()) {
    case FirstError:
        // do something based on FirstError
    case OtherError:
        // do something based on OtherError
    default:
        // default behavior for any other error
}

que se traduce en

n, err := foo()
if errors.Is(err, FirstError) {
    // do something based on FirstError
if errors.Is(err, OtherError) {
    // do something based on OtherError
} else {
    // default behavior for any other error
}

Para mí, el manejo de errores es una de las partes más importantes de un código base.
Ya hay demasiado código go if err != nil { return err } , que devuelve un error desde lo más profundo de la pila sin agregar contexto adicional, o incluso (posiblemente) peor agregando contexto enmascarando el error subyacente con fmt.Errorf envolviendo.

Proporcionar una nueva palabra clave que es una especie de magia que no hace más que reemplazar if err != nil { return err } parece un camino peligroso.
Ahora todo el código se envolverá en una llamada para probar. Esto está algo bien (aunque la legibilidad apesta) para el código que se ocupa solo de errores en el paquete, como:

func foo() error {
  /// stuff
  try(bar())
  // more stuff
}

Pero diría que el ejemplo dado es realmente horrible y básicamente deja a la persona que llama tratando de entender un error que está muy profundo en la pila, muy parecido al manejo de excepciones.
Por supuesto, todo depende del desarrollador para hacer lo correcto aquí, pero le da al desarrollador una excelente manera de no preocuparse por sus errores con tal vez un "arreglaremos esto más tarde" (y todos sabemos cómo funciona eso ).

Me gustaría que analizáramos el problema desde una perspectiva diferente a *"cómo podemos reducir la repetición" y más sobre "cómo podemos hacer que el manejo (adecuado) de errores sea más simple y que los desarrolladores sean más productivos".
Deberíamos pensar en cómo afectará esto a la ejecución del código de producción.

*Nota: Esto en realidad no reduce la repetición, solo cambia lo que se repite, al mismo tiempo que hace que el código sea menos legible porque todo está encerrado en un try() .

Un último punto: al principio, leer la propuesta parece agradable, luego comienzas a entender todos los problemas (al menos los enumerados) y es como "bueno, sí, esto es demasiado".


Me doy cuenta de que mucho de esto es subjetivo, pero es algo que me importa. Esta semántica es increíblemente importante.
Lo que quiero ver es una forma de simplificar la escritura y el mantenimiento del código de nivel de producción, de modo que también podría hacer los errores "correctamente", incluso para el código de nivel POC/demo.

Dado que el contexto de error parece ser un tema recurrente...

Hipótesis: la mayoría de las funciones de Go devuelven (T, error) en lugar de (T1, T2, T3, error)

¿Qué pasa si, en lugar de definir try como try(T1, T2, T3, error) (T1, T2, T3) lo definimos como
try(func (args) (T1, T2, T3, error))(T1, T2, T3) ? (esto es una aproximación)

lo que quiere decir que la estructura sintáctica de una llamada try es siempre un primer argumento que es una expresión que devuelve múltiples valores, el último de los cuales es un error.

Luego, al igual que make , esto abre la puerta a una forma de llamada de 2 argumentos, donde el segundo argumento es el contexto del intento (por ejemplo, una cadena fija, una cadena con un %v , una función que toma un argumento de error y devuelve otro error, etc.)

Esto aún permite el encadenamiento para el caso de (T, error) , pero ya no puede encadenar varias devoluciones, lo que, en mi opinión, normalmente no se requiere.

@ cpuguy83 Si lee la propuesta, verá que no hay nada que le impida envolver el error. De hecho, hay varias formas de hacerlo sin dejar de usar try . Sin embargo, muchas personas parecen asumir eso por alguna razón.

if err != nil { return err } es igual que "lo arreglaremos más tarde" como try excepto que es más molesto cuando se crean prototipos.

No sé cómo las cosas que están dentro de un par de paréntesis son menos legibles que los pasos de función que están cada cuatro líneas de repetitivo tampoco.

Sería bueno si señalaras algunos de estos "errores" particulares que te molestaron, ya que ese es el tema.

La legibilidad parece ser un problema, pero ¿qué pasa con go fmt presentando try() para que se destaque, algo como:

f := try(
    os.Open("file.txt")
)

@MrTravisB

El problema que tengo con esto es que asume que siempre desea devolver el error cuando sucede.

Estoy en desacuerdo. Se supone que desea hacerlo con la suficiente frecuencia como para garantizar una abreviatura para eso. Si no lo hace, no interfiere en el manejo de errores claramente.

Cuando tal vez quiera agregar contexto al error y devolverlo o tal vez solo quiera comportarse de manera diferente cuando ocurre un error.

La propuesta describe un patrón para agregar contexto de bloque completo a los errores. Sin embargo, @josharian señaló que hay un error en los ejemplos y no está claro cuál es la mejor manera de evitarlo. He escrito un par de ejemplos de formas de manejarlo.

Para un contexto de error más específico, nuevamente, try hace una cosa, y si no quiere esa cosa, no use try .

@boomlinde Exactamente mi punto. Esta propuesta intenta resolver un caso de uso singular en lugar de proporcionar una herramienta para resolver el problema más amplio del manejo de errores. Creo que la pregunta fundamental es exactamente lo que usted señaló.

Se supone que desea hacerlo con la suficiente frecuencia como para garantizar una abreviatura para eso.

En mi opinión y experiencia, este caso de uso es una pequeña minoría y no garantiza una sintaxis abreviada.

Además, el enfoque de usar defer para manejar errores tiene problemas en el sentido de que asume que desea manejar todos los errores posibles de la misma manera. Los extractos defer no se pueden cancelar.

defer fmt.HandleErrorf(&err, “foobar”)

n := try(foo())

x : try(foo2())

¿Qué pasa si quiero un manejo de errores diferente para los errores que pueden devolverse desde foo() frente a foo2() ?

@MrTravisB

¿Qué sucede si quiero un manejo de errores diferente para los errores que pueden devolverse desde foo() frente a foo2()?

Entonces usas otra cosa. Ese es el punto que @boomlinde estaba haciendo.

Tal vez usted personalmente no vea este caso de uso a menudo, pero mucha gente sí, y agregar try realmente no le afecta. De hecho, cuanto más raro es el caso de uso para usted, menos le afecta que se agregue try .

@qrpnxz

f := try(os.Open("/foo"))
data := try(ioutil.ReadAll(f))
try(send(data))

(sí, entiendo que hay ReadFile y que este ejemplo en particular no es la mejor manera de copiar datos en algún lugar, no es el punto)

Esto requiere más esfuerzo para leer porque tiene que analizar los intentos en línea. La lógica de la aplicación está envuelta en otra llamada.
También diría que un controlador de errores defer aquí no sería bueno, excepto para simplemente envolver el error con un nuevo mensaje... lo cual es bueno, pero hay más para lidiar con los errores que hacer que sea fácil para el humano para leer lo que pasó.

En rust, al menos, el operador es un sufijo ( ? agregado al final de una llamada) que no impone una carga adicional para desenterrar la lógica real.

Control de flujo basado en expresiones

panic puede ser otra función de control de flujo, pero no devuelve un valor, por lo que es efectivamente una declaración. Compare esto con try , que es una expresión y puede ocurrir en cualquier lugar.

recover tiene un valor y afecta el control de flujo, pero debe aparecer en una instrucción defer . Estos defer son típicamente funciones literales, recover solo se llama una vez, por lo que recover también aparece efectivamente como una declaración. Nuevamente, compare esto con try que puede ocurrir en cualquier lugar.

Creo que esos puntos significan que try hace que sea significativamente más difícil seguir el flujo de control de una manera que no hemos tenido antes, como se señaló anteriormente, pero no vi la distinción entre declaraciones y expresiones. señaló


otra propuesta

Permitir declaraciones como

if err != nil {
    return nil, 0, err
}

para ser formateado en una línea por gofmt cuando el bloque solo contiene una instrucción return y esa instrucción no contiene saltos de línea. Por ejemplo:

if err != nil { return nil, 0, err }

Razón fundamental

  • No requiere cambios de idioma
  • La regla de formato es simple y clara.
  • La regla se puede diseñar para que se opte donde gofmt mantiene las líneas nuevas si ya existen (como los literales de estructura). Optar también le permite al escritor hacer énfasis en el manejo de errores
  • Si no está habilitado, el código se puede transferir automáticamente al nuevo estilo con una llamada a gofmt
  • Es solo para declaraciones de return , por lo que no se abusará del código de golf innecesariamente
  • Interactúa bien con los comentarios que describen por qué pueden ocurrir algunos errores y por qué se devuelven. Usar muchas expresiones try anidadas maneja esto mal
  • Reduce el espacio vertical de manejo de errores en un 66%
  • Sin flujo de control basado en expresiones
  • El código se lee con mucha más frecuencia de lo que se escribe, por lo que debe optimizarse para el lector. El código repetitivo que ocupa menos espacio es útil para el lector, donde try se inclina más hacia el escritor
  • La gente ya ha estado proponiendo try existentes en varias líneas. Por ejemplo, este comentario o este comentario que introduce un estilo como
f, err := os.Open(file)
try(maybeWrap(err))
  • El estilo "pruebe en su propia línea" elimina cualquier ambigüedad sobre err que se devuelve. Por lo tanto, sospecho que este formulario se usará comúnmente. Permitir uno forrado si los bloques son casi lo mismo, excepto que también es explícito sobre cuáles son los valores de retorno
  • No promueve el uso de devoluciones con nombre o un envoltorio poco claro basado defer . Ambos elevan la barrera de los errores de ajuste y el primero puede requerir cambios godoc
  • No es necesario discutir cuándo usar try en lugar de usar el manejo de errores tradicional
  • No excluye hacer try o algo más en el futuro. El cambio puede ser positivo incluso si se acepta try
  • Sin interacción negativa con la biblioteca $# testing 7$#$ o las funciones main . De hecho, si la propuesta permite cualquier declaración de una sola línea en lugar de solo devoluciones, puede reducir el uso de bibliotecas basadas en aserciones. Considerar
value, err := something()
if err != nil { t.Fatal(err) }
  • No hay interacción negativa con la verificación de errores específicos. Considerar
n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }

En resumen, esta propuesta tiene un costo pequeño, se puede diseñar para ser opcional, no excluye más cambios ya que es solo de estilo y reduce el dolor de leer un código detallado de manejo de errores mientras mantiene todo explícito. Creo que al menos debería considerarse como un primer paso antes de apostar try .


Algunos ejemplos portados

Desde https://github.com/golang/go/issues/32437#issuecomment-498941435

con probar

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        try(dbfile.RunMigrations(db, dbMigrations))
        t := &Thing{
                thingy:  thingy,
                scanner: try(newScanner(thingy, db, client)),
        }
        t.initOtherThing()
        return t, nil
}

Con este

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        err := dbfile.RunMigrations(db, dbMigrations))
        if err != nil { return nil, fmt.Errorf("running migrations: %v", err) }

        t := &Thing{thingy: thingy}
        t.scanner, err = newScanner(thingy, db, client)
        if err != nil { return nil, fmt.Errorf("creating scanner: %v", err) }

        t.initOtherThing()
        return t, nil
}

Es competitivo en el uso del espacio al tiempo que permite agregar contexto a los errores.

Desde https://github.com/golang/go/issues/32437#issuecomment-499007288

con probar

func (c *Config) Build() error {
    pkgPath := try(c.load())
    b := bytes.NewBuffer(nil)
    try(emplates.ExecuteTemplate(b, "main", c))
    buf := try(format.Source(b.Bytes()))
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    try(ioutil.WriteFile(target, buf, 0644))
    // ...
}

Con este

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil { return nil, errors.WithMessage(err, "load config dir") }

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err != nil { return nil, errors.WithMessage(err, "execute main template") }

    buf, err := format.Source(b.Bytes())
    if err != nil { return nil, errors.WithMessage(err, "format main template") }

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
    // ...
}

El comentario original usaba un tryf hipotético para adjuntar el formato, que se eliminó. No está claro cuál es la mejor manera de agregar todos los contextos distintos, y tal vez try ni siquiera sería aplicable.

@cpuguy83
Para mí, es más legible con try . En este ejemplo, leo "abrir un archivo, leer todos los bytes, enviar datos". Con el manejo de errores regular, leería "abrir un archivo, verificar si hubo un error, el manejo de errores hace esto, luego leer todos los bytes, ahora verificar si sucedió algo ..." Sé que puedes escanear a través de err != nil s, pero para mí try es más fácil porque cuando lo veo sé el comportamiento de inmediato: devuelve si err != nil. Si tienes una sucursal, tengo que ver qué hace. Podría hacer cualquier cosa.

También diría que un controlador de error diferido aquí no sería bueno, excepto para envolver el error con un nuevo mensaje

Estoy seguro de que hay otras cosas que puede hacer en el aplazamiento, pero independientemente, try es para el caso general simple de todos modos. Cada vez que quieras hacer algo más, siempre hay un buen manejo de errores de Go. Eso no va a desaparecer.

@zeebo Sí, me gusta eso.
El artículo de @kungfusheep usó una verificación de error de una línea como esa y me emocioné por probarlo. Luego, tan pronto como guardé, gofmt lo expandió en tres líneas, lo cual fue triste. Muchas funciones en stdlib se definen en una línea como esa, por lo que me sorprendió que gofmt lo expandiera.

@qrpnxz

Sucede que leo mucho código go. Una de las mejores cosas del lenguaje es la facilidad que brinda la mayoría de los códigos que siguen un estilo particular (gracias gofmt).
No quiero leer un montón de código envuelto en try(f()) .
Esto significa que habrá una divergencia en el estilo/práctica del código, o linters como "oh, deberías haber usado try() aquí" (que de nuevo ni siquiera me gusta, que es el punto de que yo y otros comentemos sobre esta propuesta).

No es objetivamente mejor que if err != nil { return err } , solo menos para escribir.


Una última cosa:

Si lees la propuesta verás que nada te impide

¿Podemos por favor abstenernos de ese lenguaje? Por supuesto que leí la propuesta. Da la casualidad de que lo leí anoche y luego comenté esta mañana después de pensarlo y no expliqué las minucias de lo que pretendía.
Este es un tono increíblemente contradictorio.

@cpuguy83
Mi chico malo de la CPU. No lo dije de esa manera.

Y supongo que debes señalar que el código que usa try se verá bastante diferente del código que no lo usa, así que puedo imaginar que eso afectaría la experiencia de analizar ese código, pero no puedo estar totalmente de acuerdo con esa diferencia. significa peor en este caso, aunque entiendo que a usted personalmente no le gusta como a mí personalmente me gusta. Muchas cosas en Go son así. En cuanto a lo que los linters te digan que hagas es otra cuestión completamente diferente, creo.

Seguro que no es objetivamente mejor. Estaba expresando que era más legible de esa manera para . Cuidadosamente redacté eso.

Una vez más, lo siento por sonar de esa manera. Aunque este es un argumento, no fue mi intención enemistarme contigo.

https://github.com/golang/go/issues/32437#issuecomment-498908380

Nadie te obligará a probar.

Ignorando la ligereza, creo que es una forma bastante tonta de descartar una crítica de diseño.

Claro, no tengo que usarlo. Pero cualquiera con quien escriba el código podría usarlo y obligarme a tratar de descifrar try(try(try(to()).parse().this)).easily()) . es como decir

Nadie te obligará a usar la interfaz vacía{}.

De todos modos, Go es bastante estricto con la simplicidad: gofmt hace que todo el código tenga el mismo aspecto. El camino feliz se mantiene a la izquierda y todo lo que puede resultar costoso o sorprendente es explícito . try como se propone es un giro de 180 grados de esto. Simplicidad != conciso.

Como mínimo try debería ser una palabra clave con lvalues.

No es _objetivamente_ mejor que if err != nil { return err } , solo menos para escribir.

Hay una diferencia objetiva entre los dos: try(Foo()) es una expresión. Para algunos, esa diferencia es una desventaja (la crítica try(strconv.Atoi(x))+try(strconv.Atoi(y)) ). Para otros, esa diferencia es una ventaja por la misma razón. Todavía no es objetivamente mejor o peor, pero tampoco creo que la diferencia deba esconderse debajo de la alfombra y afirmar que es "solo menos para escribir" no le hace justicia a la propuesta.

@elagergren-spideroak es difícil decir que try es molesto de ver en un respiro y luego decir que no es explícito en el siguiente. Tienes que elegir uno.

es común ver que los argumentos de función se colocan primero en variables temporales. Estoy seguro de que sería más común ver

this := try(to()).parse().this
that := try(this.easily())

que tu ejemplo.

try no hacer nada es el camino feliz, por lo que se ve como se esperaba. En el camino infeliz lo único que hace es volver. Ver que hay un try es suficiente para recopilar esa información. Tampoco hay nada costoso en regresar de una función, por lo que, según esa descripción, no creo que try esté haciendo un 180

@josharian Con respecto a su comentario en https://github.com/golang/go/issues/32437#issuecomment -498941854, no creo que haya un error de evaluación inicial aquí.

diferir fmt.HandleErrorf(&err, “foobar: %v”, err)

El valor no modificado de err se pasa a HandleErrorf y se pasa un puntero a err . Verificamos si err es nil (usando el puntero). Si no, formateamos la cadena, usando el valor no modificado de err . Luego establecemos err en el valor de error formateado, usando el puntero.

@Merovius Sin embargo, la propuesta realmente es solo una macro de azúcar de sintaxis, por lo que terminará siendo sobre lo que la gente piensa que se ve mejor o que causará menos problemas. Si crees que no, por favor explícamelo. Por eso estoy a favor, personalmente. Es una buena adición sin agregar palabras clave desde mi perspectiva.

@ianlancetaylor , creo que @josharian tiene razón: el valor "no modificado" de err es el valor en el momento en que defer se coloca en la pila, no el valor (presuntamente previsto) de err establecido por try antes de regresar.

El otro problema que tengo con try es que hace que sea mucho más fácil para las personas volcar más y lógica en una sola línea. Este es mi principal problema con la mayoría de los otros idiomas, es que hacen que sea muy fácil poner como 5 expresiones en una sola línea, y no quiero que eso pase.

this := try(to()).parse().this
that := try(this.easily())

^^ incluso esto es francamente horrible. La primera línea, tengo que saltar de un lado a otro haciendo coincidir los padres en mi cabeza. Incluso la segunda línea, que en realidad es bastante simple... es realmente difícil de leer.
Las funciones anidadas son difíciles de leer.

parser, err := to()
if err != nil {
    return err
}
this := parser.parse().this
that, err := this.easily()
if err != nil {
    return err
}

^^ Esto es mucho más fácil y mejor en mi opinión. Es súper simple y claro. sí, son muchas más líneas de código, no me importa. Es muy obvio.

@bcmills @josharian Ah, por supuesto, gracias. Entonces tendría que ser

defer func() { fmt.HandleErrorf(&err, “foobar: %v”, err) }()

No tan agradable Tal vez fmt.HandleErrorf debería pasar implícitamente el valor del error como el último argumento después de todo.

Este problema ha recibido muchos comentarios muy rápidamente, y me parece que muchos de ellos repiten comentarios que ya se han hecho. Por supuesto, siéntase libre de comentar, pero me gustaría sugerir amablemente que si desea reiterar un punto que ya se ha planteado, lo haga utilizando los emojis de GitHub, en lugar de repetir el punto. Gracias.

@ianlancetaylor si fmt.HandleErrorf envía err como el primer argumento después del formato, entonces la implementación será mejor y el usuario podrá hacer referencia a ella mediante %[1]v siempre.

@natefinch Absolutamente de acuerdo.

Me pregunto si un enfoque de estilo oxidado sería más aceptable.
Tenga en cuenta que esta no es una propuesta solo pensando en ella ...

this := to()?.parse().this
that := this.easily()?

Al final, creo que esto es mejor, pero (podría usar un ! o algo más también...), pero aún así no soluciona bien el problema del manejo de errores.


por supuesto, rust también tiene try() más o menos así, pero... el otro estilo rust.

No es _objetivamente_ mejor que if err != nil { return err } , solo menos para escribir.

Hay una diferencia objetiva entre los dos: try(Foo()) es una expresión. Para algunos, esa diferencia es una desventaja (la crítica try(strconv.Atoi(x))+try(strconv.Atoi(y)) ). Para otros, esa diferencia es una ventaja por la misma razón. Todavía no es objetivamente mejor o peor, pero tampoco creo que la diferencia deba esconderse debajo de la alfombra y afirmar que es "solo menos para escribir" no le hace justicia a la propuesta.

Esta es una de las principales razones por las que me gusta esta sintaxis; me permite usar una función de devolución de errores como parte de una expresión más grande sin tener que nombrar todos los resultados intermedios. En algunas situaciones, nombrarlos es fácil, pero en otras no hay un nombre particularmente significativo o no redundante para darles, en cuyo caso prefiero no darles ningún nombre.

@MrTravisB

Exactamente mi punto. Esta propuesta intenta resolver un caso de uso singular en lugar de proporcionar una herramienta para resolver el problema más amplio del manejo de errores. Creo que la pregunta fundamental es exactamente lo que usted señaló.

¿Qué dije específicamente que es exactamente tu punto? Más bien me parece que básicamente malinterpretaste mi punto si crees que estamos de acuerdo.

En mi opinión y experiencia, este caso de uso es una pequeña minoría y no garantiza una sintaxis abreviada.

En la fuente de Go hay miles de casos que podrían ser manejados por try listos para usar, incluso si no hubiera forma de agregar contexto a los errores. Si es menor, sigue siendo una causa común de queja.

Además, el enfoque de usar aplazar para manejar errores tiene problemas en el sentido de que asume que desea manejar todos los errores posibles de la misma manera. las declaraciones diferidas no se pueden cancelar.

De manera similar, el enfoque de usar + para manejar la aritmética asume que no desea restar, por lo que no lo hace si no lo hace. La pregunta interesante es si el contexto de error de todo el bloque representa al menos un patrón común.

¿Qué pasa si quiero un manejo de errores diferente para los errores que pueden ser devueltos por foo() frente a foo2()?

Nuevamente, entonces no usa try . Entonces no ganas nada con try , pero tampoco pierdes nada.

@cpuguy83

Me pregunto si un enfoque de estilo oxidado sería más aceptable.

La propuesta presenta un argumento en contra de esto.

En este punto, creo que tener try{}catch{} es más legible :upside_down_face:

  1. El uso de importaciones con nombre para dar la vuelta a los casos de esquina defer no solo es horrible para cosas como godoc, sino que, lo que es más importante, es muy propenso a errores. No me importa, puedo envolver todo con otros func() para solucionar el problema, son solo más cosas que debo tener en cuenta, creo que fomenta una "mala práctica".
  2. Nadie te obligará a probar.

    Eso no significa que sea una buena solución, estoy señalando que la idea actual tiene una falla en el diseño y pido que se aborde de una manera que sea menos propensa a errores.

  3. Creo que ejemplos como try(try(try(to()).parse().this)).easily()) no son realistas, esto ya podría hacerse con otras funciones y creo que sería justo que aquellos que revisan el código pidan que se divida.
  4. ¿Qué pasa si tengo 3 lugares que pueden fallar y quiero envolver cada lugar por separado? try() hace que esto sea muy difícil, de hecho, try() ya está desalentando los errores de ajuste dada la dificultad, pero aquí hay un ejemplo de lo que quiero decir:

    func before() error {
      x, err := foo()
      if err != nil {
        wrap(err, "error on foo")
      }
      y, err := bar(x)
      if err != nil {
        wrapf(err, "error on bar with x=%v", x)
      }
      fmt.Println(y)
      return nil
    }
    
    func after() (err error) {
      defer fmt.HandleErrorf(&err, "something failed but I don't know where: %v", err)
      x := try(foo())
      y := try(bar(x))
      fmt.Println(y)
      return nil
    }
    
  5. Nuevamente, entonces no usa try . Entonces no ganas nada con try , pero tampoco pierdes nada.

    Digamos que es una buena práctica envolver los errores con un contexto útil, try() se consideraría una mala práctica porque no agrega ningún contexto. Esto significa que try() es una característica que nadie quiere usar y se convierte en una característica que se usa tan raramente que bien podría no haber existido.

    En lugar de simplemente decir "bueno, si no te gusta, no lo uses y cállate" (así es como se lee), creo que sería mejor tratar de abordar lo que muchos de los usuarios consideran una falla. en el diseño. ¿Podemos discutir en cambio qué podría modificarse del diseño propuesto para que nuestra preocupación se maneje de una mejor manera?

@boomlinde El punto en el que estamos de acuerdo es que esta propuesta está tratando de resolver un caso de uso menor y el hecho de que "si no lo necesita, no lo use" es el argumento principal para promover ese punto. Como dijo @elagergren-spideroak, ese argumento no funciona porque incluso si no quiero usarlo, otros lo harán, lo que me obliga a usarlo. Según la lógica de su argumento, Go también debería tener una declaración ternaria. Y si no le gustan las declaraciones ternarias, no las use.

Descargo de responsabilidad: creo que Go debería tener una declaración ternaria, pero dado que el enfoque de Go para las características del lenguaje es no introducir características que podrían hacer que el código sea más difícil de leer de lo que no debería.

Se me ocurre otra cosa: veo muchas críticas basadas en la idea de que tener try podría alentar a los desarrolladores a manejar los errores sin cuidado. Pero en mi opinión, esto es, en todo caso, más cierto en el lenguaje actual; el modelo de manejo de errores es lo suficientemente molesto como para alentar a uno a tragar o ignorar algunos errores para evitarlo. Por ejemplo, he escrito cosas como esta un par de veces:

func exists(filename string) bool {
  _, err := os.Stat(filename)
  return err == nil
}

para poder escribir if exists(...) { ... } , aunque este código ignora silenciosamente algunos posibles errores. Si tuviera try , probablemente no me molestaría en hacer eso y simplemente devolvería (bool, error) .

Siendo caótico aquí, lanzaré la idea de agregar una segunda función integrada llamada catch que recibirá una función que recibe un error y devuelve un error sobrescrito, luego si un catch posterior se llama sobrescribiría el controlador. por ejemplo:

func catch(handler func(err error) error) {
  // .. impl ..
}

Ahora, esta función incorporada también será una función similar a una macro que manejaría el siguiente error que devolverá try esta manera:

func wrapf(format string, ...values interface{}) func(err error) error {
  // user defined
  return func(err error) error {
    return fmt.Errorf(format + ": %v", ...append(values, err))
  }
}
func sample() {
  catch(wrapf("something failed in foo"))
  try(foo()) // "something failed in foo: <error>"
  x := try(foo2()) // "something failed in foo: <error>"
  // Subsequent calls for catch overwrite the handler
  catch(wrapf("something failed in bar with x=%v", x))
  try(bar(x)) // "something failed in bar with x=-1: <error>"
}

Esto es bueno porque puedo envolver errores sin defer que puede ser propenso a errores a menos que usemos valores devueltos con nombre o envolvamos con otra función, también es bueno porque defer agregaría el mismo controlador de errores para todos los errores incluso si quiero manejar 2 de ellos de manera diferente. También puedes usarlo como mejor te parezca, por ejemplo:

func foo(a, b string) (int64, error) {
  return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func withContext(a, b string) (int64, error) {
  catch(func (err error) error {
    return fmt.Errorf("can't parse a: %s, b: %s, err: %v", a, b, err)
  })
  return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func moreExplicitContext(a, b string) (int64, error) {
  catch(func (err error) error {
    return fmt.Errorf("can't parse a: %s, err: %v", a, err)
  })
  x := try(strconv.Atoi(a))
  catch(func (err error) error {
    return fmt.Errorf("can't parse b: %s, err: %v", b, err)
  })
  y := try(strconv.Atoi(b))
  return x + y
}
func withHelperWrapf(a, b string) (int64, error) {
  catch(wrapf("can't parse a: %s", a))
  x := try(strconv.Atoi(a))
  catch(wrapf("can't parse b: %s", b))
  y := try(strconv.Atoi(b))
  return x + y
}
func before(a, b string) (int64, error) {
  x, err := strconv.Atoi(a)
  if err != nil {
    return 0,  fmt.Errorf("can't parse a: %s, err: %v", a, err)
  }
  y, err := strconv.Atoi(b)
  if err != nil {
    return 0,  fmt.Errorf("can't parse b: %s, err: %v", b, err)
  }
  return x + y
}

Y aún en el estado de ánimo caótico (para ayudarlo a empatizar) Si no le gusta catch , no tiene que usarlo.

Ahora... Realmente no lo digo en serio en la última oración, pero parece que no es útil para la discusión, en mi opinión es muy agresivo.
Aún así, si seguimos esta ruta, creo que también podríamos tener try{}catch(error err){} en su lugar :stuck_out_tongue:

Ver también #27519 - el modelo de error #id/catch

Nadie te obligará a probar.

Ignorando la ligereza, creo que es una forma bastante tonta de descartar una crítica de diseño.

Lo siento, simplista no era mi intención.

Lo que estoy tratando de decir es que try no pretende ser una solución al 100%. Hay varios paradigmas de manejo de errores que try no maneja bien. Por ejemplo, si necesita agregar un contexto dependiente del sitio de llamadas al error. Siempre puede volver a usar if err != nil { para manejar esos casos más complicados.

Ciertamente es un argumento válido que try no puede manejar X, para varias instancias de X. Pero a menudo manejar el caso X significa hacer que el mecanismo sea más complicado. Aquí hay una compensación, manejar X por un lado pero complicar el mecanismo para todo lo demás. Lo que hacemos depende de cuán común es X y cuánta complicación requeriría manejar X.

Entonces, con "Nadie te va a obligar a probar", quiero decir que creo que el ejemplo en cuestión está en el 10%, no en el 90%. Esa afirmación ciertamente está sujeta a debate, y estoy feliz de escuchar contraargumentos. Pero eventualmente tendremos que trazar la línea en algún lugar y decir "sí, try no manejará ese caso. Tendrá que usar el manejo de errores al viejo estilo. Lo siento".

No es que "intentar no pueda manejar este caso específico de manejo de errores" ese es el problema, es "intentar lo alienta a no envolver sus errores". La idea check-handle lo obligó a escribir una declaración de devolución, por lo que escribir un ajuste de error fue bastante trivial.

Según esta propuesta, debe usar un retorno con nombre con defer , que no es intuitivo y parece muy complicado.

La idea check-handle lo obligó a escribir una declaración de devolución, por lo que escribir un ajuste de error fue bastante trivial.

Eso no es cierto: en el borrador de diseño, cada función que devuelve un error tiene un controlador predeterminado que simplemente devuelve el error.

Sobre la base del punto travieso de @Goodwine , realmente no necesita funciones separadas como HandleErrorf si tiene una sola función de puente como

func handler(err *error, handle func(error) error) {
  // nil handle is treated as the identity
  if *err != nil && handle != nil {
    *err = handle(*err)
  }
}

que usarías como

defer handler(&err, func(err error) error {
  if errors.Is(err, io.EOF) {
    return nil
  }
  return fmt.Errorf("oops: %w", err)
})

Podría hacer que handler en sí mismo sea un elemento integrado semimágico como try .

Si es mágico, podría tomar implícitamente su primer argumento, lo que permitiría usarlo incluso en funciones que no mencionan su retorno error , eliminando uno de los aspectos menos afortunados de la propuesta actual y haciéndolo menos quisquilloso y propenso a errores para decorar errores. Por supuesto, eso no reduce mucho el ejemplo anterior:

defer handler(func(err error) error {
  if errors.Is(err, io.EOF) {
    return nil
  }
  return fmt.Errorf("oops: %w", err)
})

Si fuera mágico de esta manera, tendría que ser un error de tiempo de compilación si se usara en cualquier lugar excepto como argumento para defer . Podría ir un paso más allá y hacer que diferir implícitamente, pero defer handler se lee bastante bien.

Dado que usa defer , podría llamar a su función handle cada vez que se devolviera un error no nulo, lo que lo hace útil incluso sin try ya que podría agregar un

defer handler(wrapErrWithPackageName)

en la parte superior a fmt.Errorf("mypkg: %w", err) todo.

Eso le da mucho de la antigua propuesta check / handle pero funciona con aplazamiento natural (y explícito) mientras elimina la necesidad, en la mayoría de los casos, de nombrar explícitamente un err retorno. Como try , es una macro relativamente sencilla que (me imagino) podría implementarse por completo en la interfaz.

Eso no es cierto: en el borrador de diseño, cada función que devuelve un error tiene un controlador predeterminado que simplemente devuelve el error.

Mala mía, tienes razón.

Quiero decir que creo que el ejemplo en cuestión está en el 10%, no en el 90%. Esa afirmación ciertamente está sujeta a debate, y estoy feliz de escuchar contraargumentos. Pero eventualmente tendremos que trazar la línea en algún lugar y decir "sí, intentar no manejará ese caso. Tendrá que usar el manejo de errores al viejo estilo. Lo siento".

De acuerdo, mi opinión es que esta línea debe dibujarse al verificar EOF o similar, no al envolver. Pero tal vez si los errores tuvieran más contexto, esto ya no sería un problema.

¿Podría try() corregir automáticamente los errores con un contexto útil para la depuración? Por ejemplo, si xerrors se convierte en errors , los errores deberían tener algo parecido a un seguimiento de pila que try() podría agregar, ¿no? Si es así, tal vez eso sería suficiente 🤔

Si los objetivos son (leer https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md):

  • eliminar el repetitivo
  • cambios mínimos de idioma
  • cubriendo los "escenarios más comunes"
  • agregando muy poca complejidad al lenguaje

Tomaría la sugerencia de darle un ángulo y permitir la migración de código de "pequeños pasos" para todos los miles de millones de líneas de código que existen.

en lugar de lo sugerido:

func printSum(a, b string) error {
        defer fmt.HandleErrorf(&err, "sum %s %s: %v", a,b, err) 
        x := try(strconv.Atoi(a))
        y := try(strconv.Atoi(b))
        fmt.Println("result:", x + y)
        return nil
}

Podemos:

func printSum(a, b string) error {
        var err ErrHandler{HandleFunc : twoStringsErr("printSum",a,b)} 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        return nil
}

¿Qué ganaríamos?
twoStringsErr se puede incorporar a printSum, o un controlador general que sabe cómo capturar errores (en este caso con 2 parámetros de cadena), por lo que si tengo las mismas firmas de funciones repetidas utilizadas en muchas de mis funciones, no necesito reescribir el controlador cada una hora
De la misma manera, puedo extender el tipo ErrHandler de la siguiente manera:

type ioErrHandler ErrHandler
func (i ErrHandler) Handle() ...{

}

o

type parseErrHandler ErrHandler
func (p parseErrHandler) Handle() ...{

}

o

type str2IntErrHandler ErrHandler
func (s str2IntErrHandler) Handle() ...{

}

y usa esto en todo mi código:

func printSum(a, b string) error {
        var pErr str2IntErrHandler 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        return nil
}

Por lo tanto, la necesidad real sería desarrollar un disparador cuando err.Error se establece en no nulo
Usando este método también podemos:

func (s str2IntErrHandler) Handle() bool{
   **return false**
}

Lo que le diría a la función de llamada que continúe en lugar de regresar

Y use diferentes controladores de errores en la misma función:

func printSum(a, b string) error {
        var pErr str2IntErrHandler 
        var oErr overflowError 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        totalAsByte,oErr := sumBytes(x,y)
        sunAsByte,oErr := subtractBytes(x,y)
        return nil
}

etc

Repasando los goles otra vez

  • eliminar el repetitivo - hecho
  • cambios mínimos de idioma - hecho
  • cubriendo los "escenarios más comunes" - más que la OMI sugerida
  • agregando muy poca complejidad al lenguaje - sone
    Además: migración de código más sencilla desde
x, err := strconv.Atoi(a)

para

x, err.Error := strconv.Atoi(a)

y, de hecho, mejor legibilidad (en mi opinión, de nuevo)

@guybrand eres el último adherente a este tema recurrente (que me gusta).

Consulte https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -themes

@guybrand Eso parece una propuesta completamente diferente; Creo que deberías archivarlo como un problema propio para que este pueda centrarse en discutir la propuesta de @griesemer .

@natefinch está de acuerdo. Creo que esto está más orientado a mejorar la experiencia al escribir Go en lugar de optimizar para la lectura. Me pregunto si las macros o fragmentos de IDE podrían resolver el problema sin que esto se convierta en una característica del lenguaje.

@Buen vino

Digamos que es una buena práctica envolver los errores con un contexto útil, try() se consideraría una mala práctica porque no agrega ningún contexto. Esto significa que try() es una característica que nadie quiere usar y se convierte en una característica que se usa tan raramente que bien podría no haber existido.

Como se indica en la propuesta (y se muestra en el ejemplo), try no impide fundamentalmente que agregue contexto. Diría que la forma en que se propone, agregar contexto a los errores, es completamente ortogonal. Esto se aborda específicamente en las preguntas frecuentes de la propuesta.

Reconozco que try no será útil dentro de una sola función si hay una multitud de contextos diferentes que desea agregar a diferentes errores de llamadas a funciones. Sin embargo, también creo que algo en la vena general de HandleErrorf cubre una gran área de uso porque no es inusual agregar solo el contexto de toda la función a los errores.

En lugar de simplemente decir "bueno, si no te gusta, no lo uses y cállate" (así es como se lee), creo que sería mejor tratar de abordar lo que muchos de los usuarios consideran una falla. en el diseño.

Si es así como se lee, pido disculpas. Mi punto no es que debas fingir que no existe si no te gusta. Es que es obvio que hay casos en los que try serían inútiles y que no deberías usarlo en esos casos, lo que para esta propuesta creo que logra un buen equilibrio entre KISS y la utilidad general. No pensé que no estaba claro en ese punto.

Gracias a todos por los prolíficos comentarios hasta ahora; esto es muy informativo.
Aquí está mi intento de un resumen inicial, para tener una mejor idea de los comentarios. Disculpas de antemano por cualquier persona que haya perdido o tergiversado; Espero haber entendido bien la esencia general.

0) En el lado positivo, @rasky , @adg , @eandre , @dpinela y otros expresaron explícitamente su felicidad por la simplificación del código que proporciona try .

1) La preocupación más importante parece ser que try no fomenta un buen estilo de manejo de errores, sino que promueve la "salida rápida". ( @agnivade , @peterbourgon , @politician , @a8m , @eandre , @prologic , @kungfusheep , @cpuguy y otros han expresado su preocupación por esto).

2) A muchas personas no les gusta la idea de una función integrada o la sintaxis de la función que viene con ella porque oculta un return . Sería mejor utilizar una palabra clave. ( @sheerun , @Redundancy , @dolmen , @komuw , @RobertGrantEllis , @elagergren-spideroak). try también puede pasarse por alto fácilmente (@peterbourgon), especialmente porque puede aparecer en expresiones que pueden anidarse arbitrariamente. A @natefinch le preocupa que try haga que sea "demasiado fácil volcar demasiado en una línea", algo que generalmente tratamos de evitar en Go. Además, la compatibilidad con IDE para enfatizar try puede no ser suficiente (@dominikh); try necesita "sostenerse por sí mismo".

3) Para algunos, el status quo de declaraciones explícitas de if no es un problema, están contentos con eso ( @bitfield , @marwan-at-work, @natefinch). Es mejor tener una sola forma de hacer las cosas (@gbbr); y las declaraciones if explícitas son mejores que las return implícitas ( @DavexPro , @hmage , @prologic , @natefinch).
Del mismo modo, a @mattn le preocupa el "enlace implícito" del resultado del error a try : la conexión no es explícitamente visible en el código.

4) El uso try hará que sea más difícil depurar el código; por ejemplo, puede ser necesario volver a escribir una expresión try en una declaración if solo para que se puedan insertar declaraciones de depuración ( @deanveloper , @typeless , @networkimprov , otros).

5) Existe cierta preocupación sobre el uso de devoluciones con nombre ( @buchanae , @adg).

Varias personas han brindado sugerencias para mejorar o modificar la propuesta:

6) Algunos han retomado la idea de un controlador de errores opcional (@beoran) o una cadena de formato proporcionada a try ( @unexge , @a8m , @eandre , @gotwarlost) para fomentar un buen manejo de errores.

7) @pierrec sugirió que gofmt podría formatear adecuadamente las expresiones try para hacerlas más visibles.
Alternativamente, se podría hacer que el código existente sea más compacto al permitir que gofmt formatee declaraciones if para verificar si hay errores en una línea (@zeebo).

8) @marwan-at-work argumenta que try simplemente cambia el manejo de errores de if declaraciones a try expresiones. En cambio, si realmente queremos resolver el problema, Go debería "poseer" el manejo de errores haciéndolo verdaderamente implícito. El objetivo debería ser hacer que el manejo (adecuado) de errores sea más simple y que los desarrolladores sean más productivos (@cpuguy).

9) Finalmente, a algunas personas no les gusta el nombre try ( @beoran , @HiImJC , @dolmen) o preferirían un símbolo como ? ( @twisted1919 , @leaxoy , otros) .

Algunos comentarios sobre esta retroalimentación (numerados en consecuencia):

0) ¡Gracias por los comentarios positivos! :-)

1) Sería bueno aprender más sobre esta preocupación. El estilo de codificación actual que usa declaraciones if para probar errores es tan explícito como puede ser. Es muy fácil agregar información adicional a un error, de forma individual (para cada if ). A menudo tiene sentido manejar todos los errores detectados en una función de manera uniforme, lo que se puede hacer con defer ; esto ya es posible ahora. Es el hecho de que ya tenemos todas las herramientas para un buen manejo de errores en el lenguaje, y el problema de una construcción de controlador que no es ortogonal a defer , lo que nos llevó a dejar de lado un nuevo mecanismo únicamente para aumentar los errores. .

2) Por supuesto, existe la posibilidad de utilizar una palabra clave o una sintaxis especial en lugar de una incorporada. Una nueva palabra clave no será compatible con versiones anteriores. Un nuevo operador podría, pero parece incluso menos visible. La propuesta detallada analiza los diversos pros y contras en detalle. Pero tal vez estemos juzgando mal esto.

3) El motivo de esta propuesta es que la gestión de errores (específicamente el código repetitivo asociado) se mencionó como un problema importante en Go (junto a la falta de genéricos) por parte de la comunidad de Go. Esta propuesta aborda directamente la preocupación repetitiva. No hace más que resolver el caso más básico porque cualquier caso más complejo se maneja mejor con lo que ya tenemos. Entonces, aunque un buen número de personas están contentas con el statu quo, hay un contingente (probablemente) igualmente grande de personas a las que les encantaría un enfoque más simplificado como try , sabiendo que esto es "simplemente" azúcar sintáctica.

4) El punto de depuración es una preocupación válida. Si es necesario agregar código entre la detección de un error y un return , tener que reescribir una expresión try en una instrucción if podría ser molesto.

5) Valores devueltos con nombre: el documento detallado analiza esto en detalle. Si esta es la principal preocupación sobre esta propuesta, creo que estamos en un buen lugar.

6) Argumento de controlador opcional para try : el documento detallado también analiza esto. Consulte la sección sobre iteraciones de diseño.

7) Usar gofmt para dar formato a las expresiones try modo que sean más visibles sería una opción. Pero quitaría algunos de los beneficios de try cuando se usa en una expresión.

8) Hemos considerado ver el problema desde el punto de vista del manejo de errores ( handle ) en lugar de desde el punto de vista de la prueba de errores ( try ). Específicamente, consideramos brevemente solo introducir la noción de un controlador de errores (similar al borrador de diseño original presentado en el Gophercon del año pasado). La idea era que si (y solo si) se declara un controlador, en asignaciones de valores múltiples donde el último valor es del tipo error , ese valor simplemente puede dejarse de lado en una asignación. El compilador verificaría implícitamente si no es nulo y, de ser así, se ramificaría al controlador. Eso haría que el manejo explícito de errores desapareciera por completo y alentaría a todos a escribir un controlador en su lugar. Esto parecía un enfoque extremo porque sería completamente implícito: el hecho de que ocurra un control sería invisible.

9) ¿Puedo sugerir que no nos deshagamos del nombre en este punto? Una vez que se resuelvan todas las demás preocupaciones, es un mejor momento para afinar el nombre.

Esto no quiere decir que las preocupaciones no sean válidas: las respuestas anteriores simplemente indican nuestro pensamiento actual. En el futuro, sería bueno comentar sobre nuevas inquietudes (o nueva evidencia que respalde estas inquietudes); solo reiterar lo que ya se ha dicho no nos brinda más información.

Y finalmente, parece que no todos los que comentan sobre el tema han leído el documento detallado. Por favor, hágalo antes de comentar para evitar repetir lo que ya se ha dicho. Gracias.

Este no es un comentario sobre la propuesta, sino un informe de error tipográfico. No se solucionó desde que se publicó la propuesta completa, así que pensé en mencionarlo:

func try(t1 T1, t1 T2, … tn Tn, te error) (T1, T2, … Tn)

debiera ser:

func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)

¿Valdría la pena analizar el código Go disponible abiertamente para las declaraciones de verificación de errores para tratar de averiguar si la mayoría de las verificaciones de errores son realmente repetitivas o si, en la mayoría de los casos, varias verificaciones dentro de la misma función agregan información contextual diferente? La propuesta tendría mucho sentido para el primer caso, pero no ayudaría al segundo. En el último caso, las personas continuarán usando if err != nil o dejarán de agregar contexto adicional, usarán try() y recurrirán a agregar contexto de error común por función que, en mi opinión, sería dañino. Con las próximas funciones de valores de error, creo que esperamos que las personas envuelvan los errores con más información con más frecuencia. Probablemente entendí mal la propuesta, pero AFAIU, esto ayuda a reducir el modelo solo cuando todos los errores de una sola función deben envolverse exactamente de una manera y no ayuda si una función trata con cinco errores que podrían necesitar envolverse de manera diferente. No estoy seguro de qué tan comunes son estos casos en la naturaleza (bastante comunes en la mayoría de mis proyectos), pero me preocupa que try() pueda alentar a las personas a usar envoltorios comunes por función, incluso cuando tendría sentido envolver diferentes errores. diferentemente.

Solo un comentario rápido respaldado con datos de un pequeño conjunto de muestra:

Proponemos una nueva función integrada llamada try, diseñada específicamente para eliminar las declaraciones repetitivas if típicamente asociadas con el manejo de errores en Go.

Si este es el problema central que se resuelve con esta propuesta, encuentro que este "repetitivo" solo representa ~ 1.4% de mi código en docenas de proyectos de código abierto disponibles públicamente por un total de ~ 60k SLOC.

¿Curioso si alguien más tiene estadísticas similares?

En una base de código mucho más grande como Go, con un total de alrededor de ~ 1,6 millones de SLOC, esto equivale a aproximadamente ~ 0,5% de la base de código con líneas como if err != nil .

¿Es este realmente el problema más impactante para resolver con Go 2?

Muchas gracias @griesemer por tomarse el tiempo de revisar las ideas de todos y proporcionar pensamientos de forma explícita. Creo que realmente ayuda con la percepción de que la comunidad está siendo escuchada en el proceso.

  1. @pierrec sugirió que gofmt podría formatear expresiones de prueba adecuadamente para hacerlas más visibles.
    Alternativamente, se podría hacer que el código existente sea más compacto al permitir que gofmt formatee declaraciones if que verifiquen errores en una línea (@zeebo).
  1. Usar gofmt para dar formato a las expresiones try modo que sean más visibles sería sin duda una opción. Pero quitaría algunos de los beneficios de try cuando se usa en una expresión.

Estos son pensamientos valiosos acerca de requerir gofmt para formatear try , pero estoy interesado si hay algún pensamiento en particular sobre gofmt que permita la verificación de declaraciones if el error de ser una línea. La propuesta se agrupó con el formato de try , pero creo que es algo completamente ortogonal. Gracias.

@griesemer gracias por el increíble trabajo de revisar todos los comentarios y responder a la mayoría, si no a todos, los comentarios 🎉

Una cosa que no se abordó en sus comentarios fue la idea de usar la parte de herramientas/examen del lenguaje Go para mejorar la experiencia de manejo de errores, en lugar de actualizar la sintaxis de Go.

Por ejemplo, con el aterrizaje del nuevo LSP ( gopls ), parece un lugar perfecto para analizar la firma de una función y ocuparse del manejo de errores repetitivo para el desarrollador, con el envoltorio y la verificación adecuados también.

@griesemer Estoy seguro de que esto no está bien pensado, pero traté de modificar su sugerencia más cerca de algo con lo que me sentiría cómodo aquí: https://www.reddit.com/r/golang/comments/bwvyhe /proposal_a_builtin_go_error_check_function_try/eq22bqa?utm_source=share&utm_medium=web2x

@zeebo Sería fácil hacer que el formato $ gofmt sea if err != nil { return ...., err } en una sola línea. Presumiblemente, solo sería para este tipo específico de patrón if , no para todas las declaraciones "cortas" if .

Del mismo modo, existía la preocupación de que try fuera invisible porque está en la misma línea que la lógica empresarial. Tenemos todas estas opciones:

Estilo actual:

a, b, c, ... err := BusinessLogic(...)
if err != nil {
   return ..., err
}

Una línea if :

a, b, c, ... err := BusinessLogic(...)
if err != nil { return ..., err }

try en una línea separada (!):

a, b, c, ... err := BusinessLogic(...)
try(err)

try según lo propuesto:

a, b, c := try(BusinessLogic(...))

La primera y la última línea parecen las más claras (para mí), especialmente una vez que se usa para reconocer try como lo que es. Con la última línea, se verifica explícitamente un error, pero dado que (generalmente) no es la acción principal, está un poco más en segundo plano.

@marwan-at-work No estoy seguro de lo que está proponiendo que las herramientas hagan por usted. ¿Sugieres que oculten el manejo de errores de alguna manera?

@dpinela

@guybrand Eso parece una propuesta completamente diferente; Creo que deberías archivarlo como un problema propio para que este pueda centrarse en discutir la propuesta de @griesemer .

En mi opinión, mi propuesta difiere solo en la sintaxis, lo que significa:

  • Los objetivos son similares en contenido y prioridad.
  • La idea de capturar cada error dentro de su propia línea y, en consecuencia (si no es nulo), salir de la función mientras se pasa a través de una función de controlador es similar (pseudo asm: es un "jnz" y una "llamada").
  • Esto incluso significa que la cantidad de líneas en el cuerpo de una función (sin el aplazamiento) y el flujo se verían exactamente iguales (y, en consecuencia, AST probablemente también resultaría igual)

por lo que la diferencia principal es si estamos envolviendo la llamada de función original con try(func()) que siempre analizaría la última variable para jnz la llamada o usaría el valor de retorno real para hacer eso.

Sé que parece diferente, pero en realidad es muy similar en concepto.
Por otro lado, si realiza el intento habitual ... captura en muchos lenguajes similares a c, sería una implementación muy diferente, una legibilidad diferente, etc.

Sin embargo, pienso seriamente en escribir una propuesta, gracias por la idea.

@griesemer

No estoy seguro de lo que está proponiendo que las herramientas hagan por usted. ¿Sugieres que oculten el manejo de errores de alguna manera?

Todo lo contrario: estoy sugiriendo que gopls puede opcionalmente escribir el error de manejo repetitivo para usted.

Como mencionaste en tu último comentario:

El motivo de esta propuesta es que la gestión de errores (específicamente el código repetitivo asociado) se mencionó como un problema importante en Go (junto con la falta de genéricos) por parte de la comunidad de Go.

Entonces, el corazón del problema es que el programador termina escribiendo una gran cantidad de código repetitivo. Así que el problema es sobre escribir, no sobre leer. Por lo tanto, mi sugerencia es: deje que la computadora (herramientas/gopls) haga la escritura para el programador analizando la firma de la función y colocando las cláusulas de manejo de errores adecuadas.

Por ejemplo:

// user begins to write this function: 
func openFile(path string) ([]byte, error) {
  file, err := os.Open(path)
  defer file.Close()
  bts, err := ioutil.ReadAll(file)
  return bts, nil
}

Luego, el usuario activa la herramienta, tal vez simplemente guardando el archivo (similar a cómo funciona normalmente gofmt/goimports) y gopls miraría esta función, analizaría su firma de retorno y aumentaría el código para que sea esto:

// user has triggered the tool (by saving the file, or code action)
func openFile(path string) ([]byte, error) {
  file, err := os.Open(path)
  if err != nil {
    return nil, fmt.Errorf("openFile: %w", err)
  }
  defer file.Close()
  bts, err := ioutil.ReadAll(file)
  if err != nil {
    return nil, fmt.Errorf("openFile: %w", err)
  }
  return bts, nil
}

De esta manera, obtenemos lo mejor de ambos mundos: obtenemos la legibilidad/explicidad del sistema de manejo de errores actual, y el programador no escribió ningún texto estándar de manejo de errores. Aún mejor, el usuario puede seguir adelante y modificar los bloques de manejo de errores más adelante para tener un comportamiento diferente: gopls puede entender que el bloque existe y no lo modificaría.

¿Cómo sabría la herramienta que tenía la intención de manejar el err más adelante en la función en lugar de regresar antes? Aunque es raro, pero el código que he escrito no obstante.

Pido disculpas si esto se ha mencionado antes, pero no pude encontrar ninguna mención al respecto.

try(DoSomething()) me parece bien y tiene sentido: el código está tratando de hacer algo. try(err) , OTOH, se siente un poco fuera de lugar, semánticamente hablando: ¿cómo se intenta un error? En mi opinión, uno podría _probar_ o _verificar_ un error, pero _intentarlo_ no me parece correcto.

Me doy cuenta de que permitir try(err) es importante por razones de consistencia: supongo que sería extraño si try(DoSomething()) funcionara, pero err := DoSomething(); try(err) no. Aún así, parece que try(err) se ve un poco extraño en la página. No se me ocurre ninguna otra función integrada que se pueda hacer para que se vea tan extraña tan fácilmente.

No tengo sugerencias concretas al respecto, pero sin embargo quería hacer esta observación.

@griesemer Gracias. De hecho, la propuesta era solo para return , pero sospecho que sería bueno permitir que cualquier declaración individual sea una sola línea. Por ejemplo, en una prueba uno podría, sin cambios en la biblioteca de prueba, tener

if err != nil { t.Fatal(err) }

La primera y la última línea parecen las más claras (para mí), especialmente una vez que uno se acostumbra a reconocer try como lo que es. Con la última línea, se verifica explícitamente un error, pero dado que (generalmente) no es la acción principal, está un poco más en segundo plano.

Con la última línea, se oculta parte del costo. Si desea anotar el error, que creo que la comunidad ha dicho verbalmente que es la mejor práctica deseada y debe alentarse, habría que cambiar la firma de la función para nombrar los argumentos y esperar que se aplique un solo defer a cada salida en el cuerpo de la función, de lo contrario try no tiene valor; tal vez incluso negativo debido a su facilidad.

No tengo nada más que añadir que creo que no se haya dicho ya.


No vi cómo responder a esta pregunta del documento de diseño. Qué hace este código:

func foo() (err error) {
    src := try(getReader())
    if src != nil {
        n, err := src.Read(nil)
        if err == io.EOF {
            return nil
        }
        try(err)
        println(n)
    }
    return nil
}

Mi entendimiento es que se desazúcar en

func foo() (err error) {
    tsrc, te := getReader()
    if err != nil {
        err = te
        return
    }
    src := tsrc

    if src != nil {
        n, err := src.Read(nil)
        if err == io.EOF {
            return nil
        }

        terr := err
        if terr != nil {
            err = terr
            return
        }

        println(n)
    }
    return nil
}

que falla al compilar porque err se sombrea durante un retorno desnudo. ¿Esto no compilaría? Si es así, es una falla muy sutil y no parece muy improbable que suceda. Si no, entonces está pasando algo más que un poco de azúcar.

@marwan-en-el-trabajo

Como mencionaste en tu último comentario:

El motivo de esta propuesta es que la gestión de errores (específicamente el código repetitivo asociado) se mencionó como un problema importante en Go (junto con la falta de genéricos) por parte de la comunidad de Go.

Entonces, el corazón del problema es que el programador termina escribiendo una gran cantidad de código repetitivo. Así que el problema es sobre escribir, no sobre leer.

Creo que en realidad es al revés: para mí, la mayor molestia con el modelo de manejo de errores actual no es tanto tener que escribirlo, sino más bien cómo dispersa el camino feliz de la función verticalmente en la pantalla, lo que lo hace más difícil de entender en un vistazo. El efecto es particularmente pronunciado en el código de E/S pesado, donde generalmente hay un bloque de repetitivo entre cada dos operaciones. Incluso una versión simplista de CopyFile requiere ~20 líneas, aunque en realidad solo realiza cinco pasos: fuente abierta, fuente cerrada aplazada, destino abierto, fuente copiada -> destino, destino cerrado.

Otro problema con la sintaxis actual es que, como señalé anteriormente, si tiene una cadena de operaciones, cada una de las cuales puede devolver un error, la sintaxis actual lo obliga a dar nombres a todos los resultados intermedios, incluso si prefiere dejar algunos anónimos. Cuando esto sucede, también perjudica la legibilidad porque tienes que pasar ciclos cerebrales analizando esos nombres, aunque no son muy informativos.

Me gusta try en una línea separada.
Y espero que pueda especificar la función handler forma independiente.

func try(error, optional func(error)error)
func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    try(err)

    handle := func(err error) error {
        tx.Rollback()
        return err
    }

    var res int64
    _, err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    try(err, handle)

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    try(err, handle)

    return tx.Commit()
}

@zeebo : Los ejemplos que di son traducciones 1:1. El primero (tradicional if ) no manejó el error, al igual que los demás. Si el primero manejó el error, y si este fuera el único lugar donde se verifica un error en una función, el primer ejemplo (usando un if ) podría ser la opción adecuada para escribir el código. Si hay varias verificaciones de errores, todas las cuales usan el mismo manejo de errores (envoltura), digamos porque todos agregan información sobre la función actual, se podría usar una instrucción defer para manejar todos los errores en un solo lugar. Opcionalmente, uno podría reescribir los if en try (o dejarlos en paz). Si hay varios errores para verificar, y todos manejan los errores de manera diferente (lo que podría ser una señal de que la preocupación de la función es demasiado amplia y es posible que deba dividirse), usar if 's es el camino a seguir. Sí, hay más de una forma de hacer lo mismo, y la elección correcta depende tanto del código como del gusto personal. Si bien nos esforzamos en Go por "una forma de hacer una cosa", por supuesto, este ya no es el caso, especialmente para las construcciones comunes. Por ejemplo, cuando una secuencia if - else - if se vuelve demasiado larga, a veces una switch puede ser más apropiada. A veces, una declaración de variable var x int expresa la intención mejor que x := 0 , y así sucesivamente (aunque no todo el mundo está contento con esto).

Con respecto a su pregunta sobre la "reescritura": No, no habría un error de compilación. Tenga en cuenta que la reescritura ocurre internamente (y puede ser más eficiente de lo que sugiere el patrón de código), y no es necesario que el compilador se queje de un retorno sombreado. En su ejemplo, declaró una variable local err en un ámbito anidado. try aún tendría acceso directo a la variable resultado err , por supuesto. La reescritura podría verse más así debajo de las sábanas.

[editado] PD: una mejor respuesta sería: try no es un retorno desnudo (aunque la reescritura lo parezca). Después de todo, uno da explícitamente try un argumento que contiene (o es) el error que se devuelve si no es nil . El error de sombra para los retornos desnudos es un error en la fuente (no en la traducción subyacente de la fuente. El compilador no necesita el error.

Si el tipo de retorno final de la función general no es de tipo error, ¿podemos entrar en pánico?

Hará que el incorporado sea más versátil (como satisfacer mi preocupación en #32219)

@pjebs Esto ha sido considerado y decidido en contra. Lea el documento de diseño detallado (que se refiere explícitamente a su problema sobre este tema).

También quiero señalar que try() se trata como una expresión aunque funciona como declaración de retorno. Sí, sé que try es una macro incorporada, pero supongo que la mayoría de los usuarios usarán esto como programación funcional.

func doSomething() (error, error, error, error, error) {
   ...
}
try(try(try(try(try(doSomething)))))

El diseño dice que exploraste usando panic en lugar de regresar con el error.

Estoy destacando una sutil diferencia:

Haga exactamente lo que establece su propuesta actual, excepto eliminar la restricción de que la función general debe tener un tipo de retorno final de tipo error .

Si no tiene un tipo de retorno final de error => pánico
Si usa try para declaraciones de variables a nivel de paquete => panic (elimina la necesidad de la convención MustXXX( ) )

Para las pruebas unitarias, un modesto cambio de idioma.

@mattn , dudo mucho que un número significativo de personas escriba un código como ese.

@pjebs , esa semántica (pánico si no hay un resultado de error en la función actual) es exactamente lo que está discutiendo el documento de diseño en https://github.com/golang/proposal/blob/master/design/32437-try-builtin. md#discusión.

Además, en un intento de hacer que try sea útil no solo dentro de las funciones con un resultado de error, la semántica de try dependía del contexto: si try se usara a nivel de paquete, o si se invocara dentro de una función sin un resultado de error, try entraría en pánico al encontrar un error. (Aparte, debido a esa propiedad, lo incorporado se llamó must en lugar de probar en esa propuesta). Hacer que try (o must) se comporte de esta manera sensible al contexto parecía natural y también bastante útil: permitiría la eliminación de muchas funciones auxiliares definidas por el usuario que se utilizan actualmente en expresiones de inicialización de variables a nivel de paquete. También abriría la posibilidad de usar pruebas unitarias de prueba a través del paquete de prueba.

Sin embargo, la sensibilidad contextual de try se consideró tensa: por ejemplo, el comportamiento de una función que contiene llamadas de prueba podría cambiar silenciosamente (de posiblemente entrar en pánico a no entrar en pánico, y viceversa) si se agrega o elimina un resultado de error de la firma. Esto parecía una propiedad demasiado peligrosa. La solución obvia habría sido dividir la funcionalidad de prueba en dos funciones separadas, debe y prueba (muy similar a lo que sugiere el problema #31442). Pero eso habría requerido dos nuevas funciones integradas, con solo probar conectado directamente a la necesidad inmediata de un mejor soporte de manejo de errores.

@pjebs Eso es _exactamente_ lo que consideramos en una propuesta anterior (consulte el documento detallado, sección sobre iteraciones de diseño, cuarto párrafo):

Además, en un intento de hacer que try sea útil no solo dentro de las funciones con un resultado de error, la semántica de try dependía del contexto: si try se usara a nivel de paquete, o si se invocara dentro de una función sin un resultado de error, try entraría en pánico al encontrar un error. (Aparte, debido a esa propiedad, lo incorporado se denominó debe en lugar de probar en esa propuesta).

El consenso (interno de Go Team) fue que sería confuso que try dependiera del contexto y actuara de manera tan diferente. Por ejemplo, agregar un resultado de error a una función (o eliminarlo) podría cambiar silenciosamente el comportamiento de la función de entrar en pánico a no entrar en pánico (o viceversa).

@griesemer Gracias por la aclaración sobre la reescritura. Me alegro de que se compilará.

Entiendo que los ejemplos fueron traducciones que no anotaron los errores. Traté de argumentar que try hace que sea más difícil hacer una buena anotación de errores en situaciones comunes, y que la anotación de errores es muy importante para la comunidad. Una gran parte de los comentarios hasta ahora han estado explorando formas de agregar un mejor soporte de anotación a try .

Acerca de tener que manejar los errores de manera diferente, no estoy de acuerdo con que sea una señal de que la preocupación de la función es demasiado amplia. He estado traduciendo algunos ejemplos de código real reclamado de los comentarios y colocándolos en un menú desplegable en la parte inferior de mi comentario original , y el ejemplo en https://github.com/golang/go/issues/32437#issuecomment - 499007288 Creo que demuestra bien un caso común:

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil { return nil, errors.WithMessage(err, "load config dir") }

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err != nil { return nil, errors.WithMessage(err, "execute main template") }

    buf, err := format.Source(b.Bytes())
    if err != nil { return nil, errors.WithMessage(err, "format main template") }

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
    // ...
}

El propósito de esa función es ejecutar una plantilla en algunos datos en un archivo. No creo que deba dividirse, y sería desafortunado si todos esos errores obtuvieran la línea en la que se crearon a partir de un aplazamiento. Eso puede estar bien para los desarrolladores, pero es mucho menos útil para los usuarios.

Creo que también es una pequeña señal de lo sutiles que eran los errores defer wrap(&err, "message: %v", err) y de cómo tropezaron incluso con los programadores experimentados de Go.


Para resumir mi argumento : creo que la anotación de errores es más importante que la verificación de errores basada en expresiones, y podemos reducir bastante el ruido al permitir que la verificación de errores basada en declaraciones sea una línea en lugar de tres. Gracias.

@griesemer , lo siento, leí una sección diferente que discutía el pánico y no vi la discusión sobre los peligros.

@zeebo Gracias por este ejemplo. Parece que usar una instrucción if es exactamente la elección correcta en este caso. Pero claro, formatear los condicionales en una sola línea puede agilizar esto un poco.

Me gustaría mencionar una vez más la idea de un controlador como segundo argumento para try , pero con la adición de que el argumento del controlador sea _requerido_, pero nulo. Esto hace que el manejo del error sea el predeterminado, en lugar de la excepción. En los casos en los que realmente desee pasar el error sin cambios, simplemente proporcione un valor nulo al controlador y try se comportará como en la propuesta original, pero el argumento nulo actuará como una señal visual de que el no se maneja el error. Será más fácil de detectar durante la revisión del código.

file := try(os.Open("my_file.txt"), nil)

¿Qué debería suceder si se proporciona el controlador pero es nulo? ¿Debería probar pánico o tratarlo como un controlador de errores ausente?

Como se mencionó anteriormente, try se comportará de acuerdo con la propuesta original. No existiría un controlador de errores ausente, solo uno nulo.

¿Qué sucede si se invoca el controlador con un error no nulo y luego devuelve un resultado nulo? ¿Significa esto que el error está "cancelado"? ¿O debería la función envolvente regresar con un error nulo?

Creo que la función envolvente regresaría con un error nulo. Sería potencialmente muy confuso si try a veces pudiera continuar la ejecución incluso después de recibir un valor de error distinto de cero. Esto permitiría que los controladores "se ocupen" del error en algunas circunstancias. Este comportamiento podría ser útil en una función de estilo "obtener o crear", por ejemplo.

func getOrCreateObject(obj *object) error {
    defaultObjectHandler := func(err error) error {
        if err == ObjectDoesNotExistErr {
            *obj = object{}
            return nil
        }
        return fmt.Errorf("getting or creating object: %v", err)
    }

    *obj = try(db.getObject(), defaultObjectHandler)
}

Tampoco estaba claro si permitir un controlador de errores opcional llevaría a los programadores a ignorar por completo el manejo adecuado de errores. También sería fácil hacer un manejo adecuado de errores en todas partes, pero perder una sola ocurrencia de un intento. Etcétera.

Creo que ambas preocupaciones se alivian al hacer que el controlador sea un argumento necesario y nulo. Requiere que los programadores tomen una decisión consciente y explícita de que no manejarán su error.

Como beneficio adicional, creo que requerir el controlador de errores también desalienta los try profundamente anidados porque son menos breves. Algunos pueden ver esto como un inconveniente, pero creo que es un beneficio.

@velovix Me encanta la idea, pero ¿por qué se requiere el controlador de errores? ¿No puede ser nil por defecto? ¿Por qué necesitamos una "pista visual"?

@griesemer ¿Qué pasa si se adopta la idea de @velovix pero con builtin que contiene una función predefinida que convierte err en pánico Y eliminamos el requisito de que la función general tenga un valor de retorno de error?

La idea es que, si la función general no devuelve un error, usar try sin el controlador de errores es un error de tiempo de compilación.

El controlador de errores también se puede usar para envolver el error que pronto se devolverá usando varias bibliotecas, etc. en la ubicación del error, en lugar de un defer en la parte superior que modifica un error devuelto con nombre.

@pjebs

¿Por qué se requiere el controlador de errores? ¿No puede ser nulo por defecto? ¿Por qué necesitamos una "pista visual"?

Esto es para abordar las preocupaciones que

  1. La propuesta try , tal como está ahora, podría desanimar a las personas a brindar contexto a sus errores porque hacerlo no es tan sencillo.

Tener un controlador en primer lugar hace que proporcionar contexto sea más fácil, y que el controlador sea un argumento requerido envía un mensaje: el caso recomendado común es manejar o contextualizar el error de alguna manera, no simplemente pasarlo a la pila. Está en línea con la recomendación general de la comunidad Go.

  1. Una preocupación del documento de propuesta original. Lo cité en mi primer comentario:

Tampoco estaba claro si permitir un controlador de errores opcional llevaría a los programadores a ignorar por completo el manejo adecuado de errores. También sería fácil hacer un manejo adecuado de errores en todas partes, pero perder una sola ocurrencia de un intento. Etcétera.

Tener que pasar un nil explícito hace que sea más difícil olvidarse de manejar un error correctamente. Debe decidir explícitamente no manejar el error en lugar de hacerlo implícitamente omitiendo un argumento.

Pensando más en el retorno condicional mencionado brevemente en https://github.com/golang/go/issues/32437#issuecomment -498947603.
Parece
return if f, err := os.Open("/my/file/path"); err != nil
sería más compatible con el aspecto del if existente de Go.

Si agregamos una regla para la instrucción return if que
cuando la expresión de la última condición (como err != nil ) no está presente,y la última variable de la declaración en la sentencia return if es del tipo error ,entonces el valor de la última variable se comparará automáticamente con nil como condición implícita.

Entonces, la instrucción return if se puede abreviar en:
return if f, err := os.Open("my/file/path")

Lo cual está muy cerca de la relación señal-ruido que proporciona el try .
Si cambiamos return if a try , se convierte en
try f, err := os.Open("my/file/path")
De nuevo se vuelve similar a otras variaciones propuestas de try en este hilo, al menos sintácticamente.
Personalmente, sigo prefiriendo return if a try en este caso porque hace que los puntos de salida de una función sean muy explícitos. Por ejemplo, cuando depuro, a menudo resalto la palabra clave return dentro del editor para identificar todos los puntos de salida de una función grande.

Desafortunadamente, tampoco parece ayudar lo suficiente con el inconveniente de insertar el registro de depuración.
A menos que también permitamos un bloque de $ body por return if , como
Original:

        return if f, err := os.Open("my/path") 

Al depurar:

-       return if f, err := os.Open("my/path") 
+       return if f, err := os.Open("my/path") {
+               fmt.Printf("DEBUG: os.Open: %s\n", err)
+       }

Supongo que el significado del bloque de cuerpo de return if es obvio. Se ejecutará antes defer y regresará.

Dicho esto, no tengo quejas con el enfoque de manejo de errores existente en Go.
Estoy más preocupado por cómo la adición del nuevo manejo de errores afectaría la bondad actual de Go.

@velovix Nos gustó bastante la idea de un try con una función de controlador explícita como segundo argumento. Pero había demasiadas preguntas que no tenían respuestas obvias, como dice el documento de diseño. Ha respondido algunas de ellas de una manera que le parece razonable. Es bastante probable (y esa fue nuestra experiencia dentro del Go Team), que alguien más piense que la respuesta correcta es bastante diferente. Por ejemplo, está afirmando que siempre se debe proporcionar el argumento del controlador, pero que puede ser nil , para que quede explícito, no nos importa manejar el error. Ahora, ¿qué sucede si uno proporciona un valor de función (no un literal nil ) y ese valor de función (almacenado en una variable) resulta ser nulo? Por analogía con el valor explícito nil , no se requiere manipulación. Pero otros podrían argumentar que se trata de un error en el código. O, alternativamente, uno podría permitir argumentos de controlador de valor nulo, pero luego una función podría manejar errores de manera inconsistente en algunos casos y no en otros, y no es necesariamente obvio a partir del código cuál lo hace, porque parece como si un controlador estuviera siempre presente . Otro argumento fue que es mejor tener una declaración de nivel superior de un controlador de errores porque deja muy claro que la función maneja los errores. De ahí el defer . Probablemente haya más.

Sería bueno aprender más sobre esta preocupación. El estilo de codificación actual que usa declaraciones if para probar errores es tan explícito como puede ser. Es muy fácil agregar información adicional a un error, de forma individual (para cada si). A menudo, tiene sentido manejar todos los errores detectados en una función de manera uniforme, lo que se puede hacer con un aplazamiento; esto ya es posible ahora. Es el hecho de que ya tenemos todas las herramientas para un buen manejo de errores en el lenguaje, y el problema de que una construcción de controlador no sea ortogonal para diferir, nos llevó a dejar de lado un nuevo mecanismo únicamente para aumentar errores.

@griesemer - IIUC, está diciendo que para contextos de error dependientes del sitio de llamadas, la declaración if actual está bien. Mientras que esta nueva función try es útil para los casos en los que es útil manejar múltiples errores en un solo lugar.

Creo que la preocupación era que, si bien simplemente hacer un if err != nil { return err} puede estar bien en algunos casos, generalmente se recomienda decorar el error antes de regresar. Y esta propuesta parece abordar lo anterior y no hace mucho por lo segundo. Lo que esencialmente significa que se alentará a la gente a usar un patrón de devolución fácil.

@agnivade Tiene razón, esta propuesta no hace exactamente nada para ayudar con la decoración de errores (pero para recomendar el uso de defer ). Una razón es que ya existen mecanismos de lenguaje para esto. Tan pronto como se requiere la decoración de errores, especialmente sobre la base de un error individual, la cantidad adicional de texto fuente para el código de decoración hace que el if sea menos oneroso en comparación. Son los casos en los que no se requiere decoración, o donde la decoración es siempre la misma, donde el texto modelo se convierte en una molestia visible y luego resta valor al código importante.

Ya se alienta a la gente a usar un patrón de devolución fácil, try o no try , solo hay menos para escribir. Ahora que lo pienso, _la única forma de fomentar la decoración de errores es hacer que sea obligatoria_, porque no importa qué soporte de idioma esté disponible, la decoración de errores requerirá más trabajo.

Una forma de endulzar el trato sería permitir solo algo como try (o cualquier notación abreviada análoga) _si_ se proporciona un controlador explícito (posiblemente vacío) en alguna parte (tenga en cuenta que el borrador del diseño original no tenía tal requisito, tampoco).

No estoy seguro de que queramos ir tan lejos. Permítanme reafirmar que un montón de código perfectamente fino, digamos partes internas de una biblioteca, no necesita decorar errores en todas partes. Está bien simplemente propagar los errores y decorarlos justo antes de que abandonen los puntos de entrada de la API, por ejemplo. (De hecho, decorarlos en todas partes solo conducirá a errores excesivamente decorados que, con los verdaderos culpables ocultos, dificultan la localización de los errores importantes; al igual que un registro demasiado detallado puede dificultar ver lo que realmente está sucediendo).

Creo que también podemos agregar una función de captura , que sería un buen par, entonces:

func a() int {
  x := randInt()
  // let's assume that this is what recruiters should "fix" for us
  // or this happens in 3rd-party package.
  if x % 1337 != 0 {
    panic("not l33t enough")
  }
  return x
}

func b() error {
  // if a() panics, then x = 0, err = error{"not l33t enough"}
  x, err := catch(a())
  if err != nil {
    return err
  }
  sendSomewhereElse(x)
  return nil
}

// which could be simplified even further

func c() error {
  x := try(catch(a()))
  sendSomewhereElse(x)
  return nil
}

en este ejemplo, catch() sería recover() pánico y return ..., panicValue .
por supuesto, tenemos un caso de esquina obvio en el que tenemos una función, que también devuelve un error. en este caso, creo que sería conveniente simplemente pasar el valor del error.

así que, básicamente, puede usar catch() para recuperar() los pánicos y convertirlos en errores.
esto me parece bastante divertido, porque Go en realidad no tiene excepciones, pero en este caso tenemos un patrón try()-catch() bastante bueno, que tampoco debería hacer estallar toda su base de código con algo como Java ( catch(Throwable) en Principal + throws LiterallyAnything ). puede procesar fácilmente los pánicos de alguien como si fueran errores habituales. Actualmente tengo alrededor de 6mln+ LoC en Go en mi proyecto actual, y creo que esto simplificaría las cosas al menos para mí.

@griesemer Gracias por su resumen de la discusión.

Me doy cuenta de que falta un punto: algunas personas han argumentado que deberíamos esperar con esta característica hasta que tengamos genéricos, lo que con suerte nos permitirá resolver este problema de una manera más elegante.

Además, también me gusta la sugerencia de @velovix , y aunque aprecio que esto plantee algunas preguntas como se describe en la especificación, creo que se pueden responder fácilmente de una manera razonable, como ya lo hizo @velovix .

Por ejemplo:

  • ¿Qué sucede si uno proporciona un valor de función (no un literal nulo) y ese valor de función (almacenado en una variable) resulta ser nulo? => No manejes el error, punto. Esto es útil en caso de que el manejo de errores dependa del contexto y la variable del controlador se establezca dependiendo de si se requiere o no el manejo de errores. No es un error, más bien, es una característica. :)

  • Otro argumento fue que es mejor tener una declaración de nivel superior de un controlador de errores porque deja muy claro que la función maneja los errores. => Por lo tanto, defina el controlador de errores en la parte superior de la función como una función de cierre con nombre y utilícelo, de modo que también quede muy claro que se debe manejar el error. Esto no es un problema serio, más bien un requisito de estilo.

¿Qué otras preocupaciones había? Estoy bastante seguro de que todas pueden responderse de manera similar de una manera razonable.

Finalmente, como usted dice, "una forma de endulzar el trato sería permitir solo algo como probar (o cualquier notación de atajo análoga) si se proporciona un controlador explícito (posiblemente vacío) en alguna parte". Creo que si vamos a proceder con esta propuesta, en realidad deberíamos llevarla "hasta aquí", para fomentar un manejo de errores adecuado, "explícito es mejor que implícito".

@griesemer

Ahora, ¿qué sucede si uno proporciona un valor de función (no un literal nulo) y ese valor de función (almacenado en una variable) resulta ser nulo? Por analogía con el valor nulo explícito, no se requiere manipulación. Pero otros podrían argumentar que se trata de un error en el código.

En teoría, esto parece un problema potencial, aunque me está costando conceptualizar una situación razonable en la que un controlador terminaría siendo nulo por accidente. Me imagino que los controladores provendrían más comúnmente de una función de utilidad definida en otro lugar, o como un cierre definido en la función misma. Es probable que ninguno de estos se convierta en cero inesperadamente. En teoría, podría tener un escenario en el que las funciones del controlador se transmitan como argumentos a otras funciones, pero a mis ojos parece bastante exagerado. Tal vez hay un patrón como este del que no estoy al tanto.

Otro argumento fue que es mejor tener una declaración de nivel superior de un controlador de errores porque deja muy claro que la función maneja los errores. De ahí el defer .

Como mencionó @beoran , definir el controlador como un cierre cerca de la parte superior de la función tendría un estilo muy similar, y así es como personalmente espero que las personas usen los controladores con más frecuencia. Si bien aprecio la claridad ganada por el hecho de que todas las funciones que manejan errores usarán defer , puede volverse menos claro cuando una función necesita pivotar en su estrategia de manejo de errores a la mitad de la función. Luego, habrá dos defer para mirar y el lector tendrá que razonar sobre cómo interactuarán entre sí. Esta es una situación en la que creo que un argumento del controlador sería más claro y ergonómico, y creo que este será un escenario _relativamente_ común.

¿Es posible hacerlo funcionar sin corchetes?

es decir algo como:
a := try func(some)

@Cyberax : como ya se mencionó anteriormente, es muy importante que lea detenidamente el documento de diseño antes de publicarlo. Dado que este es un tema de alto tráfico, con mucha gente suscrita.

El documento analiza los operadores frente a las funciones en detalle.

Me gusta mucho más esto que la versión de agosto.

Creo que gran parte de los comentarios negativos, que no se oponen directamente a las devoluciones sin la palabra clave return , se pueden resumir en dos puntos:

  1. a la gente no le gustan los parámetros de resultados con nombre, que serían necesarios en la mayoría de los casos
  2. desaconseja agregar contexto detallado a los errores

Ver por ejemplo:

La refutación de esas dos objeciones es respectivamente:

  1. "decidimos que [los parámetros de resultado nombrados] estaban bien"
  2. "Nadie te va a obligar a usar try " / no va a ser apropiado para el 100% de los casos

Realmente no tengo nada que decir sobre 1 (no me siento fuertemente al respecto). Pero con respecto a 2, señalaría que la propuesta de agosto no tenía este problema, la mayoría de las contrapropuestas tampoco tienen este problema.

En particular, ni la contrapropuesta tryf (que se ha publicado de forma independiente dos veces en este hilo) ni la contrapropuesta try(X, handlefn) (que formaba parte de las iteraciones de diseño) tenían este problema.

Creo que es difícil argumentar que try , tal como es, alejará a las personas de la decoración de errores con contexto relevante y hacia una única decoración de error genérica por función.

Por estas razones creo que vale la pena tratar de abordar este tema y quiero proponer una posible solución:

  1. Actualmente, el parámetro de defer solo puede ser una función o llamada de método. Permita que defer también tenga un nombre de función o un literal de función, es decir
defer func(...) {...}
defer packageName.functionName
  1. Cuando panic o deferreturn encuentran este tipo de aplazamiento, llamarán a la función pasando el valor cero para todos sus parámetros.

  2. Permitir que try tenga más de un parámetro

  3. Cuando try encuentre el nuevo tipo de aplazamiento, llamará a la función pasando un puntero al valor de error como primer parámetro seguido de todos los parámetros propios de try , excepto el primero.

Por ejemplo, dado:

func errorfn() error {
    return errors.New("an error")
}


func f(fail bool) {
    defer func(err *error, a, b, c int) {
        fmt.Printf("a=%d b=%d c=%d\n", a, b, c)
    }
    if fail {
        try(errorfn, 1, 2, 3)
    }
}

sucederá lo siguiente:

f(false)        // prints "a=0 b=0 c=0"
f(true)         // prints "a=1 b=2 c=3"

El código en https://github.com/golang/go/issues/32437#issuecomment -499309304 por @zeebo podría reescribirse como:

func (c *Config) Build() error {
    defer func(err *error, msg string, args ...interface{}) {
        if *err == nil || msg == "" {
            return
        }
        *err = errors.WithMessagef(err, msg, args...)
    }
    pkgPath := try(c.load(), "load config dir")

    b := bytes.NewBuffer(nil)
    try(templates.ExecuteTemplate(b, "main", c), "execute main template")

    buf := try(format.Source(b.Bytes()), "format main template")

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)
    // ...
}

Y definiendo ErrorHandlef como:

func HandleErrorf(err *error, format string, args ...interface{}) {
        if *err != nil && format != "" {
                *err = fmt.Errorf(format + ": %v", append(args, *err)...)
        }
}

daría a todos el tan buscado tryf de forma gratuita, sin tener que introducir cadenas de formato de estilo fmt en el lenguaje principal.

Esta función es compatible con versiones anteriores porque defer no permite expresiones de función como argumento. No introduce nuevas palabras clave.
Los cambios que deben realizarse para implementarlo, además de los descritos en https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md , son:

  1. enseñar al analizador sobre el nuevo tipo de aplazamiento
  2. cambie el verificador de tipos para verificar que dentro de una función, todos los diferidos que tienen una función como parámetro (en lugar de una llamada) también tienen la misma firma
  3. cambie el verificador de tipos para verificar que los parámetros pasados ​​a try coincidan con la firma de las funciones pasadas a defer
  4. cambie el backend (?) para generar la llamada deferproc apropiada
  5. cambie la implementación de try para copiar sus argumentos en los argumentos de la llamada diferida cuando encuentre una llamada diferida por el nuevo tipo de diferido.

Después de las complejidades del diseño preliminar de check/handle , me sorprendió gratamente ver esta propuesta mucho más simple y pragmática, aunque me decepcionó que haya habido tanto rechazo en su contra.

Es cierto que gran parte del rechazo proviene de personas que están bastante contentas con la verbosidad actual (una posición perfectamente razonable) y que, presumiblemente, no agradecerían ninguna propuesta para aliviarlo. Para el resto de nosotros, creo que esta propuesta alcanza el punto óptimo de ser simple y similar a Go, sin tratar de hacer demasiado y encajar bien con las técnicas existentes de manejo de errores a las que siempre se puede recurrir si try no hizo exactamente lo que querías.

En cuanto a algunos puntos específicos:

  1. Lo único que no me gusta de la propuesta es la necesidad de tener un parámetro de retorno de error con nombre cuando se usa defer pero, dicho esto, no puedo pensar en ninguna otra solución que no esté en desacuerdo con la forma en que funciona el resto del lenguaje. Así que creo que tendremos que aceptar esto si se adopta la propuesta.

  2. Es una pena que try no funcione bien con el paquete de prueba para funciones que no devuelven un valor de error. Mi propia solución preferida para esto sería tener una segunda función integrada (quizás ptry o must ) que siempre entrara en pánico en lugar de regresar al encontrar un error no nulo y que, por lo tanto, podría ser utilizado con las funciones antes mencionadas (incluyendo main ). Aunque esta idea ha sido rechazada en la presente iteración de la propuesta, tuve la impresión de que era una 'llamada cercana' y, por lo tanto, puede ser elegible para reconsideración.

  3. Creo que sería difícil para la gente entender lo que estaban haciendo go try(f) o defer try(f) y, por lo tanto, es mejor prohibirlos por completo.

  4. Estoy de acuerdo con aquellos que piensan que las técnicas de manejo de errores existentes se verían menos detalladas si go fmt no reescribiera declaraciones de una sola línea if . Personalmente, preferiría una regla simple que permitiera esto para _cualquier_ declaración única if ya sea que se trate del manejo de errores o no. De hecho, nunca he podido entender por qué esto no está permitido actualmente cuando se escriben funciones de una sola línea donde el cuerpo se coloca en la misma línea en la que se permite la declaración.

En el caso de errores de decoración

func myfunc()( err error){
try(thing())
defer func(){
err = errors.Wrap(err,"more context")
}()
}

Esto se siente considerablemente más detallado y doloroso que los paradigmas existentes, y no tan conciso como verificar/manejar. La variante try() sin envoltorio es más concisa, pero parece que las personas terminarán usando una combinación de try y regresa un error simple. No estoy seguro de que me guste la idea de mezclar intentos y devoluciones de errores simples, pero estoy totalmente convencido de los errores de decoración (y espero con ansias Is/As). Hazme pensar que si bien esto es sintácticamente ordenado, no estoy seguro de querer usarlo. verificar/manejar sentí algo que abrazaría más a fondo.

Realmente me gusta la simplicidad de esto y el enfoque de "hacer una cosa bien". En mi intérprete GoAWK sería muy útil: tengo alrededor de 100 construcciones if err != nil { return nil } que simplificaría y ordenaría, y eso está en una base de código bastante pequeña.

He leído la justificación de la propuesta para convertirla en una palabra clave integrada en lugar de una, y se reduce a no tener que ajustar el analizador. Pero, ¿no es una cantidad relativamente pequeña de dolor para los compiladores y los escritores de herramientas, mientras que tener los paréntesis adicionales y los problemas de legibilidad de esto parece una función pero no es algo será algo que todos los codificadores y codificadores de Go? los lectores tienen que soportar. En mi opinión, el argumento (¿excusa? :-) de que "pero panic() sí controla el flujo" no es suficiente, porque el pánico y la recuperación son, por su propia naturaleza, excepcionales , mientras que try() lo harán ser normal el manejo de errores y el flujo de control.

Definitivamente lo agradecería incluso si esto fuera como está, pero mi fuerte preferencia sería que el flujo de control normal sea claro, es decir, hecho a través de una palabra clave.

Estoy a favor de esta propuesta. Evita mi mayor reserva sobre la propuesta anterior: la no ortogonalidad de handle con respecto a defer .

Me gustaría mencionar dos aspectos que no creo que se hayan destacado anteriormente.

En primer lugar, aunque esta propuesta no facilita agregar texto de error específico del contexto a un error, _sí_ facilita agregar información de seguimiento de errores de marco de pila a un error: https://play.golang.org/p /YL1MoqR08E6

En segundo lugar, try podría decirse que es una solución justa para la mayoría de los problemas subyacentes a https://github.com/golang/go/issues/19642. Para tomar un ejemplo de ese problema, podría usar try para evitar escribir todos los valores devueltos cada vez. Esto también es potencialmente útil cuando se devuelven tipos de estructura por valor con nombres largos.

func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
    xx := int(x)
    if f.NumGlyphs() <= xx {
        try(ErrNotFound)
    }
    i := f.cached.locations[xx+0]
    j := f.cached.locations[xx+1]
    if j < i {
        try(errInvalidGlyphDataLength)
    }
    if j-i > maxGlyphDataLength {
        try(errUnsupportedGlyphDataLength)
    }
    buf, err = b.view(&f.src, int(i), int(j-i))
    return buf, i, j - i, err
}

A mí también me gusta esta propuesta.

Y tengo un pedido.

Como make , ¿podemos permitir que try tome una cantidad variable de parámetros?

  • probar (f):
    como anteriormente.
    un valor de error de retorno es obligatorio (como último parámetro de retorno).
    MODELO DE USO MÁS COMÚN
  • try(f, doPanic bool):
    como arriba, pero si doPanic, entonces panic(err) en lugar de regresar.
    En este modo, no es necesario un valor de error de retorno.
  • probar (f, fn):
    como arriba, pero llama a fn(err) antes de regresar.
    En este modo, no es necesario un valor de error de retorno.

De esta manera, es un elemento integrado que puede manejar todos los casos de uso, sin dejar de ser explícito. Sus ventajas:

  • siempre explícito: no es necesario inferir si entrar en pánico o establecer un error y regresar
  • admite un controlador específico del contexto (pero no una cadena de controladores)
  • admite casos de uso en los que no hay una variable de retorno de error
  • admite la semántica must(...)

Si bien if err !=nil { return ... err } repetitivo es ciertamente un tartamudeo feo, estoy con esos
que piensan que la propuesta try() es muy baja en legibilidad y algo inexplícita.
El uso de retornos con nombre también es problemático.

Si se necesita este tipo de limpieza, ¿por qué no try(err) como azúcar sintáctico para
if err !=nil { return err } :

file, err := os.Open("file.go")
try(err)

por

file, err := os.Open("file.go")
if err != nil {
   return err
}

Y si hay más de un valor de retorno, try(err) podría return t1, ... tn, err
donde t1, ... tn son los valores cero de los otros valores devueltos.

Esta sugerencia puede obviar la necesidad de valores de retorno con nombre y ser,
en mi opinión, más fácil de entender y más legible.

Aún mejor, creo que sería:

file, try(err) := os.Open("file.go")

O incluso

file, err? := os.Open("file.go")

Este último es compatible con versiones anteriores (? actualmente no está permitido en los identificadores).

(Esta sugerencia está relacionada con https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes. Pero los ejemplos de temas recurrentes parecen diferentes porque estaba en una etapa en la que todavía se discutía un identificador explícito en lugar de dejar eso a un aplazamiento.)

Gracias al equipo de go por esta cuidada e interesante propuesta.

@rogpeppe comenta si try agrega automáticamente el marco de la pila, no yo, estoy de acuerdo con que desaconseje agregar contexto.

@aarzilli : de acuerdo con su propuesta, ¿es obligatoria una cláusula de aplazamiento cada vez que asignamos parámetros adicionales a tryf ?

que pasa si hago

try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)

y no escribir una función diferida?

@agnivade

¿Qué sucede si hago (...) y no escribo una función diferida?

error de verificación de tipo.

En mi opinión, usar try para evitar escribir todos los valores devueltos es en realidad otro golpe en su contra.

func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
    xx := int(x)
    if f.NumGlyphs() <= xx {
        try(ErrNotFound)
    }
    //...

Entiendo completamente el deseo de evitar tener que escribir return nil, 0, 0, ErrNotFound , pero preferiría resolverlo de otra manera.

La palabra try no significa "retorno". Y así es como se está usando aquí. De hecho, preferiría que la propuesta cambie para que try no pueda tomar un error directamente, porque no quiero que nadie escriba código como ese ^^. Se lee mal . Si le mostraras ese código a un novato, no tendría ni idea de lo que estaba haciendo ese intento.

Si queremos una forma de devolver fácilmente los valores predeterminados y un valor de error, resolvámoslo por separado. Tal vez otro incorporado como

return default(ErrNotFound)

Al menos eso se lee con algún tipo de lógica.

Pero no abusemos try para resolver algún otro problema.

@natefinch si el try incorporado se llama check como en la propuesta original, sería check(err) que se lee considerablemente mejor, en mi opinión.

Dejando eso de lado, no sé si es realmente un abuso escribir try(err) . Se sale de la definición limpiamente. Pero, por otro lado, eso también significa que esto es legal:

a, b := try(1, f(), err)

Supongo que mi principal problema con try es que en realidad es solo un panic que solo sube un nivel... excepto que, a diferencia de panic, es una expresión, no una declaración, por lo que puedes ocultar en medio de una declaración en alguna parte. Eso casi lo hace peor que el pánico.

@natefinch Si lo conceptualizas como un pánico que sube un nivel y luego hace otras cosas, parece bastante complicado. Sin embargo, lo conceptualizo de manera diferente. Las funciones que devuelven errores en Go están devolviendo efectivamente un resultado, para tomar prestado libremente de la terminología de Rust. try es una utilidad que desempaqueta el resultado y devuelve un "resultado de error" si error != nil , o desempaqueta la parte T del resultado si error == nil .

Por supuesto, en Go en realidad no tenemos objetos de resultado, pero es efectivamente el mismo patrón y try parece una codificación natural de ese patrón. Creo que cualquier solución a este problema tendrá que codificar algún aspecto del manejo de errores, y la interpretación de try me parece razonable. Otros y yo sugerimos ampliar un poco la capacidad de try para que se ajuste mejor a los patrones de manejo de errores de Go existentes, pero el concepto subyacente sigue siendo el mismo.

@ugorji La variante try(f, bool) que propone suena como la must de #32219.

@ugorji La variante try(f, bool) que propone suena como la must de #32219.

Sí lo es. Simplemente sentí que los 3 casos podrían manejarse con una función incorporada singular y satisfacer todos los casos de uso con elegancia.

Dado que try() ya es mágico y consciente del valor de retorno de error, ¿podría aumentarse para devolver también un puntero a ese valor cuando se llama en forma nula (argumento cero)? Eso eliminaría la necesidad de devoluciones con nombre, y creo que ayudaría a correlacionar visualmente de dónde se espera que provenga el error en las declaraciones diferidas. Por ejemplo:

func foo() error {
  defer fmt.HandleErrorf(try(), "important foo context info")
  try(bar())
  try(baz())
  try(etc())
}

@ugorji
Creo que el booleano en try(f, bool) haría que fuera difícil de leer y fácil de pasar por alto. Me gusta su propuesta, pero para el caso de pánico, creo que podría omitirse para que los usuarios escriban eso dentro del controlador de su tercera viñeta, por ejemplo, try(f(), func(err error) { panic('at the disco'); }) , esto lo hace más explícito para los usuarios que un try(f(), true) oculto.

@ugorji
Creo que el booleano en try(f, bool) haría que fuera difícil de leer y fácil de pasar por alto. Me gusta su propuesta, pero para el caso de pánico, creo que podría omitirse para que los usuarios escriban eso dentro del controlador de su tercera viñeta, por ejemplo, try(f(), func(err error) { panic('at the disco'); }) , esto lo hace más explícito para los usuarios que un try(f(), true) oculto.

Pensándolo bien, tiendo a estar de acuerdo con su posición y su razonamiento, y todavía parece elegante como una sola línea.

@patrick-nyt es otro defensor de la _sintaxis de asignación_ para activar una prueba nula, en https://github.com/golang/go/issues/32437#issuecomment -499533464

Este concepto aparece en 13 respuestas separadas a la propuesta de verificación/manejo
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -temas

f, ?return := os.Open(...)
f, ?panic  := os.Open(...)

¿Por qué? Porque se lee como Go 1, mientras que try() y check no.

Una objeción a try parece ser que es una expresión. Supongamos, en cambio, que hay una declaración de sufijo unario ? que significa retorno si no es nulo. Aquí está el ejemplo de código estándar (suponiendo que se agregue mi paquete diferido propuesto ):

func CopyFile(src, dst string) error {
    var err error // Don't need a named return because err is explicitly named
    defer deferred.Annotate(&err, "copy %s %s", src, dst)

    r, err := os.Open(src)
    err?
    defer deferred.AnnotatedExec(&err, r.Close)

    w, err := os.Create(dst)
    err?
    defer deferred.AnnotatedExec(&err, r.Close)

    defer deferred.Cond(&err, func(){ os.Remove(dst) })
    _, err = io.Copy(w, r)

    return err
}

El ejemplo de pgStore:

func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    err?

    defer deferred.Cond(&err, func(){ tx.Rollback() })

    var res int64 
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    // tricky bit: this would not change the value of err 
    // but the deferred.Cond would still be triggered by err being set before
    deferred.Format(err, "insert table")?

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    deferred.Format(err, "insert table2")?

    return tx.Commit()
}

Me gusta esto de @jargv :

Dado que try() ya es mágico y consciente del valor de retorno de error, ¿podría aumentarse para devolver también un puntero a ese valor cuando se llama en forma nula (argumento cero)? Eso eliminaría la necesidad de devoluciones con nombre

Pero en lugar de sobrecargar el nombre try en función de la cantidad de argumentos, creo que podría haber otra función mágica incorporada, digamos reterr o algo así.

He informado a través de algunos paquetes que se usan con mucha frecuencia, buscando el código go que "sufre" del manejo de errores, pero debe haber sido bien pensado antes de escribirlo, tratando de descubrir qué "magia" haría el try() propuesto.
Actualmente, a menos que no haya entendido bien la propuesta, muchas de ellas (por ejemplo, el manejo de errores no superbásico) no ganarían mucho, o tendrían que quedarse con el estilo de manejo de errores "antiguo".
Ejemplo de net/http/request.go:

func (r *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) {
`

trace := httptrace.ContextClientTrace(r.Context())
if trace != nil && trace.WroteRequest != nil {
    defer func() {
        trace.WroteRequest(httptrace.WroteRequestInfo{
            Err: err,
        })
    }()
}

// Find the target host. Prefer the Host: header, but if that
// is not given, use the host from the request URL.
//
// Clean the host, in case it arrives with unexpected stuff in it.
host := cleanHost(r.Host)
if host == "" {
    if r.URL == nil {
        return errMissingHost
    }
    host = cleanHost(r.URL.Host)
}

// According to RFC 6874, an HTTP client, proxy, or other
// intermediary must remove any IPv6 zone identifier attached
// to an outgoing URI.
host = removeZone(host)

ruri := r.URL.RequestURI()
if usingProxy && r.URL.Scheme != "" && r.URL.Opaque == "" {
    ruri = r.URL.Scheme + "://" + host + ruri
} else if r.Method == "CONNECT" && r.URL.Path == "" {
    // CONNECT requests normally give just the host and port, not a full URL.
    ruri = host
    if r.URL.Opaque != "" {
        ruri = r.URL.Opaque
    }
}
if stringContainsCTLByte(ruri) {
    return errors.New("net/http: can't write control character in Request.URL")
}
// TODO: validate r.Method too? At least it's less likely to
// come from an attacker (more likely to be a constant in
// code).

// Wrap the writer in a bufio Writer if it's not already buffered.
// Don't always call NewWriter, as that forces a bytes.Buffer
// and other small bufio Writers to have a minimum 4k buffer
// size.
var bw *bufio.Writer
if _, ok := w.(io.ByteWriter); !ok {
    bw = bufio.NewWriter(w)
    w = bw
}

_, err = fmt.Fprintf(w, "%s %s HTTP/1.1\r\n", valueOrDefault(r.Method, "GET"), ruri)
if err != nil {
    return err
}

// Header lines
_, err = fmt.Fprintf(w, "Host: %s\r\n", host)
if err != nil {
    return err
}
if trace != nil && trace.WroteHeaderField != nil {
    trace.WroteHeaderField("Host", []string{host})
}

// Use the defaultUserAgent unless the Header contains one, which
// may be blank to not send the header.
userAgent := defaultUserAgent
if r.Header.has("User-Agent") {
    userAgent = r.Header.Get("User-Agent")
}
if userAgent != "" {
    _, err = fmt.Fprintf(w, "User-Agent: %s\r\n", userAgent)
    if err != nil {
        return err
    }
    if trace != nil && trace.WroteHeaderField != nil {
        trace.WroteHeaderField("User-Agent", []string{userAgent})
    }
}

// Process Body,ContentLength,Close,Trailer
tw, err := newTransferWriter(r)
if err != nil {
    return err
}
err = tw.writeHeader(w, trace)
if err != nil {
    return err
}

err = r.Header.writeSubset(w, reqWriteExcludeHeader, trace)
if err != nil {
    return err
}

if extraHeaders != nil {
    err = extraHeaders.write(w, trace)
    if err != nil {
        return err
    }
}

_, err = io.WriteString(w, "\r\n")
if err != nil {
    return err
}

if trace != nil && trace.WroteHeaders != nil {
    trace.WroteHeaders()
}

// Flush and wait for 100-continue if expected.
if waitForContinue != nil {
    if bw, ok := w.(*bufio.Writer); ok {
        err = bw.Flush()
        if err != nil {
            return err
        }
    }
    if trace != nil && trace.Wait100Continue != nil {
        trace.Wait100Continue()
    }
    if !waitForContinue() {
        r.closeBody()
        return nil
    }
}

if bw, ok := w.(*bufio.Writer); ok && tw.FlushHeaders {
    if err := bw.Flush(); err != nil {
        return err
    }
}

// Write body and trailer
err = tw.writeBody(w)
if err != nil {
    if tw.bodyReadError == err {
        err = requestBodyReadError{err}
    }
    return err
}

if bw != nil {
    return bw.Flush()
}
return nil

}
`

o como se usa en una prueba exhaustiva como pprof/profile/profile_test.go:
`
func checkAggregation(prof *Perfil, a *aggTest) error {
// Comprobar que se conservó el número total de muestras para las filas.
totales := int64(0)

samples := make(map[string]bool)
for _, sample := range prof.Sample {
    tb := locationHash(sample)
    samples[tb] = true
    total += sample.Value[0]
}

if total != totalSamples {
    return fmt.Errorf("sample total %d, want %d", total, totalSamples)
}

// Check the number of unique sample locations
if a.rows != len(samples) {
    return fmt.Errorf("number of samples %d, want %d", len(samples), a.rows)
}

// Check that all mappings have the right detail flags.
for _, m := range prof.Mapping {
    if m.HasFunctions != a.function {
        return fmt.Errorf("unexpected mapping.HasFunctions %v, want %v", m.HasFunctions, a.function)
    }
    if m.HasFilenames != a.fileline {
        return fmt.Errorf("unexpected mapping.HasFilenames %v, want %v", m.HasFilenames, a.fileline)
    }
    if m.HasLineNumbers != a.fileline {
        return fmt.Errorf("unexpected mapping.HasLineNumbers %v, want %v", m.HasLineNumbers, a.fileline)
    }
    if m.HasInlineFrames != a.inlineFrame {
        return fmt.Errorf("unexpected mapping.HasInlineFrames %v, want %v", m.HasInlineFrames, a.inlineFrame)
    }
}

// Check that aggregation has removed finer resolution data.
for _, l := range prof.Location {
    if !a.inlineFrame && len(l.Line) > 1 {
        return fmt.Errorf("found %d lines on location %d, want 1", len(l.Line), l.ID)
    }

    for _, ln := range l.Line {
        if !a.fileline && (ln.Function.Filename != "" || ln.Line != 0) {
            return fmt.Errorf("found line %s:%d on location %d, want :0",
                ln.Function.Filename, ln.Line, l.ID)
        }
        if !a.function && (ln.Function.Name != "") {
            return fmt.Errorf(`found file %s location %d, want ""`,
                ln.Function.Name, l.ID)
        }
    }
}

return nil

}
`
Estos son dos ejemplos que se me ocurren en los que uno diría: "Me gustaría una mejor opción de manejo de errores"

¿Alguien puede demostrar cómo mejorarían estos usando try() ?

Estoy mayoritariamente a favor de esta propuesta.

Mi principal preocupación, compartida con muchos comentaristas, es sobre los parámetros de resultados con nombre. La propuesta actual sin duda fomenta mucho más el uso de parámetros de resultados con nombre y creo que sería un error. No creo que esto sea simplemente una cuestión de estilo, como dice la propuesta: los resultados con nombre son una característica sutil del lenguaje que, en muchos casos, hace que el código sea más propenso a errores o menos claro. Después de ~8 años de leer y escribir código Go, realmente solo uso parámetros de resultado con nombre para dos propósitos:

  • Documentación de parámetros de resultados
  • Manipular un valor de resultado (generalmente un error ) dentro de un aplazamiento

Para atacar este problema desde una nueva dirección, aquí hay una idea que no creo que se alinee estrechamente con nada de lo que se haya discutido en el documento de diseño o en este hilo de comentarios del problema. Llamémoslo "error-diferir":

Permita que se use defer para llamar a funciones con un parámetro de error implícito.

Así que si tienes una función

func f(err error, t1 T1, t2 T2, ..., tn Tn) error

Luego, en una función g donde el último parámetro de resultado tiene tipo error (es decir, cualquier función donde se pueda usar try ), una llamada a f puede diferir de la siguiente manera:

func g() (R0, R0, ..., error) {
    defer f(t0, t1, ..., tn) // err is implicit
}

La semántica de error-defer es:

  1. La llamada diferida a f se llama con el último parámetro de resultado de g como primer parámetro de entrada de f
  2. f solo se llama si ese error no es nulo
  3. El resultado de f se asigna al último parámetro de resultado de g

Entonces, para usar un ejemplo del antiguo documento de diseño de manejo de errores, usando error-defer and try, podríamos hacer

func printSum(a, b string) error {
    defer func(err error) error {
        return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
    }()
    x := try(strconv.Atoi(a))
    y := try(strconv.Atoi(b))
    fmt.Println("result:", x+y)
    return nil
}

Así es como funcionaría HandleErrorf:

func printSum(a, b string) error {
    defer handleErrorf("printSum(%q + %q)", a, b)
    x := try(strconv.Atoi(a))
    y := try(strconv.Atoi(b))
    fmt.Println("result:", x+y)
    return nil
}

func handleErrorf(err error, format string, args ...interface{}) error {
    return fmt.Errorf(format+": %v", append(args, err)...)
}

Un caso de esquina que debería resolverse es cómo manejar los casos en los que es ambiguo qué forma de aplazamiento estamos usando. Creo que eso solo sucede con funciones (muy inusuales) con firmas como esta:

func(error, ...error) error

Parece razonable decir que este caso se maneja de forma que no se aplacen los errores (y esto preserva la compatibilidad con versiones anteriores).


Pensando en esta idea durante los últimos días, es un poco mágica, pero evitar los parámetros de resultado con nombre es una gran ventaja a su favor. Dado que try fomenta un mayor uso de defer para la manipulación de errores, tiene sentido que defer podría extenderse para adaptarse mejor a ese propósito. Además, hay una cierta simetría entre try y error-defer.

Finalmente, los aplazamientos de errores son útiles hoy en día incluso sin intentarlo, ya que suplantan el uso de parámetros de resultados con nombre para manipular las devoluciones de errores. Por ejemplo, aquí hay una versión editada de un código real:

// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) (_ []*File, err error) {
    files := make([]*file, len(keys))

    defer func() {
        if err != nil {
            // Return any successfully retrieved files.
            for _, f := range files {
                if f != nil {
                    c.put(f)
                }
            }
        }
    }()

    // ...
}

Con error-defer, esto se convierte en:

// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) ([]*File, error) {
    files := make([]*file, len(keys))

    defer func(err error) error {
        // Return any successfully retrieved files.
        for _, f := range files {
            if f != nil {
                c.put(f)
            }
        }
        return err
    }()

    // ...
}

@beoran Con respecto a su comentario de que deberíamos esperar a los genéricos. Los genéricos no ayudarán aquí; lea las preguntas frecuentes .

Con respecto a sus sugerencias sobre el comportamiento predeterminado de try de 2 argumentos de @velovix : como dije antes , su idea de cuál es la opción obviamente razonable es la pesadilla de otra persona.

¿Puedo sugerir que continuemos esta discusión una vez que evolucione un amplio consenso de que try con un controlador de errores explícito es una mejor idea que el try mínimo actual? En ese punto, tiene sentido discutir los puntos finos de tal diseño.

(De hecho, me gusta tener un manejador. Es una de nuestras propuestas anteriores. Y si adoptamos try tal como están, aún podemos avanzar hacia un try con un manejador en un avance -forma compatible - al menos si el controlador es opcional. Pero demos un paso a la vez.)

@aarzilli Gracias por tu sugerencia .

Siempre que los errores de decoración sean opcionales, la gente se inclinará por no hacerlo (después de todo, es un trabajo extra). Véase también mi comentario aquí .

Por lo tanto, no creo que el try propuesto _desaliente_ a las personas a cometer errores en la decoración (ya están desanimados incluso con el if por la razón anterior); es que try no lo _fomenta_.

(Una forma de alentarlo es vincularlo a try : solo se puede usar try si también se decora el error, o se excluye explícitamente).

Pero volviendo a tus sugerencias: creo que estás introduciendo mucha más maquinaria aquí. Cambiar la semántica de defer solo para que funcione mejor para try no es algo que nos gustaría considerar a menos que esos cambios defer sean beneficiosos de una manera más general. Además, su sugerencia vincula defer junto con try y, por lo tanto, hace que ambos mecanismos sean menos ortogonales; algo que querríamos evitar.

Pero lo que es más importante, dudo que quieras obligar a todos a escribir un defer solo para que puedan usar try . Pero sin hacer eso, volvemos al punto de partida: la gente se inclinará por no decorar los errores.

(Me gusta tener un controlador, para el caso. Es una de nuestras propuestas anteriores. Y si adoptamos la prueba tal como está, aún podemos avanzar hacia una prueba con un controlador de una manera compatible con versiones posteriores, al menos si el controlador es opcional. Pero demos un paso a la vez).

Claro, tal vez un enfoque de varios pasos sea el camino a seguir. Si agregamos un argumento de controlador opcional en el futuro, se podrían crear herramientas para advertir al escritor de un try no controlado con el mismo espíritu que la herramienta errcheck . De todos modos, agradezco sus comentarios!

@alanfo Gracias por sus comentarios positivos.

Con respecto a los puntos que planteas:

1) Si el único problema con try es el hecho de que uno tendrá que nombrar un retorno de error para que podamos decorar un error a través defer , creo que estamos bien. Si nombrar el resultado resulta ser un problema real, podríamos abordarlo. Un mecanismo simple en el que puedo pensar sería una variable predeclarada que es un alias para un resultado de error (piense en ello como que contiene el error que desencadenó el try más reciente). Puede haber mejores ideas. No propusimos esto porque ya existe un mecanismo en el lenguaje, que es nombrar el resultado.
2) try y pruebas: esto se puede abordar y hacer que funcione. Ver el documento detallado.
3) Esto se aborda explícitamente en el documento detallado.
4) Reconocido.

@benhoyt Gracias por sus comentarios positivos.

Si el argumento principal en contra de esta propuesta es el hecho de que try está integrado, estamos en un buen lugar. El uso de un integrado es simplemente una solución pragmática para el problema de la compatibilidad con versiones anteriores (sucede que no causa trabajo adicional para el analizador, las herramientas, etc., pero eso es solo un buen beneficio secundario, no la razón principal). También hay algunos beneficios de tener que escribir paréntesis, esto se analiza en detalle en el documento de diseño (sección sobre Propiedades del diseño propuesto).

Dicho todo esto, si el uso de una función incorporada es lo más sensacional, deberíamos considerar la palabra clave try . Sin embargo, no será compatible con versiones anteriores del código existente, ya que la palabra clave puede entrar en conflicto con los identificadores existentes.

(Para completar, también existe la opción de un operador como ? , que sería compatible con versiones anteriores. Sin embargo, no me parece la mejor opción para un lenguaje como Go. Pero, de nuevo, si eso es todo lo que se necesita para hacer que try sea apetecible, tal vez deberíamos considerarlo).

@ugorji Gracias por sus comentarios positivos.

try podría extenderse para tomar un argumento adicional. Nuestra preferencia sería tomar solo una función con la firma func (error) error . Si quiere entrar en pánico, es fácil proporcionar una función de ayuda de una línea:

func doPanic(err error) error { panic(err) }

Es mejor mantener el diseño de try simple.

@ patrick-nyt Lo que estás sugiriendo :

file, err := os.Open("file.go")
try(err)

será posible con la propuesta actual.

@dpinela , @ugorji Lea también el documento de diseño sobre el tema de must vs try . Es mejor mantener try lo más simple posible. must es un "patrón" común en las expresiones de inicialización, pero no hay una necesidad urgente de "arreglarlo".

@jargv Gracias por tu sugerencia . Esta es una idea interesante (ver también mi comentario aquí sobre este tema). Para resumir:

  • try(x) funciona según lo propuesto
  • try() devuelve un *error que apunta al resultado del error

De hecho, esta sería otra forma de llegar al resultado sin tener que nombrarlo.

@cespare La sugerencia de @jargv me parece mucho más simple que lo que estás proponiendo . Resuelve el mismo problema de acceso al error de resultado. ¿Qué piensas?

Según https://github.com/golang/go/issues/32437#issuecomment -499320588:

func doPanic(err error) error { panic(err) }

Anticipo que esta función sería bastante común. ¿Podría esto estar predefinido en "incorporado" (o en algún otro lugar en un paquete estándar, por ejemplo errors )?

Lástima que no anticipes genéricos lo suficientemente potentes como para implementar
intentarlo, en realidad hubiera esperado que fuera posible hacerlo.

Sí, esta propuesta podría ser un primer paso, aunque no le veo mucha utilidad
yo mismo tal como está ahora.

Por supuesto, este problema quizás se centre demasiado en alternativas detalladas,
pero demuestra que muchos participantes no están completamente satisfechos con
eso. Lo que parece faltar es un amplio consenso sobre esta propuesta...

Op vr 7 jun. 2019 01:04 schreef pj [email protected] :

Asper #32437 (comentario)
https://github.com/golang/go/issues/32437#issuecomment-499320588 :

func doPanic(err error) error { panic(err) }

Anticipo que esta función sería bastante común. ¿Podría ser esto predefinido?
en "incorporado"?


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAARM6OOOLLYO5ZCE6VVL2TPZGJWRA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXEMYZY#issue,98-4919 6
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AAARM6K5AOR2DES4QDTNLSTPZGJWRANCNFSM4HTGCZ7Q
.

@pjebs , he escrito la función equivalente docenas de veces. Normalmente lo llamo “orDie” o “check”. Es tan simple que no hay necesidad real de hacerlo parte de la biblioteca estándar. Además, diferentes personas pueden querer iniciar sesión o lo que sea antes de la terminación.

@beoran Quizás podría ampliar la conexión entre los genéricos y el manejo de errores. Cuando pienso en ellos, parecen dos cosas diferentes. Los genéricos no son un cajón de sastre que puede abordar todos los problemas con el idioma. Es la capacidad de escribir una sola función que puede operar en múltiples tipos.

Esta propuesta específica de manejo de errores trata de reducir el modelo mediante la introducción de una función predeclarada try que cambia el control de flujo en algunas circunstancias. Los genéricos nunca cambiarán el flujo de control. Así que realmente no veo la relación.

Mi reacción inicial a esto fue un 👎 ya que imaginé que manejar varias llamadas propensas a errores dentro de una función haría que el error defer fuera confuso. Después de leer toda la propuesta, cambié mi reacción a un ❤️ y un 👍 cuando aprendí que esto todavía se puede lograr con una complejidad relativamente baja.

@carlmjohnson Sí, es simple pero...

He escrito la función equivalente docenas de veces.

Las ventajas de una función predeclarada son:

  1. Podemos una sola línea
  2. No necesitamos volver a declarar la función err => panic en cada paquete que usamos, o mantener una ubicación común para ella. Dado que probablemente sea común para todos en la comunidad de Go, el "paquete estándar" es _ la _ ubicación común para él.

@griesemer Con la variante del controlador de errores de la propuesta de prueba original, ya no se requiere el requisito de la función general para devolver el error.

Cuando pregunté por primera vez sobre el err => pánico, me señalaron que la propuesta lo consideraba pero lo consideraba demasiado peligroso (por una buena razón). Pero si usamos try() sin un controlador de errores en un escenario donde la función general no devuelve un error, convertirlo en un error en tiempo de compilación alivia la preocupación discutida en la propuesta.

@pjebs El requisito de la función general para devolver un error no se requería en el diseño original _si_ se proporcionó un controlador de errores. Pero es solo otra complicación de try . Es _mucho_ mejor mantenerlo simple. En cambio, sería más claro tener una función must separada, que siempre entra en pánico en caso de error (pero de lo contrario es como try ). Entonces es obvio lo que sucede en el código y uno no tiene que mirar el contexto.

El principal atractivo de tener un must este tipo sería que podría usarse con pruebas unitarias; especialmente si el paquete testing se ajustó adecuadamente para recuperarse de los pánicos causados ​​por must y reportarlos como fallas de prueba de una manera agradable. Pero, ¿por qué agregar otro nuevo mecanismo de idioma cuando solo podemos ajustar el paquete de prueba para que también acepte la función de prueba de la forma TestXxx(t *testing.T) error ? Si devuelven un error, lo que parece bastante natural después de todo (quizás deberíamos haber hecho esto desde el principio), entonces try funcionará bien. Las pruebas locales necesitarán un poco más de trabajo, pero probablemente sea factible.

El otro uso relativamente común de must es en expresiones de inicialización global ( must(regexp.Compile... , etc.). Si sería "bueno tenerlo", pero eso no necesariamente lo eleva al nivel requerido para una nueva función de idioma.

@griesemer Dado que must está vagamente relacionado con try , y dado que el impulso es hacia la implementación try , ¿no cree que es bueno considerar must ?

Lo más probable es que si no se discute en esta ronda, simplemente no se implementará ni se considerará seriamente, al menos durante más de 3 años (o tal vez nunca). La superposición en la discusión también sería buena en lugar de comenzar desde cero y reciclar las discusiones.

Mucha gente ha dicho que must complementa muy bien a try .

@pjebs Ciertamente, no parece que haya ningún "impulso hacia la implementación try " en este momento... - Y también publicamos esto hace solo dos días. Tampoco se ha decidido nada. Démosle un poco de tiempo a esto.

No se nos ha escapado que must encaja muy bien con try , pero eso no es lo mismo que hacerlo parte del lenguaje. Solo hemos comenzado a explorar este espacio con un grupo más amplio de personas. Realmente no sabemos todavía qué podría surgir a favor o en contra. Gracias.

Después de pasar horas leyendo todos los comentarios y el documento de diseño detallado, quería agregar mis puntos de vista a esta propuesta.

Haré todo lo posible para respetar la solicitud de @ianlancetaylor de no solo reafirmar los puntos anteriores, sino agregar nuevos comentarios a la discusión. Sin embargo, no creo que pueda hacer los nuevos comentarios sin hacer referencia a los comentarios anteriores.

Preocupaciones

Lamentable sobrecarga de diferir

La preferencia por sobrecargar la naturaleza obvia y directa de defer como alarmante. Si escribo defer closeFile(f) es claro y obvio para mí lo que sucede y por qué; al final de la función que será llamada. Y aunque usar defer para panic() y recover() es menos obvio, casi nunca lo uso y casi nunca lo veo cuando leo el código de otros.

Spoo para sobrecargar defer para manejar también los errores no es obvio y confuso. ¿Por qué la palabra clave defer ? ¿ defer no significa _"Hacer más tarde"_ en lugar de _"Quizás para más tarde?"_

También existe la preocupación mencionada por el equipo de Go sobre el rendimiento de defer . Dado eso, parece doblemente desafortunado que se esté considerando defer para el flujo de código _"ruta activa"_.

No hay estadísticas que verifiquen un caso de uso significativo

Como mencionó @prologic , ¿esta propuesta try() se basa en un gran porcentaje de código que usaría este caso de uso, o se basa en cambio en intentar aplacar a aquellos que se han quejado del manejo de errores de Go?

Ojalá supiera cómo brindarles estadísticas de mi base de código sin revisar exhaustivamente cada archivo y tomar notas; No sé cómo @prologic pudo hacerlo, aunque me alegro de haberlo hecho.

Pero, como anécdota, me sorprendería si try() abordara el 5 % de mis casos de uso y sospecharía que abordaría menos del 1 %. ¿Sabes con certeza que otros tienen resultados muy diferentes? ¿Ha tomado un subconjunto de la biblioteca estándar y ha intentado ver cómo se aplicaría?

Debido a que sin estadísticas conocidas de que esto es apropiado para una gran cantidad de código en la naturaleza, tengo que preguntar si este nuevo cambio complicado en el lenguaje que requerirá que todos aprendan los nuevos conceptos realmente aborda una cantidad convincente de casos de uso.

Hace que sea más fácil para los desarrolladores ignorar los errores

Esto es una repetición total de lo que otros han comentado, pero lo que básicamente proporciona try() es análogo en muchos sentidos a simplemente adoptar lo siguiente como código idomático, y este es un código que nunca encontrará su camino en ningún código por sí mismo. -respetando las naves de los desarrolladores:

f, _ := os.Open(filename)

Sé que puedo ser mejor en mi propio código, pero también sé que muchos de nosotros dependemos de la generosidad de otros desarrolladores de Go que publican algunos paquetes tremendamente útiles, pero por lo que he visto en _"Other People's Code(tm)"_ Las mejores prácticas en el manejo de errores a menudo se ignoran.

En serio, ¿realmente queremos que sea más fácil para los desarrolladores ignorar los errores y permitirles contaminar GitHub con paquetes no robustos?

Puede (en su mayoría) ya implementar try() en el espacio del usuario

A menos que no entienda bien la propuesta, que probablemente sea así, aquí hay try() en el Go Playground implementado en userland , aunque con solo un (1) valor de retorno y una interfaz en lugar del tipo esperado:

package main

import (
    "errors"
    "fmt"
    "strings"
)
func main() {
    defer func() {
        r := recover()
        if r != nil && strings.HasPrefix(r.(string),"TRY:") {
            fmt.Printf("Ouch! %s",strings.TrimPrefix(r.(string),"TRY: "))
        }
    }()
    n := try(badjuju()).(int)
    fmt.Printf("Just chillin %dx!",n)   
}
func badjuju() (int,error) {
    return 10, errors.New("this is a really bad error")
}
func try(args ...interface{}) interface{} {
    err,ok := args[1].(error)
    if ok && err != nil {
        panic(fmt.Sprintf("TRY: %s",err.Error()))
    }
    return args[0]
}

Entonces, el usuario podría agregar un try2() , try3() y así sucesivamente dependiendo de cuántos valores devueltos necesitaba devolver.

Pero Go solo necesitaría una (1) función de idioma simple _ pero universal _ para permitir a los usuarios que desean try() implementar su propio soporte, aunque aún requiere una afirmación de tipo explícita. Agregue una capacidad _(totalmente compatible con versiones anteriores)_ para un Go func para devolver un número variable de valores de retorno, por ejemplo:

func try(args ...interface{}) ...interface{} {
    err,ok := args[1].(error)
    if ok && err != nil {
        panic(fmt.Sprintf("TRY: %s",err.Error()))
    }
    return args[0:len(args)-2]
}

Y si aborda los genéricos primero, entonces las aserciones de tipo ni siquiera serían necesarias _ (aunque creo que los casos de uso para los genéricos deberían reducirse agregando elementos integrados para abordar los casos de uso de los genéricos en lugar de agregar la semántica confusa y la ensalada de sintaxis de los genéricos de Java y otros)_

falta de obviedad

Al estudiar el código de la propuesta, encuentro que el comportamiento no es obvio y es algo difícil de razonar.

Cuando veo try() envolviendo una expresión, ¿qué sucederá si se devuelve un error?

¿Se ignorará el error? ¿O saltará al primero o al más reciente defer , y si es así establecerá automáticamente una variable llamada err dentro del cierre que, o lo pasará como un parámetro _(I no ve un parámetro?)_. Y si no es un nombre de error automático, ¿cómo lo nombro? ¿Y eso significa que no puedo declarar mi propia variable err en mi función para evitar conflictos?

¿Y llamará a todos los defer s? ¿En orden inverso o en orden regular?

¿O regresará tanto del cierre como del func donde se devolvió el error? _(Algo que nunca hubiera considerado si no hubiera leído aquí palabras que implican eso.)_

Después de leer la propuesta y todos los comentarios hasta el momento, sinceramente, todavía no sé las respuestas a las preguntas anteriores. ¿Es ese el tipo de característica que queremos agregar a un lenguaje cuyos defensores defienden ser _"Capitán Obvio?"_

Falta de control

Usando defer , parece que el único control que se les daría a los desarrolladores es ramificarse a _(¿el más reciente?)_ defer . Pero en mi experiencia con cualquier método más allá de un func trivial, por lo general es más complicado que eso.

A menudo me ha resultado útil compartir el aspecto del manejo de errores dentro de un func , o incluso en un package , pero luego también tener un manejo más específico compartido en uno o más paquetes.

Por ejemplo, puedo llamar a cinco (5) llamadas de func que devuelven un error() desde dentro de otro func ; vamos a etiquetarlos como A() , B() , C() , D() y E() . Es posible que necesite C() para tener su propio manejo de errores, A() , B() , D() y E() para compartir algunos manejos de errores, y B() y E() para tener un manejo específico.

Pero no creo que sea posible hacer eso con esta propuesta. Al menos no fácilmente.

Sin embargo, irónicamente, Go ya tiene funciones de lenguaje que permiten un alto nivel de flexibilidad que no necesita limitarse a un pequeño conjunto de casos de uso; func sy cierres. Así que mi pregunta retórica es:

_ "¿Por qué no podemos simplemente agregar ligeras mejoras al lenguaje existente para abordar estos casos de uso y no necesitamos agregar nuevas funciones integradas o aceptar una semántica confusa?" _

Es una pregunta retórica porque planeo presentar una propuesta como alternativa, que concebí durante el estudio de esta propuesta y considerando todos sus inconvenientes.

Pero me estoy desviando, eso vendrá más adelante y este comentario es sobre por qué la propuesta actual necesita ser reconsiderada.

Falta de apoyo declarado para break

Esto puede parecer que sale del campo izquierdo ya que la mayoría de las personas usan retornos anticipados para el manejo de errores, pero he descubierto que es preferible usar break para el manejo de errores que envuelve la mayor parte o la totalidad de una función antes de return .

He usado este enfoque durante un tiempo y sus beneficios para facilitar la refactorización solo lo hacen preferible a return temprano, pero tiene varios otros beneficios, incluido un punto de salida único y la capacidad de terminar una sección de una función antes pero aún ser Capaz de ejecutar la limpieza _ (que es probablemente la razón por la que rara vez uso defer , que me resulta más difícil razonar en términos de flujo del programa).

Para usar break en lugar del retorno anticipado, use un bucle for range "1" {...} para crear un bloque para que la ruptura salga de _ (de hecho, creo un paquete llamado only que solo contiene una constante llamado Once con un valor de "1" ):_

func (me *Config) WriteFile() (err error) {
    for range only.Once {
        var j []byte
        j, err = json.MarshalIndent(me, "", "    ")
        if err != nil {
            err = fmt.Errorf("unable to marshal config; %s", 
                err.Error(),
            )
            break
        }
        err = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
        if err != nil {
            err = fmt.Errorf("unable to make directory'%s'; %s", 
                me.GetDir(), 
                err.Error(),
            )
            break
        }
        err = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
        if err != nil {
            err = fmt.Errorf("unable to write to config file '%s'; %s", 
                me.GetFilepath(), 
                err.Error(),
            )
            break
        }
    }
    return err
}

Planeo escribir un blog sobre el patrón en detalle en un futuro cercano, y discutir las diversas razones por las que he descubierto que funciona mejor que los retornos anticipados.

Pero yo divago. La razón por la que lo mencioné aquí es que Go implementaría un manejo de errores que asume return tempranos e ignora el uso break para el manejo de errores.

Mi opinión err == nil es problemática

Como digresión adicional, quiero mencionar la preocupación que he sentido sobre el manejo de errores idiomáticos en Go. Si bien creo firmemente en la filosofía de Go para manejar los errores cuando ocurren en lugar de usar el manejo de excepciones, siento que el uso de nil para indicar que no hay errores es problemático porque a menudo encuentro que me gustaría devolver un mensaje de éxito de una rutina, para usar en las respuestas de la API, y no solo devolver un valor no nulo solo cuando hay un error.

Entonces, para Go 2, realmente me gustaría ver que Go considere agregar un nuevo tipo integrado de status y tres funciones integradas iserror() , iswarning() , issuccess() . status podría implementar error , lo que permite mucha compatibilidad con versiones anteriores y un nil pasado a issuccess() devolvería true , pero status tendría un estado interno adicional para el nivel de error, de modo que la prueba del nivel de error siempre se realizaría con una de las funciones integradas e, idealmente, nunca con una verificación nil . Eso permitiría algo como el siguiente enfoque en su lugar:

func (me *Config) WriteFile() (sts status) {
    for range only.Once {
        var j []byte
        j, sts = json.MarshalIndent(me, "", "    ")
        if iserror(sts) {
            sts.AddMessage("unable to marshal config")
            break
        }
        sts = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
        if iserror(sts) {
            sts.AddMessage("unable to make directory'%s'", me.GetDir())
            break
        }
        sts = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
        if iserror(sts) {
            sts.AddMessage("unable to write to config file '%s'", 
                me.GetFilepath(), 
            )
            break
        }
        sts = fmt.Status("config file written")
    }
    return sts
}

Ya estoy usando un enfoque de espacio de usuario en un paquete de uso interno actual de nivel pre-beta que es similar al anterior para el manejo de errores. Francamente , paso mucho menos tiempo pensando en cómo estructurar el código cuando utilizo este enfoque que cuando intentaba seguir el manejo idiomático de errores de Go.

Si cree que existe alguna posibilidad de que el código Go idiomático evolucione hacia este enfoque, tómelo en cuenta al implementar el manejo de errores, incluso al considerar esta propuesta try() .

_"No para todos"_ justificación

Una de las respuestas clave del equipo de Go ha sido _"Nuevamente, esta propuesta no intenta resolver todas las situaciones de manejo de errores"._
Y esa es probablemente la preocupación más preocupante, desde una perspectiva de gobernanza.

¿Este nuevo cambio complicado en el lenguaje que requerirá que todos aprendan los nuevos conceptos realmente aborda una cantidad convincente de casos de uso?

¿Y no es esa la misma justificación que los miembros del equipo central han negado numerosas solicitudes de funciones de la comunidad? La siguiente es una cita directa de un comentario realizado por un miembro del equipo de Go en una respuesta arquetípica a una solicitud de función enviada hace aproximadamente 2 años _ (no estoy nombrando a la persona o la solicitud de función específica porque esta discusión no debería poder la gente sino sobre el idioma):_

_"Una nueva característica del lenguaje necesita casos de uso convincentes. Todas las características del lenguaje son útiles, o nadie las propondría; la pregunta es: ¿son lo suficientemente útiles como para justificar complicar el lenguaje y requerir que todos aprendan los nuevos conceptos? ¿Cuáles son los usos convincentes? casos aquí? ¿Cómo la gente los usará? Por ejemplo, ¿la gente esperaría poder... y si es así, cómo lo harían? ¿Esta propuesta hace más que permitirle...?"_
— Un miembro central del equipo de Go

Francamente, cuando he visto esas respuestas, he sentido uno de dos sentimientos:

  1. Indignación si es una característica con la que estoy de acuerdo, o
  2. Elation si es una característica con la que no estoy de acuerdo.

Pero en cualquier caso, mis sentimientos eran/son irrelevantes; Entiendo y estoy de acuerdo en que parte de la razón por la que Go es el idioma en el que muchos de nosotros elegimos desarrollarnos es por esa celosa protección de la pureza del idioma.

Y es por eso que esta propuesta me preocupa tanto, porque el equipo central de Go parece estar profundizando en esta propuesta al mismo nivel que alguien que quiere dogmáticamente una característica esotérica que de ninguna manera la comunidad de Go tolerará.

_(Y realmente espero que el equipo no dispare al mensajero y tome esto como una crítica constructiva de alguien que quiere ver a Go seguir siendo lo mejor que puede ser para todos nosotros, ya que tendría que ser considerado "Persona non grata" por el equipo central.)_

Si exigir un conjunto convincente de casos de uso del mundo real es la barra para todas las propuestas de características generadas por la comunidad, ¿no debería ser también la misma barra para _todas_ las propuestas de características?

Anidamiento de try()

Esto también fue cubierto por algunos, pero quiero hacer una comparación entre try() y la solicitud continua de operadores ternarios. Citando los comentarios de otro miembro del equipo de Go hace unos 18 meses:

_"cuando se 'programa en grande' (bases de código grandes con equipos grandes durante largos períodos de tiempo), el código se lee MUCHO más a menudo de lo que se escribe, por lo que optimizamos la legibilidad, no la escritura"._

Una de las razones _principales_ declaradas para no agregar operadores ternarios es que son difíciles de leer y/o fáciles de leer mal cuando están anidados. Sin embargo, lo mismo puede ser cierto de declaraciones try() anidadas como try(try(try(to()).parse().this)).easily()) .

Razones adicionales para argumentar en contra de los operadores ternarios han sido que son _"expresiones"_ con el argumento de que las expresiones anidadas pueden agregar complejidad. ¿Pero try() crea también una expresión anidable?

Ahora alguien aquí dijo _"Creo que ejemplos como [ try() anidados] no son realistas"_ y esa declaración no fue cuestionada.

Pero si la gente acepta como postulado que los desarrolladores no anidarán try() entonces ¿por qué no se da la misma deferencia a los operadores ternarios cuando la gente dice _"Creo que los operadores ternarios profundamente anidados no son realistas?"_

En pocas palabras para este punto, creo que si el argumento en contra de los operadores ternarios es realmente válido, entonces también deberían considerarse argumentos válidos en contra de esta propuesta try() .

En resumen

En el momento de escribir esto, los 58% votos negativos contra 42% votos positivos. Creo que esto por sí solo debería ser suficiente para indicar que esta propuesta es lo suficientemente divisiva como para que sea hora de volver a la mesa de dibujo sobre este tema.

por favor

PD Para ponerlo más irónico, creo que deberíamos seguir la sabiduría parafraseada de Yoda:

_"No hay try() . Solo do() ."_

@ianlancetaylor

@beoran Quizás podría ampliar la conexión entre los genéricos y el manejo de errores.

No hablo por @beoran, pero en mi comentario de hace unos minutos verán que si tuviéramos genéricos _(más parámetros de retorno variados)_ entonces podríamos construir nuestro propio try() .

Sin embargo, y repetiré lo que dije anteriormente sobre los genéricos aquí, donde será más fácil de ver:

_"Creo que los casos de uso para los genéricos deberían reducirse agregando elementos integrados para abordar los casos de uso de los genéricos en lugar de agregar la confusa semántica y la sintaxis de los genéricos de Java et. al.)"_

@ianlancetaylor

Al tratar de formular una respuesta a su pregunta, traté de implementar la función try en Go tal como está y, para mi deleite, ya es posible emular algo bastante similar:

func try(v interface{}, err error) interface{} {
   if err != nil { 
     panic(err)
   }
   return v
}

Vea aquí cómo se puede usar: https://play.golang.org/p/Kq9Q0hZHlXL

Las desventajas de este enfoque son:

  1. Se necesita un rescate diferido, pero con try como en esta propuesta, también se necesita un controlador diferido si queremos realizar un manejo de errores adecuado. Así que siento que esto no es un inconveniente serio. Incluso podría ser mejor si Go tuviera algún tipo de super(arg1, ..., argn) incorporado que haga que la persona que llama, un nivel más arriba en la pila de llamadas, regrese con los argumentos dados arg1,...argn, una especie de súper retorno Si tu quieres.
  2. Este try que implementé solo puede funcionar con una función que devuelve un solo resultado y un error.
  3. Debe escribir afirmar los resultados de la interfaz emtpy devueltos.

Los genéricos suficientemente potentes podrían resolver el problema 2 y 3, dejando solo 1, que podría resolverse agregando un super() . Con esas dos características en su lugar, podríamos obtener algo como:

func (T ... interface{})try(T, err error) super {
   if err != nil { 
      super(err)
   }
  super(T...)
}

Y entonces el rescate diferido ya no sería necesario. Este beneficio estaría disponible incluso si no se agregan genéricos a Go.

En realidad, esta idea de un super() incorporado es tan poderosa e interesante que podría publicar una propuesta por separado.

@beoran Es bueno ver que llegamos exactamente a las mismas restricciones de forma independiente con respecto a la implementación try() en el área de usuario, excepto por la parte superior que no incluí porque quería hablar sobre algo similar en una propuesta alternativa. :-)

Me gusta la propuesta, pero el hecho de que tuvieras que especificar explícitamente que defer try(...) y go try(...) no están permitidos me hizo pensar que algo no estaba del todo bien... La ortogonalidad es una buena guía de diseño. Al leer más y ver cosas como
x = try(foo(...)) y = try(bar(...))
¡Me pregunto si puede ser try necesita ser un contexto ! Considerar:
try ( x = foo(...) y = bar(...) )
Aquí foo() y bar() devuelven dos valores, el segundo de los cuales es error . Intente que la semántica solo importe para las llamadas dentro del bloque try donde el valor de error devuelto se omite (sin receptor) en lugar de ignorarlo (el receptor es _ ). Incluso puede manejar algunos errores entre llamadas foo y bar .

Resumen:
a) el problema de no permitir try por go y defer desaparece en virtud de la sintaxis.
b) el manejo de errores de múltiples funciones puede eliminarse.
c) su naturaleza mágica se expresa mejor como una sintaxis especial que como una llamada de función.

Si try es un contexto, acabamos de crear bloques try/catch que estamos tratando de evitar específicamente (y por una buena razón)

No hay trampa. Se generaría exactamente el mismo código que cuando la propuesta actual tiene
x = try(foo(...)) y = try(bar(...))
Esta es solo una sintaxis diferente, no semántica.
````

Supongo que hice algunas suposiciones al respecto que no debería haber hecho, aunque todavía tiene un par de inconvenientes.

¿Qué sucede si foo o bar no devuelven un error? ¿También se pueden colocar en el contexto de prueba? Si no, parece que sería un poco feo cambiar entre funciones de error y no error, y si pueden, entonces volvemos a los problemas de los bloques de prueba en lenguajes más antiguos.

Lo segundo es que, por lo general, la sintaxis keyword ( ... ) significa que antepone la palabra clave en cada línea. Entonces, para import, var, const, etc.: cada línea comienza con la palabra clave. Hacer una excepción a esa regla no parece una buena decisión

En lugar de usar una función, ¿sería más idiomático usar un identificador especial?

Ya tenemos el identificador en blanco _ que ignora los valores.
Podríamos tener algo como # que solo se puede usar en funciones que tienen el último valor devuelto de tipo error.

func foo() (error) {
    f, # := os.Open()
    defer f.Close()
    _, # = f.WriteString("foo")
    return nil
}

cuando se asigna un error a # la función regresa inmediatamente con el error recibido. En cuanto a las demás variables sus valores serían:

  • si no se nombran valor cero
  • el valor asignado a las variables nombradas de lo contrario

@deanveloper , la semántica del bloque try solo importa para las funciones que devuelven un valor de error y donde el valor de error no está asignado. Entonces, el último ejemplo de la presente propuesta también podría escribirse como
try(x = foo(...)) try(y = bar(...))
poner ambas sentencias dentro del mismo bloque es similar a lo que hacemos para las sentencias import , const y var repetidas.

Ahora si tienes, por ejemplo
try( x = foo(...)) go zee(...) defer fum() y = bar(...) )
Esto es equivalente a escribir
try(x = foo(...)) go zee(...) defer fum() try(y = bar(...))
Factorizar todo eso en un bloque de prueba lo hace menos ocupado.

Considerar
try(x = foo())
Si foo() no devuelve un valor de error, esto es equivalente a
x = foo()

Considerar
try(f, _ := os.open(filename))
Dado que se ignora el valor de error devuelto, esto es equivalente a solo
f, _ := os.open(filename)

Considerar
try(f, err := os.open(filename))
Dado que el valor de error devuelto no se ignora, esto es equivalente a
f, err := os.open(filename) if err != nil { return ..., err }
Como se especifica actualmente en la propuesta.

¡Y también ordena muy bien los intentos anidados!

Aquí hay un enlace a la propuesta alternativa que mencioné anteriormente:

Requiere agregar dos (2) funciones de lenguaje pequeñas pero de propósito general para abordar los mismos casos de uso que try()

  1. Capacidad para llamar a un func /cierre en una instrucción de asignación.
  2. Habilidad para break , continue o return más de un nivel.

Con estas dos características, no habría _"magia"_ y creo que su uso produciría un código Go que es más fácil de entender y más en línea con el código idiomático Go con el que todos estamos familiarizados.

He leído la propuesta y realmente me gusta a dónde va Try.

Dado lo frecuente que será el intento, me pregunto si convertirlo en un comportamiento más predeterminado lo haría más fácil de manejar.

Considere los mapas. Esto es válido:

v := m[key]

como es esto:

v, ok := m[key]

¿Qué pasa si manejamos los errores exactamente de la manera que sugiere try, pero eliminamos el incorporado. Así que si comenzamos con:

v, err := fn()

En lugar de escribir:

v := try(fn())

En su lugar, podríamos escribir:

v := fn()

Cuando el valor de err no se captura, se maneja exactamente como lo hace el intento. Tomaría un poco de tiempo acostumbrarse, pero se siente muy similar a v, ok := m[key] y v, ok := x.(string) . Básicamente, cualquier error no controlado hace que la función regrese y se establezca el valor err.

Para volver a las conclusiones de los documentos de diseño y los requisitos de implementación:

• Se conserva la sintaxis del idioma y no se introducen nuevas palabras clave
• Sigue siendo azúcar sintáctico como try y, con suerte, es fácil de explicar.
• No requiere nueva sintaxis
• Debe ser completamente compatible con versiones anteriores.

Me imagino que esto tendría casi los mismos requisitos de implementación que try, ya que la principal diferencia es que, en lugar de que el incorporado active el azúcar sintáctico, ahora es la ausencia del campo err.

Entonces, usando el ejemplo CopyFile de la propuesta junto con defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) , obtenemos:

func CopyFile(src, dst string) (err error) {
        defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)

        r := os.Open(src)
        defer r.Close()

        w := os.Create(dst)
        defer func() {
                err := w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        io.Copy(w, r)
        w.Close()
        return nil
}

@savaki Me gusta esto y estaba pensando en lo que se necesitaría para que Go cambie el manejo de errores al manejar siempre los errores de forma predeterminada y dejar que el programador especifique cuándo no hacerlo (capturando el error en una variable) pero falta total de cualquier El identificador haría que el código fuera difícil de seguir, ya que uno no podría ver todos los puntos de retorno. Puede haber una convención para nombrar funciones que podrían devolver un error de manera diferente (como usar mayúsculas en los identificadores públicos). Puede ser que si una función devuelve un error, siempre debe terminar con, digamos ? . Entonces, Go siempre podría manejar implícitamente el error y devolverlo automáticamente a la función de llamada, como intentarlo. Esto lo hace muy similar a algunas propuestas que sugieren usar un identificador ? en lugar de intentarlo, pero una diferencia importante es que aquí ? sería parte del nombre de la función y no un identificador adicional. De hecho, una función que devuelve error como el último valor devuelto ni siquiera se compilaría si no tuviera el sufijo ? . Por supuesto ? es arbitrario y podría reemplazarse con cualquier otra cosa que hiciera más explícita la intención. operation?() sería equivalente a envolver try(someFunc()) pero ? sería parte del nombre de la función y su único propósito sería indicar que la función puede devolver un error al igual que las mayúsculas la primera letra de una variable.

Superficialmente, esto termina siendo muy similar a otras propuestas que solicitan reemplazar try con ? pero una diferencia fundamental es que hace que el manejo de errores sea implícito (automático) y, en cambio, hace explícitos los errores de ignorar (o envolver) que una especie de mejor práctica de todos modos. El problema más obvio con esto, por supuesto, es que no es compatible con versiones anteriores y estoy seguro de que hay muchos más.

Dicho esto, estaría muy interesado en ver cómo Go puede hacer que el manejo de errores sea el caso predeterminado/implícito al automatizarlo y dejar que el programador escriba un poco de código adicional para ignorar/anular el manejo. Creo que el desafío es cómo hacer que todos los puntos de retorno sean obvios en este caso porque sin ellos los errores se volverán más como excepciones en el sentido de que podrían provenir de cualquier parte ya que el flujo del programa no lo haría obvio. Se podría decir que cometer errores implícitos con el indicador visual es lo mismo que implementar try y hacer que errcheck sea una falla del compilador.

¿podríamos hacer algo como las excepciones de C++ con decoradores para funciones antiguas?

func some_old_test() (int, error){
    return 0, errors.New("err1")
}
func some_new_test() (int){
        if true {
             return 1
        }
    throw errors.New("err2")
}
func throw_res(int, e error) int {
    if e != nil {
        throw e
    }
    return int
}
func main() {
    fmt.Println("Hello, playground")
    try{
        i := throw_res(some_old_test())
        fmt.Println("i=", i + some_new_test())
    } catch(err io.Error) {
        return err
    } catch(err error) {
        fmt.Println("unknown err", err)
    }
}

@owais Estaba pensando que la semántica sería exactamente la misma que intentar, por lo que al menos tendría que declarar el tipo de error. Así que si comenzamos con:

func foo() error {
  _, err := fn() 
  if err != nil {
    return err
  }

  return nil
} 

Si entiendo la propuesta de prueba, simplemente haciendo esto:

func foo() error {
  _  := fn() 
  return nil
} 

no compilaría. Una buena ventaja es que le da a la compilación la oportunidad de decirle al usuario lo que falta. Algo en el sentido de que usar el manejo implícito de errores requiere que se nombre el tipo de retorno de error, err.

Esto entonces, funcionaría:

func foo() (err error) {
  _  := fn() 
  return nil
} 

¿Por qué no simplemente manejar el caso de un error que no está asignado a una variable?

  • elimine la necesidad de devoluciones con nombre, el compilador puede hacer todo esto por sí solo.
  • permite añadir contexto.
  • maneja el caso de uso común.
  • compatible con versiones anteriores
  • no interactúa extrañamente con aplazamiento, bucles o interruptores.

retorno implícito para el caso if err != nil, el compilador puede generar un nombre de variable local para retornos si es necesario, el programador no puede acceder.
personalmente no me gusta este caso particular desde el punto de vista de la legibilidad del código

f := os.Open("foo.txt")

prefiero un retorno explícito, sigue el código se lee más que el mantra escrito

f := os.Open("foo.txt") else return

Curiosamente, podríamos aceptar ambas formas y hacer que gofmt agregue automáticamente el retorno else.

agregando contexto, también denominación local de la variable. return se vuelve explícito porque queremos agregar contexto.

f := os.Open("foo.txt") else err {
  return errors.Wrap(err, "some context")
}

agregar contexto con múltiples valores de retorno

f := os.Open("foo.txt") else err {
  return i, j, errors.Wrap(err, "some context")
}

las funciones anidadas requieren que las funciones externas manejen todos los resultados en el mismo orden
menos el error final.

bits := ioutil.ReadAll(os.Open("foo")) else err {
  // either error ends up here.
  return i, j, errors.Wrap(err, "some context")
}

el compilador rechaza la compilación debido a que falta el valor de retorno de error en la función

func foo(s string) int {
   i := strconv.Atoi(s) // cannot implicitly return error due to missing error return value for foo.
   return i * 2
}

felizmente compila porque el error se ignora explícitamente.

func foo(s string) int {
   i, _ := strconv.Atoi(s)
   return i * 2
} 

el compilador está feliz. ignora el error como lo hace actualmente porque no se produce ninguna asignación o sufijo.

func foo() error {
  return errors.New("whoops")
}

func bar() {
  foo()
}

dentro de un ciclo puede usar continuar.

for _, s := range []string{"1","2","3","4","5","6"} {
  i := strconv.Atoi(s) else continue
}

editar: reemplazó ; con else

@savaki Creo que entendí su comentario original y me gusta la idea de que Go maneje los errores de forma predeterminada, pero no creo que sea viable sin agregar algunos cambios de sintaxis adicionales y, una vez que lo hagamos, se vuelve sorprendentemente similar a la propuesta actual.

El mayor inconveniente de lo que propones es que no expone todos los puntos desde donde una función puede devolver a diferencia del actual if err != nil {return err} o la función de prueba presentada en esta propuesta. Aunque funcionaría exactamente de la misma manera debajo del capó, visualmente el código se vería muy diferente. Al leer el código, no habría forma de saber qué llamadas de función podrían devolver un error. Eso terminaría siendo una experiencia peor que las excepciones en mi opinión.

Es posible que el manejo de errores se haga implícito si el compilador forzó alguna convención semántica en las funciones que podrían devolver errores. Al igual que deben comenzar o terminar con una determinada frase o carácter. Eso haría que todos los puntos de retorno fueran muy obvios y creo que sería mejor que el manejo manual de errores, pero no estoy seguro de cuán significativamente mejor teniendo en cuenta que ya hay controles de pelusa que gritan carga cuando detectan que se ignora un error. Sería muy interesante ver si el compilador puede forzar que las funciones sean nombradas de cierta manera dependiendo de si pueden devolver posibles errores.

El principal inconveniente de este enfoque es que se debe nombrar el parámetro de resultado del error, lo que posiblemente genere API menos bonitas (pero consulte las preguntas frecuentes sobre este tema). Creemos que nos acostumbraremos una vez que este estilo se haya consolidado.

No estoy seguro si algo como esto se ha sugerido antes, no puedo encontrarlo aquí o en la propuesta. ¿Ha considerado otra función integrada que devuelve un puntero al valor de retorno de error de la función actual?
p.ej:

func example() error {
        var err *error = funcerror() // always return a non-nil pointer
        fmt.Print(*err) // always nil if the return parameters are not named and not in a defer

        defer func() {
                err := funcerror()
                fmt.Print(*err) // "x"
        }

        return errors.New("x")
}
func exampleNamed() (err error) {
        funcErr := funcerror()
        fmt.Print(*funcErr) // "nil"

        err = errors.New("x")
        fmt.Print(*funcErr) // "x", named return parameter is reflected even before return is called

        *funcErr = errors.New("y")
        fmt.Print(err) // "y", unfortunate side effect?

        defer func() {
                funcErr := funcerror()
                fmt.Print(*funcErr) // "z"
                fmt.Print(err) // "z"
        }

        return errors.New("z")
}

uso con prueba:

func CopyFile(src, dst string) (error) {
        defer func() {
                err := funcerror()
                if *err != nil {
                        *err = fmt.Errorf("copy %s %s: %v", src, dst, err)
                }
        }()
        // one liner alternative
        // defer fmt.HandleErrorf(funcerror(), "copy %s %s", src, dst)

        r := try(os.Open(src))
        defer r.Close()

        w := try(os.Create(dst))
        defer func() {
                w.Close()
                err := funcerror()
                if *err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        try(io.Copy(w, r))
        try(w.Close())
        return nil
}

Alternativamente, funerrorr (el nombre es un trabajo en progreso: D) podría devolver nil si no se llama dentro de defer.

Otra alternativa es que funcror devuelva una interfaz de "Error" para que sea de solo lectura:

type interface Errorer() {
        Error() error
}

@savaki De hecho, me gusta su propuesta de omitir try() y permitir que sea más como probar un mapa o una afirmación de tipo. Eso se siente mucho más _"Go-like"._

Sin embargo, todavía veo un problema evidente, y es que su propuesta supone que todos los errores que usan este enfoque activarán un return y dejarán la función. Lo que no contempla es emitir un break de los for actuales ni un continue de los for actuales.

Los primeros return son un mazo cuando muchas veces un bisturí es la mejor opción.

Por lo tanto, afirmo que se debe permitir que break y continue sean estrategias válidas de manejo de errores y, actualmente, su propuesta supone solo return mientras que try() supone eso o llamar a un error controlador que en sí mismo solo puede return , no break o continue .

Parece que savaki y yo teníamos ideas similares, solo agregué la semántica de bloque para lidiar con el error si lo desea. Por ejemplo, agregando contexto, para bucles en los que desea cortocircuitar, etc.

@mikeschinkel ve mi extensión, él y yo teníamos ideas similares, simplemente la extendí con una declaración de bloque opcional

@james-lawrence

@mikesckinkel ve mi extensión, él y yo teníamos ideas similares, simplemente la extendí con una declaración de bloque opcional

Tomando tu ejemplo:

f := os.Open("foo.txt"); err {
  return errors.Wrap(err, "some context")
}

Que se compara con lo que hacemos hoy:

f,err := os.Open("foo.txt"); 
if err != nil {
  return errors.Wrap(err, "some context")
}

Definitivamente es preferible para mí. Excepto que tiene algunos problemas:

  1. err parece estar _"mágicamente"_ declarado. La magia debe minimizarse, ¿no? Así que vamos a declararlo:
f, err := os.Open("foo.txt"); err {
  return errors.Wrap(err, "some context")
}
  1. Pero eso todavía no funciona porque Go no interpreta los valores de nil como false ni los valores de puntero como true , por lo que tendría que ser:
f, err := os.Open("foo.txt"); err != nil {
  return errors.Wrap(err, "some context")
}

Y lo que eso funciona, comienza a parecer mucho trabajo y mucha sintaxis en una línea, así que podría continuar haciendo lo antiguo para mayor claridad.

Pero, ¿y si Go agregara dos (2) funciones integradas? iserror() y error() ? Entonces podríamos hacer esto, que no me parece tan malo:

f := os.Open("foo.txt"); iserror() {
  return errors.Wrap(error(), "some context")
}

O mejor _(algo así como):_

f := os.Open("foo.txt"); iserror() {
  return error().Extend("some context")
}

¿Qué piensas tú y los demás?

Aparte, revisa la ortografía de mi nombre de usuario. No me habrían notificado de tu mención si no hubiera estado prestando atención de todos modos...

@mikeschinkel lo siento por el nombre que estaba en mi teléfono y github no estaba sugiriendo automáticamente.

err parece estar declarado "mágicamente". La magia debe minimizarse, ¿no? Así que vamos a declararlo:

meh, toda la idea de insertar automáticamente un retorno es mágica. esto no es lo más mágico que sucede en toda esta propuesta. Además, diría que se declaró el error; justo al final dentro del contexto de un bloque con ámbito, lo que evita que contamine el ámbito principal y, al mismo tiempo, mantenga todas las cosas buenas que obtenemos normalmente con el uso de declaraciones if.

En general, estoy bastante contento con el manejo de errores de go con las próximas adiciones al paquete de errores. No veo nada en esta propuesta como muy útil. Solo estoy tratando de ofrecer el ajuste más natural para el objetivo si estamos decididos a hacerlo.

_"Toda la idea de insertar automáticamente una devolución es mágica"._

No obtendrás ningún argumento de mí allí.

_"Esto no es lo más mágico que está pasando en toda esta propuesta"._

Supongo que estaba tratando de argumentar que _"toda la magia es problemática"._

_"Además, diría que se declaró el error; justo al final dentro del contexto de un bloque con ámbito..."_

Entonces, si quisiera llamarlo err2 ¿esto también funcionaría?

f := os.Open("foo.txt"); err2 {
  return errors.Wrap(err, "some context")
}

Así que asumo que también está proponiendo un manejo especial de casos de err / err2 después del punto y coma, es decir, que se supondría que es nil o no nil en lugar de bool como cuando miras un mapa?

if _,ok := m[a]; !ok {
   print("there is no 'a' in 'm'")
}

En general, estoy bastante contento con el manejo de errores de go con las próximas adiciones al paquete de errores.

Yo también estoy contento con el manejo de errores, cuando se combina con break y continue _(pero no return .)_

Tal como está, veo esta propuesta try() como más dañina que útil, y preferiría no ver nada antes que esta implementación según lo propuesto. #jmtcw.

@beoran @mikeschinkel Anteriormente sugerí que no podíamos implementar esta versión de try usando genéricos, porque cambia el flujo de control. Si estoy leyendo correctamente, ambos están sugiriendo que podríamos usar genéricos para implementar try haciendo que llame a panic . Pero esta versión de try muy explícitamente no panic . Por lo tanto, no podemos usar genéricos para implementar esta versión de try .

Sí, podríamos usar genéricos (una versión de genéricos significativamente más poderosa que la del borrador de diseño en https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md) para escribir una función. que entra en pánico ante el error. Pero entrar en pánico ante un error no es el tipo de manejo de errores que los programadores de Go escriben hoy, y no me parece una buena idea.

El manejo especial de @mikeschinkel sería que el bloque se ejecuta solo cuando hay un error.
```
f := os.Open('foo'); err { return err } // err siempre sería no-nil aquí.

@ianlancetaylor

_"Sí, podríamos usar genéricos... Pero entrar en pánico ante un error no es el tipo de manejo de errores que los programadores de Go escriben hoy, y no me parece una buena idea"._

De hecho, estoy muy de acuerdo con usted en esto, por lo que parece que puede haber malinterpretado la intención de mi comentario. No estaba sugiriendo en absoluto que el equipo de Go implementaría un manejo de errores que usaba panic() , por supuesto que no.

En cambio, estaba tratando de seguir su ejemplo de muchos de sus comentarios anteriores sobre otros problemas y sugerí que evitemos hacer cambios en Go que no sean absolutamente necesarios porque, en cambio, son posibles en el espacio del usuario . Entonces, _si_ se abordaran los genéricos _entonces_ las personas que querrían try() podrían implementarlo ellos mismos, aunque aprovechando panic() . Y esa sería una característica menos que el equipo necesitaría agregar y documentar para Go.

Lo que no estaba haciendo, y tal vez eso no estaba claro, era recomendar que las personas realmente usen panic() para implementar try() , solo que podrían hacerlo si realmente quisieran, y tenían las características de genéricos.

¿Eso aclara?

Para mí, llamar a panic , como sea que se haga, es bastante diferente de esta propuesta para try . Entonces, aunque creo que entiendo lo que dices, no estoy de acuerdo en que sean equivalentes. Incluso si tuviéramos genéricos lo suficientemente potentes como para implementar una versión de try que entre en pánico, creo que todavía habría un deseo razonable por la versión de try presentada en esta propuesta.

@ianlancetaylor Reconocido. Nuevamente, estaba buscando una razón por la que try() no necesitaría agregarse en lugar de encontrar una manera de agregarlo. Como dije anteriormente, preferiría no tener nada nuevo para el manejo de errores que tener try() como se propone aquí.

Personalmente, me gustó más la propuesta anterior check que esta, basada en aspectos puramente visuales; check tenía el mismo poder que este try() pero bar(check foo()) es más legible para mí que bar(try(foo())) (¡solo necesitaba un segundo para contar los paréntesis!).

Más importante aún, mi principal queja sobre handle / check era que no permitía envolver cheques individuales de diferentes maneras, y ahora esta propuesta try() tiene el mismo defecto, mientras invoca características engañosas raramente utilizadas que confunden a los novatos de diferidos y devoluciones con nombre. Y con handle al menos teníamos la opción de usar ámbitos para definir bloques de control, con defer incluso eso no es posible.

En lo que a mí respecta, esta propuesta pierde frente a la propuesta anterior handle / check en todos los aspectos.

Aquí hay otra preocupación con el uso de diferidos para el manejo de errores.

try es una salida controlada/intencionada de una función. aplaza la ejecución siempre, incluidas las salidas no controladas/involuntarias de las funciones. Ese desajuste podría causar confusión. He aquí un escenario imaginario:

func someHTTPHandlerGuts() (err error) {
  defer func() {
    recordMetric("db call failed")
    return fmt.HandleErrorf("db unavailable: %v", err)
  }()
  data := try(makeDBCall)
  // some code that panics due to a bug
  return nil
}

Recuerde que net/http se recupera de pánicos e imagine depurar un problema de producción en torno al pánico. Vería su instrumentación y vería un aumento en las fallas de llamadas a la base de datos, de las llamadas recordMetric . Esto podría enmascarar el verdadero problema, que es el pánico en la siguiente línea.

No estoy seguro de cuán grave es esta preocupación en la práctica, pero (lamentablemente) quizás sea otra razón para pensar que el aplazamiento no es un mecanismo ideal para el manejo de errores.

Aquí hay una modificación que puede ayudar con algunas de las inquietudes planteadas: trate try como goto en lugar de como return . Escúchame. :)

try sería azúcar sintáctico para:

t1, … tn, te := f()  // t1, … tn, te are local (invisible) temporaries
if te != nil {
        err = te   // assign te to the error result parameter
        goto error // goto "error" label
}
x1, … xn = t1, … tn  // assignment only if there was no error

Beneficios:

  • No se requiere defer para decorar errores. (Sin embargo, aún se requieren devoluciones con nombre).
  • La existencia de la etiqueta error: es una pista visual de que hay un try en algún lugar de la función.

Esto también proporciona un mecanismo para agregar controladores que elude los problemas del controlador como función: use etiquetas como controladores. try(fn(), wrap) sería goto wrap en lugar de goto error . El compilador puede confirmar que wrap: está presente en la función. Tenga en cuenta que tener controladores también ayuda con la depuración: puede agregar/modificar el controlador para proporcionar una ruta de depuración.

Código de muestra:

func CopyFile(src, dst string) (err error) {
    r := try(os.Open(src))
    defer r.Close()

    w := try(os.Create(dst))
    defer func() {
        w.Close()
        if err != nil {
            os.Remove(dst) // only if a “try” fails
        }
    }()

    try(io.Copy(w, r), copyfail)
    try(w.Close())
    return nil

error:
    return fmt.Errorf("copy %s %s: %v", src, dst, err)

copyfail:
    recordMetric("copy failure") // count incidents of this failure
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
}

Otros comentarios:

  • Es posible que deseemos exigir que cualquier etiqueta utilizada como destino de un try esté precedida por una declaración de terminación. En la práctica, esto los obligaría al final de la función y podría evitar algún código de espagueti. Por otro lado, podría impedir algunos usos razonables y útiles.
  • try podría usarse para crear un bucle. Creo que esto cae bajo el lema de "si duele, no lo hagas", pero no estoy seguro.
  • Esto requeriría arreglar https://github.com/golang/go/issues/26058.

Crédito: Creo que una variante de esta idea fue sugerida por primera vez por @griesemer en persona en GopherCon el año pasado.

@josharian Pensar en la interacción con panic es importante aquí, y me alegro de que lo hayas mencionado, pero tu ejemplo me parece extraño. En el siguiente código, no tiene sentido para mí que el aplazamiento siempre registre una métrica "db call failed" . Sería una métrica falsa si someHTTPHandlerGuts tiene éxito y devuelve nil . El defer se ejecuta en todos los casos de salida, no solo en los casos de error o pánico, por lo que el código parece incorrecto incluso si no hay pánico.

func someHTTPHandlerGuts() (err error) {
  defer func() {
    recordMetric("db call failed")
    return fmt.HandleErrorf("db unavailable: %v", err)
  }()
  data := try(makeDBCall)
  // some code that panics due to a bug
  return nil
}

@josharian Sí, esta es más o menos exactamente la versión que discutimos el año pasado (excepto que usamos check en lugar de try ). Creo que sería crucial que uno no pudiera saltar "hacia atrás" al resto del cuerpo de la función, una vez que estemos en la etiqueta error . Eso aseguraría que el goto esté algo "estructurado" (no es posible un código de espagueti). Una preocupación que surgió fue que la etiqueta del controlador de errores (la error: ) siempre terminaría al final de la función (de lo contrario, habría que sortearla de alguna manera). Personalmente, me gusta que el código de manejo de errores esté fuera del camino (al final), pero otros sintieron que debería ser visible desde el principio.

@mikeshenkel Veo regresar de un bucle como una ventaja en lugar de una negativa. Supongo que eso alentaría a los desarrolladores a usar una función separada para manejar el contenido de un bucle o usar explícitamente err como lo hacemos actualmente. Ambos me parecen buenos resultados.

Desde mi punto de vista, no siento que esta sintaxis de prueba tenga que manejar cada caso de uso al igual que no siento que deba usar el

V, vale:= m[clave]

Formulario a partir de la lectura de un mapa

Puede evitar que las etiquetas Goto obliguen a los controladores a finalizar la función al resucitar la propuesta handle / check en una forma simplificada. ¿Qué pasa si usamos la sintaxis handle err { ... } pero no permitimos que los controladores se encadenen, sino que solo se usa el último? Simplifica mucho esa propuesta y es muy similar a la idea de ir a, excepto que acerca el manejo al punto de uso.

func CopyFile(src, dst string) (err error) {
    handle err {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    defer func() {
        w.Close()
        if err != nil {
            os.Remove(dst) // only if a “check” fails
        }
    }()

    {
        // handlers are scoped, after this scope the original handle is used again.
        // as an alternative, we could have repeated the first handle after the io.Copy,
        // or come up with a syntax to name the handlers, though that's often not useful.
        handle err {
            recordMetric("copy failure") // count incidents of this failure
            return fmt.Errorf("copy %s %s: %v", src, dst, err)
        }
        check io.Copy(w, r)
    }
    check w.Close()
    return nil
}

Como beneficio adicional, esto tiene un camino futuro para permitir que los controladores se encadenen, ya que todos los usos existentes tendrían un retorno.

@josharian @griesemer si introduce controladores con nombre (que muchas respuestas para verificar/manejar solicitan, ver temas recurrentes ), hay opciones de sintaxis preferibles a try(f(), err) :

try.err f()
try?err f()
try#err f()

?err    f() // because 'try' is redundant
?return f() // no handler
?panic  f() // no handler

(?err f()).method()

f?err() // lead with function name, instead of error handling
f?err().method()

file, ?err := os.Open(...) // many check/handle responses also requested this style

Una de las cosas que más me gustan de Go es que su sintaxis está relativamente libre de puntuación y se puede leer en voz alta sin mayores problemas. Realmente odiaría que Go terminara como $#@!perl .

Para mí, hacer "probar" una función integrada y habilitar cadenas tiene 2 problemas:

  • Es inconsistente con el resto del flujo de control en go (por ejemplo, palabras clave for/if/return/etc).
  • Hace que el código sea menos legible.

Preferiría hacerlo una declaración sin paréntesis. Los ejemplos de la propuesta requerirían varias líneas, pero serían más legibles (es decir, sería más difícil pasar por alto las instancias de "prueba" individuales). Sí, rompería los analizadores externos, pero prefiero preservar la coherencia.

El operador ternario es otro lugar donde ir no tiene nada y requiere más pulsaciones de teclas pero al mismo tiempo mejora la legibilidad/mantenibilidad. Agregar "probar" en esta forma más restringida equilibrará mejor la expresividad frente a la legibilidad, en mi opinión.

FWIW, panic afecta el flujo de control y tiene paréntesis, pero go y defer también afectan el flujo y no. Tiendo a pensar que try es más similar a defer en que es una operación de flujo inusual y que hace que sea más difícil de hacer try (try os.Open(file)).Read(buf) es bueno porque queremos desalentar las frases ingeniosas de todos modos, pero lo que sea. Cualquiera esta bien.

Sugerencia que todos odiarán por un nombre implícito para una variable de retorno de error final: $err . Es mejor que try() mi opinión. :-)

@griesemer

_"Personalmente, me gusta el código de manejo de errores fuera del camino (al final)"_

+1 a eso!

Encuentro que el manejo de errores implementado _antes_ de que ocurra el error es mucho más difícil de razonar que el manejo de errores implementado _después_ de que ocurra el error. Tener que retroceder mentalmente y obligarme a seguir el flujo lógico se siente como si estuviera en 1980 escribiendo Basic con GOTO.

Permítanme proponer otra forma potencial de manejar los errores usando CopyFile() como ejemplo nuevamente:

func CopyFile(src, dst string) (err error) {

    r := os.Open(src)
    defer r.Close()

    w := os.Create(dst)
    defer w.Close()

    io.Copy(w, r)
    w.Close()

    for err := error {
        switch err.Source() {
        case w.Close:
            os.Remove(dst) // only if a “try” fails
            fallthrough
        case os.Open, os.Create, io.Copy:
            err = fmt.Errorf("copy %s %s: %v", src, dst, err)
        default:
            err = fmt.Errorf("an unexpected error occurred")
        }
    }

    return err
}

Los cambios de idioma requeridos serían:

  1. Permitir una construcción for error{} , similar a for range{} pero solo ingresada en caso de error y solo ejecutada una vez.

  2. Permitir omitir la captura de valores devueltos que implementan <object>.Error() string pero solo cuando existe una construcción for error{} dentro del mismo func .

  3. Hace que el flujo de control del programa salte a la primera línea de la construcción for error{} cuando un func devuelve un _"error"_ en su último valor de retorno.

  4. Al devolver un _"error"_ Go agregaría asignar una referencia a la función que devolvió el error que debería ser recuperable por <error>.Source()

¿Qué es un _"error"_?

Actualmente, un _"error"_ se define como cualquier objeto que implementa Error() string y, por supuesto, no es nil .

Sin embargo, a menudo existe la necesidad de extender el error _incluso en caso de éxito_ para permitir la devolución de los valores necesarios para los resultados de éxito de una API RESTful. Por lo tanto, le pediría al equipo de Go que no asuma automáticamente que err!=nil significa _"error"_, sino que verifique si un objeto de error implementa un IsError() y si IsError() devuelve true antes de asumir que cualquier valor que no sea nil es un _"error"._

_(No estoy hablando necesariamente del código en la biblioteca estándar, pero principalmente si elige que su flujo de control se bifurque en un _"error"_. Si solo observa err!=nil , estaremos muy limitados en lo que puede hacer en términos de valores devueltos en nuestras funciones.)_

Por cierto, permitir que todos prueben un _"error"_ de la misma manera probablemente podría hacerse más fácilmente agregando una nueva función incorporada iserror() :

type ErrorIser interface {
    IsError() bool
}
func iserror(err error) bool {
    if err == nil { 
        return false
    }
    if _,ok := err.(ErrorIser); !ok {
        return true
    }
    return err.IsError()
}

Un lado se beneficia de permitir la no captura de _"errores"_

Tenga en cuenta que permitir que no se capture el último _"error"_ de las llamadas func permitiría una refactorización posterior para devolver errores de func que inicialmente no necesitaban devolver errores. Y permitiría esta refactorización sin romper ningún código existente que use esta forma de recuperación de errores y llame a dichos func s.

Para mí, esa decisión de _"¿Debería devolver un error o renunciar al manejo de errores por simplicidad en las llamadas?"_ es uno de mis mayores dilemas al escribir código Go. Permitir que no se capturen los _"errores"_ anteriores eliminaría prácticamente ese dilema.

De hecho, traté de implementar esta idea como traductor de Go hace aproximadamente medio año. No tengo una opinión firme sobre si esta característica debería agregarse como parte integrada de Go, pero permítanme compartir la experiencia (aunque no estoy seguro de que sea útil).

https://github.com/rhysd/trygo

Llamé al lenguaje extendido TryGo e implementé el traductor TryGo to Go.

Con el traductor, el código

func CreateFileInSubdir(subdir, filename string, content []byte) error {
    cwd := try(os.Getwd())

    try(os.Mkdir(filepath.Join(cwd, subdir)))

    p := filepath.Join(cwd, subdir, filename)
    f := try(os.Create(p))
    defer f.Close()

    try(f.Write(content))

    fmt.Println("Created:", p)
    return nil
}

se puede traducir a

func CreateFileInSubdir(subdir, filename string, content []byte) error {
    cwd, _err0 := os.Getwd()
    if _err0 != nil {
        return _err0
    }

    if _err1 := os.Mkdir(filepath.Join(cwd, subdir)); _err1 != nil {
        return _err1
    }

    p := filepath.Join(cwd, subdir, filename)
    f, _err2 := os.Create(p)
    if _err2 != nil {
        return _err2
    }
    defer f.Close()

    if _, _err3 := f.Write(content); _err3 != nil {
        return _err3
    }

    fmt.Println("Created:", p)
    return nil
}

Por la restricción del idioma, no pude implementar la llamada genérica try() . esta restringida a

  • RHS de declaración de definición
  • RHS de declaración de asignación
  • Declaración de llamada

pero podría probar esto con mi pequeño proyecto. mi experiencia fue

  • básicamente funciona bien y ahorra varias líneas
  • el valor de retorno nombrado es en realidad inutilizable para err ya que el valor de retorno de su función está determinado tanto por la asignación como por la función especial try() . muy confuso
  • esta función try() carecía de la función de 'error de ajuste' como se mencionó anteriormente.

_"Ambos me parecen buenos resultados"._

Tendremos que estar de acuerdo en estar en desacuerdo aquí.

_"esta sintaxis de prueba (no tiene que) manejar todos los casos de uso"_

Ese meme es probablemente el más preocupante. Al menos teniendo en cuenta lo resistente que ha sido el equipo/comunidad de Go a cualquier cambio en el pasado que no sea ampliamente aplicable.

Si permitimos esa justificación aquí, ¿por qué no podemos revisar propuestas anteriores que han sido rechazadas porque no eran ampliamente aplicables?

¿Y ahora estamos abiertos a abogar por cambios en Go que solo sean útiles para casos extremos seleccionados?

Supongo que establecer este precedente no producirá buenos resultados a largo plazo...

_"@mikeshenkel"_

PD No vi tu mensaje al principio por falta de ortografía. _(esto no me ofende, simplemente no me notifican cuando mi nombre de usuario está mal escrito...)_

Agradezco el compromiso con la compatibilidad con versiones anteriores que lo motiva a hacer de try una palabra clave incorporada, en lugar de una palabra clave, pero después de luchar con la absoluta _rareza_ de tener una función de uso frecuente que puede cambiar el flujo de control ( panic y recover son extremadamente raros), comencé a preguntarme: ¿alguien ha realizado algún análisis a gran escala de la frecuencia de try como identificador en bases de código de código abierto? Tenía curiosidad y escepticismo, así que hice una búsqueda preliminar en lo siguiente:

En las 11 108 770 líneas significativas de Go que viven en estos repositorios, solo se usaron 63 instancias de try como identificador. Por supuesto, me doy cuenta de que estas bases de código (si bien son grandes, ampliamente utilizadas e importantes por derecho propio) representan solo una fracción del código Go que existe y, además, no tenemos forma de analizar directamente las bases de código privadas, pero ciertamente es un resultado interesante

Además, debido a que try , como cualquier palabra clave, está en minúsculas, nunca la encontrará en la API pública de un paquete. Las adiciones de palabras clave solo afectarán las partes internas del paquete.

Todo esto es un prefacio de algunas ideas que quería incluir en la combinación que se beneficiaría de try como palabra clave.

Yo propondría las siguientes construcciones.

1) Sin controlador

// The existing proposal, but as a keyword rather than builtin.  When an error is 
// "caught", the function returns all zero values plus the error.  Nothing 
// particularly new here.
func doSomething() (int, error) {
    try SomeFunc()
    a, b := try AnotherFunc()

    // ...

    return 123, nil
}

2) Manejador

Tenga en cuenta que los controladores de errores son bloques de código simples, destinados a estar en línea, en lugar de funciones. Más sobre esto a continuación.

func doSomething() (int, error) {
    // Inline error handler
    a, b := try SomeFunc() else err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    handler logAndContinue err {
        log.Errorf("non-critical error: %v", err)
    }
    handler annotateAndReturn err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d := try SomeFunc() else logAndContinue
    e, f := try OtherFunc() else annotateAndReturn

    // ...

    return 123, nil
}

Restricciones propuestas:

  • Solo puede try una llamada de función. No try err .
  • Si no especifica un controlador, solo puede try desde dentro de una función que devuelve un error como su valor de retorno más a la derecha. No hay cambios en el comportamiento try según su contexto. Nunca entra en pánico (como se discutió mucho antes en el hilo).
  • No hay "cadena de manejadores" de ningún tipo. Los controladores son solo bloques de código en línea.

Beneficios:

  • La sintaxis try / else podría reducirse trivialmente al "compuesto si" existente:
    go a, b := try SomeFunc() else err { return 0, errors.Wrap(err, "error in doSomething:") }
    se convierte
    go if a, b, err := SomeFunc(); err != nil { return 0, errors.Wrap(err, "error in doSomething:") }
    En mi opinión, los condicionales siempre me han parecido más confusos que útiles por una razón muy simple: los condicionales generalmente ocurren _después_ de una operación, y tienen algo que ver con el procesamiento de sus resultados. Si la operación está encajada dentro de la declaración condicional, simplemente es menos obvio que está sucediendo. El ojo está distraído. Además, el alcance de las variables definidas no es tan obvio como cuando se encuentran en el extremo izquierdo de una línea.
  • Los controladores de errores no se definen intencionalmente como funciones (ni con nada parecido a la semántica de funciones). Esto hace varias cosas por nosotros:

    • El compilador puede simplemente alinear un controlador con nombre donde sea que se haga referencia. Es mucho más como una simple plantilla macro/codegen que una llamada de función. El tiempo de ejecución ni siquiera necesita saber que existen controladores.

    • No estamos limitados con respecto a lo que podemos hacer dentro de un controlador. Evitamos las críticas de check / handle que "este marco de manejo de errores solo es bueno para rescates". También sorteamos la crítica de la "cadena de manejadores". Cualquier código arbitrario se puede colocar dentro de uno de estos controladores y no se implica ningún otro flujo de control.

    • No tenemos que secuestrar return dentro del controlador para que signifique super return . Secuestrar una palabra clave es extremadamente confuso. return solo significa return , y no hay una necesidad real de super return .

    • defer no necesita luz de luna como mecanismo de manejo de errores. Podemos seguir pensando en ello principalmente como una forma de limpiar los recursos, etc.

  • Con respecto a agregar contexto a los errores:

    • Agregar contexto con controladores es extremadamente simple y se ve muy similar a los bloques if err != nil existentes

    • Aunque la construcción "probar sin controlador" no fomenta directamente la adición de contexto, es muy sencillo refactorizar en el formulario del controlador. Su uso previsto sería principalmente durante el desarrollo, y sería extremadamente sencillo escribir una verificación de go vet para resaltar los errores no controlados.

Disculpas si estas ideas son muy similares a otras propuestas. He tratado de mantenerme al día con todas ellas, pero es posible que me haya perdido una buena parte.

@brynbellomy Gracias por el análisis de palabras clave; es información muy útil. Parece que try como palabra clave podría estar bien. (Usted dice que las API no se ven afectadas; eso es cierto, pero try aún podría aparecer como nombre de parámetro o similar, por lo que es posible que la documentación tenga que cambiar. Pero estoy de acuerdo en que eso no afectaría a los clientes de esos paquetes).

Con respecto a su propuesta: estaría bien incluso sin controladores designados, ¿no es así? (Eso simplificaría la propuesta sin pérdida de potencia. Uno podría simplemente llamar a una función local desde el controlador en línea).

Con respecto a su propuesta: estaría bien incluso sin controladores designados, ¿no es así? (Eso simplificaría la propuesta sin pérdida de potencia. Uno podría simplemente llamar a una función local desde el controlador en línea).

@griesemer De hecho, me sentía bastante tibio al incluirlos. Ciertamente más Go-ish sin.

Por otro lado, parece que la gente quiere la capacidad de manejar errores de una sola línea, incluidas las frases de una sola línea que return . Un caso típico sería log, luego return . Si pagamos a una función local en la cláusula else , probablemente perdamos eso:

a, b := try SomeFunc() else err {
    someLocalFunc(err)
    return 0, err
}

(Sin embargo, todavía prefiero esto a los condicionales compuestos)

Sin embargo, aún podría obtener devoluciones de una sola línea que agreguen contexto de error al implementar un ajuste simple gofmt discutido anteriormente en el hilo:

a, b := try SomeFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }

¿Es necesaria la nueva palabra clave en la propuesta anterior? Por qué no:

SomeFunc() else return
a, b := SomeOtherFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }

@griesemer si los manejadores están nuevamente sobre la mesa, le sugiero que haga un nuevo problema para discutir sobre probar/manejar o probar/_etiqueta_. Esta propuesta omitió específicamente los controladores, y hay innumerables formas de definirlos e invocarlos.

Cualquier persona que sugiera controladores debe leer primero la wiki de comentarios de control/control. Es muy probable que lo que sea que sueñes ya esté descrito allí :-)
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

@smonkewitz no, no es necesaria una nueva palabra clave en esa versión, ya que está vinculada a las declaraciones de asignación, que se ha mencionado varias veces hasta ahora en varios azúcares de sintaxis.

https://github.com/golang/go/issues/32437#issuecomment-499808741
https://github.com/golang/go/issues/32437#issuecomment-499852124
https://github.com/golang/go/issues/32437#issuecomment-500095505

@ianlancetaylor , ¿el equipo go ya ha considerado este tipo particular de manejo de errores? No es tan fácil de implementar como el intento incorporado propuesto, pero se siente más idiomático. ~Declaración innecesaria, lo siento.~

Me gustaría repetir algo que @deanveloper y algunos otros han dicho, pero con mi propio énfasis. En https://github.com/golang/go/issues/32437#issuecomment -498939499 @deanveloper dijo:

try es una devolución condicional. El flujo de control Y los retornos se sostienen sobre pedestales en Go. Todo el flujo de control dentro de una función está sangrado y todos los retornos comienzan con return . La combinación de estos dos conceptos en una llamada de función fácil de perder se siente un poco fuera de lugar.

Además, en esta propuesta try es una función que devuelve valores, por lo que puede usarse como parte de una expresión más grande.

Algunos han argumentado que panic ya sentó el precedente para una función incorporada que cambia el flujo de control, pero creo que panic es fundamentalmente diferente por dos razones:

  1. El pánico no es condicional; siempre aborta la función de llamada.
  2. Panic no devuelve ningún valor y, por lo tanto, solo puede aparecer como una declaración independiente, lo que aumenta su visibilidad.

Prueba por otro lado:

  1. es condicional; puede o no regresar de la función de llamada.
  2. Devuelve valores y puede aparecer en una expresión compuesta, posiblemente varias veces, en una sola línea, posiblemente más allá del margen derecho de la ventana de mi editor.

Por estas razones, creo que try se siente más que un "bit off", creo que fundamentalmente daña la legibilidad del código.

Hoy, cuando nos encontramos con algún código Go por primera vez, podemos hojearlo rápidamente para encontrar los posibles puntos de salida y los puntos de flujo de control. Creo que es una propiedad muy valiosa del código Go. Usando try se vuelve demasiado fácil escribir código que carece de esa propiedad.

Admito que es probable que los desarrolladores de Go que valoran la legibilidad del código converjan en modismos de uso por try que evitan estos problemas de legibilidad. Espero que eso suceda, ya que la legibilidad del código parece ser un valor central para muchos desarrolladores de Go. Pero no es obvio para mí que try agregue suficiente valor sobre los modismos de código existentes para soportar el peso de agregar un nuevo concepto al lenguaje para que todos lo aprendan y eso puede dañar fácilmente la legibilidad.

````
si != "rompió" {
no lo arregles
}

@ChrisHines A su punto (que se repite en otra parte de este hilo), agreguemos otra restricción:

  • cualquier declaración try (incluso aquellas sin un controlador) debe ocurrir en su propia línea.

Aún se beneficiaría de una gran reducción en el ruido visual. Luego, tiene rendimientos garantizados anotados por return y rendimientos condicionales anotados por try , y esas palabras clave siempre se encuentran al principio de una línea (o, en el peor de los casos, directamente después de una asignación de variable).

Así que nada de este tipo de tonterías:

try EmitEvent(try (try DecodeMsg(m)).SaveToDB())

sino mas bien esto:

dm := try DecodeMsg(m)
um := try dm.SaveToDB()
try EmitEvent(um)

que todavía se siente más claro que esto:

dm, err := DecodeMsg(m)
if err != nil {
    return nil, err
}

um, err := dm.SaveToDB()
if err != nil {
    return nil, err
}

err = EmitEvent(um)
if err != nil {
    return nil, err
}

Una cosa que me gusta de este diseño es que es imposible ignorar silenciosamente los errores sin anotar que podría ocurrir uno . Mientras que en este momento, a veces ves x, _ := SomeFunc() (¿cuál es el valor de retorno ignorado? ¿un error? ¿algo más?), ahora tienes que anotar claramente:

x := try SomeFunc() else err {}

Desde mi publicación anterior en apoyo de la propuesta, he visto dos ideas publicadas por @jagv (sin parámetros try devuelve *error ) y por @josharian (controladores de errores etiquetados) que creo en un una forma ligeramente modificada mejoraría considerablemente la propuesta.

Juntando estas ideas con otra que he tenido yo mismo, tendríamos cuatro versiones de try :

  1. tratar()
  2. probar (parámetros)
  3. probar (parámetros, etiqueta)
  4. intentar (parámetros, pánico)

1 simplemente devolvería un puntero al parámetro de retorno de error (ERP) o nulo si no hubiera uno (#4 solamente). Esto proporcionaría una alternativa a un ERP con nombre sin la necesidad de agregar un integrado adicional.

2 funcionaría exactamente como se prevé actualmente. Se devolvería inmediatamente un error no nulo, pero se podría decorar con una instrucción defer .

3 funcionaría como lo sugiere @josharian , es decir, en un error no nulo, el código se ramificaría a la etiqueta. Sin embargo, no habría una etiqueta de controlador de errores predeterminada, ya que ese caso ahora degeneraría en el n. ° 2.

Me parece que esta suele ser una mejor manera de decorar los errores (o manejarlos localmente y luego devolverlos a cero) que defer , ya que es más simple y rápido. Cualquiera a quien no le haya gustado todavía podría usar el #2.

Sería una buena práctica colocar la etiqueta/código de manejo de errores cerca del final de la función y no volver al resto del cuerpo de la función. Sin embargo, no creo que el compilador deba hacer cumplir tampoco, ya que puede haber ocasiones extrañas en las que sean útiles y la aplicación puede ser difícil en cualquier caso.

Por lo tanto, la etiqueta normal y el comportamiento goto se aplicarían sujetos (como dijo @josharian ) a que #26058 se solucione primero, pero creo que debería solucionarse de todos modos.

El nombre de la etiqueta no puede ser panic ya que entraría en conflicto con el #4.

4 panic inmediatamente en lugar de regresar o ramificarse. En consecuencia, si esta fuera la única versión de try utilizada en una función particular, no se requeriría ningún ERP.

Agregué esto para que el paquete de prueba pueda funcionar como lo hace ahora sin la necesidad de más cambios integrados u otros. Sin embargo, también podría ser útil en otros escenarios _fatales_.

Esta debe ser una versión separada de try como la alternativa de bifurcarse a un controlador de errores y luego entrar en pánico aún requeriría un ERP.

Una de las reacciones más fuertes a la propuesta inicial fue la preocupación por perder la visibilidad del flujo normal de donde regresa una función.

Por ejemplo, @deanveloper expresó muy bien esa preocupación en https://github.com/golang/go/issues/32437#issuecomment -498932961, que creo que es el comentario más votado aquí.

@dominikh escribió en https://github.com/golang/go/issues/32437#issuecomment -499067357:

En código gofmt'ed, un retorno siempre coincide con /^\t*return / – es un patrón muy trivial para detectar a simple vista, sin ninguna ayuda. try, por otro lado, puede ocurrir en cualquier parte del código, anidado arbitrariamente en profundidad en las llamadas a funciones. Ninguna cantidad de capacitación nos permitirá detectar de inmediato todo el flujo de control en una función sin la ayuda de herramientas.

Para ayudar con eso, @brynbellomy sugirió ayer:

cualquier declaración de prueba (incluso aquellas sin un controlador) debe ocurrir en su propia línea.

Yendo más allá, se podría requerir que try sea el comienzo de la línea, incluso para una tarea.

Entonces podría ser:

try dm := DecodeMsg(m)
try um := dm.SaveToDB()
try EmitEvent(um)

en lugar de lo siguiente (del ejemplo de @brynbellomy ):

dm, err := DecodeMsg(m)
if err != nil {
    return nil, err
}

um, err := dm.SaveToDB()
if err != nil {
    return nil, err
}

err = EmitEvent(um)
if err != nil {
    return nil, err
}

Eso parece que preservaría una buena cantidad de visibilidad, incluso sin ningún editor o asistencia de IDE, al tiempo que reduce el modelo.

Eso podría funcionar con el enfoque basado en aplazamiento propuesto actualmente que se basa en parámetros de resultado con nombre, o podría funcionar especificando funciones de controlador normales. (Especificar funciones de controlador sin requerir valores de retorno con nombre me parece mejor que requerir valores de retorno con nombre, pero ese es un punto aparte).

La propuesta incluye este ejemplo:

info := try(try(os.Open(file)).Stat())    // proposed try built-in

Eso podría ser en cambio:

try f := os.Open(file)
try info := f.Stat()

Eso sigue siendo una reducción en el modelo estándar en comparación con lo que alguien podría escribir hoy, incluso si no es tan corto como la sintaxis propuesta. ¿Quizás eso sería lo suficientemente corto?

@elagergren-spideroak proporcionó este ejemplo:

try(try(try(to()).parse().this)).easily())

Creo que tiene paréntesis que no coinciden, lo que quizás sea un punto deliberado o un poco de humor sutil, por lo que no estoy seguro de si ese ejemplo pretende tener 2 try o 3 try . En cualquier caso, tal vez sería mejor exigir que se distribuya en 2 o 3 líneas que comiencen con try .

@thepudds , esto es a lo que me refería en mi comentario anterior. Excepto lo dado

try f := os.Open(file)
try info := f.Stat()

Una cosa obvia que hacer es pensar en try como un bloque de prueba donde se puede poner más de una oración entre paréntesis. Entonces lo anterior puede convertirse

try (
    f := os.Open(file)
    into := f.Stat()
)

Si el compilador sabe cómo lidiar con esto, lo mismo funciona para anidar también. Así que ahora lo anterior puede convertirse

try info := os.Open(file).Stat()

A partir de las firmas de funciones, el compilador sabe que Open puede devolver un valor de error y, como se encuentra en un bloque de prueba, necesita generar un manejo de errores y luego llamar a Stat() en el valor primario devuelto y así sucesivamente.

Lo siguiente es permitir declaraciones en las que no se genere ningún valor de error o se maneje localmente. Así que ahora puedes decir

try (
    f := os.Open(file)
    debug("f: %v\n", f) // debug returns nothing
    into := f.Stat()
)

Esto permite la evolución del código sin tener que reorganizar los bloques de prueba. ¡Pero por alguna extraña razón, la gente parece pensar que el manejo de errores debe explicarse explícitamente! Ellos quieren

try(try(try(to()).parse()).this)).easily())

Si bien estoy perfectamente bien con

try to().parse().this().easily()

Aunque en ambos casos se puede generar exactamente el mismo código de comprobación de errores. Mi opinión es que siempre puede escribir un código especial para el manejo de errores si es necesario. try (o como prefiera llamarlo) simplemente elimina el manejo de errores predeterminado (que consiste en enviarlo a la persona que llama).

Otro beneficio es que si el compilador genera el manejo de errores predeterminado, puede agregar más información de identificación para que sepa cuál de las cuatro funciones anteriores falló.

Estaba algo preocupado por la legibilidad de los programas donde aparece try dentro de otras expresiones. Así que ejecuté grep "return .*err$" en la biblioteca estándar y comencé a leer bloques al azar. Hay 7214 resultados, solo leí un par de cientos.

Lo primero a tener en cuenta es que donde se aplica try , hace que casi todos estos bloques sean un poco más legibles.

Lo segundo es que muy pocos de estos, menos de 1 de cada 10, pondrían try dentro de otra expresión. El caso típico son declaraciones de la forma x := try(...) o ^try(...)$ .

Aquí hay algunos ejemplos donde try aparecerían dentro de otra expresión:

texto/plantilla

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        lessOrEqual, err := le(arg1, arg2)
        if err != nil {
                return false, err
        }
        return !lessOrEqual, nil
}

se convierte en:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        return !try(le(arg1, arg2)), nil
}

texto/plantilla

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
        switch v.Kind() {
        case reflect.Map:
                index, err := prepareArg(index, v.Type().Key())
                if err != nil {
                        return reflect.Value{}, err
                }
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

se convierte

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                if x := v.MapIndex(try(prepareArg(index, v.Type().Key()))); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

(este es el ejemplo más cuestionable que vi)

expresión regular/sintaxis:

regexp/syntax/parse.go

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        if c, t, err = nextRune(t); err != nil {
                return nil, err
        }
        p.literal(c)
        ...
}

se convierte

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        c, t = try(nextRune(t))
        p.literal(c)
        ...
}

Este no es un ejemplo de probar dentro de otra expresión, pero quiero mencionarlo porque mejora la legibilidad. Es mucho más fácil ver aquí que los valores de c y t están más allá del alcance de la instrucción if.

red/http

net/http/request.go:readRequest

        mimeHeader, err := tp.ReadMIMEHeader()
        if err != nil {
                return nil, err
        }
        req.Header = Header(mimeHeader)

se convierte en:

        req.Header = Header(try(tp.ReadMIMEHeader())

base de datos/sql

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                connector, err := driverCtx.OpenConnector(dataSourceName)
                if err != nil {
                        return nil, err
                }
                return OpenDB(connector), nil
        }

se convierte

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
        }

base de datos/sql

        si, err := ctxDriverPrepare(ctx, dc.ci, query)
        if err != nil {
                return nil, err
        }
        ds := &driverStmt{Locker: dc, si: si}

se convierte

        ds := &driverStmt{
                Locker: dc,
                si: try(ctxDriverPrepare(ctx, dc.ci, query)),
        }

red/http

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                cc, err := p.t.dialclientconn(addr, singleuse)
                if err != nil {
                        return nil, err
                }
                return cc, nil
        }

se convierte

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                return try(p.t.dialclientconn(addr, singleuse))
        }

red/http

func (f *http2Framer) endWrite() error {
        ...
        n, err := f.w.Write(f.wbuf)
        if err == nil && n != len(f.wbuf) {
                err = io.ErrShortWrite
        }
        return err
}

se convierte

func (f *http2Framer) endWrite() error {
        ...
        if try(f.w.Write(f.wbuf) != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

(Este me gusta mucho).

red/http

        if f, err := fr.ReadFrame(); err != nil {
                return nil, err
        } else {
                hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        }

se convierte

        hc = try(fr.ReadFrame()).(*http2ContinuationFrame)// guaranteed by checkFrameOrder

}

(También agradable.)

neto :

        if ctrlFn != nil {
                c, err := newRawConn(fd)
                if err != nil {
                        return err
                }
                if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
                        return err
                }
        }

se convierte

        if ctrlFn != nil {
                try(ctrlFn(fd.ctrlNetwork(), laddr.String(), try(newRawConn(fd))))
        }

tal vez esto es demasiado, y en su lugar debería ser:

        if ctrlFn != nil {
                c := try(newRawConn(fd))
                try(ctrlFn(fd.ctrlNetwork(), laddr.String(), c))
        }

En general, disfruto bastante el efecto de try en el código de la biblioteca estándar que leo.

Un punto final: Ver try aplicado para leer código más allá de los pocos ejemplos en la propuesta fue esclarecedor. Creo que vale la pena considerar escribir una herramienta para convertir automáticamente el código para usar try (donde no cambia la semántica del programa). Sería interesante leer una muestra de las diferencias que se producen con los paquetes populares en github para ver si se mantiene lo que encontré en la biblioteca estándar. El resultado de dicho programa podría proporcionar información adicional sobre el efecto de la propuesta.

@crawshaw gracias por hacer esto, fue genial verlo en acción. Pero verlo en acción me hizo tomar más en serio los argumentos en contra del manejo de errores en línea que hasta ahora había estado descartando.

Dado que esto estaba tan cerca de la interesante sugerencia de @thepudds de hacer try una declaración, reescribí todos los ejemplos usando esa sintaxis y lo encontré mucho más claro que la expresión- try o la status quo, sin requerir demasiadas líneas adicionales:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                try index := prepareArg(index, v.Type().Key())
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}
func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        try c, t = nextRune(t)
        p.literal(c)
        ...
}
        try mimeHeader := tp.ReadMIMEHeader()
        req.Header = Header(mimeHeader)
        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                try connector := driverCtx.OpenConnector(dataSourceName)
                return OpenDB(connector), nil
        }

Podría decirse que este sería mejor con una expresión try si hubiera varios campos que tuvieran que ser try -ed, pero sigo prefiriendo el equilibrio de esta compensación

        try si := ctxDriverPrepare(ctx, dc.ci, query)
        ds := &driverStmt{Locker: dc, si: si}

Este es básicamente el peor de los casos para esto y se ve bien:

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                try cc := p.t.dialclientconn(addr, singleuse)
                return cc, nil
        }

Debatí conmigo mismo si if try sería o debería ser legal, pero no pude encontrar una explicación razonable de por qué no debería serlo y funciona bastante bien aquí:

func (f *http2Framer) endWrite() error {
        ...
        if try n := f.w.Write(f.wbuf); n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}
        try f := fr.ReadFrame()
        hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        if ctrlFn != nil {
                try c := newRawConn(fd)
                try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
        }

Examinar los ejemplos de @crawshaw solo me hace sentir más seguro de que el flujo de control a menudo se volverá lo suficientemente críptico como para ser aún más cuidadoso con el diseño. Relacionar incluso una pequeña cantidad de complejidad se vuelve difícil de leer y fácil de estropear. Me alegra ver las opciones consideradas, pero complicar el flujo de control en un lenguaje tan cauteloso parece excepcionalmente fuera de lugar.

func (f *http2Framer) endWrite() error {
        ...
        if try(f.w.Write(f.wbuf) != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

Además, try no está "intentando" nada. Es un "relé de protección". Si la semántica base de la propuesta está desactivada, no me sorprende que el código resultante también sea problemático.

func (f *http2Framer) endWrite() error {
        ...
        relay n := f.w.Write(f.wbuf)
        return checkShortWrite(n, len(f.wbuf))
}

Si hace una declaración de prueba, podría usar una bandera para indicar qué valor de retorno y qué acción:

try c, @      := newRawConn(fd) // return
try c, <strong i="6">@panic</strong> := newRawConn(fd) // panic
try c, <strong i="7">@hname</strong> := newRawConn(fd) // invoke named handler
try c, <strong i="8">@_</strong>     := newRawConn(fd) // ignore, or invoke "ignored" handler if defined

Todavía necesita una sintaxis de subexpresión (Russ ha declarado que es un requisito), al menos para acciones de pánico e ignorar.

En primer lugar, aplaudo a @crawshaw por tomarse el tiempo de ver aproximadamente 200 ejemplos reales y por tomarse el tiempo para su reflexivo artículo anterior.

En segundo lugar, @jimmyfrasche , con respecto a su respuesta aquí sobre el ejemplo http2Framer :


Debatí conmigo mismo si if try sería o debería ser legal, pero no pude encontrar una explicación razonable de por qué no debería serlo y funciona bastante bien aquí:

```
func (f *http2Framer) endWrite() error {
...
si prueba n := fwWrite(f.wbuf); n != len(f.wbuf) {
volver io.ErrShortWrite
}
devolver cero
}

At least under what I was suggesting above in https://github.com/golang/go/issues/32437#issuecomment-500213884, under that proposal variation I would suggest `if try` is not allowed.

That `http2Framer` example could instead be:

func (f *http2Framer) endWrite() error {
...
prueba n := fwWrite(f.wbuf)
si n != len(f.wbuf) {
volver io.ErrShortWrite
}
devolver cero
}
`` That is one line longer, but hopefully still "light on the page". Personally, I think that (arguably) reads more cleanly, but more importantly it is easier to see the probar`.

@deanveloper escribió arriba en https://github.com/golang/go/issues/32437#issuecomment -498932961:

Regresar de una función parece haber sido algo "sagrado" de hacer

Ese ejemplo específico http2Framer termina no siendo tan corto como podría ser. Sin embargo, se mantiene regresando de una función más "sagrada" si el try debe ser lo primero en una línea.

@crawshaw mencionó:

Lo segundo es que muy pocos de estos, menos de 1 de cada 10, pondrían try dentro de otra expresión. El caso típico son las sentencias de la forma x := try(...) o ^try(...)$.

Tal vez esté bien ayudar solo parcialmente a esos 1 de cada 10 ejemplos con una forma más restringida de try , especialmente si el caso típico de esos ejemplos termina con el mismo número de líneas, incluso si try es requerido para ser lo primero en una línea?

@jimmyfrasche

@crawshaw gracias por hacer esto, fue genial verlo en acción. Pero verlo en acción me hizo tomar más en serio los argumentos en contra del manejo de errores en línea que hasta ahora había estado descartando.

Dado que esto estaba tan cerca de la interesante sugerencia de @thepudds de hacer try una declaración, reescribí todos los ejemplos usando esa sintaxis y lo encontré mucho más claro que la expresión- try o la status quo, sin requerir demasiadas líneas adicionales:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}

Su primer ejemplo ilustra bien por qué prefiero la expresión- try . En su versión, tengo que poner el resultado de la llamada a le en una variable, pero esa variable no tiene un significado semántico que el término le no implica. Así que no hay ningún nombre que pueda darle que no tenga sentido (como x ) o redundante (como lessOrEqual ). Con expression- try , no se necesita una variable intermedia, por lo que este problema ni siquiera surge.

Prefiero no tener que gastar esfuerzo mental inventando nombres para cosas que es mejor dejar en el anonimato.

Estoy feliz de expresar mi apoyo detrás de las últimas publicaciones donde try (la palabra clave) se ha movido al principio de la línea. Realmente debería compartir el mismo espacio visual que return .

Re: la sugerencia de @jimmyfrasche de permitir try dentro de declaraciones compuestas if , ese es exactamente el tipo de cosas que creo que muchos aquí están tratando de evitar, por algunas razones:

  • combina dos mecanismos de flujo de control muy diferentes en una sola línea
  • la expresión try en realidad se evalúa primero y puede hacer que la función regrese, pero aparece después de if
  • regresan con errores totalmente diferentes, uno de los cuales en realidad no vemos en el código y otro que hacemos
  • hace que sea menos obvio que try en realidad no se maneja, porque el bloque se parece mucho a un bloque controlador (aunque está manejando un problema totalmente diferente)

Se podría abordar esta situación desde un ángulo ligeramente diferente que favorezca empujar a las personas a manejar try s. ¿Qué tal permitir que la sintaxis try / else contenga condicionales subsiguientes (que es un patrón común con muchas funciones de E/S que devuelven err y n , cualquiera de los cuales podría indicar un problema):

func (f *http2Framer) endWrite() error {
        // ...
        try n := f.w.Write(f.wbuf) else err {
                return errors.Wrap(err, "error writing:")
        } else if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

En el caso de que no maneje el error devuelto por .Write , aún tendría una anotación clara de que .Write podría tener un error (como lo señaló @thepudds):

func (f *http2Framer) endWrite() error {
        // ...
        try n := f.w.Write(f.wbuf)
        if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

Secundo la respuesta de @daved . En mi opinión, cada ejemplo que destacó @crawshaw se volvió menos claro y más propenso a errores como resultado de try .

Estoy feliz de expresar mi apoyo detrás de las últimas publicaciones donde try (la palabra clave) se ha movido al principio de la línea. Realmente debería compartir el mismo espacio visual que return .

Dadas las dos opciones para este punto y suponiendo que se eligió una y, por lo tanto, sentó un precedente para futuras características potenciales:

A.)

try f := os.Open(filepath) else err {
    return errors.Wrap(err, "can't open")
}

B.)

f := try os.Open(filepath) else err {
    return errors.Wrap(err, "can't open")
}

¿Cuál de los dos proporciona más flexibilidad para el uso futuro de nuevas palabras clave? _(No sé la respuesta a esto porque no domino el oscuro arte de escribir compiladores)._ ¿Sería un enfoque más limitante que otro?

@davecheney @daved @crawshaw
Tiendo a estar de acuerdo con los Dave en esto: en los ejemplos de @crawshaw , hay muchas declaraciones try incrustadas en lo profundo de las líneas que tienen muchas otras cosas en marcha. Puntos de salida realmente difíciles de detectar. Además, los try parecen desordenar bastante las cosas en algunos de los ejemplos.

Ver un montón de código stdlib transformado de esta manera es muy útil, así que tomé los mismos ejemplos pero los reescribí según la propuesta alternativa, que es más restrictiva:

  • try como palabra clave
  • solo un try por línea
  • try debe estar al principio de una línea

Espero que esto nos ayude a comparar. Personalmente, encuentro que estos ejemplos parecen mucho más concisos que los originales, pero sin oscurecer el flujo de control. try permanece muy visible en cualquier lugar donde se use.

texto/plantilla

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        lessOrEqual, err := le(arg1, arg2)
        if err != nil {
                return false, err
        }
        return !lessOrEqual, nil
}

se convierte en:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}

texto/plantilla

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
        switch v.Kind() {
        case reflect.Map:
                index, err := prepareArg(index, v.Type().Key())
                if err != nil {
                        return reflect.Value{}, err
                }
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

se convierte

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                try index := prepareArg(index, v.Type().Key())
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

expresión regular/sintaxis:

regexp/syntax/parse.go

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        if c, t, err = nextRune(t); err != nil {
                return nil, err
        }
        p.literal(c)
        ...
}

se convierte

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        try c, t = nextRune(t)
        p.literal(c)
        ...
}

red/http

net/http/request.go:readRequest

        mimeHeader, err := tp.ReadMIMEHeader()
        if err != nil {
                return nil, err
        }
        req.Header = Header(mimeHeader)

se convierte en:

        try mimeHeader := tp.ReadMIMEHeader()
        req.Header = Header(mimeHeader)

base de datos/sql

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                connector, err := driverCtx.OpenConnector(dataSourceName)
                if err != nil {
                        return nil, err
                }
                return OpenDB(connector), nil
        }

se convierte

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                try connector := driverCtx.OpenConnector(dataSourceName)
                return OpenDB(connector), nil
        }

base de datos/sql

        si, err := ctxDriverPrepare(ctx, dc.ci, query)
        if err != nil {
                return nil, err
        }
        ds := &driverStmt{Locker: dc, si: si}

se convierte

        try si := ctxDriverPrepare(ctx, dc.ci, query)
        ds := &driverStmt{Locker: dc, si: si}

red/http

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                cc, err := p.t.dialclientconn(addr, singleuse)
                if err != nil {
                        return nil, err
                }
                return cc, nil
        }

se convierte

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                try cc := p.t.dialclientconn(addr, singleuse)
                return cc, nil
        }

red/http
Este en realidad no nos ahorra ninguna línea, pero lo encuentro mucho más claro porque if err == nil es una construcción relativamente poco común.

func (f *http2Framer) endWrite() error {
        ...
        n, err := f.w.Write(f.wbuf)
        if err == nil && n != len(f.wbuf) {
                err = io.ErrShortWrite
        }
        return err
}

se convierte

func (f *http2Framer) endWrite() error {
        ...
        try n := f.w.Write(f.wbuf)
        if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

red/http

        if f, err := fr.ReadFrame(); err != nil {
                return nil, err
        } else {
                hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        }

se convierte

        try f := fr.ReadFrame()
        hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
}

neto:

        if ctrlFn != nil {
                c, err := newRawConn(fd)
                if err != nil {
                        return err
                }
                if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
                        return err
                }
        }

se convierte

        if ctrlFn != nil {
                try c := newRawConn(fd)
                try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
        }

@james-lawrence En respuesta a https://github.com/golang/go/issues/32437#issuecomment -500116099: No recuerdo que se hayan considerado seriamente ideas como un , err opcional, no. Personalmente, creo que es una mala idea, porque significa que si una función cambia para agregar un parámetro final error , el código existente continuará compilando, pero actuará de manera muy diferente.

El uso de defer para manejar los errores tiene mucho sentido, pero lleva a la necesidad de nombrar el error y un nuevo tipo de repetitivo if err != nil .

Los controladores externos deben hacer esto:

func handler(err *error) {
  if *err != nil {
    *err = handle(*err)
  }
} 

que se usa como

defer handler(&err)

Los controladores externos solo deben escribirse una vez, pero sería necesario que hubiera dos versiones de muchas funciones de manejo de errores: la que se pretende diferir y la que se usa de manera regular.

Los controladores internos deben hacer esto:

defer func() {
  if err != nil {
    err = handle(err)
  }
}()

En ambos casos, se debe nombrar el error de la función externa para poder acceder.

Como mencioné anteriormente en el hilo, esto se puede resumir en una sola función:

func catch(err *error, handle func(error) error) {
  if *err != nil && handle != nil {
    *err = handle(*err)
  }
}

Eso va en contra de la preocupación de @griesemer sobre la ambigüedad de las funciones del controlador nil y tiene su propio modelo defer y func(err error) error , además de tener que nombrar err en la función exterior.

Si try termina como una palabra clave, entonces podría tener sentido tener una palabra clave catch , que también se describe a continuación.

Sintácticamente, sería muy parecido a handle :

catch err {
  return handleThe(err)
}

Semánticamente, sería azúcar para el código del controlador interno anterior:

defer func() {
  if err != nil {
    err = handleThe(err)
  }
}()

Dado que es algo mágico, podría capturar el error de la función externa, incluso si no se nombró. (El err después catch es más como un nombre de parámetro para el bloque catch ).

catch tendría la misma restricción que try que debe estar en una función que devuelve un error final, ya que ambos son azúcar que depende de eso.

Eso no es tan poderoso como la propuesta original handle , pero obviaría el requisito de nombrar un error para poder manejarlo y eliminaría el nuevo texto estándar discutido anteriormente para los controladores internos mientras lo hace lo suficientemente fácil como para no requieren versiones separadas de funciones para controladores externos.

El manejo de errores complicado puede requerir no usar catch al igual que puede requerir no usar try .

Dado que ambos son azúcar, no es necesario usar catch con try . Los controladores catch se ejecutan cada vez que la función devuelve un error que no es nil , lo que permite, por ejemplo, realizar un registro rápido:

catch err {
  log.Print(err)
  return err
}

o simplemente envolviendo todos los errores devueltos:

catch err {
  return fmt.Errorf("foo: %w", err)
}

@ianlancetaylor

_"Creo que es una mala idea, porque significa que si una función cambia para agregar un parámetro final error , el código existente continuará compilando, pero actuará de manera muy diferente"._

Esa es probablemente la forma correcta de verlo, si puede controlar tanto el código ascendente como el descendente, de modo que si necesita cambiar la firma de una función para que también devuelva un error, entonces puede hacerlo.

Pero le pediría que considere lo que sucede cuando alguien no controla ni el flujo ascendente ni el flujo descendente de sus propios paquetes. Y también para considerar los casos de uso en los que se pueden agregar errores, y ¿qué sucede si es necesario agregar errores pero no puede forzar el cambio del código descendente?

¿Puede pensar en un ejemplo en el que alguien cambiaría la firma para agregar un valor de retorno? Para mí, por lo general, han caído en la categoría de _"No me di cuenta de que ocurriría un error"_ o _"Me siento perezoso y no quiero hacer el esfuerzo porque el error probablemente no sucederá". _

En ambos casos, podría agregar un retorno de error porque se hace evidente que es necesario manejar un error. Cuando eso sucede, si no puedo cambiar la firma porque no quiero romper la compatibilidad para otros desarrolladores que usan mis paquetes, ¿qué hacer? Supongo que la gran mayoría de las veces ocurrirá el error y que el código que llamó a la función que no devuelve el error actuará de manera muy diferente, _de todos modos._

En realidad, rara vez hago lo último, pero con demasiada frecuencia hago lo primero. Pero he notado que los paquetes de terceros con frecuencia ignoran los errores de captura donde deberían estar, y lo sé porque cuando abro su código en GoLand, las marcas en naranja brillante cada instancia. Me encantaría poder enviar solicitudes de extracción para agregar el manejo de errores a los paquetes que uso mucho, pero si lo hago, la mayoría no los aceptará porque estaría rompiendo sus firmas de código.

Al no ofrecer una forma compatible con versiones anteriores de agregar errores para que las funciones los devuelvan, los desarrolladores que distribuyen código y se preocupan por no romper cosas para sus usuarios no podrán evolucionar sus paquetes para incluir el manejo de errores como deberían.


¿Tal vez en lugar de considerar que el problema es que el código actuará de manera diferente, en lugar de ver el problema como un desafío de ingeniería con respecto a cómo minimizar la desventaja de un método que no captura activamente un error? Eso tendría un valor más amplio y a más largo plazo.

Por ejemplo, considere agregar un controlador de errores de paquete que se debe configurar antes de poder ignorar los errores.


Para ser sincero, el idioma de Go de devolver errores además de los valores de retorno regulares fue una de sus mejores innovaciones. Pero como sucede tan a menudo cuando mejora las cosas, a menudo expone otras debilidades y argumentaré que el manejo de errores de Go no innovó lo suficiente.

Nosotros, los Gophers, nos hemos empeñado en devolver un error en lugar de lanzar una excepción, por lo que la pregunta que tengo es _"¿Por qué no deberíamos estar devolviendo errores de cada función?"_ No siempre lo hacemos porque escribir código sin manejo de errores es más conveniente que codificar con él. Por lo tanto, omitimos el manejo de errores cuando creemos que podemos evitarlo. Pero con frecuencia adivinamos mal.

Entonces, si fuera posible descubrir cómo hacer que el código sea elegante y legible, diría que los valores devueltos y los errores realmente deberían manejarse por separado, y que _todas_ las funciones deberían tener la capacidad de devolver errores independientemente de sus firmas de funciones anteriores. Y lograr que el código existente maneje correctamente el código que ahora genera errores sería un esfuerzo que valdría la pena.

No he propuesto nada porque no he podido imaginar una sintaxis viable, pero si queremos ser honestos con nosotros mismos, ¿no ha sido todo en este hilo y relacionado con el manejo de errores de Go en general sobre el hecho de que el manejo de errores y la lógica del programa son extraños compañeros de cama, por lo que, idealmente, los errores se manejarían mejor fuera de banda de alguna manera.

try como palabra clave ciertamente ayuda con la legibilidad (frente a una llamada de función) y parece menos complejo. @brynbellomy @crawshaw gracias por tomarse el tiempo para escribir los ejemplos.

Supongo que mi pensamiento general es que try hace demasiado. Resuelve: llamar a la función, asignar variables, verificar el error y devolver el error si existe. Propongo que, en cambio, cortemos el alcance y resolvamos solo para el retorno condicional: "devolver si el último argumento no es nulo".

Probablemente no sea una idea nueva... Pero después de hojear las propuestas en la wiki de comentarios de error , no la encontré (no significa que no esté allí)

Mini propuesta de devolución condicionada

extracto:

err, thing := newThing(name)
refuse nil, err

También lo agregué a la wiki en "ideas alternativas"

No hacer nada también parece una opción muy razonable.

@alexhornbake eso me da una idea un poco diferente que sería más útil

assert(nil, err)
assert(len(a), len(b))
assert(true, condition)
assert(expected, given)

de esta manera, no solo se aplicaría a la verificación de errores, sino a muchos tipos de errores lógicos.

Lo dado se envolvería en un error y se devolvería.

@alexhornbake

Así como try en realidad no lo está intentando, refuse en realidad no se está "rechazando". La intención común aquí ha sido que estamos configurando un "relé de protección" ( relay es corto, preciso y aliterado a return ) que se "dispara" cuando uno de los valores cableados cumple una condición (es decir, es un error no nulo). Es una especie de disyuntor y, creo, puede agregar valor si su diseño se limitó a casos poco interesantes para simplemente reducir algunos de los repetitivos más bajos. Cualquier cosa remotamente compleja debe basarse en el código Go simple.

También felicito a cranshaw por su trabajo de revisión de la biblioteca estándar, pero llegué a una conclusión muy diferente... Creo que hace que casi todos esos fragmentos de código sean más difíciles de leer y más propensos a malentendidos.

        req.Header = Header(try(tp.ReadMIMEHeader())

Muy a menudo extrañaré que esto pueda fallar. Una lectura rápida me dice "bien, establezca el encabezado en Encabezado de ReadMimeHeader de la cosa".

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
        }

Este, mis ojos simplemente se cruzan tratando de analizar esa línea OpenDB. Hay tanta densidad allí... Esto muestra el principal problema que tienen todas las llamadas a funciones anidadas, ya que tienes que leer de adentro hacia afuera, y tienes que analizarlo en tu cabeza para descubrir dónde está la parte más interna. .

También tenga en cuenta que esto puede regresar desde dos lugares diferentes en la misma línea... va a estar depurando, y va a decir que hubo un error devuelto desde esta línea, y lo primero que todos van a hacer es intentar descubra por qué OpenDB está fallando con este extraño error, cuando en realidad está fallando OpenConnector (o viceversa).

        ds := &driverStmt{
                Locker: dc,
                si: try(ctxDriverPrepare(ctx, dc.ci, query)),
        }   

Este es un lugar donde el código puede fallar donde antes sería imposible. Sin try , la construcción literal de la estructura no puede fallar . Mis ojos lo hojearán como "bien, construyendo un driverStmt... continuando..." y será tan fácil pasarlo por alto que, en realidad, esto puede causar un error en la función. La única forma en que eso hubiera sido posible antes es si ctxDriverPrepare entrara en pánico... y todos sabemos que ese es un caso que 1.) básicamente nunca debería suceder y 2.) si sucede, significa que algo anda drásticamente mal.

Probar una palabra clave y una declaración soluciona muchos de mis problemas con ella. Sé que no es compatible con versiones anteriores, pero no creo que usar una versión peor sea la solución al problema de la compatibilidad con versiones anteriores.

@daved No estoy seguro de seguir. ¿No te gusta el nombre o no te gusta la idea?

De todos modos, publiqué esto aquí como una alternativa... Si hay un interés legítimo, puedo abrir un nuevo tema para la discusión, no quiero contaminar este hilo (¿quizás demasiado tarde?) Los pulgares hacia arriba o hacia abajo en la idea original darán nosotros un sentido... Por supuesto abierto a nombres alternativos. La parte importante es el "retorno condicional sin tratar de manejar la asignación".

Aunque me gusta la propuesta de captura de @jimmyfrasche , me gustaría proponer una alternativa:
go handler fmt.HandleErrorf("copy %s %s", src, dst)
sería equivalente a:
go defer func(){ if(err != nil){ fmt.HandleErrorf(&err,"copy %s %s", src, dst) } }()
donde err es el último valor de retorno nombrado, con tipo error. Sin embargo, los controladores también se pueden usar cuando no se nombran los valores devueltos. El caso más general también estaría permitido:
go handler func(err *error){ *err = fmt.Errorf("foo: %w", *err) }() `
El principal problema que tengo con el uso de valores de retorno con nombre (que catch no resuelve) es que err es superfluo. Al diferir una llamada a un controlador como fmt.HandleErrorf , no hay un primer argumento razonable, excepto un puntero al valor de retorno del error, ¿por qué darle al usuario la opción de cometer un error?

En comparación con catch, la principal diferencia es que handler hace un poco más fácil llamar a los controladores predefinidos a expensas de hacer más detallado su definición en el lugar. No estoy seguro de que esto sea ideal, pero creo que está más en línea con la propuesta original.

@yiyus catch , como lo definí, no requiere que se nombre err en la función que contiene catch .

En catch err { , el err es el nombre del error dentro del bloque catch . Es como un nombre de parámetro de función.

Con eso, no hay necesidad de algo como fmt.HandleErrorf porque puedes usar el fmt.Errorf normal:

func f() error {
  catch err {
    return fmt.Errorf("foo: %w", err)
  }
  return errors.New("bar")
}

que devuelve un error que se imprime como foo: bar .

No me gusta este enfoque, debido a:

  • La llamada a la función try() interrumpe la ejecución del código en la función principal.
  • no hay palabra clave return , pero el código realmente regresa.

Se están proponiendo muchas formas de hacer controladores, pero creo que a menudo pasan por alto dos requisitos clave:

  1. Tiene que ser significativamente diferente y mejor que if x, err := thingie(); err != nil { handle(err) } . Creo que las sugerencias del tipo try x := thingie else err { handle(err) } no alcanzan ese nivel. ¿Por qué no decir simplemente if ?

  2. Debe ser ortogonal a la funcionalidad existente de defer . Es decir, debe ser lo suficientemente diferente como para que quede claro que el mecanismo de manejo propuesto es necesario por derecho propio sin crear casos extraños en las esquinas cuando interactúan el manejo y el aplazamiento.

Tenga en cuenta estos deseos mientras discutimos mecanismos alternativos para try /handle.

@carlmjohnson Me gusta la idea de catch de @jimmyfrasche con respecto a su punto 2: es solo azúcar de sintaxis para un defer que ahorra 2 líneas y le permite evitar tener que nombrar el valor de retorno del error (que en turn también requeriría que nombre todos los demás si aún no lo ha hecho). No plantea un problema de ortogonalidad con defer , porque es defer .

haciéndose eco de lo que dijo @ubombi :

La llamada a la función try() interrumpe la ejecución del código en la función principal.; no hay palabra clave de retorno, pero el código realmente regresa.

En Ruby, procs y lambdas son un ejemplo de lo que hace try ... Un proc es un bloque de código cuya declaración de retorno no devuelve el bloque en sí, sino la persona que llama.

Esto es exactamente lo que hace try ... es solo un proceso de Ruby predefinido.

Creo que si fuéramos a seguir esa ruta, tal vez podamos dejar que el usuario defina su propia función try introduciendo proc functions

Sigo prefiriendo if err != nil , porque es más legible, pero creo que try sería más beneficioso si el usuario definiera su propio proceso:

proc try(err *error, msg string) {
  if *err != nil {
    *err = fmt.Errorf("%v: %w", msg, *err)
    return
  }
}

Y luego puedes llamarlo:

func someFunc() (string, error) {
  err := doSomething()
  try(&err, "someFunc failed")
}

El beneficio aquí es que puede definir el manejo de errores en sus propios términos. Y también puede hacer un proc expuesto, privado o interno.

También es mejor que la cláusula handle {} en la propuesta original de Go2 porque puede definir esto solo una vez para todo el código base y no en cada función.

Una consideración para la legibilidad es que un func() y un proc() pueden llamarse de manera diferente, como func() y proc!() modo que un programador sepa que una llamada a proc en realidad podría regresar fuera del función de llamada.

@marwan-at-work, ¿no debería try(err, "someFunc failed") ser try(&err, "someFunc failed") en su ejemplo?

@dpinela gracias por la corrección, actualicé el código :)

La práctica común que estamos tratando de anular aquí es lo que sugiere el desenrollado estándar de la pila en muchos idiomas en una excepción (y por lo tanto, se seleccionó la palabra "intentar"...).
Pero si solo pudiéramos permitir una función (... probar () u otra) que retrocediera dos niveles en la traza, entonces

try := handler(err error) {     //which corelates with - try := func(err error) 
   if err !=nil{
       //do what ever you want to do when there's an error... log/print etc
       return2   //2 levels
   }
} 

y luego un código como
f := try(os.Open(nombre de archivo))
podría hacer exactamente lo que aconseja la propuesta, pero como es una función (o en realidad una "función de controlador"), el desarrollador tendrá mucho más control sobre lo que hace la función, cómo formatea el error en diferentes casos, use un controlador similar en todo el código para manejar (digamos) os.Open, en lugar de escribir fmt.Errorf("error al abrir el archivo %s ....") cada vez.
Esto también forzaría el manejo de errores como si "intentar" no estuviera definido; es un error de tiempo de compilación.

@guybrand Tener un retorno de dos niveles return2 (o "retorno no local" como se llama el concepto general en Smalltalk) sería un buen mecanismo de propósito general (también sugerido por @mikeschinkel en #32473) . Pero parece que todavía se necesita try en su sugerencia, por lo que no veo una razón para el return2 : el try solo puede hacer el return . Sería más interesante si también se pudiera escribir try localmente, pero eso no es posible para firmas arbitrarias.

@griesemer

_"así que no veo una razón para el return2 - el try solo puede hacer el return ."_

Una razón, como señalé en #32473 _(gracias por la referencia)_, sería permitir múltiples niveles de break y continue , además de return .

Gracias de nuevo a todos por todos los nuevos comentarios; es una inversión de tiempo significativa mantenerse al día con la discusión y escribir comentarios extensos. Y mejor aún, a pesar de los argumentos a veces apasionados, este ha sido un hilo bastante civilizado hasta ahora. ¡Gracias!

Aquí hay otro resumen rápido, esta vez un poco más condensado; disculpas a aquellos que no mencioné, olvidé o malinterpreté. En este punto, creo que están surgiendo algunos temas más importantes:

1) En general, se considera que usar una función integrada para la funcionalidad try es una mala elección: dado que afecta el flujo de control, debería ser _al menos_ una palabra clave ( @carloslenz "prefiere que sea una declaración sin paréntesis"); try como expresión no parece buena idea, perjudica la legibilidad ( @ChrisHines , @jimmyfrasche), son "devoluciones sin return ". @brynbellomy hizo un análisis real de try usados ​​como identificadores; parece haber muy pocos porcentajes, por lo que podría ser posible seguir la ruta de la palabra clave sin afectar demasiado el código.

2) @crawshaw se tomó un tiempo para analizar un par de cientos de casos de uso de la biblioteca estándar y llegó a la conclusión de que try como se proponía, casi siempre mejoraba la legibilidad. @jimmyfrasche llegó a la conclusión opuesta .

3) Otro tema es que usar defer para la decoración de errores no es lo ideal. @josharian señala que los defer siempre se ejecutan al regresar la función, pero si están aquí para la decoración de errores, solo nos preocupamos por su cuerpo si hay un error, lo que podría ser una fuente de confusión.

4) Muchos escribieron sugerencias para mejorar la propuesta. @zeebo , @patrick-nyt apoyan gofmt formato gofmt de declaraciones simples if en una sola línea (y estén contentos con el status quo). @jargv sugirió que try() (sin argumentos) podría devolver un puntero al error actualmente "pendiente", lo que eliminaría la necesidad de nombrar el resultado del error solo para que uno tenga acceso a él en un defer ; @masterada sugirió usar errorfunc() en su lugar. @velovix revivió la idea de un try de 2 argumentos donde el segundo argumento sería un controlador de errores.

@klaidliadon , @networkimprov están a favor de "operadores de asignación" especiales como f, # := os.Open() en lugar de try . @networkimprov presentó una propuesta alternativa más completa que investiga tales enfoques (consulte el número 32500). @mikeschinkel también presentó una propuesta alternativa que sugiere introducir dos nuevas funciones de lenguaje de propósito general que también podrían usarse para el manejo de errores, en lugar de try específicos de errores (consulte el problema n.º 32473). @josharian revivió una posibilidad que discutimos en GopherCon el año pasado donde try no regresa en caso de error sino que salta (con un goto ) a una etiqueta llamada error (alternativamente , try podría tomar el nombre de una etiqueta de destino).

5) Sobre el tema de try como palabra clave, han aparecido dos líneas de pensamiento. @brynbellomy sugirió una versión que alternativamente podría especificar un controlador:

a, b := try f()
a, b := try f() else err { /* handle error */ }

@thepudds va un paso más allá y sugiere try al principio de la línea, dando a try la misma visibilidad que return :

try a, b := f()

Ambos podrían funcionar con defer .

@griesemer

Gracias por la referencia a @mikeschinkel #32473, tiene mucho en común.

con respecto a

Pero parece que aún se necesita probar en su sugerencia
Aunque mi sugerencia se puede implementar con "cualquier" controlador y no con una "compilación/palabra clave/expresión" reservada, no creo que "intentar()" sea una mala idea (y, por lo tanto, no la voté), lo estoy intentando para "ampliarlo", por lo que mostraría más ventajas, muchos esperaban "una vez que se introduzca go 2.0"

Creo que esa también puede ser la fuente de las "vibraciones mixtas" que informó en su último resumen: no es "intentar () no mejora el manejo de errores", claro que sí, es "esperar a que Go 3.0 resuelva algún otro error importante manejo de dolores" personas mencionadas anteriormente, parece demasiado largo :)

Estoy realizando una encuesta sobre "problemas en el manejo de errores" (y algunas de las molestias son simplemente "no utilizo buenas prácticas", mientras que otras ni siquiera las imaginé que la gente (en su mayoría provenientes de otros idiomas) quisiera hacer - de cool a WTF).

Espero poder compartir algunos resultados interesantes pronto.

por último, ¡gracias por el increíble trabajo y la paciencia!

Mirando simplemente la longitud de la sintaxis propuesta actual en comparación con lo que está disponible ahora, el caso en el que el error solo debe devolverse sin manipularlo o decorarlo es el caso en el que se obtiene la mayor comodidad. Un ejemplo con mi sintaxis favorita hasta ahora:

try a, b := f() else err { return fmt.Errorf("Decorated: %s", err); }
if a,b, err :=f;  err != nil { return fmt.Errorf("Decorated: %s", err); }
try a, b := f()
if a,b, err :=f;  err != nil { return err; }

Entonces, a diferencia de lo que pensaba antes, tal vez sea simplemente suficiente para alterar go fmt, al menos para el caso de error decorado/manejado. Mientras que para pasar el caso de error, algo como intentar podría ser deseable como azúcar sintáctico para este caso de uso muy común.

Con respecto a try else , creo que las funciones de error condicional como fmt.HandleErrorf (editar: asumo que devuelve cero cuando la entrada es cero) en el comentario inicial funciona bien, por lo que agregar else es innecesario

a, b, err := doSomething()
try fmt.HandleErrorf(&err, "Decorated "...)

Como muchos otros aquí, prefiero que try sea una declaración en lugar de una expresión, principalmente porque una expresión que altera el flujo de control es completamente ajena a Go. También porque esto no es una expresión, debe estar al principio de la línea.

También estoy de acuerdo con @daved en que el nombre no es apropiado. Después de todo, lo que estamos tratando de lograr aquí es una asignación protegida, entonces, ¿por qué no usar guard como en Swift y hacer que la cláusula else sea opcional? Algo como

GuardStmt = "guard" ( Assignment | ShortVarDecl | Expression ) [  "else" Identifier Block ] .

donde Identifier es el nombre de la variable de error vinculado en el siguiente Block . Sin la cláusula else , simplemente regrese de la función actual (y use un controlador diferido para decorar los errores si es necesario).

Inicialmente no me gustó una cláusula else porque es solo azúcar sintáctica en torno a la asignación habitual seguida de if err != nil , pero después de ver algunos de los ejemplos, tiene sentido: usar guard aclara la intención.

EDITAR: algunos sugirieron usar cosas como catch para especificar de alguna manera diferentes controladores de errores. Encuentro else igualmente viable semánticamente hablando y ya está en el idioma.

Si bien me gusta la declaración try-else, ¿qué tal esta sintaxis?

a, b, (err) := func() else { return err }

La expresión try - else es un operador ternario.

a, b := try f() else err { ... }
fmt.Println(try g() else err { ... })`

Declaración try - else es una declaración if .

try a, b := f() else err { ... }
// (modulo scope of err) same as
a, b, err := f()
if err != nil { ... }

El try incorporado con un controlador opcional se puede lograr con una función de ayuda (a continuación) o sin usar try (no se muestra, todos sabemos cómo se ve).

a, b := try(f(), decorate)
// same as
a, b := try(g())
// where g is
func g() (whatever, error) {
  x, err := f()
  if err != nil {
    try(decorate(err))
  }
  return x, nil
}

Los tres reducen el modelo y ayudan a contener el alcance de los errores.

Ofrece los mayores ahorros para try incorporados, pero tiene los problemas mencionados en el documento de diseño.

Para la declaración try - else , proporciona una ventaja sobre el uso if en lugar de try . Pero la ventaja es tan marginal que me cuesta ver que se justifique, aunque me gusta.

Los tres asumen que es común necesitar un manejo especial de errores para errores individuales.

El manejo de todos los errores por igual se puede hacer en defer . Si se realiza el mismo manejo de errores en cada bloque else , eso es un poco repetitivo:

func printSum(a, b string) error {
  try x := strconv.Atoi(a) else err {
    return fmt.Errorf("printSum: %w", err)
  }
  try y := strconv.Atoi(b) else err {
    return fmt.Errorf("printSum: %w", err)
  }
  fmt.Println("result:", x + y)
  return nil
}

Ciertamente sé que hay momentos en que un cierto error requiere un manejo especial. Esos son los casos que sobresalen en mi memoria. Pero, si eso solo sucede, digamos, 1 de cada 100 veces, ¿no sería mejor mantener try simple y simplemente no usar try en esas situaciones? Por otro lado, si es más como 1 de cada 10 veces, agregar else /handler parece más razonable.

Sin embargo, sería interesante ver una distribución real de la frecuencia con la que try sin un controlador else frente a try con un controlador else Esos datos no son fáciles de recopilar.

Quiero ampliar el comentario reciente de @jimmyfrasche .

El objetivo de esta propuesta es reducir el modelo

    a, b, err := f()
    if err != nil {
        return nil, err
    }

Este código es fácil de leer. Solo vale la pena extender el lenguaje si podemos lograr una reducción considerable en el modelo. Cuando veo algo como

    try a, b := f() else err { return nil, err }

No puedo evitar sentir que no estamos ahorrando tanto. Estamos guardando tres líneas, lo cual es bueno, pero según mi cuenta, estamos recortando de 56 a 46 caracteres. Eso no es mucho. Comparar con

    a, b := try(f())

que pasa de 56 a 18 caracteres, una reducción mucho más significativa. Y aunque la instrucción try hace que el posible cambio de flujo de control sea más claro, en general no encuentro que la instrucción sea más legible. Aunque en el lado positivo, la instrucción try hace que sea más fácil anotar el error.

De todos modos, mi punto es: si vamos a cambiar algo, debería reducir significativamente el texto estándar o debería ser significativamente más legible. Este último es bastante difícil, por lo que cualquier cambio debe funcionar realmente en el primero. Si solo obtenemos una pequeña reducción en el modelo, en mi opinión, no vale la pena hacerlo.

Al igual que otros, me gustaría agradecer a @crawshaw por los ejemplos.

Al leer esos ejemplos, animo a las personas a intentar adoptar una mentalidad en la que no se preocupe por el flujo de control debido a la función try . Creo, quizás incorrectamente, que ese flujo de control se convertirá rápidamente en una segunda naturaleza para las personas que conocen el idioma. En el caso normal, creo que la gente simplemente dejará de preocuparse por lo que sucede en el caso de error. Intente leer esos ejemplos mientras mira try tal como ya lo hace con if err != nil { return err } .

Después de leer todo aquí, y después de una mayor reflexión, no estoy seguro de ver siquiera como una declaración algo que valga la pena agregar.

  1. la justificación de esto parece ser la reducción de errores en el manejo del código de la placa de la caldera. En mi humilde opinión, "ordena" el código, pero en realidad no elimina la complejidad; simplemente lo oscurece . Esto no parece una razón lo suficientemente fuerte. La ida" sintaxis bellamente capturada al iniciar un hilo simultáneo. No tengo ese tipo de sensación de "¡ajá!" aquí. No se siente bien. La relación costo/beneficio no es lo suficientemente grande.

  2. su nombre no refleja su función. En su forma más simple, lo que hace es esto: "si una función devuelve un error, la persona que llama regresa con un error" pero eso es demasiado largo :-) Como mínimo, se necesita un nombre diferente.

  3. con el retorno implícito de try en caso de error, parece que Go está retrocediendo de mala gana al manejo de excepciones. Es decir, si A llama a estar en una guardia de prueba y B llama a C en una guardia de prueba, y C llama a D en una guardia de prueba, si D devuelve un error en efecto, ha causado un goto no local. Se siente demasiado "mágico".

  4. y, sin embargo, creo que una mejor manera puede ser posible. Al elegir probar ahora, se cerrará esa opción.

@ianlancetaylor
Si entiendo correctamente la propuesta "intentar otra cosa", parece que el bloque else es opcional y está reservado para el manejo proporcionado por el usuario. En su ejemplo try a, b := f() else err { return nil, err } , la cláusula else es realmente redundante, y la expresión completa se puede escribir simplemente como try a, b := f()

Estoy de acuerdo con @ianlancetaylor ,
La legibilidad y el texto estándar son dos preocupaciones principales y quizás el impulso para
el manejo de errores de go 2.0 (aunque puedo agregar algunas otras preocupaciones importantes)

Asimismo, que la corriente

a, b, err := f()
if err != nil {
    return nil, err
}

Es muy legible.
Y como creo

if a, b, err := f(); err != nil {
    return nil, err
}

Es casi tan legible, pero tenía sus "problemas" de alcance, tal vez un

ifErr a, b, err := f() {
    return nil, err
}

Eso sería solo el ; err != parte nula, y no crearía un alcance, o

similar

intente a, b, err := f() {
devuelve cero, err
}

Conserva las dos líneas adicionales, pero sigue siendo legible.

El martes, 11 de junio de 2019, 20:19 Dmitriy Matrenichev, [email protected]
escribió:

@ianlancetaylor https://github.com/ianlancetaylor
Si entiendo correctamente la propuesta de "intentar otra cosa", parece que el bloque else
es opcional y está reservado para el manejo proporcionado por el usuario. en tu ejemplo
intente a, b := f() else err { return nil, err } la cláusula else es en realidad
redundante, y toda la expresión se puede escribir simplemente como try a, b :=
F()


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=ABNEY4XPURMASWKZKOBPBVDPZ7NALA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXN3VDA#issuecomment-50093944
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/ABNEY4SAFK4M5NLABF3NZO3PZ7NALANCNFSM4HTGCZ7Q
.

@ianlancetaylor

De todos modos, mi punto es: si vamos a cambiar algo, debería reducir significativamente el texto estándar o debería ser significativamente más legible. Este último es bastante difícil, por lo que cualquier cambio debe funcionar realmente en el primero. Si solo obtenemos una pequeña reducción en el modelo, en mi opinión, no vale la pena hacerlo.

De acuerdo, y teniendo en cuenta que un else solo sería azúcar sintáctico (¡con una sintaxis extraña!), Muy probablemente se use solo en raras ocasiones, no me importa mucho. Sin embargo, todavía prefiero que try sea una declaración.

@ianlancetaylor Haciéndose eco de @DmitriyMV , el bloque else sería opcional. Permítanme presentar un ejemplo que ilustra ambos (y no parece demasiado equivocado en términos de la proporción relativa de bloques try manejados frente a no manejados en código real):

func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    headRef, err := r.Head()
    if err != nil {
        return err
    }

    parentObjOne, err := headRef.Peel(git.ObjectCommit)
    if err != nil {
        return err
    }

    parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit)
    if err != nil {
        return err
    }

    parentCommitOne, err := parentObjOne.AsCommit()
    if err != nil {
        return err
    }

    parentCommitTwo, err := parentObjTwo.AsCommit()
    if err != nil {
        return err
    }

    treeOid, err := index.WriteTree()
    if err != nil {
        return err
    }

    tree, err := r.LookupTree(treeOid)
    if err != nil {
        return err
    }

    remoteBranchName, err := remoteBranch.Name()
    if err != nil {
        return err
    }

    userName, userEmail, err := r.UserIdentityFromConfig()
    if err != nil {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    _, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    if err != nil {
        return err
    }
    return nil
}
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    try headRef := r.Head()
    try parentObjOne := headRef.Peel(git.ObjectCommit)
    try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    try parentCommitOne := parentObjOne.AsCommit()
    try parentCommitTwo := parentObjTwo.AsCommit()
    try treeOid := index.WriteTree()
    try tree := r.LookupTree(treeOid)
    try remoteBranchName := remoteBranch.Name()
    try userName, userEmail := r.UserIdentityFromConfig() else err {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    try r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    return nil
}

Si bien el patrón try / else no guarda muchos caracteres sobre el compuesto if , sí lo hace:

  • unificar la sintaxis de manejo de errores con el try no manejado
  • dejar en claro de un vistazo que un bloque condicional está manejando una condición de error
  • danos la oportunidad de reducir las rarezas de alcance que sufren los compuestos if

Sin embargo, es probable que los try no manejados sean los más comunes.

@ianlancetaylor

Trate de leer esos ejemplos mientras observa, intente tal como ya lo hizo si err != nil { return err }.

No creo que eso sea posible/equivalente. Extrañar que existe un intento en una línea abarrotada, o qué envuelve exactamente, o que hay múltiples instancias en una línea... Esto no es lo mismo que marcar fácil/rápidamente un punto de retorno y no preocuparse por los detalles del mismo.

@ianlancetaylor

Cuando veo una señal de alto, la reconozco por su forma y color más que leyendo la palabra impresa en ella y reflexionando sobre sus implicaciones más profundas.

Mis ojos pueden ponerse vidriosos por if err != nil { return err } pero al mismo tiempo todavía lo registra, clara e instantáneamente.

Lo que me gusta de la variante de declaración try es que reduce el texto modelo pero de una manera que es fácil de disimular pero difícil de pasar por alto.

Puede significar una línea adicional aquí o allá, pero aún son menos líneas que el statu quo.

@brynbellomy

  1. ¿Cómo ofrece manejar funciones que devuelven múltiples valores, como:
    func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) (hash, error) {
  2. ¿Cómo mantendría el rastro de la línea correcta que devolvió el error?
  3. descartando el problema del alcance (que puede resolverse de otras maneras), no estoy seguro
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {

    if headRef, err := r.Head(); err != nil {
        return err
    } else if parentObjOne, err := headRef.Peel(git.ObjectCommit); err != nil {
        return err
    } else parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit); err != nil {
        return err
    } ...

no es tan diferente en cuanto a la legibilidad, sin embargo (o fmt.Errorf("error al obtener el encabezado: %s", err.Error() ) le permite modificar y proporcionar datos adicionales fácilmente.

Lo que sigue siendo un fastidio es el

  1. tener que volver a comprobar; error! = cero
  2. devolver el error tal como está si no queremos dar la información adicional, lo que en algunos casos no es una buena práctica, porque depende de la función que llame para reflejar un error "bueno" que insinuará "lo que salió mal ", en casos de funciones file.Open, close, Remove, Db, etc., muchas de las llamadas a funciones pueden devolver el mismo error (podemos discutir si eso significa que el desarrollador que escribió el error hizo un buen trabajo o no... pero SUCEDE) - y luego - tiene un error, probablemente lo registre desde la función que llamó
    " createMergeCommit ", pero no puedo rastrearlo hasta la línea exacta en la que ocurrió.

Lo siento si alguien ya publicó algo como esto (hay muchas buenas ideas: P) ¿Qué tal esta sintaxis alternativa?

fail := func(err error) error {
  log.Print("unexpected error", err)
  return err
}

a, b, err := f1()          // normal
c, d := f2() -> throw      // calls throw(err)
e, f := f3() -> panic      // calls panic(err)
g, h := f4() -> t.Error    // calls t.Error(err)
i, j := f5() -> fail       // calls fail(err)

es decir, tiene un -> handler a la derecha de una llamada de función que se llama si se devuelve err != nil. El controlador es cualquier función que acepta un error como único argumento y, opcionalmente, devuelve un error (es decir, func(error) o func(error) error ). Si el controlador devuelve un error nulo, la función continúa; de lo contrario, se devuelve el error.

entonces a := b() -> handler es equivalente a:

a, err := b()
if err != nil {
  if herr := handler(err); herr != nil {
    return herr
  }
}

Ahora, como atajo, podría admitir un try incorporado (o una palabra clave o un operador ?= o lo que sea) que es la abreviatura de a := b() -> throw por lo que podría escribir algo como:

func() error {
  a, b := try(f1())
  c, d := try(f2())
  e, f := try(f3())
  ...
  return nil
}() -> panic // or throw/fail/whatever

Personalmente, encuentro que un operador ?= es más fácil de leer que una palabra clave/integrada de prueba:

func() error {
  a, b ?= f1()
  c, d ?= f2()
  e, f ?= f3()
  ...
  return nil
}() -> panic

nota : aquí estoy usando throw como marcador de posición para un elemento integrado que devolvería el error a la persona que llama.

No he comentado sobre las propuestas de manejo de errores hasta ahora porque generalmente estoy a favor y me gusta la forma en que se dirigen. Tanto la función de prueba definida en la propuesta como la declaración de prueba propuesta por @thepudds parecen ser adiciones razonables al lenguaje. Confío en que lo que se le ocurra al equipo de Go será bueno.

Quiero mencionar lo que veo como un problema menor con la forma en que se define try en la propuesta y cómo podría afectar futuras extensiones.

Try se define como una función que toma un número variable de argumentos.

func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)

Pasar el resultado de una llamada de función a try como en try(f()) funciona implícitamente debido a la forma en que funcionan los valores de retorno múltiples en Go.

Según mi lectura de la propuesta, los siguientes fragmentos son válidos y semánticamente equivalentes.

a, b = try(f())
//
u, v, err := f()
a, b = try(u, v, err)

La propuesta también plantea la posibilidad de ampliar try con argumentos extra.

Si determinamos en el camino que es una buena idea tener algún tipo de función de manejo de errores proporcionada explícitamente, o cualquier otro parámetro adicional, es trivialmente posible pasar ese argumento adicional a una llamada de prueba.

Supongamos que queremos agregar un argumento de controlador. Puede ir al principio o al final de la lista de argumentos.

var h handler
a, b = try(h, f())
// or
a, b = try(f(), h)

Ponerlo al principio no funciona porque (dada la semántica anterior) try no podría distinguir entre un argumento de controlador explícito y una función que devuelve un controlador.

func f() (handler, error) { ... }
func g() (error) { ... }
try(f())
try(h, g())

Ponerlo al final probablemente funcionaría, pero luego intentar sería único en el lenguaje como la única función con un parámetro varargs al principio de la lista de argumentos.

Ninguno de estos problemas es sensacional, pero hacen que try se sienta inconsistente con el resto del lenguaje, por lo que no estoy seguro de que try sea fácil de extender en el futuro como el establece la propuesta.

@mágico

Tener un controlador es poderoso, tal vez:
Yo ya declaraste h,

puede

var h handler
a, b, h = f()

o

a, b, h.err = f()

si es una función como:

h:= handler(err error){
 log(...)
 return ....
} 

Entonces hubo una sugerencia para

a, b, h(err) = f()

Todos pueden invocar al controlador.
Y también puede "seleccionar" el controlador que devuelve o solo captura el error (continuar/romper/regresar) como algunos sugirieron.

Y así se acabó el problema de los varargs.

Una alternativa a la sugerencia de else de @brynbellomy de:

a, b := try f() else err { /* handle error */ }

podría ser para apoyar una función de decoración inmediatamente después del else:

decorate := func(err error) error { return fmt.Errorf("foo failed: %v", err) }

try a, b := f() else decorate
try c, d := g() else decorate

Y tal vez también algunas funciones de utilidad algo como:

decorate := fmt.DecorateErrorf("foo failed")

La función de decoración podría tener la firma func(error) error y ser llamada por try en presencia de un error, justo antes de que try regrese de la función asociada que se está probando.

Eso sería similar en espíritu a una de las "iteraciones de diseño" anteriores del documento de propuesta:

f := try(os.Open(filename), handler)              // handler will be called in error case

Si alguien quiere algo más complejo o un bloque de declaraciones, podría usar if (tal como lo hace hoy).

Dicho esto, hay algo bueno en la alineación visual de try que se muestra en el ejemplo de @brynbellomy en https://github.com/golang/go/issues/32437#issuecomment -500949780.

Todo esto aún podría funcionar con defer si se elige ese enfoque para la decoración de errores uniforme (o incluso en teoría podría haber una forma alternativa de registrar una función de decoración, pero ese es un punto aparte).

En cualquier caso, no estoy seguro de qué es lo mejor aquí, pero quería hacer explícita otra opción.

Aquí está el ejemplo de @brynbellomy reescrito con la función try , usando un bloque var para conservar la buena alineación que @thepudds señaló en https://github.com/golang/go/issues /32437#issuecomentario -500998690.

package main

import (
    "fmt"
    "time"
)

func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    var (
        headRef          = try(r.Head())
        parentObjOne     = try(headRef.Peel(git.ObjectCommit))
        parentObjTwo     = try(remoteBranch.Reference.Peel(git.ObjectCommit))
        parentCommitOne  = try(parentObjOne.AsCommit())
        parentCommitTwo  = try(parentObjTwo.AsCommit())
        treeOid          = try(index.WriteTree())
        tree             = try(r.LookupTree(treeOid))
        remoteBranchName = try(remoteBranch.Name())
    )

    userName, userEmail, err := r.UserIdentityFromConfig()
    if err != nil {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    _, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    return err
}

Es tan sucinto como la versión de declaración try , y diría que es igual de legible. Dado que try es una expresión, algunas de esas variables intermedias podrían eliminarse, a costa de cierta legibilidad, pero eso parece más una cuestión de estilo que cualquier otra cosa.

Sin embargo, plantea la cuestión de cómo funciona try en un bloque var . Supongo que cada línea de var cuenta como una declaración separada, en lugar de que todo el bloque sea una sola declaración, en cuanto al orden de lo que se asigna y cuándo.

Sería bueno que la propuesta de "probar" mencionara explícitamente las consecuencias de herramientas como cmd/cover que aproximan las estadísticas de cobertura de prueba mediante el conteo de declaraciones ingenuas. Me preocupa que el flujo de control de errores invisible pueda resultar en un conteo insuficiente.

@thepudds

prueba a, b := f() sino decora

Tal vez es una quemadura demasiado profunda en mis células cerebrales, pero esto me golpea demasiado como una

try a, b := f() ;catch(decorate)

y una pendiente resbaladiza a un

a, b := f()
catch(decorate)

Creo que puedes ver a dónde lleva eso, y para mí comparar

    try headRef := r.Head()
    try parentObjOne := headRef.Peel(git.ObjectCommit)
    try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    try parentCommitOne := parentObjOne.AsCommit()
    try parentCommitTwo := parentObjTwo.AsCommit()
    try treeOid := index.WriteTree()
    try tree := r.LookupTree(treeOid)
    try remoteBranchName := remoteBranch.Name()

con

    try ( 
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
    remoteBranchName := remoteBranch.Name()
    )

(o incluso una captura al final)
El segundo es más legible, pero enfatiza el hecho de que las funciones a continuación devuelven 2 vars, y mágicamente descartamos uno, recopilándolo en un "error mágico devuelto".

    try(err) ( 
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
    remoteBranchName := remoteBranch.Name()
    ); err!=nil {
      //handle the err
    }

al menos configura explícitamente la variable para que regrese, y déjame manejarla dentro de la función, cuando quiera.

Solo intercalando un comentario específico ya que no vi a nadie más mencionarlo explícitamente, específicamente sobre cambiar gofmt para admitir el siguiente formato de una sola línea, o cualquier variante:

if f() { return nil, err }

Por favor no. Si queremos una sola línea if , haga una sola línea if , por ejemplo:

if f() then return nil, err

Pero por favor, por favor, no adopte la ensalada de sintaxis eliminando los saltos de línea que facilitan la lectura del código que usa llaves.

Me gusta enfatizar un par de cosas que pueden haberse olvidado en el fragor de la discusión:

1) El punto central de esta propuesta es hacer que el manejo de errores comunes se desvanezca en segundo plano: el manejo de errores no debe dominar el código. Pero aún debe ser explícito. Cualquiera de las sugerencias alternativas que hacen que el manejo de errores se destaque aún más no tiene sentido. Como @ianlancetaylor ya dijo, si estas sugerencias alternativas no reducen significativamente la cantidad de repetitivo, podemos quedarnos con las declaraciones if . (Y la solicitud de reducir el modelo estándar proviene de usted, la comunidad de Go).

2) Una de las quejas sobre la propuesta actual es la necesidad de nombrar el resultado del error para poder acceder a él. Cualquier propuesta alternativa tendrá el mismo problema a menos que la alternativa introduzca una sintaxis adicional, es decir, más repetitivo (como ... else err { ... } y similares) para nombrar explícitamente esa variable. Pero lo que es interesante: si no nos importa decorar un error y no nombramos los parámetros de resultado, pero aún así requerimos un return explícito porque hay una especie de controlador explícito, esa instrucción return tendrá que enumerar todos los valores de resultado (normalmente cero) ya que en este caso no se permite una devolución simple. Especialmente si una función devuelve muchos errores sin decorar el error, esos retornos explícitos ( return nil, err , etc.) se suman al modelo. La propuesta actual y cualquier alternativa que no requiera un return explícito elimina eso. Por otro lado, si uno quiere decorar el error, la propuesta actual _requiere_ que uno nombre el resultado del error (y con eso todos los demás resultados) para tener acceso al valor del error. Esto tiene el agradable efecto secundario de que en un controlador explícito se puede usar un retorno simple y no es necesario repetir todos los demás valores de resultado. (Sé que hay algunos sentimientos fuertes acerca de las devoluciones desnudas, pero la realidad es que cuando todo lo que nos importa es el resultado del error, es una verdadera molestia tener que enumerar todos los demás valores de resultado (normalmente cero); no agrega nada a la comprensión del código). En otras palabras, tener que nombrar el resultado del error para que se pueda decorar permite una mayor reducción del modelo.

@magical Gracias por señalar esto . Me di cuenta de lo mismo poco después de publicar la propuesta (pero no la mencioné para no causar más confusión). Tiene razón en que tal como está, try no se pudo extender. Afortunadamente, la solución es bastante fácil. (Sucede que nuestras propuestas internas anteriores no tenían este problema; se introdujo cuando reescribí nuestra versión final para su publicación e intenté simplificar try para que coincidiera más con las reglas de paso de parámetros existentes. Parecía una buena - pero resulta que es defectuoso y en su mayoría inútil - beneficio de poder escribir try(a, b, c, handle) .)

Una versión anterior de try lo definía más o menos de la siguiente manera: try(expr, handler) toma una (o quizás dos) expresiones como argumentos, donde la primera expresión puede tener varios valores (solo puede ocurrir si la expresión es una llamada de función). El último valor de esa expresión (posiblemente de varios valores) debe ser del tipo error , y ese valor se compara con cero. (etc. - el resto te lo puedes imaginar).

De todos modos, el punto es que try sintácticamente acepta solo una, o quizás dos expresiones. (Pero es un poco más difícil describir la semántica de try ). La consecuencia sería ese código como:

a, b := try(u, v, err)

ya no estaría permitido. Pero hay pocas razones para hacer que esto funcione en primer lugar: en la mayoría de los casos (a menos que a y b sean resultados nombrados) este código, si es importante por alguna razón, podría reescribirse fácilmente en

a, b := u, v  // we don't care if the assignment happens in case of an error
try(err)

(o use una instrucción if según sea necesario). Pero, de nuevo, esto parece poco importante.

esa declaración de devolución tendrá que enumerar todos los valores de resultado (normalmente cero) ya que no se permite una devolución simple en este caso

No se permite una devolución desnuda, pero intentarlo sí. Una cosa que me gusta de try (ya sea como una función o una declaración) es que ya no tendré que pensar cómo establecer valores que no sean de error cuando devuelva un error, solo usaré try.

@griesemer Gracias por la explicación. Esa es la conclusión a la que también llegué.

Un breve comentario sobre try como declaración: como creo que se puede ver en el ejemplo en https://github.com/golang/go/issues/32437#issuecomment -501035322, el try entierra el lede. El código se convierte en una serie de declaraciones try , lo que oscurece lo que realmente está haciendo el código.

El código existente puede reutilizar una variable de error recién declarada después del bloque if err != nil . Ocultar la variable rompería eso, y agregar una variable de retorno con nombre a la firma de la función no siempre lo solucionará.

Tal vez sea mejor dejar la declaración/asignación de error como está y encontrar una instrucción de manejo de errores de una línea.

err := f() // followed by one of

on err, return err            // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err)      // doesn't stop the function
on err, hname err             // handler invocation without parens
on err, ignore err            // optional ignore handler logs error if defined

if err, return err            // alternatively, use if

handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
   if err == io.Bad { return err }
   fmt.Println(clr.name, err)
}

Una subexpresión try podría entrar en pánico, lo que significa que nunca se espera un error. Una variante de eso podría ignorar cualquier error.

f(try g()) // panic on error
f(try_ g()) // ignore any error

El objetivo de esta propuesta es hacer que el manejo de errores comunes pase a un segundo plano: el manejo de errores no debe dominar el código. Pero aún debe ser explícito.

Me gusta la idea de que los comentarios incluyan try como declaración. Es explícito, todavía fácil de pasar por alto (ya que tiene una longitud fija), pero no tan fácil de pasar por alto (ya que siempre está en el mismo lugar) que se pueden ocultar en una línea llena de gente. También se puede combinar con el defer fmt.HandleErrorf(...) como se indicó anteriormente, sin embargo, tiene la trampa de abusar de los parámetros con nombre para envolver errores (lo que todavía me parece un truco inteligente. Los trucos inteligentes son malos).

Una de las razones por las que no me gustó try como expresión es que es demasiado fácil de pasar por alto o no lo suficientemente fácil de pasar por alto. Tome los siguientes dos ejemplos:

Prueba como una expresión

// Too hidden, it's in a crowded function with many symbols that complicate the function.

f, err := os.OpenFile(try(FileName()), os.O_APPEND|os.O_WRONLY, 0600)

// Not easy enough, the word "try" already increases horizontal complexity, and it
// being an expression only encourages more horizontal complexity.
// If this code weren't collapsed to multiple lines, it would be extremely
// hard to read and unclear as to what's executing when.

fullContents := try(io.CopyN(
    os.Stdout,
    try(net.Dial("tcp", "localhost:"+try(buf.ReadString("\n"))),
    try(strconv.Atoi(try(buf.ReadString("\n")))),
))

Prueba como declaración

// easy to see while still not being too verbose

try name := FileName()
os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)

// does not allow for clever one-liners, code remains much more readable.
// also, the code reads in sequence from top-to-bottom.

try port := r.ReadString("\n")
try lengthStr := r.ReadString("\n")
try length := strconv.Atoi(lengthStr)

try con := net.Dial("tcp", "localhost:"+port)
try io.CopyN(os.Stdout, con, length)

comida para llevar

Este código es definitivamente artificial, lo admito. Pero lo que quiero decir es que, en general, try como expresión no funciona bien en:

  1. El medio de expresiones abarrotadas que no necesitan mucha verificación de errores
  2. Declaraciones de varias líneas relativamente simples que requieren una gran cantidad de verificación de errores

Sin embargo, estoy de acuerdo con @ianlancetaylor en que comenzar cada línea con try parece interferir con la parte importante de cada declaración (la variable que se define o la función que se ejecuta). Sin embargo, creo que debido a que está en la misma ubicación y tiene un ancho fijo, es mucho más fácil pasarlo por alto, sin dejar de notarlo. Sin embargo, los ojos de todos son diferentes.

También creo que fomentar frases ingeniosas en el código es una mala idea en general. Me sorprende que pudiera crear una línea tan poderosa como en mi primer ejemplo, es un fragmento que merece su función completa porque está haciendo mucho, pero cabe en una línea si no lo hubiera colapsado. a múltiples por el bien de la legibilidad. Todo en una línea:

fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))

Lee un puerto desde un *bufio.Reader , inicia una conexión TCP y copia una cantidad de bytes especificados por el mismo *bufio.Reader a stdout . Todo con manejo de errores. Para un lenguaje con convenciones de codificación tan estrictas, no creo que esto deba permitirse. Sin embargo, supongo que gofmt podría ayudar con esto.

Para un lenguaje con convenciones de codificación tan estrictas, no creo que esto deba permitirse.

Es posible escribir código abominable en Go. Incluso es posible formatearlo terriblemente; solo hay normas y herramientas fuertes en su contra. Ir incluso tiene goto .

Durante las revisiones de código, a veces pido a las personas que dividan expresiones complicadas en varias declaraciones, con nombres intermedios útiles. Yo haría algo similar para try s profundamente anidados, por la misma razón.

Lo cual es todo para decir: no intentemos demasiado prohibir el código incorrecto, a costa de distorsionar el lenguaje. Tenemos otros mecanismos para mantener limpio el código que son más adecuados para algo que involucra fundamentalmente el juicio humano caso por caso.

Es posible escribir código abominable en Go. Incluso es posible formatearlo terriblemente; solo hay normas y herramientas fuertes en su contra. Ir incluso tiene goto.

Durante las revisiones de código, a veces pido a las personas que dividan expresiones complicadas en varias declaraciones, con nombres intermedios útiles. Yo haría algo similar para intentos profundamente anidados, por la misma razón.

Lo cual es todo para decir: no intentemos demasiado prohibir el código incorrecto, a costa de distorsionar el lenguaje. Tenemos otros mecanismos para mantener limpio el código que son más adecuados para algo que involucra fundamentalmente el juicio humano caso por caso.

Este es un buen punto. No deberíamos prohibir una buena idea solo porque puede usarse para crear un código incorrecto. Sin embargo, creo que si tenemos una alternativa que promueva un mejor código, puede ser una buena idea. Realmente no he visto mucho hablar _contra_ la idea cruda detrás try como una declaración (sin toda la basura else { ... } ) hasta el comentario de @ianlancetaylor , sin embargo, es posible que me lo haya perdido.

Además, no todos tienen revisores de código, algunas personas (especialmente en un futuro lejano) tendrán que mantener el código Go sin revisar. Go as a language normalmente hace un muy buen trabajo al asegurarse de que casi todo el código escrito se pueda mantener bien (al menos después de un go fmt ), lo cual no es una hazaña para pasar por alto.

Dicho esto, estoy siendo terriblemente crítico con esta idea cuando en realidad no es horrible.

Try como sentencia reduce significativamente el repetitivo, y más que try como expresión, si permitimos que funcione en un bloque de expresiones como se propuso antes, incluso sin permitir un bloque else o un controlador de errores. Usando esto, el ejemplo de deandeveloper se convierte en:

try (
    name := FileName()
    file := os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)
    port := r.ReadString("\n")
    lengthStr := r.ReadString("\n")
    length := strconv.Atoi(lengthStr)
    con := net.Dial("tcp", "localhost:"+port)
    io.CopyN(os.Stdout, con, length)
)

Si el objetivo es reducir el modelo estándar if err!= nil {return err} , entonces creo que la declaración try que permite tomar un bloque de código tiene el mayor potencial para hacerlo, sin volverse confuso.

@beoran Entonces, ¿por qué intentarlo? Simplemente permita una asignación en la que falte el último valor de error y haga que se comporte como si fuera una declaración de prueba (o llamada de función). No es que lo esté proponiendo, pero reduciría aún más el repetitivo.

Creo que estos bloques var reducirían eficientemente el modelo estándar, pero me temo que puede llevar a que una gran cantidad de código se sangra un nivel adicional, lo que sería desafortunado.

@deanveloper

fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))

Debo admitir que no es legible para mí, probablemente sentiría que debo:

fullContents := try(io.CopyN(os.Stdout, 
                               try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))),
                                     try(strconv.Atoi(try(r.ReadString("\n"))))))

o similar, para facilitar la lectura, y luego volvemos con un "intentar" al principio de cada línea, con sangría.

Bueno, creo que aún necesitaríamos probar la compatibilidad con versiones anteriores y también ser explícitos sobre un retorno que puede ocurrir en el bloque. Pero tenga en cuenta que solo estoy siguiendo la lógica de reducir la placa de la caldera y luego ver a dónde nos lleva. Siempre hay una tensión entre la reducción de repetitivo y la claridad. Creo que el problema principal en este tema es que todos parecemos estar en desacuerdo sobre dónde debería estar el equilibrio.

En cuanto a las sangrías, para eso está go fmt, así que personalmente no creo que sea un gran problema.

Me gustaría unirme a la refriega para mencionar otras dos posibilidades, cada una de las cuales son independientes, por lo que las mantendré en publicaciones separadas.

Pensé que la sugerencia de que try() (sin argumentos) podría definirse para devolver un puntero a la variable de retorno de error era interesante, pero no estaba interesado en ese tipo de juego de palabras: huele a sobrecarga de funciones. , algo que Go evita.

Sin embargo, me gustó la idea general de un identificador predefinido que hace referencia al valor de error local.

Entonces, ¿qué hay de predefinir el identificador err en sí mismo para que sea un alias para la variable de retorno de error? Así que esto sería válido:

func foo() error {
    defer handleError(&err, etc)
    try(something())
}

Sería funcionalmente idéntico a:

func foo() (err error) {
    defer handleError(&err, etc)
    try(something())
}

El identificador err se definiría en el ámbito del universo, aunque actúa como un alias local de función, por lo que cualquier definición a nivel de paquete o definición local de función de err lo anularía. Esto puede parecer peligroso, pero escaneé las líneas de 22 m de Go en el corpus de Go y es muy raro. Solo hay 4 instancias distintas err que se usan como global (todas como una variable, no como un tipo o constante); esto es algo sobre lo que vet podría advertir.

Es posible que haya dos variables de retorno de error de función en el alcance; en este caso, creo que es mejor que el compilador se queje de que hay una ambigüedad y requiera que el usuario nombre explícitamente la variable de retorno correcta. Entonces esto no sería válido:

func foo() error {
    f := func() error {
        defer handleError(&err, etc)
        try(something())
        return nil
    }
    return f()
}

pero siempre puedes escribir esto en su lugar:

func foo() error {
    f := func() (err error) {
        defer handleError(&err, etc)
        try(something())
        return nil
    }
    return f()
}

Sobre el tema de try como un identificador predefinido en lugar de un operador,
Me encontré con una tendencia hacia una preferencia por este último después de equivocarme repetidamente entre paréntesis al escribir:

try(try(os.Create(filename)).Write(data))

En "¿Por qué no podemos usar? como Rust", las preguntas frecuentes dicen:

Hasta ahora hemos evitado abreviaturas o símbolos crípticos en el idioma, incluidos operadores inusuales como ?, que tienen significados ambiguos o no obvios.

No estoy completamente seguro de que eso sea cierto. El operador .() es inusual hasta que conoce Go, al igual que los operadores de canal. Si agregáramos un operador ? , creo que pronto se volvería lo suficientemente omnipresente como para que no fuera una barrera significativa.

Sin embargo, el operador Rust ? se agrega después del paréntesis de cierre de una llamada de función, y eso significa que es fácil pasarlo por alto cuando la lista de argumentos es larga.

¿Qué tal agregar ?() como operador de llamadas?

Así que en lugar de:

x := try(foo(a, b))

harías:

x := foo?(a, b)

La semántica de ?() sería muy similar a la del try propuesto incorporado. Actuaría como una llamada de función, excepto que la función o el método que se llama debe devolver un error como último argumento. Al igual que con try , si el error no es nulo, la instrucción ?() lo devolverá.

Parece que la discusión se ha enfocado lo suficiente como para ahora dar vueltas en torno a una serie de compensaciones bien definidas y discutidas. Esto es alentador, al menos para mí, ya que el compromiso está muy en el espíritu de este lenguaje.

@ianlancetaylor Estoy absolutamente de acuerdo en que terminaremos con docenas de líneas con el prefijo try . Sin embargo, no veo cómo eso es peor que docenas de líneas postfijadas por una expresión condicional de dos a cuatro líneas que establece explícitamente la misma expresión return . En realidad, try (con cláusulas else ) hace que sea un poco más fácil detectar cuando un controlador de errores está haciendo algo especial/no predeterminado. También, tangencialmente, re: expresiones condicionales if , creo que entierran el lede más que el propuesto try -como-declaración: la llamada a la función vive en la misma línea que la condicional , el condicional en sí mismo termina al final de una línea ya llena, y las asignaciones de variables se limitan al bloque (lo que requiere una sintaxis diferente si necesita esas variables después del bloque).

@josharian He tenido este pensamiento recientemente. Go se esfuerza por el pragmatismo, no por la perfección, y su desarrollo con frecuencia parece estar impulsado por datos en lugar de principios. Puedes escribir un Go terrible, pero generalmente es más difícil que escribir un Go decente (que es lo suficientemente bueno para la mayoría de las personas). También vale la pena señalar que tenemos muchas herramientas para combatir el código incorrecto: no solo gofmt y go vet , sino también nuestros colegas y la cultura que esta comunidad ha creado (muy cuidadosamente) para guiarse a sí misma. Odiaría alejarme de las mejoras que ayudan al caso general simplemente porque alguien en algún lugar podría atacarse a sí mismo.

@beoran Esto es elegante, y cuando lo piensas, en realidad es semánticamente diferente de los bloques try de otros idiomas, ya que solo tiene un resultado posible: regresar de la función con un error no controlado. Sin embargo: 1) esto probablemente sea confuso para los nuevos codificadores de Go que han trabajado con esos otros lenguajes (honestamente, no es mi mayor preocupación; confío en la inteligencia de los programadores), y 2) esto conducirá a que se indenten grandes cantidades de código en muchos bases de código. En lo que respecta a mi código, incluso tiendo a evitar los bloques type / const / var existentes por este motivo. Además, las únicas palabras clave que actualmente permiten bloques como este son definiciones, no declaraciones de control.

@yiyus No estoy de acuerdo con eliminar la palabra clave, ya que la claridad es (en mi opinión) una de las virtudes de Go. Pero estoy de acuerdo en que sangrar grandes cantidades de código para aprovechar las expresiones try es una mala idea. Entonces, ¿tal vez no hay bloques try en absoluto?

@rogpeppe Creo que ese tipo de operador sutil solo es razonable para las llamadas que nunca deberían devolver un error, por lo que entra en pánico si lo hacen. O llamadas donde siempre ignoras el error. Pero ambos parecen ser raros. Si está abierto a un nuevo operador, vea #32500.

Sugerí que f(try g()) entrara en pánico en https://github.com/golang/go/issues/32437#issuecomment -501074836, junto con una instrucción de manejo de 1 línea:
on err, return ...

Creo que el else opcional en try ... else { ... } empujará el código demasiado hacia la derecha, posiblemente oscureciéndolo. Espero que el bloque de error tome al menos 25 caracteres la mayor parte del tiempo. Además, hasta ahora los bloques no se mantienen en la misma línea por go fmt y espero que este comportamiento se mantenga por try else . Por lo tanto, deberíamos discutir y comparar ejemplos en los que el bloque else está en una línea separada. Pero incluso entonces no estoy seguro de la legibilidad de else { al final de la línea.

@yiyus https://github.com/golang/go/issues/32437#issuecomment -501139662

@beoran Entonces, ¿por qué intentarlo? Simplemente permita una asignación en la que falte el último valor de error y haga que se comporte como si fuera una declaración de prueba (o llamada de función). No es que lo esté proponiendo, pero reduciría aún más el repetitivo.

Eso no se puede hacer porque Go1 ya permite llamar a func foo() error como solo foo() . Agregar , error a los valores de retorno de la persona que llama cambiaría el comportamiento del código existente dentro de esa función. Consulte https://github.com/golang/go/issues/32437#issuecomment -500289410

@rogpeppe En su comentario sobre cómo acertar los paréntesis con try anidados: ¿Tiene alguna opinión sobre la precedencia de try ? Consulte también el documento de diseño detallado sobre este tema .

@griesemer De hecho, no estoy tan interesado en try como operador de prefijo unario por las razones señaladas allí. Se me ha ocurrido que un enfoque alternativo sería permitir try como un pseudométodo en una tupla de retorno de función:

 f := os.Open(path).try()

Creo que eso resuelve el problema de la precedencia, pero en realidad no es muy parecido a Go.

@rogpeppe

¡Muy interesante! . Usted realmente puede estar en algo aquí.

¿Y qué tal si ampliamos esa idea así?

for _,fp := range filepaths {
    f := os.Open(path).try(func(err error)bool{
        fmt.Printf( "Cannot open file %s\n", fp );
        continue;
    });
}

Por cierto, podría preferir un nombre diferente a try() como tal vez guard() pero no debería cambiar el nombre antes de que otros discutan la arquitectura.

contra:

for _,fp := range filepaths {
    if f,err := os.Open(path);err!=nil{
        fmt.Printf( "Cannot open file %s\n", fp )
    }
}

?

Me gusta el try a,b := foo() en lugar de if err!=nil {return err} porque reemplaza un repetitivo para un caso realmente simple. Pero para todo lo demás que agregue contexto, ¿realmente necesitamos algo más que if err!=nil {...} (será muy difícil encontrar algo mejor)?

Si generalmente se requiere una línea adicional para la decoración/envoltura, simplemente "asignemos" una línea para ello.

f, err := os.Open(path)  // normal Go \o/
on err, return fmt.Errorf("Cannot open %s, due to %v", path, err)

@networkimprov Creo que también me podría gustar. Empujando un término más aliterado y descriptivo que ya mencioné...

f, err := os.Open(path)
relay err { nil, fmt.Errorf("Cannot open %s, due to %v", path, err) }

// marginally shorter, doesn't trigger vertical formatting unless excessively wide
// enclosed expression restricted to a list of values that match the return args

o

f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)

// somewhere between statement and func, prob more pleasing to type w/out completion
// trailing expression restricted to a list of values that match the return args
// maybe excessive width triggers linting noise - with a reformatter available
// providing a reformatter would make swapping old (narrow enough) code easy

@daved me alegra que te guste! on err, ... permitiría cualquier controlador de sentencia única:

err := f() // followed by one of

on err, return err            // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err)      // doesn't stop the function
on err, continue              // retry in a loop
on err, hname err             // named handler invocation without parens
on err, ignore err            // logs error if handle ignore() defined

handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
   if err == io.Bad { return err } // non-local return
   fmt.Println(clr, err)
}

EDITAR: on toma prestado de Javascript. No quería sobrecargar if .
Una coma no es esencial, pero no me gusta el punto y coma allí. ¿Quizás colon?

No sigo exactamente relay ; significa retorno en caso de error?

Un relé de protección se dispara cuando se cumple alguna condición. En este caso, cuando un valor de error no es nulo, el relé altera el flujo de control para regresar usando los valores subsiguientes.

*No me gustaría sobrecargar , para este caso, y no soy partidario del término on , pero me gusta la premisa y el aspecto general de la estructura del código.

Para el punto anterior de @josharian , siento que una gran parte de la discusión sobre paréntesis coincidentes es principalmente hipotética y usa ejemplos artificiales. No sé ustedes, pero no me resulta difícil escribir llamadas a funciones en mi programación diaria. Si llego a un punto en el que una expresión se vuelve difícil de leer o comprender, la divido en varias expresiones utilizando variables intermedias. No veo por qué try() con sintaxis de llamada de función sería diferente a este respecto en la práctica.

@eandre Normalmente, las funciones no tienen una definición tan dinámica. Muchas formas de esta propuesta reducen la seguridad en torno a la comunicación del flujo de control, y eso es problemático.

@networkimprov @daved No me disgustan estas dos ideas, pero no se sienten como una mejora suficiente en comparación con simplemente permitir que las declaraciones if err != nil { ... } de una sola línea justifiquen un cambio de idioma. Además, ¿hace algo para reducir la repetición repetitiva en el caso de que simplemente devuelva el error? ¿O es la idea de que siempre tienes que escribir el return ?

@brynbellomy En mi ejemplo, no hay return . relay es un relé de protección definido como "si este error no es nulo, se devolverá lo siguiente".

Usando mi segundo ejemplo de antes:

f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)

También podría ser algo como:

f, err := os.Open(path)
relay(err)

Con el error que dispara el relé devuelto junto con valores cero para otros valores devueltos (o cualquier valor que se establezca para los valores devueltos con nombre). Otro formulario que puede ser útil:

wrap := func(err error, msg string) error {
    if err != nil {
        fmt.Errorf("%s: %s", msg, err)
    }
    return nil
}

// ...

f, err := os.Open(path)
relay(err, wrap(err, fmt.Sprintf("Cannot open %s", path)))

Donde no se llama al segundo relé arg a menos que el primer relé arg dispare el relé. El segundo error de relé opcional arg sería el valor devuelto.

¿Debería _go fmt_ permitir una sola línea if pero no case, for, else, var () ? Los quiero todos, por favor ;-)

El equipo de Go ha rechazado muchas solicitudes de verificaciones de errores de una sola línea.

Las declaraciones on err, return err pueden ser repetitivas, pero son explícitas, concisas y claras.

@magical Sus comentarios se abordaron en la versión actualizada de la propuesta detallada .

Una pequeña cosa, pero si try es una palabra clave, podría reconocerse como una declaración final, por lo que en lugar de

func f() error {
  try(g())
  return nil
}

solo puedes hacer

func f() error {
  try g()
}

( try -la declaración lo obtiene gratis, try -el operador necesitaría un manejo especial, me doy cuenta de que lo anterior no es un gran ejemplo: pero es mínimo)

@jimmyfrasche try podría reconocerse como una declaración final incluso si no es una palabra clave; ya lo hacemos con panic , no se necesita un manejo especial adicional además de lo que ya hacemos. Pero además de ese punto, try no es una declaración final, y tratar de convertirlo en uno artificialmente parece extraño.

Todos los puntos válidos. Supongo que solo podría considerarse de manera confiable como una declaración final si es la última línea de una función que solo devuelve un error, como CopyFile en la propuesta detallada, o si se usa como try(err) en un if donde se sabe que err != nil . No parece valer la pena.

Dado que este hilo se está volviendo largo y difícil de seguir (y comienza a repetirse hasta cierto punto), creo que todos estaríamos de acuerdo en que deberíamos comprometernos con "algunas de las ventajas que ofrece cualquier propuesta".

A medida que nos siguen gustando o disgustando las permutaciones de código propuestas anteriormente, no nos estamos ayudando a tener una idea real de "¿es este un compromiso más sensato que otro/lo que ya se ha ofrecido"?

Creo que necesitamos algunos criterios objetivos para calificar nuestras variaciones de "prueba" y propuestas alternativas.

  • ¿Disminuye el repetitivo?
  • Legibilidad
  • Complejidad añadida al lenguaje
  • Estandarización de errores
  • go-ish
    ...
    ...
  • esfuerzo de implementación y riesgos
    ...

Por supuesto, también podemos establecer algunas reglas básicas para los no-go (no hay compatibilidad con versiones anteriores) y dejar un área gris para "¿se ve atractivo/intuición, etc.?" (los criterios "duros" anteriores también pueden ser debatibles... .).

Si probamos cualquier propuesta con esta lista y calificamos cada punto (repetitivo 5 puntos, legibilidad 4 puntos, etc.), entonces creo que podemos alinearnos en:
Nuestras opciones son probablemente A, B y C, además, alguien que desee agregar una nueva propuesta, podría probar (hasta cierto punto) si su propuesta cumple con los criterios.

Si esto tiene sentido, dale me gusta , podemos intentar repasar la propuesta original.
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

Y tal vez algunas de las otras propuestas en línea con los comentarios o vinculadas, tal vez aprendamos algo, o incluso propongamos una combinación que tenga una calificación más alta.

Criterios += reutilización del código de manejo de errores, en todo el paquete y dentro de la función

Gracias a todos por los continuos comentarios sobre esta propuesta.

La discusión se ha desviado un poco del tema central. También se ha vuelto dominado por una docena o más de colaboradores (usted sabe quiénes son) que analizan lo que equivale a propuestas alternativas.

Así que permítanme hacer un recordatorio amistoso de que este problema se trata de una propuesta _específica_. Esto _no_ es una solicitud de ideas sintácticas novedosas para el manejo de errores (lo cual está bien, pero no es _este_ problema).

Enfoquemos la discusión nuevamente y volvamos a encarrilarla.

La retroalimentación es más productiva si ayuda a identificar los _hechos_ técnicos que no nos dimos cuenta, como "esta propuesta no funciona bien en este caso" o "tendrá esta implicación de la que no nos dimos cuenta".

Por ejemplo, @magical señaló que la propuesta, tal como está escrita, no era tan extensible como se afirmaba (el texto original habría hecho imposible agregar un segundo argumento en el futuro). Afortunadamente, este fue un problema menor que se solucionó fácilmente con un pequeño ajuste a la propuesta. Su aporte ayudó directamente a mejorar la propuesta.

@crawshaw se tomó el tiempo de analizar un par de cientos de casos de uso de la biblioteca std y demostró que try rara vez termina dentro de otra expresión, refutando así directamente la preocupación de que try podría quedar enterrado e invisible. Esa es una retroalimentación basada en hechos muy útil, en este caso validando el diseño.

Por el contrario, los juicios estéticos personales no son muy útiles. Podemos registrar esos comentarios, pero no podemos actuar en consecuencia (aparte de presentar otra propuesta).

En cuanto a la elaboración de propuestas alternativas: La propuesta actual es fruto de mucho trabajo, comenzando con el borrador del diseño del año pasado. Repetimos ese diseño varias veces y solicitamos comentarios de muchas personas antes de sentirnos lo suficientemente cómodos como para publicarlo y recomendar avanzarlo a la fase de experimento real, pero aún no lo hemos hecho. Tiene sentido volver a la mesa de dibujo si el experimento falla, o si la retroalimentación nos dice de antemano que claramente fallará. Si rediseñamos sobre la marcha, basándonos en las primeras impresiones, solo estamos perdiendo el tiempo de todos y, lo que es peor, no aprendemos nada en el proceso.

Dicho todo esto, la preocupación más significativa expresada por muchos con esta propuesta es que no fomenta explícitamente la decoración de errores además de lo que ya podemos hacer en el lenguaje. Gracias, hemos registrado ese comentario. Hemos recibido los mismos comentarios internamente, antes de publicar esta propuesta. Pero ninguna de las alternativas que hemos considerado es mejor que la que tenemos ahora (y hemos analizado muchas en profundidad). En su lugar, hemos decidido proponer una idea mínima que aborda bien una parte del manejo de errores, y que puede ampliarse si es necesario, exactamente para abordar esta preocupación (la propuesta habla de esto en detalle).

Gracias.

(Observo que un par de personas que abogan por propuestas alternativas han iniciado sus propios problemas por separado. Eso es bueno y ayuda a mantener enfocados los problemas respectivos. Gracias).

@griesemer
Estoy totalmente de acuerdo en que debemos concentrarnos y eso es exactamente lo que me llevó a escribir:

Si esto tiene sentido, dale me gusta , podemos intentar repasar la propuesta original.
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

Dos preguntas:

  1. ¿Está de acuerdo si marcamos las ventajas (reducción repetitiva, legibilidad, etc.) frente a las desventajas (sin decoración de error explícita/menor trazabilidad de la fuente de la línea de error) en realidad podemos afirmar: esta propuesta tiene como objetivo resolver a,b, algo ayuda c, no pretende resolver d,e
    Y con eso, pierda todo el desorden de "pero no d", "cómo puede e" y dirija más hacia problemas técnicos como señaló @magical
    Y también desanimar los comentarios de "pero la solución XXX resuelve d,e mejor".
  2. muchas publicaciones en línea son "sugerencias para cambios menores en la propuesta". Sé que es una línea muy fina, pero creo que tiene sentido mantenerlos.

LMKWYT.

Todavía se está considerando el uso try() con cero argumentos (o un componente diferente) o se ha descartado.

Después de los cambios en la propuesta, todavía me preocupa cómo hace que el uso de valores de retorno con nombre sea más "común". Sin embargo, no tengo datos para respaldar eso :upside_down_ face:.
Si se agrega a la propuesta try() sin argumentos (o un componente diferente), ¿se podrían actualizar los ejemplos de la propuesta para usar try() (o un componente diferente) para evitar devoluciones con nombre?

@guybrand Votar a favor y en contra es algo bueno para expresar _sentimiento_, pero eso es todo. No hay más información allí. No vamos a tomar una decisión basada en el conteo de votos, es decir, solo en el sentimiento. Por supuesto, si todos, digamos más del 90%, odian una propuesta, probablemente sea una mala señal y deberíamos pensar dos veces antes de seguir adelante. Pero ese no parece ser el caso aquí. Una buena cantidad de personas parecen estar contentas con probar cosas y han pasado a otras cosas (y no se molesten en comentar en este hilo).

Como traté de expresar anteriormente , el sentimiento en esta etapa de la propuesta no se basa en ninguna experiencia real con la característica; Es un sentimiento. Los sentimientos tienden a cambiar con el tiempo, especialmente cuando uno tuvo la oportunidad de experimentar el tema del que tratan los sentimientos... :-)

@Goodwine Nadie ha descartado try() para llegar al valor del error; aunque _si_ se necesita algo como esto, puede ser mejor tener una variable err predeclarada como sugirió @rogpeppe (creo).

Una vez más, esta propuesta no descarta nada de esto. Vayamos allí si descubrimos que es necesario.

@griesemer
Creo que me entendiste totalmente mal.
No estoy buscando votar a favor o en contra de esta o cualquier propuesta, solo estaba buscando una manera de tener una buena idea de "¿Creemos que tiene sentido tomar una decisión basada en criterios estrictos en lugar de 'Me gusta x' ' o 'no se ve bien' "

Por lo que escribiste, eso es EXACTAMENTE lo que piensas... así que por favor vota mi comentario diciendo:

"Creo que deberíamos hacer una lista de lo que esta propuesta pretende mejorar, y en base a eso podemos
A. decidir si eso es lo suficientemente significativo
B. decidir si parece que la propuesta realmente resuelve lo que pretende resolver
C. (como agregaste) haz un esfuerzo extra tratando de ver si es factible...

@guybrand evidentemente están convencidos de que vale la pena crear prototipos en la versión preliminar 1.14 (?) y recopilar comentarios de los usuarios prácticos. OIA se ha tomado una decisión.

Además, presentó #32611 para discusión de on err, <statement>

@guybrand Mis disculpas. Sí, estoy de acuerdo en que debemos analizar las diversas propiedades de una propuesta, como la reducción repetitiva, si resuelve el problema en cuestión, etc. Pero una propuesta es más que la suma de sus partes: al final del día, hay que mirar el panorama general. Esto es ingeniería, y la ingeniería es complicada: hay muchos factores que intervienen en un diseño, e incluso si objetivamente (basado en criterios estrictos) una parte de un diseño no es satisfactoria, aún puede ser el diseño "correcto" en general. Así que dudo un poco en apoyar una decisión basada en algún tipo de calificación _independiente_ de los aspectos individuales de una propuesta.

(Esperemos que esto aborde mejor lo que quisiste decir).

Pero en cuanto a los criterios relevantes, creo que esta propuesta deja claro lo que trata de abordar. Es decir, la lista a la que te refieres ya existe:

..., nuestro objetivo es hacer que el manejo de errores sea más liviano al reducir la cantidad de código fuente dedicado únicamente a la verificación de errores. También queremos que sea más conveniente escribir código de manejo de errores, para aumentar la probabilidad de que los programadores se tomen el tiempo para hacerlo. Al mismo tiempo, queremos mantener el código de manejo de errores explícitamente visible en el texto del programa.

Da la casualidad de que para la decoración de errores, sugerimos usar defer y parámetros de resultado con nombre (o la declaración anterior if ) porque eso no necesita un cambio de idioma, lo cual es algo fantástico. porque los cambios de idioma tienen enormes costos ocultos. Entendemos que muchos comentaristas sienten que esta parte del diseño "apesta totalmente". Aún así, en este punto, en el panorama general, con todo lo que sabemos, creemos que puede ser lo suficientemente bueno. Por otro lado, necesitamos un cambio de idioma, soporte de idioma, más bien, para deshacernos de la plantilla, y try es el cambio mínimo que podríamos hacer. Y claramente, todo sigue siendo explícito en el código.

Diría que las razones por las que hay tantas reacciones y tantas minipropuestas es que este es un problema en el que casi todos están de acuerdo en que el lenguaje Go necesita hacer algo para disminuir el estándar de manejo de errores, pero en realidad no lo hacemos. estar de acuerdo en cómo hacerlo.

Esta propuesta, en esencia, se reduce a una "macro" incorporada para un caso muy común pero específico de repetitivo, muy parecido a la función append() incorporada. Entonces, si bien es útil para el caso de uso particular id err!=nil { return err } , también es todo lo que hace. Dado que no es muy útil en otros casos, ni es realmente aplicable en general, diría que es decepcionante. Tengo la sensación de que la mayoría de los programadores de Go esperaban un poco más, por lo que la discusión en este hilo continúa.

Es contra intuitivo como una función. Porque no es posible en Go tener una función con este orden de argumentos func(... interface{}, error) .
Escrito primero, luego el número variable de cualquier patrón está en todas partes en los módulos Go.

Cuanto más pienso, me gusta la propuesta actual, tal como está.

Si necesitamos manejo de errores, siempre tenemos la instrucción if.

Hola a todos. Gracias por la discusión tranquila, respetuosa y constructiva hasta ahora. Pasé un tiempo tomando notas y finalmente me frustré lo suficiente como para crear un programa que me ayudara a mantener una vista diferente de este hilo de comentarios que debería ser más navegable y completo que lo que muestra GitHub. (¡También se carga más rápido!) Consulte https://swtch.com/try.html. Lo mantendré actualizado pero en lotes, no minuto a minuto. (Esta es una discusión que requiere una reflexión cuidadosa y el "tiempo de Internet" no ayuda).

Tengo algunas ideas para agregar, pero eso probablemente tendrá que esperar hasta el lunes. Gracias de nuevo.

@mishak87 Abordamos esto en la propuesta detallada . Tenga en cuenta que tenemos otros integrados ( try , make , unsafe.Offsetof , etc.) que son "irregulares"; para eso están los integrados.

@rsc , ¡muy útil! Si todavía lo está revisando, ¿quizás vincule las referencias del problema #id? ¿Y el estilo de fuente sans-serif?

Esto probablemente ya se haya tratado antes, así que me disculpo por agregar aún más ruido, pero solo quería dejar claro que la idea de probar incorporada frente a probar... otra idea.

Creo que probar la función incorporada puede ser un poco frustrante durante el desarrollo. Ocasionalmente, es posible que deseemos agregar símbolos de depuración o agregar más contexto específico de error antes de regresar. Uno tendría que volver a escribir una línea como

user := try(getUser(userID))

para

user, err := getUser(userID)
if err != nil {  
    // inspect error here
    return err
}

Agregar una declaración diferida puede ayudar, pero aún no es la mejor experiencia cuando una función arroja múltiples errores, ya que se activaría para cada llamada try().

Reescribir múltiples llamadas try() anidadas en la misma función sería aún más molesto.

Por otro lado, agregar contexto o código de inspección a

user := try getUser(userID)

sería tan simple como agregar una instrucción catch al final seguida del código

user := try getUser(userID) catch {
   // inspect error here
}

Eliminar o deshabilitar temporalmente un controlador sería tan simple como romper la línea antes de capturarlo y comentarlo.

Cambiar entre try() y if err != nil se siente mucho más molesto en mi opinión.

Esto también se aplica a la adición o eliminación de contexto de error. Se puede escribir try func() mientras se crea un prototipo de algo muy rápidamente y luego agregar contexto a errores específicos según sea necesario a medida que el programa madura, a diferencia de try() como una función integrada en la que habría que volver a escribir el líneas para agregar contexto o agregar código de inspección adicional durante la depuración.

Estoy seguro de que try() sería útil, pero como me imagino usándolo en mi trabajo diario, no puedo evitar imaginar cómo try ... catch sería mucho más útil y mucho menos molesto cuando d necesita agregar/eliminar código adicional específico para algunos errores.


Además, creo que agregar try() y luego recomendar usar if err != nil para agregar contexto es muy similar a tener make() frente a new() frente a := frente a var . Estas características son útiles en diferentes escenarios, pero ¿no sería bueno si tuviéramos menos formas o incluso una sola forma de inicializar las variables? Por supuesto, nadie está obligando a nadie a usar try y las personas pueden continuar usando if err != nil, pero creo que esto dividirá el manejo de errores en Go al igual que las múltiples formas de asignar nuevas variables. Creo que cualquier método que se agregue al lenguaje también debería proporcionar una forma de agregar/eliminar fácilmente controladores de errores en lugar de obligar a las personas a reescribir líneas completas para agregar/eliminar controladores. Eso no se siente como un buen resultado para mí.

Perdón nuevamente por el ruido, pero quería señalarlo en caso de que alguien quisiera escribir una propuesta detallada por separado para la idea try ... else .

//cc @brynbellomy

Gracias, @owais , por mencionar esto nuevamente; es un punto justo (y el problema de depuración ya se mencionó antes ). try deja la puerta abierta para extensiones, como un segundo argumento, que podría ser una función de controlador. Pero es cierto que una función try no facilita la depuración; es posible que haya que reescribir el código un poco más que try - catch o try - else .

@owais

Agregar una declaración diferida puede ayudar, pero aún no es la mejor experiencia cuando una función arroja múltiples errores, ya que se activaría para cada llamada try().

Siempre puede incluir un cambio de tipo en la función diferida que manejaría (o no) diferentes tipos de error de manera adecuada antes de regresar.

Dada la discusión hasta el momento, específicamente las respuestas del equipo de Go, tengo la fuerte impresión de que el equipo planea seguir adelante con la propuesta que está sobre la mesa. En caso afirmativo, entonces un comentario y una solicitud:

  1. La propuesta tal como está, en mi opinión, dará como resultado una reducción no insignificante en la calidad del código en los repositorios disponibles públicamente. Mi expectativa es que muchos desarrolladores tomen el camino de la menor resistencia, usen de manera efectiva técnicas de manejo de excepciones y elijan usar try() en lugar de manejar los errores en el punto en que ocurren. Pero dado el sentimiento predominante en este hilo, me doy cuenta de que cualquier fanfarronería ahora sería simplemente pelear una batalla perdida, así que solo estoy registrando mi objeción para la posteridad.

  2. Suponiendo que el equipo avance con la propuesta tal como está escrita actualmente, ¿puede agregar un modificador del compilador que deshabilite try() para aquellos que no desean ningún código que ignore los errores de esta manera y que no permita que los programadores contraten? de usarlo? _(a través de CI, por supuesto)._ Gracias de antemano por esta consideración.

¿Puede agregar un interruptor de compilador que deshabilite try ()?

Esto tendría que estar en una herramienta de pelusa, no en el compilador IMO, pero estoy de acuerdo

Esto tendría que estar en una herramienta de pelusa, no en el compilador IMO, pero estoy de acuerdo

Estoy solicitando explícitamente una opción de compilador y no una herramienta de pelusa para no permitir la compilación como una opción. De lo contrario, será demasiado fácil _"olvidar"_ pelusa durante el desarrollo local.

@mikeschinkel ¿No sería tan fácil olvidarse de activar la opción del compilador en esa situación?

Las banderas del compilador no deberían cambiar la especificación del lenguaje. Esto es mucho más apto para veterinario/pelusa

¿No sería igual de fácil olvidarse de activar la opción del compilador en esa situación?

No cuando se usan herramientas como GoLand, donde no hay forma de forzar la ejecución de un lint antes de una compilación.

Las banderas del compilador no deberían cambiar la especificación del lenguaje.

-nolocalimports cambia la especificación y -s advierte.

Las banderas del compilador no deberían cambiar la especificación del lenguaje.

-nolocalimports cambia la especificación y -s advierte.

No, no cambia las especificaciones. No solo la gramática del idioma sigue siendo la misma, sino que la especificación establece específicamente:

La interpretación de ImportPath depende de la implementación, pero normalmente es una subcadena del nombre de archivo completo del paquete compilado y puede ser relativa a un depósito de paquetes instalados.

No cuando se usan herramientas como GoLand, donde no hay forma de forzar la ejecución de un lint antes de una compilación.

https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint

@deanveloper

https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint

Ciertamente eso existe, pero estás comparando manzanas con naranjas. Lo que está mostrando es un observador de archivos que se ejecuta en el cambio de archivos y, dado que GoLand guarda automáticamente los archivos, significa que se ejecuta constantemente, lo que genera mucho más ruido que señal.

La pelusa no siempre y no puede (AFAIK) configurarse como una condición previa para ejecutar el compilador:

image

No, no cambia las especificaciones. No solo la gramática del idioma sigue siendo la misma, sino que la especificación establece específicamente:

Estás jugando con la semántica aquí en lugar de centrarte en el resultado. Así que haré lo mismo.

Solicito que se agregue una opción de compilador que no permita compilar código con try() . Esa no es una solicitud para cambiar la especificación del idioma, es solo una solicitud para que el compilador se detenga en este caso especial.

Y si ayuda, la especificación de idioma se puede actualizar para decir algo como:

La interpretación de try() depende de la implementación, pero por lo general desencadena una devolución cuando el último parámetro es un error; sin embargo, se puede implementar para que no se permita.

El momento de solicitar un cambio de compilador o una verificación veterinaria es después de que el prototipo try() llegue a la punta 1.14(?). En ese momento, presentaría un nuevo problema (y sí, creo que es una buena idea). Se nos ha pedido que restrinjamos los comentarios aquí a información objetiva sobre el documento de diseño actual.

Hola, solo para agregar a todo el problema al agregar declaraciones de depuración y demás durante el desarrollo.
Creo que la idea del segundo parámetro está bien para la función try() , pero otra idea para descartarla es agregar una cláusula emit para que sea una segunda parte de try() .

Por ejemplo, creo que al desarrollar y tal podría haber un caso en el que quiero llamar a fmt para este instante para imprimir el error. Así que podría ir de esto:

func writeStuff(filename string) (io.ReadCloser, error) {
    f := try(os.Open(filename))
    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Se puede reescribir en algo como esto para declaraciones de depuración o manejo general o el error antes de regresar.

func writeStuff(filename string) (io.ReadCloser, error) {
    emit err {
        fmt.Printf("something happened [%v]\n", err.Error())
        return nil, err
    }

    f := try(os.Open(filename))
    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Así que aquí terminé poniendo una propuesta para una nueva palabra clave emit que podría ser una declaración o una sola línea para retorno inmediato como la funcionalidad inicial try() :

emit return nil, err

Lo que sería la emisión es esencialmente solo una cláusula en la que puede poner cualquier lógica que desee si el try() se activa por un error que no es igual a cero. Otra habilidad con la palabra clave emit es que puede acceder al error allí mismo si agrega justo después de la palabra clave un nombre de variable como lo hice en el primer ejemplo usándolo.

Esta propuesta crea un poco de verbosidad en la función try() , pero creo que al menos es un poco más claro sobre lo que sucede con el error. De esta manera, también puede decorar los errores sin tener que atascarlos en una sola línea y puede ver cómo se manejan los errores inmediatamente cuando está leyendo la función.

Esta es una respuesta a @mikeschinkel , estoy poniendo mi respuesta en un bloque de detalles para no desordenar demasiado la discusión. De cualquier manera, @networkimprov tiene razón en que esta discusión debe posponerse hasta después de que se implemente esta propuesta (si es que se implementa).

detalles sobre una bandera para deshabilitar probar
@mikeschinkel

La pelusa no siempre y no puede (AFAIK) configurarse como una condición previa para ejecutar el compilador:

Reinstalé GoLand solo para probar esto. Esto parece funcionar bien, la única diferencia es que si la pelusa encuentra algo que no le gusta, no falla la compilación. Sin embargo, eso podría solucionarse fácilmente con un script personalizado, que ejecuta golint y falla con un código de salida distinto de cero si hay algún resultado.
image

(Editar: solucioné el error que intentaba decirme en la parte inferior. Funcionaba bien incluso cuando el error estaba presente, pero cambiar "Ejecutar tipo" al directorio eliminó el error y funcionó bien)

También otra razón por la que NO debería ser un indicador del compilador: todo el código Go se compila desde la fuente. Eso incluye bibliotecas. Eso significa que si desea desactivar try a través del compilador, también desactivará try para cada una de las bibliotecas que está utilizando. Es una mala idea que sea un indicador del compilador.

Estás jugando con la semántica aquí en lugar de centrarte en el resultado.

No, no lo soy. Las banderas del compilador no deberían cambiar la especificación del lenguaje. La especificación está muy bien diseñada y para que algo sea "Go", debe seguir la especificación. Los indicadores del compilador que ha mencionado cambian el comportamiento del lenguaje, pero pase lo que pase, se aseguran de que el lenguaje siga las especificaciones. Este es un aspecto importante de Go. Mientras siga las especificaciones de Go, su código debe compilarse en cualquier compilador de Go.

Solicito que se agregue una opción de compilador que no permita compilar código con try(). Esa no es una solicitud para cambiar la especificación del idioma, es solo una solicitud para que el compilador se detenga en este caso especial.

Es una solicitud para cambiar la especificación. Esta propuesta en sí misma es una solicitud para cambiar la especificación. Las funciones incorporadas se incluyen muy específicamente en la especificación. . Solicitar tener un indicador de compilador que elimine el try incorporado sería, por lo tanto, un indicador de compilador que cambiaría la especificación del lenguaje que se está compilando.

Dicho esto, creo que ImportPath debería estar estandarizado en la especificación. Puedo hacer una propuesta para esto.

Y si ayuda, la especificación de idioma se puede actualizar para decir algo como [...]

Si bien esto es cierto, no querrá que la implementación de try dependa de la implementación. Está diseñado para ser una parte importante del manejo de errores del lenguaje, que es algo que debería ser igual en todos los compiladores de Go.

@deanveloper

_"De cualquier manera, @networkimprov tiene razón en que esta discusión debe posponerse hasta después de que se implemente esta propuesta (si se implementa)"._

Entonces, ¿por qué decidiste ignorar esa sugerencia y publicar en este hilo de todos modos en lugar de esperar más tarde? Argumentaste tus puntos aquí y al mismo tiempo afirmaste que no debería cuestionar tus puntos. Practique lo que predica...

Dada su elección, elegiré responder también, también en un bloque de detalles

aquí:

_"Sin embargo, eso podría solucionarse fácilmente con un script personalizado, que ejecuta Golint y falla con un código de salida distinto de cero si hay algún resultado"._

Sí, con suficiente codificación _cualquier_ problema puede solucionarse. Pero ambos sabemos por experiencia que cuanto más compleja es una solución, menos personas que quieran usarla terminarán usándola.

Así que estaba pidiendo explícitamente una solución simple aquí, no una solución de rollo-tu-propia.

_"También estarías desactivando Try para cada una de las bibliotecas que estás usando"._

Y esa es _explícitamente_ la razón por la que lo pedí. Porque quiero asegurarme de que todo el código que use esta problemática _"característica"_ no llegue a los ejecutables que distribuimos.

_"Es una solicitud para cambiar la especificación. Esta propuesta en sí misma es una solicitud para cambiar la especificación._"

ABSOLUTAMENTE no es un cambio en las especificaciones. Es una solicitud de cambio para cambiar el _comportamiento_ del comando build , no un cambio en la especificación del idioma.

Si alguien solicita que el comando go tenga un interruptor para mostrar la salida de su terminal en mandarín, eso no es un cambio en la especificación del idioma.

Del mismo modo, si go build viera este cambio, simplemente emitiría un mensaje de error y se detendría cuando se encontrara con un try() . No se necesitan cambios en las especificaciones de idioma.

_"Está hecho para ser una parte importante del manejo de errores del lenguaje, que es algo que debería ser igual en todos los compiladores de Go"._

Será una parte problemática del manejo de errores del lenguaje y hacerlo opcional permitirá que aquellos que quieran evitar sus problemas puedan hacerlo.

Sin el interruptor, es probable que la mayoría de las personas simplemente lo vean como una nueva característica y lo acepten y nunca se pregunten si de hecho debería usarse.

_Con el cambio_, y los artículos que explican la nueva función que menciona el cambio, muchas personas entenderán que tiene un potencial problemático y, por lo tanto, permitirán que el equipo de Go estudie si fue una buena inclusión o no al ver cuánto código público evita usarlo. vs. cómo lo usa el código público. Eso podría informar el diseño de Go 3.

_"No, no lo soy. Los indicadores del compilador no deberían cambiar las especificaciones del lenguaje"._

Decir que no estás jugando con la semántica no significa que no estés jugando con la semántica.

Multa. Luego, en su lugar, solicito un nuevo comando de nivel superior llamado _(algo así como)_ build-guard que se usa para deshabilitar características problemáticas durante la compilación, comenzando con desautorizar try() .

Por supuesto, el mejor resultado es si la función try() se presenta con un plan para reconsiderar la solución del problema de una manera diferente en el futuro, una forma con la que la gran mayoría está de acuerdo. Pero me temo que el barco ya ha navegado en try() así que espero minimizar su inconveniente.


Entonces, si realmente está de acuerdo con @networkimprov , espere su respuesta hasta más tarde, como sugirieron.

Siento interrumpir, pero tengo hechos que informar :-)

Estoy seguro de que el equipo de Go ya ha evaluado el aplazamiento, pero no he visto ningún número...

$ go test -bench=.
goos: linux
goarch: amd64
BenchmarkAlways2-2      20000000                72.3 ns/op
BenchmarkAlways4-2      20000000                68.1 ns/op
BenchmarkAlways6-2      20000000                68.0 ns/op

BenchmarkNever2-2       100000000               16.5 ns/op
BenchmarkNever4-2       100000000               13.1 ns/op
BenchmarkNever6-2       100000000               13.5 ns/op

Fuente

package deferbench

import (
   "fmt"
   "errors"
   "testing"
)

func Always(iM, iN int) (err error) {
   defer func() {
      if err != nil {
         err = fmt.Errorf("d: %v", err)
      }
   }()
   if iN % iM == 0 {
      return errors.New("e")
   }
   return nil
}

func Never(iM, iN int) (err error) {
   if iN % iM == 0 {
      return fmt.Errorf("r: %v", errors.New("e"))
   }
   return nil
}

func BenchmarkAlways2(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e2, a) }}
func BenchmarkAlways4(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e4, a) }}
func BenchmarkAlways6(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e6, a) }}

func BenchmarkNever2(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e2, a) }}
func BenchmarkNever4(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e4, a) }}
func BenchmarkNever6(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e6, a) }}

@redimprov

De https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md#efficiency -of-defer (mi énfasis en negrita)

Independientemente, el tiempo de ejecución y el equipo del compilador de Go han estado discutiendo opciones de implementación alternativas y creemos que podemos hacer usos típicos de aplazamiento para el manejo de errores tan eficientes como el código "manual" existente. Esperamos que esta implementación de aplazamiento más rápida esté disponible en Go 1.14 (consulte también * CL 171758 *, que es un primer paso en esta dirección).

es decir, aplazar ahora es una mejora del rendimiento del 30% para go1.13 para uso común, y debería ser más rápido y tan eficiente como el modo sin aplazamiento en go 1.14

¿Tal vez alguien pueda publicar números para 1.13 y 1.14 CL?

Las optimizaciones no siempre sobreviven al contacto con el enemigo... eh, el ecosistema.

1.13 los aplazamientos serán un 30 % más rápidos:

name     old time/op  new time/op  delta
Defer-4  52.2ns ± 5%  36.2ns ± 3%  -30.70%  (p=0.000 n=10+10)

Esto es lo que obtengo en las pruebas anteriores de @networkimprov (1.12.5 a la sugerencia):

name       old time/op  new time/op  delta
Always2-4  59.8ns ± 1%  47.5ns ± 1%  -20.57%  (p=0.008 n=5+5)
Always4-4  57.9ns ± 2%  43.5ns ± 1%  -24.96%  (p=0.008 n=5+5)
Always6-4  57.6ns ± 2%  44.1ns ± 1%  -23.43%  (p=0.008 n=5+5)
Never2-4   13.7ns ± 8%   3.8ns ± 4%  -72.27%  (p=0.008 n=5+5)
Never4-4   10.5ns ± 6%   1.3ns ± 2%  -87.76%  (p=0.008 n=5+5)
Never6-4   10.8ns ± 6%   1.2ns ± 1%  -88.46%  (p=0.008 n=5+5)

(No estoy seguro de por qué los Nunca son mucho más rápidos. ¿Quizás cambios en la línea?)

Las optimizaciones para aplazamientos para 1.14 aún no están implementadas, por lo que no sabemos cuál será el rendimiento. Pero creemos que deberíamos acercarnos al rendimiento de una llamada de función regular.

Entonces, ¿por qué decidiste ignorar esa sugerencia y publicar en este hilo de todos modos en lugar de esperar más tarde?

El bloque de detalles se editó más tarde, después de haber leído el comentario de @networkimprov . Lo siento por hacer parecer que había entendido lo que dijo y lo ignoré. Estoy terminando la discusión después de esta declaración, quería explicarme ya que me preguntaron por qué publiqué el comentario.


Con respecto a las optimizaciones para diferir, estoy emocionado por ellas. Ayudan un poco a esta propuesta, haciendo que defer HandleErrorf(...) sea un poco menos pesado. Sin embargo, todavía no me gusta la idea de abusar de los parámetros con nombre para que este truco funcione. ¿Cuánto se espera que acelere para 1.14? ¿Deberían correr a velocidades similares?

@griesemer Un área que podría valer la pena expandir un poco más es cómo funcionan las transiciones en un mundo con try , tal vez incluyendo:

  • El costo de la transición entre estilos de decoración de errores.
  • Las clases de posibles errores que pueden resultar al realizar la transición entre estilos.
  • ¿Qué clases de errores serían (a) detectados inmediatamente por un error del compilador, en comparación con (b) detectados por vet o staticcheck o similar, en comparación con (c) podrían generar un error que Es posible que no se note o que sea necesario detectarlo mediante pruebas.
  • El grado en que las herramientas pueden mitigar el costo y la posibilidad de error al realizar la transición entre estilos y, en particular, si gopls (u otra utilidad) podría o debería desempeñar un papel en la automatización de las transiciones de estilos de decoración comunes.

Etapas de la decoración de errores.

Esto no es exhaustivo, pero un conjunto representativo de etapas podría ser algo como:

0. Sin decoración de errores (p. ej., usar try sin ninguna decoración).
1. Decoración de error uniforme (p. ej., usando try + defer para decoración uniforme).
2. Los puntos de salida N-1 tienen una decoración de error uniforme , pero 1 punto de salida tiene una decoración diferente (por ejemplo, quizás una decoración de error detallada permanente en una sola ubicación, o quizás un registro de depuración temporal, etc.).
3. Todos los puntos de salida tienen una decoración de error única , o algo parecido a único.

Cualquier función dada no tendrá una progresión estricta a través de esas etapas, por lo que tal vez "etapas" sea la palabra incorrecta, pero algunas funciones pasarán de un estilo de decoración a otro, y podría ser útil ser más explícito sobre cuáles son esas transiciones. son como cuando o si suceden.

Las etapas 0 y 1 parecen ser puntos óptimos para la propuesta actual y también son casos de uso bastante comunes. Una transición de etapa 0->1 parece sencilla. Si estaba usando try sin ninguna decoración en la etapa 0, puede agregar algo como defer fmt.HandleErrorf(&err, "foo failed with %s", arg1) . Es posible que en ese momento también deba introducir parámetros de retorno con nombre en la propuesta tal como se escribió inicialmente. Sin embargo, si la propuesta adopta una de las sugerencias a lo largo de las líneas de una variable incorporada predefinida que es un alias para el parámetro de resultado de error final, ¿entonces el costo y el riesgo de error aquí podrían ser pequeños?

Por otro lado, una transición de la etapa 1->2 parece incómoda (o "molesta", como han dicho otros) si la etapa 1 fue una decoración de error uniforme con defer . Para agregar un poco de decoración específica en un punto de salida, primero debe quitar el defer (para evitar la doble decoración), luego parece que se necesitaría visitar todos los puntos de retorno para quitarle el azúcar a los try se usa en declaraciones if , con N-1 de los errores decorados de la misma manera y 1 decorado de forma diferente.

Una transición de etapa 1->3 también parece incómoda si se hace manualmente.

Errores al cambiar de estilo de decoración

Algunos errores que pueden ocurrir como parte de un proceso de eliminación de azúcar manual incluyen sombrear accidentalmente una variable o cambiar la forma en que se ve afectado un parámetro de retorno con nombre, etc. Por ejemplo, si observa el primer y más grande ejemplo en la sección "Ejemplos" de la pruebe la propuesta, la función CopyFile tiene 4 usos de try , incluidos en esta sección:

        w := try(os.Create(dst))
        defer func() {
                w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

Si alguien hizo una eliminación de azúcar manual "obvia" de w := try(os.Create(dst)) , esa línea podría expandirse a:

        w, err := os.Create(dst)
        if err != nil {
            // do something here
            return err
        }

Eso se ve bien a primera vista, pero dependiendo del bloque en el que se encuentre el cambio, eso también podría ensombrecer accidentalmente el parámetro de retorno nombrado err y romper el manejo de errores en el subsiguiente defer .

Automatización de la transición entre estilos de decoración

Para ayudar con el costo de tiempo y el riesgo de errores, tal vez gopls (u otra utilidad) podría tener algún tipo de comando para eliminar el azúcar de un try específico, o un comando para eliminar el azúcar de todos los usos de try en una función dada que podría estar libre de errores el 100% del tiempo. Un enfoque podría ser cualquier comando gopls que solo se centre en eliminar y reemplazar try , pero tal vez un comando diferente podría eliminar todos los usos de try y al mismo tiempo transformar al menos casos comunes de cosas como defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) en la parte superior de la función en el código equivalente en cada una de las ubicaciones anteriores try (lo que ayudaría al pasar de la etapa 1->2 o la etapa 1->3). Esa no es una idea completamente cocida, pero tal vez valga la pena pensar más en lo que es posible o deseable o actualizar la propuesta con el pensamiento actual.

¿Resultados idiomáticos?

Un comentario relacionado es que no es inmediatamente obvio la frecuencia con la que una transformación programática sin errores de un try terminaría pareciendo un código Go idiomático normal. Adaptando uno de los ejemplos de la propuesta, si por ejemplo quisieras desazúcar:

x1, x2, x3 = try(f())

En algunos casos, una transformación programática que preserva el comportamiento podría terminar con algo como:

t1, t2, t3, te := f()  // visible temporaries
if te != nil {
        return x1, x2, x3, te
}
x1, x2, x3 = t1, t2, t3

Esa forma exacta puede ser rara, y parece que los resultados de un editor o IDE que realiza la eliminación de azúcar programática a menudo pueden terminar pareciendo más idiomáticos, pero sería interesante escuchar qué tan cierto es eso, incluso frente a los parámetros de retorno con nombre que posiblemente se conviertan en más común, y teniendo en cuenta el shadowing, := vs = , otros usos de err en la misma función, etc.

La propuesta habla sobre las posibles diferencias de comportamiento entre if y try debido a los parámetros de resultado nombrados, pero en esa sección en particular parece estar hablando principalmente sobre la transición de if a try (en la sección que concluye _"Si bien esta es una diferencia sutil, creemos que casos como estos son raros. Si se espera el comportamiento actual, mantenga la instrucción if"._). Por el contrario, puede haber diferentes errores posibles que valga la pena analizar al pasar de try a if mientras se conserva un comportamiento idéntico.


En cualquier caso, disculpe la extensión del comentario, pero parece que el temor a los altos costos de transición entre estilos subyace en algunas de las preocupaciones expresadas en algunos de los otros comentarios publicados aquí y, por lo tanto, la sugerencia de ser más explícito sobre esos costos de transición y mitigaciones potenciales.

@thepudds I love you están destacando los costos y los errores potenciales asociados con cómo las características del idioma pueden afectar positiva o negativamente la refactorización. No es un tema que veo discutido a menudo, pero uno que puede tener un gran efecto posterior.

una transición de etapa 1 -> 2 parece incómoda si la etapa 1 fue una decoración de error uniforme con un aplazamiento. Para agregar un poco específico de decoración en un punto de salida, primero necesitaría eliminar el aplazamiento (para evitar la doble decoración), luego parece que uno necesitaría visitar todos los puntos de retorno para eliminar el azúcar que usa el intento en las declaraciones if, con N -1 de los errores se decoró de la misma manera y 1 se decoró de manera diferente.

Aquí es donde el uso break en lugar de return brilla con 1.12. Úselo en un bloque for range once { ... } donde once = "1" para demarcar la secuencia de código de la que podría querer salir y luego, si necesita decorar solo un error, hágalo en el punto de break . Y si necesitas decorar todos los errores lo haces justo antes del único return al final del método.

La razón por la que es un patrón tan bueno es que es resistente a los requisitos cambiantes; rara vez tiene que romper el código de trabajo para implementar nuevos requisitos. Y en mi opinión, es un enfoque más limpio y más obvio que volver al principio del método antes de salir de él.

por favor

Los resultados de @ randall77 para mi punto de referencia muestran una sobrecarga de más de 40 ns por llamada para 1.12 y propina. Eso implica que diferir puede inhibir las optimizaciones, haciendo que las mejoras para diferir sean discutibles en algunos casos.

@networkimprov Defer actualmente inhibe las optimizaciones, y eso es parte de lo que nos gustaría arreglar. Por ejemplo, sería bueno alinear el cuerpo de la función diferida tal como lo hacemos con las llamadas regulares.

No veo cómo cualquier mejora que hagamos sería discutible. ¿De dónde viene esa afirmación?

¿De dónde viene esa afirmación?

La sobrecarga de más de 40 ns por llamada para una función con un aplazamiento para envolver el error no cambió.

Los cambios en 1.13 son una parte de la optimización de diferir. Hay otras mejoras previstas. Esto se trata en el documento de diseño y en la parte del documento de diseño citado en algún punto anterior.

Consulte swtch.com/try.html y https://github.com/golang/go/issues/32437#issuecomment -502192315:

@rsc , ¡muy útil! Si todavía lo está revisando, ¿quizás vincule las referencias del problema #id? ¿Y el estilo de fuente sans-serif?

Esa página es sobre el contenido. No se concentre en los detalles de representación. Estoy usando la salida de blackfriday en el descuento de entrada sin modificar (por lo que no hay enlaces #id específicos de GitHub), y estoy contento con la fuente serif.

Volver a inhabilitar/examinar el intento :

Lo siento, pero no habrá opciones de compilación para deshabilitar funciones específicas de Go, ni habrá controles veterinarios que digan que no se deben usar esas funciones. Si la función es lo suficientemente mala como para deshabilitarla o examinarla, no la pondremos. Por el contrario, si la función está ahí, está bien usarla. Hay un idioma de Go, no un idioma diferente para cada desarrollador según su elección de indicadores del compilador.

@mikeschinkel , dos veces en este tema ha descrito el uso de probar como _ignorar_ errores.
El 7 de junio usted escribió, bajo el título "Facilita a los desarrolladores ignorar los errores":

Esto es una repetición total de lo que otros han comentado, pero lo que básicamente proporciona try() es análogo en muchos sentidos a simplemente adoptar lo siguiente como código idomático, y este es un código que nunca encontrará su camino en ningún código por sí mismo. -respetando las naves de los desarrolladores:

f, _ := os.Open(filename)

Sé que puedo ser mejor en mi propio código, pero también sé que muchos de nosotros dependemos de la generosidad de otros desarrolladores de Go que publican algunos paquetes tremendamente útiles, pero por lo que he visto en _"Other People's Code(tm)"_ Las mejores prácticas en el manejo de errores a menudo se ignoran.

En serio, ¿realmente queremos que sea más fácil para los desarrolladores ignorar los errores y permitirles contaminar GitHub con paquetes no robustos?

Y luego, el 14 de junio , nuevamente se refirió al uso de try como "código que ignora los errores de esta manera".

Si no fuera por el fragmento de código f, _ := os.Open(filename) , pensaría que simplemente estaba exagerando al caracterizar "verificar un error y devolverlo" como "ignorar" un error. Pero el fragmento de código, junto con las muchas preguntas ya respondidas en el documento de propuesta o en la especificación del lenguaje, me hacen preguntarme si estamos hablando de la misma semántica después de todo. Así que para ser claro y responder a sus preguntas:

Al estudiar el código de la propuesta, encuentro que el comportamiento no es obvio y es algo difícil de razonar.

Cuando veo try() envolviendo una expresión, ¿qué sucederá si se devuelve un error?

Cuando vea try(f()) , si f() devuelve un error, try detendrá la ejecución del código y devolverá ese error desde la función en cuyo cuerpo se encuentra el try Aparece

¿Se ignorará el error?

No. El error nunca se ignora. Se devuelve, lo mismo que usar una declaración de devolución. Me gusta:

{ err := f(); if err != nil { return err } }

¿O saltará al primero o al más reciente defer ,

La semántica es la misma que usar una declaración de devolución.

Las funciones diferidas se ejecutan " en el orden inverso al que fueron diferidas ".

y si es así, establecerá automáticamente una variable llamada err dentro del cierre, o la pasará como un parámetro _(¿No veo un parámetro?)_.

La semántica es la misma que usar una declaración de devolución.

Si necesita hacer referencia a un parámetro de resultado en el cuerpo de una función diferida, puede darle un nombre. Consulte el ejemplo de result en https://golang.org/ref/spec#Defer_statements.

Y si no es un nombre de error automático, ¿cómo lo nombro? ¿Y eso significa que no puedo declarar mi propia variable err en mi función para evitar conflictos?

La semántica es la misma que usar una declaración de devolución.

Una declaración de retorno siempre asigna a los resultados de la función real, incluso si el resultado no tiene nombre, e incluso si el resultado tiene nombre pero está sombreado.

¿Y llamará a todos los defer s? ¿En orden inverso o en orden regular?

La semántica es la misma que usar una declaración de devolución.

Las funciones diferidas se ejecutan " en el orden inverso al que fueron diferidas ". (El orden inverso es el orden regular.)

¿O regresará tanto del cierre como del func donde se devolvió el error? _(Algo que nunca hubiera considerado si no hubiera leído aquí palabras que implican eso.)_

No sé qué significa esto, pero probablemente la respuesta sea no. Recomendaría centrarse en el texto de la propuesta y la especificación y no en otros comentarios aquí sobre lo que ese texto podría o no significar.

Después de leer la propuesta y todos los comentarios hasta el momento, sinceramente, todavía no sé las respuestas a las preguntas anteriores. ¿Es ese el tipo de característica que queremos agregar a un lenguaje cuyos defensores defienden ser _"Capitán Obvio?"_

En general, buscamos un lenguaje simple y fácil de entender. Siento que tuvieras tantas preguntas. Pero esta propuesta realmente está reutilizando la mayor cantidad posible del lenguaje existente (en particular, difiere), por lo que debería haber muy pocos detalles adicionales para aprender. Una vez que sepas que

x, y := try(f())

medio

tmp1, tmp2, tmpE := f()
if tmpE != nil {
   return ..., tmpE
}
x, y := tmp1, tmp2

casi todo lo demás debería derivarse de las implicaciones de esa definición.

Esto no es "ignorar" los errores. Ignorar un error es cuando escribes:

c, _ := net.Dial("tcp", "127.0.0.1:1234")
io.Copy(os.Stdout, c)

y el código entra en pánico porque net.Dial falló y se ignoró el error, c es nulo y la llamada de io.Copy a c.Read falla. Por el contrario, este código comprueba y devuelve el error:

 c := try(net.Dial("tcp", "127.0.0.1:1234"))
 io.Copy(os.Stdout, c)

Para responder a su pregunta sobre si queremos fomentar lo segundo sobre lo primero: sí.

@damienfamed75 Su propuesta emit se ve esencialmente igual a la declaración de handle del borrador del diseño . La razón principal para abandonar la declaración handle fue su superposición con defer . No me queda claro por qué no se puede usar un defer para obtener el mismo efecto que logra emit .

@dominikh preguntó :

¿Acme comenzará a resaltar el intento?

Gran parte de la propuesta de prueba está indecisa, en el aire, desconocida.

Pero a esta pregunta puedo responder definitivamente: no.

@rsc

Gracias por su respuesta.

_"Dos veces en este tema ha descrito el uso de probar como ignorar errores"._

Sí, estaba comentando usando mi perspectiva y no siendo técnicamente correcto.

Lo que quise decir fue _"Permitir que los errores se transmitan sin ser decorados"._ Para mí, eso es _"ignorar"_, muy parecido a cómo las personas que usan el manejo de excepciones _"ignoran"_ errores, pero ciertamente puedo ver cómo otros lo harían. Considero que mi redacción no es técnicamente correcta.

_"Cuando vea try(f()) , si f() devuelve un error, el intento detendrá la ejecución del código y devolverá ese error desde la función en cuyo cuerpo aparece el intento."_

Esa fue una respuesta a una pregunta de mi comentario hace un tiempo, pero ahora lo he descubierto.

Y termina haciendo dos cosas que me entristecen. Razones:

  1. Hará el camino de menor resistencia para evitar errores de decoración, lo que alentará a muchos desarrolladores a hacer precisamente eso, y muchos publicarán ese código para que otros lo usen, lo que dará como resultado un código disponible públicamente de menor calidad con un manejo/informe de errores menos sólido. .

  2. Para aquellos como yo que usan break y continue para el manejo de errores en lugar de return , un patrón que es más resistente a los requisitos cambiantes, ni siquiera podremos usar try() , incluso cuando realmente no hay razón para anotar el error.

_"¿O regresará tanto del cierre como de la función donde se devolvió el error? (Algo que nunca hubiera considerado si no hubiera leído aquí palabras que implican eso.)"_

_"No sé qué significa esto, pero probablemente la respuesta sea no. Recomendaría centrarse en el texto de la propuesta y la especificación y no en otros comentarios aquí sobre lo que ese texto podría o no significar"._

Nuevamente, esa pregunta fue hace más de una semana, así que ahora entiendo mejor.

Para aclarar, para la posteridad, el defer tiene cierre, ¿no? Si regresa de ese cierre, entonces, a menos que lo malinterprete, no solo regresará del cierre, sino que también regresará del func donde ocurrió el error, ¿verdad? _(No es necesario responder si es así.)_

func example() {
    defer func(err) {
       return err // returns from both defer and example()
    }
    try(SomethingThatReturnsAnError)    
} 

Por cierto, tengo entendido que la razón de try() es porque los desarrolladores se han quejado de la repetición. También me parece triste porque creo que el requisito de aceptar errores devueltos que resulta en este modelo es lo que ayuda a que las aplicaciones de Go sean más sólidas que en muchos otros idiomas.

Personalmente, preferiría verlos hacer que sea más difícil no decorar los errores que hacer que sea más fácil ignorarlos. Pero reconozco que parezco estar en minoría en esto.


Por cierto, algunas personas han propuesto una sintaxis como una de las siguientes _(he agregado .Extend() hipotéticos para que mis ejemplos sean concisos):_

f := try os.Open(filename) else err {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err   
}

O

try f := os.Open(filename) else err {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err    
}

Y luego otros afirman que realmente no guarda ningún carácter sobre esto:

f,err := os.Open(filename)
if err != nil {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err    
}

Pero una cosa que le falta a la crítica es que pasa de 5 líneas a 4 líneas, una reducción del espacio vertical y eso parece significativo, especialmente cuando se necesitan muchas construcciones de este tipo en un func .

Aún mejor sería algo como esto, que eliminaría el 40% del espacio vertical _ (aunque dados los comentarios sobre las palabras clave, dudo que esto se considere):_

try f := os.Open(filename) 
    else err().Extend("Cannot open file %s",filename)
    end //break, continue or return err    

#fwiw


DE TODOS MODOS , como dije antes, supongo que el barco ha zarpado, así que aprenderé a aceptarlo.

Objetivos

Algunos comentarios aquí han cuestionado qué es lo que estamos tratando de hacer con la propuesta. Como recordatorio, la Declaración del problema de manejo de errores que publicamos en agosto pasado dice en la sección "Objetivos" :

“Para Go 2, nos gustaría hacer que las verificaciones de errores sean más livianas, reduciendo la cantidad de texto del programa Go dedicado a la verificación de errores. También queremos que sea más conveniente escribir el manejo de errores, aumentando la probabilidad de que los programadores se tomen el tiempo para hacerlo.

Tanto las verificaciones de errores como el manejo de errores deben permanecer explícitos, es decir, visibles en el texto del programa. No queremos repetir las trampas del manejo de excepciones.

El código existente debe seguir funcionando y seguir siendo tan válido como lo es hoy. Cualquier cambio debe interoperar con el código existente”.

Para obtener más información sobre "los peligros del manejo de excepciones", consulte la discusión en la sección más larga "Problema" . En particular, las comprobaciones de errores deben adjuntarse claramente a lo que se está comprobando.

@mikeschinkel ,

Para aclarar, para la posteridad, el defer tiene cierre, ¿no? Si regresa de ese cierre, entonces, a menos que lo malinterprete, no solo regresará del cierre, sino que también regresará del func donde ocurrió el error, ¿verdad? _(No es necesario responder si es así.)_

No. No se trata de manejo de errores sino de funciones diferidas. No siempre son cierres. Por ejemplo, un patrón común es:

func (d *Data) Op() int {
    d.mu.Lock()
    defer d.mu.Unlock()

     ... code to implement Op ...
}

Cualquier devolución de d.Op ejecuta la llamada de desbloqueo diferido después de la declaración de devolución pero antes de que el código se transfiera a la persona que llama de d.Op. Nada que se haga dentro de d.mu.Unlock afecta el valor de retorno de d.Op. Una declaración de retorno en d.mu.Unlock regresa del Desbloqueo. No regresa por sí mismo de d.Op. Por supuesto, una vez que d.mu.Unlock regresa, también lo hace d.Op, pero no directamente debido a d.mu.Unlock. Es un punto sutil pero importante.

Llegando a tu ejemplo:

func example() {
    defer func(err) {
       return err // returns from both defer and example()
    }
    try(SomethingThatReturnsAnError)    
} 

Al menos como está escrito, este es un programa inválido. No estoy tratando de ser pedante aquí, los detalles importan. Aquí hay un programa válido:

func example() (err error) {
    defer func() {
        if err != nil {
            println("FAILED:", err.Error())
        }
    }()

    try(funcReturningError())
    return nil
}

Cualquier resultado de una llamada de función diferida se descarta cuando se ejecuta la llamada, por lo que en el caso de que lo diferido sea una llamada a un cierre, no tiene ningún sentido escribir el cierre para devolver un valor. Entonces, si tuviera que escribir return err dentro del cuerpo del cierre, el compilador le dirá "demasiados argumentos para devolver" .

Entonces, no, escribir return err no regresa tanto de la función diferida como de la función externa en ningún sentido real, y en el uso convencional ni siquiera es posible escribir código que parezca hacer eso.

Muchas de las contrapropuestas publicadas en este problema sugieren otras construcciones de manejo de errores más capaces que duplican las construcciones de lenguaje existentes, como la instrucción if. (O entran en conflicto con el objetivo de "hacer que las verificaciones de errores sean más livianas, reduciendo la cantidad de texto del programa Go para la verificación de errores". O ambos).

En general, Go ya tiene una construcción de manejo de errores perfectamente capaz: todo el lenguaje, especialmente las declaraciones if. @DavexPro tenía razón al volver a consultar la entrada del blog de Go Los errores son valores . No necesitamos diseñar un sublenguaje completamente separado que se preocupe por los errores, ni deberíamos hacerlo. Creo que la idea principal durante el último medio año más o menos ha sido eliminar "manejar" de la propuesta de "verificar/manejar" a favor de reutilizar el lenguaje que ya tenemos, incluido el regreso a las declaraciones if cuando sea apropiado. Esta observación sobre hacer lo menos posible elimina de la consideración la mayoría de las ideas sobre la parametrización adicional de una nueva construcción.

Gracias a @brynbellomy por sus muchos buenos comentarios, usaré su prueba de lo contrario como un ejemplo ilustrativo. Sí, podríamos escribir:

func doSomething() (int, error) {
    // Inline error handler
    a, b := try SomeFunc() else err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    handler logAndContinue err {
        log.Errorf("non-critical error: %v", err)
    }
    handler annotateAndReturn err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d := try SomeFunc() else logAndContinue
    e, f := try OtherFunc() else annotateAndReturn

    // ...

    return 123, nil
}

pero considerando todas las cosas, esto probablemente no sea una mejora significativa sobre el uso de construcciones de lenguaje existentes:

func doSomething() (int, error) {
    a, b, err := SomeFunc()
    if err != nil {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    logAndContinue := func(err error) {
        log.Errorf("non-critical error: %v", err)
    }
    annotate:= func(err error) (int, error) {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d, err := SomeFunc()
    if err != nil {
        logAndContinue(err)
    }
    e, f, err := SomeFunc()
    if err != nil {
        return annotate(err)
    }

    // ...

    return 123, nil
}

Es decir, seguir confiando en el lenguaje existente para escribir la lógica de manejo de errores parece preferible a crear una nueva declaración, ya sea try-else, try-goto, try-arrow o cualquier otra cosa.

Esta es la razón por la que try se limita a la semántica simple if err != nil { return ..., err } y nada más: acorte el patrón común pero no intente reinventar todo el flujo de control posible. Cuando una declaración if o una función de ayuda son apropiadas, esperamos que las personas continúen usándolas.

@rsc Gracias por aclarar.

Correcto, no entendí bien los detalles. Supongo que no uso defer con la frecuencia suficiente para recordar su sintaxis.

_(FWIW, encuentro que usar defer para algo más complejo que cerrar un identificador de archivo es menos obvio debido al salto hacia atrás en func antes de regresar. Así que siempre coloque ese código al final del func después de los for range once{...} mi código de manejo de errores break .)_

La sugerencia de gofmt cada llamada de prueba en varias líneas entra directamente en conflicto con el objetivo de "hacer que las comprobaciones de errores sean más ligeras, reduciendo la cantidad de texto del programa Go para la comprobación de errores".

La sugerencia de usar una declaración if de prueba de error en una sola línea también entra directamente en conflicto con este objetivo. Las comprobaciones de errores no se vuelven sustancialmente más livianas ni se reducen en cantidad al eliminar los caracteres de nueva línea interiores. En todo caso, se vuelven más difíciles de hojear.

El principal beneficio de try es tener una abreviatura clara para el caso más común, lo que hace que los inusuales se destaquen más y valgan la pena leerlos con atención.

Volviendo de gofmt a las herramientas generales, la sugerencia de centrarse en las herramientas para escribir comprobaciones de errores en lugar de un cambio de idioma es igualmente problemática. Como lo expresaron Abelson y Sussman, “los programas deben escribirse para que la gente los lea, y solo incidentalmente para que las máquinas los ejecuten”. Si se _requieren_ herramientas mecánicas para hacer frente al lenguaje, entonces el lenguaje no está haciendo su trabajo. La legibilidad no debe limitarse a las personas que utilizan herramientas específicas.

Algunas personas ejecutaron la lógica en la dirección opuesta: las personas pueden escribir expresiones complejas, por lo que inevitablemente lo harán, por lo que necesitará IDE u otra herramienta compatible para encontrar las expresiones de prueba, por lo que probar es una mala idea. Sin embargo, hay algunos saltos sin soporte aquí. La principal es la afirmación de que debido a que es _posible_ escribir código complejo e ilegible, dicho código se volverá ubicuo. Como señaló @josharian , ya es " posible escribir código abominable en Go ". Eso no es un lugar común porque los desarrolladores tienen normas sobre tratar de encontrar la forma más legible de escribir una pieza de código en particular. Por lo tanto, ciertamente _no_ se requerirá el soporte de IDE para leer programas que involucren try. Y en los pocos casos en los que la gente escribe intentos de abuso de código verdaderamente terribles, es poco probable que el soporte IDE sea de mucha utilidad. Esta objeción (la gente puede escribir un código muy malo usando la nueva función) se plantea en casi todas las discusiones sobre cada nueva función de lenguaje en todos los idiomas. No es terriblemente útil. Una objeción más útil sería de la forma "la gente escribirá código que parece bueno al principio pero resulta ser menos bueno por esta razón inesperada", como en la discusión sobre la depuración de impresiones .

Nuevamente: la legibilidad no debe limitarse a las personas que usan herramientas específicas.
(Todavía imprimo y leo programas en papel, aunque la gente a menudo me mira raro por hacerlo).

Gracias @rsc por darnos su opinión sobre cómo permitir que las declaraciones if se conviertan en una sola línea.

La sugerencia de usar una declaración if de prueba de error en una sola línea también entra directamente en conflicto con este objetivo. Las comprobaciones de errores no se vuelven sustancialmente más livianas ni se reducen en cantidad al eliminar los caracteres de nueva línea interiores. En todo caso, se vuelven más difíciles de hojear.

Estimo estas afirmaciones de manera diferente.

Considero que reducir el número de líneas de 3 a 1 es sustancialmente más ligero. ¿Gofmt no requeriría que una declaración if contenga, por ejemplo, 9 (o incluso 5) saltos de línea en lugar de 3 sería sustancialmente más pesado? Es el mismo factor (cantidad) de reducción/expansión. Yo diría que los literales de estructura tienen esta compensación exacta, y con la adición de try , permitirán controlar el flujo tanto como una instrucción if .

En segundo lugar, encuentro el argumento de que se vuelven más difíciles de hojear para aplicar igualmente bien a try , si no más. Al menos una declaración if tendría que estar en su propia línea. Pero tal vez entiendo mal lo que significa "desnatar" en este contexto. Lo estoy usando para decir "principalmente omitir pero tener en cuenta".

Dicho todo esto, la sugerencia de gofmt se basó en dar un paso aún más conservador que try y no tiene impacto en try a menos que sea suficiente. Parece que no lo es, así que si quiero discutirlo más, abriré un nuevo problema/propuesta. :+1:

Considero que reducir el número de líneas de 3 a 1 es sustancialmente más ligero.

Creo que todos están de acuerdo en que es posible que el código sea demasiado denso. Por ejemplo, si su paquete completo es una línea, creo que todos estamos de acuerdo en que es un problema. Probablemente todos no estemos de acuerdo en la línea precisa. Para mí, hemos establecido

n, err := src.Read(buf)
if err == io.EOF {
    return nil
} else if err != nil {
    return err
}

como la forma de formatear ese código, y creo que sería bastante molesto tratar de cambiar a su ejemplo

n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }

en lugar de. Si hubiéramos comenzado de esa manera, estoy seguro de que estaría bien. Pero no lo hicimos, y no es donde estamos ahora.

Personalmente, encuentro el peso más ligero anterior en la página en el sentido de que es más fácil de hojear. Puede ver el if-else de un vistazo sin leer ninguna letra real. Por el contrario, la versión más densa es difícil de distinguir a simple vista de una secuencia de tres declaraciones, lo que significa que debe mirar con más cuidado antes de que su significado se aclare.

Al final, está bien si dibujamos la línea de densidad frente a legibilidad en diferentes lugares en cuanto al número de nuevas líneas. La propuesta de prueba se centra no solo en eliminar las líneas nuevas, sino también en eliminar las construcciones por completo, y eso produce una presencia de página más liviana separada de la pregunta gofmt.

Algunas personas ejecutaron la lógica en la dirección opuesta: las personas pueden escribir expresiones complejas, por lo que inevitablemente lo harán, por lo que necesitará IDE u otra herramienta compatible para encontrar las expresiones de prueba, por lo que probar es una mala idea. Sin embargo, hay algunos saltos sin soporte aquí. La principal es la afirmación de que debido a que es _posible_ escribir código complejo e ilegible, dicho código se volverá ubicuo. Como señaló @josharian , ya es " posible escribir código abominable en Go ". Eso no es un lugar común porque los desarrolladores tienen normas sobre tratar de encontrar la forma más legible de escribir una pieza de código en particular. Por lo tanto, ciertamente _no_ se requerirá el soporte de IDE para leer programas que involucren try. Y en los pocos casos en los que la gente escribe intentos de abuso de código verdaderamente terribles, es poco probable que el soporte IDE sea de mucha utilidad. Esta objeción (la gente puede escribir un código muy malo usando la nueva función) se plantea en casi todas las discusiones sobre cada nueva función de lenguaje en todos los idiomas. No es terriblemente útil.

¿No es esta la razón por la que Go no tiene un operador ternario ?

¿No es esta la única razón por la que Go no tiene un operador ternario?

No. Podemos y debemos distinguir entre "esta función se puede usar para escribir código muy legible, pero también se puede abusar para escribir código ilegible" y "el uso dominante de esta función será escribir código ilegible".

La experiencia con C sugiere que ? : cae de lleno en la segunda categoría. (Con la posible excepción de min y max, no estoy seguro de haber visto código usando ? : eso no se mejoró al reescribirlo para usar una declaración if en su lugar. Pero este párrafo se está saliendo del tema).

Sintaxis

Esta discusión ha identificado seis sintaxis diferentes para escribir la misma semántica de la propuesta:

(¡Disculpas si me equivoqué en las historias de origen!)

Todos estos tienen pros y contras, y lo bueno es que debido a que todos tienen la misma semántica, no es demasiado importante elegir entre las diversas sintaxis para experimentar más.

Encontré este ejemplo de @brynbellomy que invita a la reflexión:

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := try(parentObjOne.AsCommit())
parentCommitTwo := try(parentObjTwo.AsCommit())
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
try parentCommitOne := parentObjOne.AsCommit()
try parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// vs

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

No hay mucha diferencia entre estos ejemplos específicos, por supuesto. Y si el intento está presente en todas las líneas, ¿por qué no alinearlas o eliminarlas? ¿No es eso más limpio? Me preguntaba sobre esto también.

Pero como observó @ianlancetaylor , “el intento entierra el plomo. El código se convierte en una serie de declaraciones de prueba, lo que oscurece lo que realmente está haciendo el código”.

Creo que ese es un punto crítico: alinear el intento de esa manera, o factorizarlo como en el bloque, implica un falso paralelismo. Implica que lo importante de estas afirmaciones es que todas lo intentan. Por lo general, eso no es lo más importante del código y no es en lo que debemos concentrarnos al leerlo.

Supongamos por el bien del argumento que AsCommit nunca falla y, en consecuencia, no devuelve un error. Ahora tenemos:

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// vs

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

Lo que ves a primera vista es que las dos líneas del medio son claramente diferentes de las otras. ¿Por qué? Resulta debido al manejo de errores. ¿Es ese el detalle más importante de este código, lo que debería notar a primera vista? Mi respuesta es no. Creo que debería notar la lógica central de lo que está haciendo el programa primero y el manejo de errores más tarde. En este ejemplo, la declaración de prueba y el bloque de prueba obstaculizan esa vista de la lógica central. Para mí, esto sugiere que no son la sintaxis correcta para esta semántica.

Eso deja las primeras cuatro sintaxis, que son aún más similares entre sí:

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

headRef := try r.Head()
parentObjOne := try headRef.Peel(git.ObjectCommit)
parentObjTwo := try remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try index.WriteTree()
tree := try r.LookupTree(treeOid)

// vs

headRef := r.Head()?
parentObjOne := headRef.Peel(git.ObjectCommit)?
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)?
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()?
tree := r.LookupTree(treeOid)?

// vs

headRef := r.Head?()
parentObjOne := headRef.Peel?(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel?(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree?()
tree := r.LookupTree?(treeOid)

Es difícil preocuparse demasiado por elegir uno sobre los demás. Todos tienen sus puntos buenos y malos. Las ventajas más importantes del formulario incorporado son que:

(1) el operando exacto es muy claro, especialmente en comparación con el operador de prefijo try x.y().z() .
(2) las herramientas que no necesitan saber sobre try pueden tratarlo como una simple llamada de función, por ejemplo, goimports funcionará bien sin ningún ajuste, y
(3) hay espacio para futuras expansiones y ajustes si es necesario.

Es muy posible que después de ver el código real usando estas construcciones, desarrollemos una mejor idea de si las ventajas de una de las otras tres sintaxis superan estas ventajas de la sintaxis de llamada de función. Solo los experimentos y la experiencia pueden decirnos esto.

Gracias por toda la aclaración. Cuanto más pienso, más me gusta la propuesta y veo cómo se ajusta a los objetivos.

¿Por qué no usar una función como recover() en lugar de err que no sabemos de dónde viene? Sería más consistente y tal vez más fácil de implementar.

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

editar: nunca uso el retorno con nombre, entonces será extraño para mí agregar el retorno con nombre solo para esto

@flibustenet , consulte también https://swtch.com/try.html#named para obtener algunas sugerencias similares.
(Respondiendo a todas: podríamos hacer eso, pero no es estrictamente necesario dados los resultados con nombre, por lo que también podríamos intentar usar el concepto existente antes de decidir que necesitamos proporcionar una segunda forma).

Una consecuencia no deseada de try() puede ser que los proyectos abandonen _go fmt_ para obtener verificaciones de errores de una sola línea. Esos son casi todos los beneficios de try() sin ninguno de los costos. Lo he hecho durante algunos años; funciona bien.

Pero prefiero poder definir un controlador de errores de último recurso para el paquete y eliminar todas las comprobaciones de errores que lo necesitan. Lo que definiría no es try() .

@networkimprov , parece que proviene de una posición diferente a la de los usuarios de Go a los que nos dirigimos, y su mensaje contribuiría más a la conversación si incluyera detalles o enlaces adicionales para que podamos entender mejor su punto de vista.

No está claro qué "costos" crees que tiene intentarlo. Y mientras dices que abandonar gofmt no tiene "ninguno de los costos" de intentar (cualesquiera que sean), pareces estar ignorando que el formato de gofmt es el que usan todos los programas que ayudan a reescribir el código fuente de Go, como goimports, por ejemplo, gorename , y así. Abandona go fmt a costa de abandonar esos ayudantes, o al menos tolerar ediciones incidentales sustanciales en su código cuando los invoca. Aun así, si funciona bien para ti hacerlo, eso es genial: sigue haciéndolo por todos los medios.

Tampoco está claro qué significa "definir un controlador de errores de último recurso para el paquete" o por qué sería apropiado aplicar una política de manejo de errores a un paquete completo en lugar de a una sola función a la vez. Si lo principal que le gustaría hacer en un controlador de errores es agregar contexto, el mismo contexto no sería apropiado en todo el paquete.

@rsc , como puede haber visto, aunque sugerí la sintaxis del bloque de prueba, luego volví al lado "no" para esta función, en parte porque me siento incómodo al ocultar uno o más errores condicionales en una declaración o aplicación de función. Pero déjame aclarar un punto. En la propuesta de bloque de prueba, permití explícitamente declaraciones que no necesitan try . Entonces, su último ejemplo de bloque de prueba sería:

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
        parentCommitOne := parentObjOne.AsCommit()
        parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

Esto simplemente dice que cualquier error devuelto dentro del bloque de prueba se devuelve a la persona que llama. Si el control supera el bloque de prueba, no hubo errores en el bloque.

Tu dijiste

Creo que debería notar la lógica central de lo que está haciendo el programa primero y el manejo de errores más tarde.

¡Esta es exactamente la razón por la que pensé en un bloque de prueba! Lo que se descarta no es solo la palabra clave, sino también el manejo de errores. No quiero tener que pensar en N lugares diferentes que pueden generar errores (excepto cuando estoy tratando explícitamente de manejar errores específicos).

Algunos puntos más que pueden valer la pena mencionar:

  1. La persona que llama no sabe de dónde proviene exactamente el error dentro de la persona a la que llama. Esto también se aplica a la propuesta simple que está considerando en general. Especulé que se puede hacer que el compilador agregue su propia anotación sobre el punto de retorno del error. Pero no he pensado mucho en eso.
  2. No me queda claro si se permiten expresiones como try(try(foo(try(bar)).fum()) . Tal uso puede estar mal visto, pero es necesario especificar su semántica. En el caso del bloque de prueba, el compilador tiene que trabajar más para detectar tales usos y exprimir todo el manejo de errores al nivel del bloque de prueba.
  3. Estoy más inclinado a que me gusten return-on-error en lugar de try . ¡Esto es más fácil de tragar a nivel de bloque!
  4. Por otro lado, cualquier palabra clave larga hace que las cosas sean menos legibles.

FWIW, todavía no creo que valga la pena hacerlo.

@rsc

[...]
La principal es la afirmación de que debido a que es posible escribir código complejo e ilegible, dicho código se volverá omnipresente. Como señaló @josharian , ya es "posible escribir código abominable en Go".
[...]

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := try(parentObjOne.AsCommit())
parentCommitTwo := try(parentObjTwo.AsCommit())

Entiendo que su posición sobre el "código incorrecto" es que podemos escribir un código horrible hoy como el siguiente bloque.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

¿Qué piensas sobre no permitir llamadas try anidadas para que no podamos escribir código incorrecto accidentalmente?

Si no permite try anidados en la primera versión, podrá eliminar esta limitación más tarde si es necesario, no sería posible al revés.

Ya discutí este punto, pero parece relevante: la complejidad del código debe escalar verticalmente, no horizontalmente.

try como expresión fomenta la complejidad del código para escalar horizontalmente fomentando las llamadas anidadas. try como declaración fomenta la complejidad del código para escalar verticalmente.

@rsc , a sus preguntas,

Mi controlador de nivel de paquete de último recurso, cuando no se espera un error:

func quit(err error) {
   fmt.Fprintf(os.Stderr, "quit after %s\n", err)
   debug.PrintStack()      // because panic(err) produces a pile of noise
   os.Exit(3)
}

Contexto: hago un uso intensivo de os.File (donde encontré dos errores: #26650 y #32088)

Un decorador a nivel de paquete que agregue contexto básico necesitaría un argumento caller , una estructura generada que proporciona los resultados de runtime.Caller().

Desearía que el reescritor _go fmt_ usara el formato existente o le permitiera especificar el formato por transformación. Me las arreglo con otras herramientas.

Los costos (es decir, las desventajas) de try() están bien documentados arriba.

Sinceramente, estoy asombrado de que el equipo de Go nos haya ofrecido primero check/handle (caritativamente, una idea novedosa), y luego el ternario try() . No veo por qué no emitió una RFP sobre el manejo de errores y luego recopiló los comentarios de la comunidad sobre algunas de las propuestas resultantes (ver #29860). ¡Hay mucha sabiduría aquí que podrías aprovechar!

@rsc

Sintaxis

Esta discusión ha identificado seis sintaxis diferentes para escribir la misma semántica de la propuesta:

try {error} {optional wrap func} {optional return args in brackets}

f, err := os.Open(file)
try err wrap { a, b }

... y, en mi opinión, mejorar la legibilidad (a través de la aliteración), así como la precisión semántica:

f, err := os.Open(file)
relay err

o

f, err := os.Open(file)
relay err wrap

o

f, err := os.Open(file)
relay err wrap { a, b }

o

f, err := os.Open(file)
relay err { a, b }

Sé que abogar por el relevo frente al intento es fácil de descartar como fuera de tema, pero puedo imaginarme intentar explicar cómo el intento es no intentar nada y no arroja nada. No está claro Y tiene equipaje. relay siendo un término nuevo permitiría una explicación clara, y la descripción tiene una base en los circuitos (que es de lo que se trata de todos modos).

Editar para aclarar:
Probar puede significar: 1. experimentar algo y luego juzgarlo subjetivamente 2. verificar algo objetivamente 3. intentar hacer algo 4. disparar múltiples flujos de control que pueden interrumpirse y lanzar una notificación interceptable si es así

En esta propuesta, try no está haciendo nada de eso. En realidad estamos ejecutando una función. Luego vuelve a cablear el flujo de control en función de un valor de error. Esta es literalmente la definición de un relé de protección. Estamos reinstalando circuitos directamente (es decir, cortocircuitando el alcance de la función actual) de acuerdo con el valor de un error probado.

En la propuesta de bloque de prueba, permití explícitamente declaraciones que no necesitan prueba

La principal ventaja que veo en el manejo de errores de Go sobre el sistema try-catch de lenguajes como Java y Python es que siempre está claro qué llamadas a funciones pueden generar un error y cuáles no. La belleza de try tal como se documenta en la propuesta original, es que puede reducir el manejo simple de errores estándar y al mismo tiempo mantener esta importante característica.

Para tomar prestado de los ejemplos de @Goodwine , a pesar de su fealdad, desde una perspectiva de manejo de errores, incluso esto:

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

... es mejor que lo que se ve a menudo en los lenguajes de prueba y captura

parentCommitOne := r.Head().Peel(git.ObjectCommit).AsCommit()
parentCommitTwo := remoteBranch.Reference.Peel(git.ObjectCommit).AsCommit()

... porque aún puede saber qué partes del código pueden desviar el flujo de control debido a un error y cuáles no.

Sé que @bakul no aboga por esta propuesta de sintaxis de bloques de todos modos, pero creo que plantea un punto interesante sobre el manejo de errores de Go en comparación con otros. Creo que es importante que cualquier propuesta de manejo de errores que Go adopte no ofusque qué partes del código pueden y no pueden fallar.

He escrito una pequeña herramienta: tryhard (que no se esfuerza mucho en este momento) funciona archivo por archivo y utiliza una coincidencia de patrones AST simple para reconocer candidatos potenciales para try y reportarlos (y reescribirlos). La herramienta es primitiva (sin verificación de tipo) y existe una posibilidad decente de falsos positivos, según el estilo de codificación predominante. Lea la documentación para más detalles.

Aplicarlo a $GOROOT/src en los informes de propinas> 5000 (!) Oportunidades para try . Puede haber muchos falsos positivos, pero revisar una muestra decente a mano sugiere que la mayoría de las oportunidades son reales.

El uso de la función de reescritura muestra cómo se verá el código usando try . Nuevamente, una mirada superficial a la salida muestra una mejora significativa en mi mente.

( Precaución: ¡La función de reescritura destruirá los archivos! Úselo bajo su propio riesgo).

Con suerte, esto proporcionará una idea concreta de cómo se vería el código usando try y nos permitirá superar la especulación inactiva e improductiva.

Gracias y disfruta.

Entiendo que su posición sobre el "código incorrecto" es que podemos escribir un código horrible hoy como el siguiente bloque.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

Mi posición es que los desarrolladores de Go hacen un trabajo decente al escribir un código claro y que, casi con certeza, el compilador no es lo único que se interpone en el camino para que usted o sus compañeros de trabajo escriban un código que se vea así.

¿Qué piensas sobre no permitir llamadas try anidadas para que no podamos escribir código incorrecto accidentalmente?

Gran parte de la simplicidad de Go se deriva de la selección de elementos ortogonales que componen de forma independiente. Agregar restricciones rompe la ortogonalidad, la composición, la independencia y, al hacerlo, rompe la simplicidad.

Hoy en día, es una regla que si tienes:

x := expression
y := f(x)

sin ningún otro uso de x en ninguna parte, entonces es una transformación de programa válida para simplificar eso a

y := f(expression)

Si tuviéramos que adoptar una restricción en las expresiones de prueba, se rompería cualquier herramienta que supusiera que esta siempre era una transformación válida. O si tuviera un generador de código que funcionara con expresiones y pudiera procesar expresiones de prueba, tendría que hacer todo lo posible para introducir temporales para satisfacer las restricciones. Y así sucesivamente y así sucesivamente.

En resumen, las restricciones agregan una complejidad significativa. Necesitan una justificación importante, no "a ver si alguien se tropieza con este muro y nos pide que lo derribemos".

Escribí una explicación más larga hace dos años en https://github.com/golang/go/issues/18130#issuecomment -264195616 (en el contexto de alias de tipo) que se aplica igualmente bien aquí.

@bakul ,

Pero déjame aclarar un punto. En la propuesta de bloque de prueba, permití explícitamente declaraciones que _no necesitan_ try .

Hacer esto no alcanzaría el segundo objetivo : "Tanto la verificación de errores como el manejo de errores deben permanecer explícitos, es decir, visibles en el texto del programa. No queremos repetir las trampas del manejo de excepciones".

El principal escollo del manejo tradicional de excepciones es no saber dónde están los controles. Considerar:

try {
    s = canThrowErrors()
    t = cannotThrowErrors()
    u = canThrowErrors() // a second call
} catch {
    // how many ways can you get here?
}

Si las funciones no tuvieran un nombre tan útil, puede ser muy difícil saber qué funciones pueden fallar y cuáles tienen éxito garantizado, lo que significa que no puede razonar fácilmente sobre qué fragmentos de código pueden ser interrumpidos por una excepción y cuáles no.

Compare esto con el enfoque de Swift , donde adoptan parte de la sintaxis tradicional de manejo de excepciones, pero en realidad están manejando errores, con un marcador explícito en cada función marcada y sin forma de relajarse más allá del marco de pila actual:

do {
    let s = try canThrowErrors()
    let t = cannotThrowErrors()
    let u = try canThrowErrors() // a second call
} catch {
    handle error from try above
}

Ya sea Rust o Swift o esta propuesta, la mejora clave y crítica sobre el manejo de excepciones es marcar explícitamente en el texto, incluso con un marcador muy ligero, cada lugar donde se encuentra una verificación.

Para obtener más información sobre el problema de los controles implícitos, consulte la sección Problema de la descripción general del problema de agosto pasado, en particular los enlaces a los dos artículos de Raymond Chen.

Editar: vea también el comentario de @velovix tres arriba, que llegó mientras estaba trabajando en este.

@daved , me alegro de que la analogía del "relé de protección" te funcione. No funciona para mí. Los programas no son circuitos.

Cualquier palabra puede ser malinterpretada:
"romper" no rompe su programa.
"continuar" no continúa la ejecución en la siguiente declaración como de costumbre.
"goto" ... bueno, goto es imposible de malinterpretar en realidad. :-)

https://www.google.com/search?q=define+try dice "hacer un intento o esfuerzo para hacer algo" y "sujeto a prueba". Ambos se aplican a "f := try(os.Open(file))". Intenta hacer os.Open (o somete a prueba el resultado del error), y si el intento (o el resultado del error) falla, regresa de la función.

Utilizamos el cheque en agosto pasado. Esa también era una buena palabra. Cambiamos a probar, a pesar del bagaje histórico de C++/Java/Python, porque el significado actual de probar en esta propuesta coincide con el significado en el intento de Swift (sin el do-catch que lo rodea) y en el intento original de Rust. . No será terrible si después de todo decidimos que cheque es la palabra correcta, pero por ahora debemos centrarnos en otras cosas además del nombre.

Aquí hay un interesante tryhard falso negativo, de github.com/josharian/pct . Lo menciono aquí porque:

  • muestra una forma en la que la detección automática try es complicada
  • ilustra que el costo visual de if err != nil afecta la forma en que las personas (al menos yo) estructuran su código, y que try puede ayudar con eso

Antes:

var err error
switch {
case *flagCumulative:
    _, err = fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s)
case *flagQuiet:
    _, err = fmt.Fprintln(w, line.s)
default:
    _, err = fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s)
}
if err != nil {
    return err
}

Después (reescritura manual):

switch {
case *flagCumulative:
    try(fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s))
case *flagQuiet:
    try(fmt.Fprintln(w, line.s))
default:
    try(fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s))
}

El cambio https://golang.org/cl/182717 menciona este problema: src: apply tryhard -r $GOROOT/src

Para obtener una idea visual de try en la biblioteca estándar, diríjase a CL 182717 .

Gracias, @josharian , por esto . Sí, puede que incluso para una buena herramienta sea imposible detectar todos los posibles candidatos de uso para try . Pero afortunadamente ese no es el objetivo principal (de esta propuesta). Tener una herramienta es útil, pero veo el principal beneficio de try en el código que aún no está escrito (porque va a haber mucho más que el código que ya tenemos).

"romper" no rompe su programa.
"continuar" no continúa la ejecución en la siguiente declaración como de costumbre.
"goto" ... bueno, goto es imposible de malinterpretar en realidad. :-)

break rompe el bucle. continue continúa el bucle y goto va al destino indicado. En última instancia, lo escucho, pero considere lo que sucede cuando una función se completa en su mayoría y devuelve un error, pero no retrocede. No fue un intento/ensayo. Creo que check es muy superior en ese sentido ("detener el progreso de" a través del "examen" es ciertamente adecuado).

Más pertinente, tengo curiosidad acerca de la forma de probar/verificar que ofrecí en comparación con las otras sintaxis.
try {error} {optional wrap func} {optional return args in brackets}

f, err := os.Open(file)
try err wrap { a, b }

La biblioteca estándar termina por no ser representativa del código Go "real", ya que no dedica mucho tiempo a coordinar o conectar otros paquetes. Hemos notado esto en el pasado como la razón por la cual hay tan poco uso de canales en la biblioteca estándar en comparación con los paquetes más arriba en la cadena alimenticia de dependencia. Sospecho que el manejo y la propagación de errores terminan siendo similares a los canales a este respecto: encontrarás más a medida que avanzas.

Por esta razón, sería interesante para alguien ejecutar Tryhard en algunas bases de código de aplicación más grandes y ver qué cosas divertidas se pueden descubrir en ese contexto. (La biblioteca estándar también es interesante, pero más como un microcosmos que como una muestra precisa del mundo).

Tengo curiosidad acerca de la forma de probar/verificar que ofrecí en comparación con las otras sintaxis.

Creo que esa forma termina recreando las estructuras de control existentes .

@networkimprov , re https://github.com/golang/go/issues/32437#issuecomment -502879351

Sinceramente, estoy asombrado de que el equipo de Go nos haya ofrecido primero verificar/manejar (caritativamente, una idea novedosa), y luego el intento ternario(). No veo por qué no emitió una RFP sobre el manejo de errores y luego recopiló los comentarios de la comunidad sobre algunas de las propuestas resultantes (ver #29860). ¡Hay mucha sabiduría aquí que podrías aprovechar!

Como discutimos en #29860, honestamente no veo mucha diferencia entre lo que sugieres que deberíamos haber hecho en cuanto a solicitar comentarios de la comunidad y lo que realmente hicimos. La página de borradores de diseños dice explícitamente que son "puntos de partida para la discusión, con el objetivo final de producir diseños lo suficientemente buenos como para convertirlos en propuestas reales". Y la gente escribió muchas cosas, desde breves comentarios hasta propuestas alternativas completas. Y la mayor parte fue útil y agradezco su ayuda en particular para organizar y resumir. Parece estar obsesionado con darle un nombre diferente o introducir capas adicionales de burocracia, que como discutimos sobre ese tema, realmente no vemos la necesidad.

Pero por favor no afirme que de alguna manera no solicitamos el consejo de la comunidad o lo ignoramos. Eso simplemente no es cierto.

Tampoco puedo ver cómo try es de alguna manera "ternaryesque", lo que sea que eso signifique.

De acuerdo, creo que ese era mi objetivo; No creo que valgan la pena mecanismos más complejos. Si estuviera en tu lugar, lo máximo que ofrecería es un poco de azúcar sintáctico para silenciar la mayoría de las quejas y nada más.

@rsc , disculpas por desviarnos del tema.
Levanté controladores de nivel de paquete en https://github.com/golang/go/issues/32437#issuecomment -502840914
y respondió a su solicitud de aclaración en https://github.com/golang/go/issues/32437#issuecomment -502879351

Veo los controladores de nivel de paquete como una característica que prácticamente todos podrían respaldar.

utilice la sintaxis try {} catch{}, no construya más ruedas

utilice la sintaxis try {} catch{}, no construya más ruedas

creo que es apropiado construir mejores ruedas cuando las ruedas que usan otras personas tienen forma de cuadrados

@jimwei

El manejo de errores basado en excepciones puede ser una rueda preexistente, pero también tiene bastantes problemas conocidos. La declaración del problema en el borrador del diseño original hace un gran trabajo al delinear estos problemas.

Para agregar mi propio comentario menos bien pensado, creo que es interesante que muchos lenguajes nuevos muy exitosos (a saber, Swift, Rust y Go) no hayan adoptado excepciones. Esto me dice que la comunidad de software en general está repensando las excepciones después de los muchos años que hemos tenido que trabajar con ellas.

En respuesta a https://github.com/golang/go/issues/32437#issuecomment -502837008 (comentario de @rsc sobre try como declaración)

Usted plantea un buen punto. Lamento haberme perdido ese comentario antes de hacer este: https://github.com/golang/go/issues/32437#issuecomment -502871889

Sus ejemplos con try como expresión se ven mucho mejor que los que tienen try como declaración. El hecho de que la declaración comience con try hace que sea mucho más difícil de leer. Sin embargo, todavía me preocupa que las personas aniden las llamadas para crear un código incorrecto, ya que try como expresión realmente _alienta_ este comportamiento en mi opinión.

Creo que apreciaría un poco más esta propuesta si golint prohibiera las llamadas try anidadas. Creo que prohibir todas las llamadas try dentro de otras expresiones es un poco demasiado estricto, tener try como expresión tiene sus méritos.

Tomando prestado su ejemplo, incluso anidar 2 llamadas de prueba juntas parece bastante horrible, y puedo ver a los programadores de Go haciéndolo, especialmente si trabajan sin revisores de código.

parentCommitOne := try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit()
parentCommitTwo := try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit()
tree := try(r.LookupTree(try(index.WriteTree())))

El ejemplo original en realidad se veía bastante bien, pero este muestra que anidar las expresiones de prueba (incluso solo con 2 de profundidad) realmente daña la legibilidad del código drásticamente. Denegar las llamadas try anidadas también ayudaría con el problema de "depuración", ya que es mucho más fácil expandir un try a un if si está fuera de una expresión.

Nuevamente, casi me gustaría decir que un try dentro de una subexpresión debe marcarse con golint , pero creo que eso podría ser demasiado estricto. También marcaría un código como este, que en mi opinión está bien:

x := 5 + try(strconv.Atoi(input))

De esta manera, obtenemos los beneficios de tener try como expresión, pero no estamos promoviendo agregar demasiada complejidad al eje horizontal.

Quizás otra solución sería que golint solo permita un máximo de 1 try por declaración, pero es tarde, me estoy cansando y necesito pensarlo más racionalmente. De cualquier manera, he sido bastante negativo con respecto a esta propuesta en algunos puntos, pero creo que realmente me puede gustar siempre que haya algunos golint estándares relacionados con ella.

@rsc

Podemos y debemos distinguir entre _"esta característica se puede usar para escribir código muy legible, pero también se puede abusar de ella para escribir código ilegible"_ y "el uso dominante de esta característica será escribir código ilegible".
La experiencia con C sugiere que ? : cae de lleno en la segunda categoría. (Con la posible excepción de min y max,

Lo primero que me llamó la atención sobre try() , frente a try como declaración, fue lo similar que era en anidabilidad con el operador ternario y, sin embargo, lo opuesto a los argumentos a favor de try() y en contra del ternario. fueron _(parafraseado):_

  • ternario: _"Si lo permitimos, la gente lo anidará y el resultado será mucho código malo"_ ignorando que algunas personas escriben código mejor con ellos, vs.
  • try(): _"Puede anidarlo, pero dudamos que muchos lo hagan porque la mayoría de la gente quiere escribir un buen código"_,

Respetuosamente, esa razón para la diferencia entre los dos se siente tan subjetiva que pediría un poco de introspección y al menos consideraría si podría estar racionalizando una diferencia para la característica que prefiere frente a una característica que no le gusta. #por favor_no_dispares_al_mensajero

_"No estoy seguro de haber visto código usando ? : eso no se mejoró al reescribirlo para usar una declaración if en su lugar. Pero este párrafo se está saliendo del tema.)"_

En otros idiomas, con frecuencia mejoro las declaraciones reescribiéndolas de if a un operador ternario, por ejemplo, del código que escribí hoy en PHP:

return isset( $_COOKIE[ CookieNames::CART_ID ] )
    ? intval( $_COOKIE[ CookieNames::CART_ID ] )
    : null;

Comparar con:

if ( isset( $_COOKIE[ CookieNames::CART_ID ] ) ) {
    return intval( $_COOKIE[ CookieNames::CART_ID ] );
} else { 
    return null;
}

En lo que a mí respecta, el primero es mucho mejor que el segundo.

por favor

Creo que las críticas a esta propuesta se deben en gran medida a las altas expectativas que generó la propuesta anterior, que habría sido mucho más completa. Sin embargo, creo que unas expectativas tan altas estaban justificadas por razones de coherencia. Creo que lo que a muchas personas les hubiera gustado ver es una construcción única y completa para el manejo de errores que sea útil en todos los casos de uso.

Compare esta característica, por ejemplo, con la función incorporada append() . Append se creó porque agregar a slice era un caso de uso muy común y, si bien era posible hacerlo manualmente, también era fácil hacerlo mal. Ahora append() permite agregar no solo uno, sino muchos elementos, o incluso un segmento completo, e incluso permite agregar una cadena a un segmento de []byte. Es lo suficientemente potente como para cubrir todos los casos de uso de agregar a una porción. Y por lo tanto, ya nadie agrega cortes manualmente.

Sin embargo, try() es diferente. No es lo suficientemente potente como para que podamos usarlo en todos los casos de manejo de errores. Y creo que ese es el defecto más grave de esta propuesta. La función incorporada try() solo es realmente útil, en el sentido de que reduce la repetición, en el más simple de los casos, es decir, simplemente pasar un error a la persona que llama, y ​​con una declaración diferida, si todos los errores de la la función debe manejarse de la misma manera.

Para un manejo de errores más complejo, aún necesitaremos usar if err != nil {} . Esto lleva a dos estilos distintos para el manejo de errores, donde antes solo había uno. Si esta propuesta es todo lo que recibimos para ayudar con el manejo de errores en Go, entonces, creo que sería mejor no hacer nada y seguir manejando el manejo de errores con if como siempre lo hemos hecho, porque al menos esto es consistente. y tuvo el beneficio de "solo hay una forma de hacerlo".

@rsc , disculpas por desviarnos del tema.
Levanté controladores a nivel de paquete en #32437 (comentario)
y respondió a su solicitud de aclaración en el #32437 (comentario)

Veo los controladores de nivel de paquete como una característica que prácticamente todos podrían respaldar.

No veo qué une el concepto de un paquete con un manejo de errores específico. Es difícil imaginar que el concepto de un controlador de nivel de paquete sea útil para, digamos, net/http . De manera similar, a pesar de escribir paquetes más pequeños que net/http en general, no puedo pensar en un solo caso de uso en el que hubiera preferido una construcción a nivel de paquete para manejar errores. En general, he descubierto que la suposición de que todos comparten sus experiencias, casos de uso y opiniones es peligrosa :)

@beoran , creo que esta propuesta hace posibles mejoras adicionales. ¿Como un decorador en el último argumento de try(..., func(err) error) , o un tryf(..., "context of my error: %w") ?

@flibustenet Si bien tales extensiones posteriores podrían ser posibles, la propuesta tal como está ahora parece desalentar tales extensiones, principalmente porque agregar un controlador de errores sería redundante con diferir.

Supongo que el problema difícil es cómo tener un manejo integral de errores sin duplicar la funcionalidad de defe. Tal vez la declaración diferida en sí podría mejorarse de alguna manera para permitir un manejo de errores más fácil en casos más complejos... Pero ese es un problema diferente.

https://github.com/golang/go/issues/32437#issuecomment-502975437

Esto lleva a dos estilos distintos para el manejo de errores, donde antes solo había uno. Si esta propuesta es todo lo que recibimos para ayudar con el manejo de errores en Go, entonces, creo que sería mejor no hacer nada y seguir manejando el manejo de errores con if como siempre lo hemos hecho, porque al menos esto es consistente. y tuvo el beneficio de "solo hay una manera de hacerlo".

@beoran De acuerdo. Es por eso que sugerí que unifiquemos la gran mayoría de los casos de error bajo la palabra clave try ( try y try / else ). Aunque la sintaxis try / else no nos da ninguna reducción significativa en la longitud del código en comparación con el estilo if err != nil existente, nos da consistencia con el try (sin else ) caso. Es probable que esos dos casos (intentar y probar si no) cubran la gran mayoría de los casos de manejo de errores. Pongo eso en oposición a la versión incorporada de try que solo se aplica en los casos en que el programador no está haciendo nada para manejar el error además de regresar (que, como otros han mencionado en este hilo, no es necesariamente algo que realmente queremos fomentar en primer lugar).

La consistencia es importante para la legibilidad.

append es la forma definitiva de agregar elementos a una porción. make es la forma definitiva de construir un nuevo canal, mapa o división (con la excepción de los literales, que no me entusiasman). Pero try() (como incorporado y sin else ) se salpicaría en las bases de código, dependiendo de cómo el programador necesita manejar un error dado, de una manera que probablemente sea un poco caótica y confusa para el lector. No parece estar en el espíritu de las otras funciones integradas (es decir, manejar un caso que es bastante difícil o absolutamente imposible de hacer de otra manera). Si esta es la versión de try que tiene éxito, la coherencia y la legibilidad me obligarán a no usarla, al igual que trato de evitar los literales de mapa/corte (y evitar new como la peste).

Si la idea es cambiar la forma en que se manejan los errores, parece prudente tratar de unificar el enfoque en tantos casos como sea posible, en lugar de agregar algo que, en el mejor de los casos, será "tómalo o déjalo". Me temo que este último en realidad agregará ruido en lugar de reducirlo.

@deanveloper escribió:

Creo que apreciaría un poco más esta propuesta si Golint prohibiera las llamadas de prueba anidadas.

Estoy de acuerdo en que try profundamente anidados podría ser difícil de leer. Pero esto también es cierto para las llamadas a funciones estándar, no solo para la función integrada try . Por lo tanto, no veo por qué golint debería prohibir esto.

@brynbellomy escribió:

Aunque la sintaxis try/else no nos da ninguna reducción significativa en la longitud del código en comparación con el estilo if err != nil existente, nos da consistencia con el caso try (no else).

El único objetivo de la función incorporada try es reducir el modelo estándar, por lo que es difícil ver por qué deberíamos adoptar la sintaxis try/else que propone cuando reconoce que "no nos brinda ninguna reducción significativa en longitud de código".

También menciona que la sintaxis que propone hace que el caso try sea consistente con el caso try/else. Pero también crea una forma inconsistente de bifurcar, cuando ya tenemos if/else. Ganas un poco de consistencia en un caso de uso específico pero pierdes mucha inconsistencia en el resto.

Siento la necesidad de expresar mis opiniones por lo que valen. Aunque no todo esto es de naturaleza académica y técnica, creo que es necesario decirlo.

Creo que este cambio es uno de esos casos en los que la ingeniería se hace por el bien de la ingeniería y el "progreso" se usa para la justificación. El manejo de errores en Go no está roto y esta propuesta viola gran parte de la filosofía de diseño que amo de Go.

Haz que las cosas sean fáciles de entender, no fáciles de hacer
Esta propuesta está eligiendo la optimización de la pereza sobre la corrección. La atención se centra en facilitar el manejo de errores y, a cambio, se pierde una gran cantidad de legibilidad. La naturaleza tediosa ocasional del manejo de errores es aceptable debido a las ganancias de legibilidad y depuración.

Evite nombrar argumentos de retorno
Hay algunos casos extremos con declaraciones defer donde es válido nombrar el argumento de retorno. Fuera de estos, debe evitarse. Esta propuesta promueve el uso de argumentos de devolución de nombres. Esto no ayudará a que el código Go sea más legible.

La encapsulación debería crear una nueva semántica donde uno sea absolutamente preciso
No hay precisión en esta nueva sintaxis. Ocultar la variable de error y el retorno no ayuda a que las cosas sean más fáciles de entender. De hecho, la sintaxis se siente muy extraña a todo lo que hacemos en Go hoy. Si alguien escribiera una función similar, creo que la comunidad estaría de acuerdo en que la abstracción oculta el costo y no vale la pena por la simplicidad que intenta brindar.

¿A quién estamos tratando de ayudar?
Me preocupa que este cambio se esté implementando en un intento de alejar a los desarrolladores empresariales de sus lenguajes actuales y llevarlos a Go. Implementar cambios de idioma, solo para aumentar los números, sienta un mal precedente. Creo que es justo hacer esta pregunta y obtener una respuesta al problema comercial que se intenta resolver y la ganancia esperada que se intenta lograr.

He visto esto antes varias veces ahora. Parece bastante claro, con toda la actividad reciente del equipo de idiomas, esta propuesta es básicamente inamovible. Hay más defensa de la implementación que debate real sobre la implementación misma. Todo esto comenzó hace 13 días. Veremos el impacto que tiene este cambio en el idioma, la comunidad y el futuro de Go.

El manejo de errores en Go no está roto y esta propuesta viola gran parte de la filosofía de diseño que amo de Go.

Bill expresa mis pensamientos perfectamente.

No puedo evitar que se presente try , pero si es así, no lo usaré yo mismo; No lo enseñaré y no lo aceptaré en las relaciones públicas que reviso. Simplemente se agregará a la lista de otras 'cosas en Go que nunca uso' (consulte la divertida charla de Mat Ryer en YouTube para obtener más de estas).

@ardan-bkennedy, gracias por tus comentarios.

Usted preguntó sobre el "problema comercial que se intenta resolver". No creo que estemos apuntando a los problemas de ningún negocio en particular, excepto tal vez "Ir a programar". Pero, de manera más general, articulamos el problema que estamos tratando de resolver en agosto pasado en el inicio de la discusión del borrador del diseño de Gophercon (consulte la Descripción general del problema , especialmente la sección Objetivos). El hecho de que esta conversación haya estado ocurriendo desde agosto pasado también contradice rotundamente su afirmación de que "Todo esto comenzó hace 13 días".

Usted no es la única persona que ha sugerido que esto no es un problema o que no vale la pena resolverlo. Consulte https://swtch.com/try.html#nonissue para ver otros comentarios similares. Los hemos anotado y queremos asegurarnos de que estamos resolviendo un problema real. Parte de la forma de averiguarlo es evaluar la propuesta sobre bases de código reales. Herramientas como Tryhard de Robert nos ayudan a hacer eso. Anteriormente pedí a las personas que nos hicieran saber lo que encuentran en sus propias bases de código. Esa información será de vital importancia para evaluar si el cambio vale la pena o no. Tú tienes una conjetura y yo tengo otra diferente, y eso está bien. La respuesta es sustituir datos por esas conjeturas.

Haremos lo que sea necesario para asegurarnos de que estamos resolviendo un problema real.

Una vez más, el camino a seguir son los datos experimentales, no las reacciones viscerales. Desafortunadamente, la recopilación de datos requiere más esfuerzo. En este punto, animaría a las personas que quieran ayudar a salir y recopilar datos.

@ardan-bkennedy, perdón por el segundo seguimiento pero con respecto a:

Me preocupa que este cambio se esté implementando en un intento de alejar a los desarrolladores empresariales de sus lenguajes actuales y llevarlos a Go. Implementar cambios de idioma, solo para aumentar los números, sienta un mal precedente.

Hay dos problemas serios con esta línea que no puedo pasar.

En primer lugar, rechazo la afirmación implícita de que hay clases de desarrolladores, en este caso "desarrolladores empresariales", que de alguna manera no son dignos de usar Go o de que se consideren sus problemas. En el caso específico de "empresa", estamos viendo muchos ejemplos de empresas pequeñas y grandes que usan Go de manera muy efectiva.

En segundo lugar, desde el comienzo del proyecto Go, nosotros (Robert, Rob, Ken, Ian y yo) hemos evaluado los cambios y las funciones del idioma en función de nuestra experiencia colectiva en la creación de muchos sistemas. Nos preguntamos "¿funcionaría bien esto en los programas que escribimos?" Esa ha sido una receta exitosa con una amplia aplicabilidad y es la que pretendemos seguir usando, nuevamente aumentada por los datos que pedí en el comentario anterior y los informes de experiencia en general. No sugeriríamos ni apoyaríamos un cambio de idioma que no podamos ver usando en nuestros propios programas o que no creamos que encaja bien en Go. Y ciertamente no sugeriríamos ni apoyaríamos un mal cambio solo para tener más programadores de Go. Nosotros también usamos Go después de todo.

@rsc
No habrá escasez de lugares donde se pueda colocar esta conveniencia. ¿Qué métrica se busca que demuestre la sustancia del mecanismo aparte de eso? ¿Existe una lista de casos de manejo de errores clasificados? ¿Cómo se derivará el valor de los datos cuando gran parte del proceso público esté impulsado por el sentimiento?

¡Las herramientas tryhard son muy informativas!
Pude ver que uso a menudo return ...,err , pero solo cuando sé que llamo a una función que ya envuelve el error (con pkg/errors ), principalmente en controladores http. Gano en legibilidad con menos línea de código.
Luego, en el controlador http de tesis, agregaría defer fmt.HandleErrorf(&err, "handler xyz") y finalmente agregaría más contexto que antes.

También veo muchos casos en los que no me importa el error en absoluto fmt.Printf y lo haré con try .
¿Será posible, por ejemplo, hacer defer try(f.Close()) ?

Entonces, tal vez try finalmente ayude a agregar contexto e impulsar las mejores prácticas en lugar de lo contrario.

Estoy muy impaciente por probar en real!

@flibustenet La propuesta tal como está no permitirá defer try(f()) (consulte la justificación ). Hay todo tipo de problemas con eso.

Al usar esta herramienta tryhard para ver cambios en una base de código, ¿podríamos también comparar la proporción de if err != nil antes y después para ver si es más común agregar contexto o simplemente devolver el error?

Mi opinión es que tal vez un gran proyecto hipotético puede ver 1000 lugares donde se agregaron try() , pero hay 10000 if err != nil que agregan contexto, por lo que aunque 1000 parece enorme, es solo el 10% de todo .

@Goodwine Sí. Probablemente no pueda hacer este cambio esta semana, pero el código es bastante sencillo y autónomo. Siéntase libre de probarlo (sin juego de palabras), clonar y ajustar según sea necesario.

defer try(f()) sería equivalente a

defer func() error {
    if err:= f(); err != nil { return err }
    return nil
}()

Esta (la versión if) actualmente no está prohibida, ¿verdad? Me parece que no debería hacer una excepción aquí, ¿puede generar una advertencia? Y no está claro si el código de aplazamiento anterior es necesariamente incorrecto. ¿Qué sucede si close(file) falla en una declaración defer ? ¿Deberíamos reportar ese error o no?

Leí la justificación que parece hablar de defer try(f) no defer try(f()) . ¿Puede ser un error tipográfico?

Se puede hacer un argumento similar para go try(f()) , que se traduce en

go func() error {
    if err:= f(); err != nil { return err }
    return nil
}()

Aquí try no hace nada útil pero es inofensivo.

@ardan-bkennedy Gracias por tus pensamientos. Con el debido respeto, creo que tergiversó la intención de esta propuesta e hizo varias afirmaciones sin fundamento .

Con respecto a algunos de los puntos que @rsc no ha abordado antes:

  • Nunca hemos dicho que el manejo de errores esté roto. El diseño se basa en la observación (¡de la comunidad de Go!) de que el manejo actual está bien, pero en muchos casos es detallado; esto es indiscutible. Esta es una premisa importante de la propuesta.

  • Hacer las cosas más fáciles de hacer también puede hacerlas más fáciles de entender: estos dos no se excluyen mutuamente, ni siquiera se implican mutuamente. Le insto a que mire este código para ver un ejemplo. El uso try elimina una cantidad significativa de repetitivo, y ese repetitivo prácticamente no agrega nada a la comprensibilidad del código. La eliminación del código repetitivo es una práctica de codificación estándar y ampliamente aceptada para mejorar la calidad del código.

  • Con respecto a "esta propuesta viola mucho la filosofía del diseño": Lo importante es que no nos volvamos dogmáticos sobre la "filosofía del diseño" - eso es a menudo la ruina de las buenas ideas (además, creo que sabemos un par de cosas sobre la filosofía de diseño de Go). Hay mucho "fervor religioso" (a falta de un término mejor) en torno a los parámetros de resultados con nombre y sin nombre. Mantras como "nunca deberás usar parámetros de resultado con nombre" fuera de contexto no tienen sentido. Pueden servir como pautas generales, pero no como verdades absolutas. Los parámetros de resultado con nombre no son inherentemente "malos". Los parámetros de resultados bien nombrados pueden agregarse a la documentación de una API de manera significativa. En resumen, no usemos eslóganes para tomar decisiones de diseño de lenguaje.

  • Es un punto de esta propuesta no introducir nueva sintaxis. Simplemente propone una nueva función. No podemos escribir esa función en el idioma, por lo que una función integrada es el lugar natural para ella en Go. No solo es una función simple, también se define con mucha precisión. Elegimos este enfoque mínimo en lugar de soluciones más completas precisamente porque hace una cosa muy bien y no deja casi nada a decisiones de diseño arbitrarias. Tampoco estamos fuera de los caminos trillados ya que otros lenguajes (por ejemplo, Rust) tienen construcciones muy similares. Sugerir que "la comunidad estaría de acuerdo en que la abstracción oculta el costo y no vale la pena por la simplicidad que intenta proporcionar" es poner palabras en boca de otras personas. Si bien podemos escuchar claramente a los opositores vocales de esta propuesta, hay un porcentaje significativo (un estimado del 40%) de personas que expresaron su aprobación de seguir adelante con el experimento. No los privemos de sus derechos con hipérboles.

Gracias.

return isset( $_COOKIE[ CookieNames::CART_ID ] )
    ? intval( $_COOKIE[ CookieNames::CART_ID ] )
    : null;

Bastante seguro de que esto debería ser return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null; FWIW. 😁

@bakul porque los argumentos se evalúan inmediatamente, en realidad es más o menos equivalente a:

<result list> := f()
defer try(<result list>)

Este puede ser un comportamiento inesperado para algunos, ya que el f() no se aplaza para más tarde, se ejecuta de inmediato. Lo mismo se aplica a go try(f()) .

@bakul El documento menciona defer try(f) (en lugar de defer try(f()) porque try en general se aplica a cualquier expresión, no solo a una llamada de función (puede decir try(err) para ejemplo, si err es del tipo error ). Por lo tanto, no es un error tipográfico, pero quizás resulte confuso al principio. f simplemente representa una expresión, que suele ser una función llamada.

@deanveloper , @griesemer No importa :-) Gracias.

@carl-mastrangelo

_"Estoy bastante seguro de que esto debería ser return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null; _

Está asumiendo PHP 7.x. Yo no estaba. Pero, de nuevo, dada tu cara sarcástica, sabes que ese no era el punto. :guiño:

Estoy preparando una breve demostración para mostrar esta discusión durante una reunión que tendrá lugar mañana y escucho algunas ideas nuevas, ya que creo que la mayoría de los participantes en este hilo (colaboradores o observadores) son los que están más involucrados en el idioma y lo más probable es que "no sea el desarrollador promedio" (solo una corazonada).

Mientras hacía eso, recordé que en realidad tuvimos una reunión sobre errores y una discusión sobre dos patrones:

  1. Extienda la estructura de error mientras admite la interfaz de error mystruct.Error()
  2. Incruste el error como un campo o un campo anónimo de la estructura
type ExtErr struct{
  error
  someOtherField string
}  

Estos se usan en algunas pilas que mis equipos realmente construyeron.

La propuesta de preguntas y respuestas establece
P: El último argumento pasado para probar debe ser de tipo error. ¿Por qué no es suficiente que el argumento entrante sea asignable a error?
R: "... Podemos revisar esta decisión en el futuro si es necesario"

¿Alguien puede comentar casos de uso similares para que podamos entender si esta necesidad es común para las dos opciones de extensión de error anteriores?

@mikeschinkel No soy el Carl que estás buscando.

@daved , re:

No habrá escasez de lugares donde se pueda colocar esta conveniencia. ¿Qué métrica se busca que demuestre la sustancia del mecanismo aparte de eso? ¿Existe una lista de casos de manejo de errores clasificados? ¿Cómo se derivará el valor de los datos cuando gran parte del proceso público esté impulsado por el sentimiento?

La decisión se basa en qué tan bien funciona esto en programas reales. Si las personas nos muestran que intentar no es efectivo en la mayor parte de su código, eso es información importante. El proceso es impulsado por ese tipo de datos. No es impulsado por el sentimiento.

Contexto de error

La preocupación semántica más importante que se ha planteado en este problema es si try fomentará una mejor o peor anotación de errores con contexto.

La Descripción general del problema de agosto pasado brinda una secuencia de ejemplos de implementaciones de CopyFile en las secciones Problema y Objetivos. Es un objetivo explícito, tanto en ese entonces como en la actualidad, que cualquier solución haga que sea _más probable_ que los usuarios agreguen el contexto apropiado a los errores. Y pensamos que try puede hacer eso, o no lo hubiéramos propuesto.

Pero antes de intentarlo, vale la pena asegurarse de que todos estemos en la misma página sobre el contexto de error apropiado. El ejemplo canónico es os.Open. Citando la publicación del blog de Go " Manejo de errores y Go ":

Es responsabilidad de la implementación del error resumir el contexto.
El error devuelto por os.Open formatea como "abrir /etc/passwd: permiso denegado", no solo "permiso denegado".

Ver también la sección de Errores de Go Efectivo .

Tenga en cuenta que esta convención puede diferir de otros lenguajes con los que está familiarizado, y también se sigue de manera inconsistente en el código Go. Un objetivo explícito de tratar de agilizar el manejo de errores es hacer que sea más fácil para las personas seguir esta convención y agregar el contexto apropiado y, por lo tanto, hacer que se siga de manera más consistente.

Hoy en día hay mucho código que sigue la convención Go, pero también hay mucho código que asume la convención opuesta. Es muy común ver código como:

f, err := os.Open(file)
if err != nil {
    log.Fatalf("opening %s: %v", file, err)
}

que, por supuesto, imprime lo mismo dos veces (muchos ejemplos en esta misma discusión se ven así). Parte de este esfuerzo tendrá que ser asegurarse de que todos conozcan y sigan la convención.

En el código que sigue la convención de contexto de error Go, esperamos que la mayoría de las funciones agreguen correctamente el mismo contexto a cada devolución de error, de modo que una decoración se aplica en general. Por ejemplo, en el ejemplo de CopyFile, lo que debe agregarse en cada caso son detalles sobre lo que se estaba copiando. Otras devoluciones específicas pueden agregar más contexto, pero generalmente se agregan en lugar de reemplazar. Si estamos equivocados acerca de esta expectativa, sería bueno saberlo. La evidencia clara de las bases del código real ayudaría.

El borrador del diseño de verificación/manejo de Gophercon habría usado un código como:

func CopyFile(src, dst string) error {
    handle err {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

Esta propuesta ha revisado eso, pero la idea es la misma:

func CopyFile(src, dst string) (err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("copy %s %s: %v", src, dst, err)
        }
    }()

    r := try(os.Open(src))
    defer r.Close()

    w := try(os.Create(dst))
    ...
}

y queremos agregar un ayudante aún sin nombre para este patrón común:

func CopyFile(src, dst string) (err error) {
    defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)

    r := try(os.Open(src))
    defer r.Close()

    w := try(os.Create(dst))
    ...
}

En resumen, la razonabilidad y el éxito de este enfoque dependen de estos supuestos y pasos lógicos:

  1. Las personas deben seguir la convención de Go establecida "la persona que llama agrega el contexto relevante que conoce".
  2. Por lo tanto, la mayoría de las funciones solo necesitan agregar un contexto de nivel de función que describa el
    operación, no la subparte específica que falló (esa subparte ya se autoinformó).
  3. Gran parte del código de Go actual no agrega el contexto de nivel de función porque es demasiado repetitivo.
  4. Proporcionar una forma de escribir el contexto de nivel de función una vez hará que sea más probable que
    los desarrolladores hacen eso.
  5. El resultado final será más código Go siguiendo la convención y agregando el contexto apropiado.

Si hay una suposición o paso lógico que cree que es falso, queremos saberlo. Y la mejor manera de decírnoslo es apuntar a la evidencia en las bases de código reales. Muéstranos patrones comunes que tienes donde intentar es inapropiado o empeora las cosas. Muéstranos ejemplos de cosas en las que intentar fue más efectivo de lo que esperabas. Trate de cuantificar cuánto de su base de código cae en un lado o en el otro. Y así. Los datos importan.

Gracias.

Gracias @rsc por la información adicional sobre las mejores prácticas de contexto de error. Este punto sobre las mejores prácticas en particular me ha aludido, pero mejora significativamente la relación de try con el contexto de error.

Por lo tanto, la mayoría de las funciones solo necesitan agregar un contexto de nivel de función que describa el
operación, no la subparte específica que falló (esa subparte ya se autoinformó).

Entonces, el lugar donde try no ayuda es cuando necesitamos reaccionar a los errores, no solo contextualizarlos.

Para adaptar un ejemplo de Cleaner, más elegante y equivocado , aquí su ejemplo de una función que es sutilmente incorrecta en su manejo de errores. Lo he adaptado a Go usando try y defer -estilo de ajuste de errores:

func AddNewGuy(name string) (guy Guy, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("adding guy %v: %v", name, err)
        }
    }()

    guy = Guy{name: name}
    guy.Team = ChooseRandomTeam()
    try(guy.Team.Add(guy))
    try(AddToLeague(guy))
    return guy, nil
}

Esta función es incorrecta porque si guy.Team.Add(guy) tiene éxito pero AddToLeague(guy) falla, el equipo tendrá un objeto Guy no válido que no está en una liga. El código correcto se vería así, donde retrocedemos guy.Team.Add(guy) y ya no podemos usar try :

func AddNewGuy(name string) (guy Guy, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("adding guy %v: %v", name, err)
        }
    }()

    guy = Guy{name: name}
    guy.Team = ChooseRandomTeam()
    try(guy.Team.Add(guy))
    if err := AddToLeague(guy); err != nil {
        guy.Team.Remove(guy)
        return Guy{}, err
    }
    return guy, nil
}

O bien, si queremos evitar tener que proporcionar valores cero para los valores de retorno sin error, podemos reemplazar return Guy{}, err con try(err) . Independientemente, la función defer -ed aún se ejecuta y se agrega contexto, lo cual es bueno.

Nuevamente, esto significa que try apunta a reaccionar a los errores, pero no a agregarles contexto. Esa es una distinción que me ha aludido a mí y quizás a otros. Esto tiene sentido porque la forma en que una función agrega contexto a un error no es de particular interés para un lector, pero la forma en que una función reacciona a los errores es importante. Deberíamos hacer que las partes menos interesantes de nuestro código sean menos detalladas, y eso es lo que hace try .

Usted no es la única persona que ha sugerido que esto no es un problema o que no vale la pena resolverlo. Consulte https://swtch.com/try.html#nonissue para ver otros comentarios similares. Los hemos anotado y queremos asegurarnos de que estamos resolviendo un problema real.

@rsc También creo que no hay problema con el código de error actual. Así que, por favor, cuenta conmigo.

Herramientas como Tryhard de Robert nos ayudan a hacer eso. Anteriormente pedí a las personas que nos hicieran saber lo que encuentran en sus propias bases de código. Esa información será de vital importancia para evaluar si el cambio vale la pena o no. Tú tienes una conjetura y yo tengo otra diferente, y eso está bien. La respuesta es sustituir datos por esas conjeturas.

Miré https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go y me gusta más el código antiguo. Me sorprende que la llamada a la función de prueba pueda interrumpir la ejecución actual. Así no es como funciona el Go actual.

Sospecho que encontrará que las opiniones variarán. Creo que esto es muy subjetivo.

Y sospecho que la mayoría de los usuarios no participan en este debate. Ni siquiera saben que este cambio se avecina. Yo mismo estoy bastante involucrado con Go, pero no participo en este cambio porque no tengo tiempo libre.

Creo que tendríamos que volver a educar a todos los usuarios de Go existentes para que piensen de manera diferente ahora.

También tendríamos que decidir qué hacer con algunos usuarios/empresas que se negarán a usar try en su código. Habrá alguno seguro.

Tal vez tendríamos que cambiar gofmt para reescribir el código actual automáticamente. Para obligar a estos usuarios "pícaros" a usar la nueva función de prueba. ¿Es posible hacer que gofmt haga eso?

¿Cómo lidiaríamos con los errores de compilación cuando las personas usan go1.13 y antes para compilar código con try?

Probablemente me perdí muchos otros problemas que tendríamos que superar para implementar este cambio. ¿Vale la pena la molestia? no lo creo

Alex

@griesemer
Mientras intentaba con fuerza en un archivo con 97 err's none capturado, descubrí que los 2 patrones no estaban traducidos
1:

    if err := updateItem(tx, fields, entityView.DataBinding, entityInstance); err != nil {
        tx.Rollback()
        return nil, err
    }

No se reemplaza, probablemente porque tx.Rollback() entre err := y la línea de retorno,
Lo que supongo que solo debe ser manejado por diferido, y si todas las rutas de error necesitan tx.Rollback()
Es esto correcto ?

  1. Tampoco sugiere:
if err := db.Error; err != nil {
        return nil, err
    } else if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
        return nil, err
    } else {
        return itemDb, nil
    }

o

    if err := db.Error; err != nil {
        return nil, err
    } else {
            if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
                return nil, err
            } else {
                return itemDb, nil
            }
        return result, nil
    }

¿Esto se debe al sombreado o al intento de anidamiento que se traduciría? significado: ¿debe este uso probar o sugerir que se deje como err := ... return err ?

@guybrand Re: los dos patrones que encontraste:

1) sí, tryhard no se esfuerza mucho. la verificación de tipo es necesaria para casos más complejos. Si se debe hacer tx.Rollback() en todas las rutas, defer podría ser el enfoque correcto. De lo contrario, mantener los if podría ser el enfoque correcto. Depende del código específico.

2) Lo mismo aquí: tryhard no busca este patrón más complejo. Tal vez podría

Nuevamente, esta es una herramienta experimental para obtener algunas respuestas rápidas. Hacerlo bien requiere un poco más de trabajo.

@alexbrainman

¿Cómo lidiaríamos con los errores de compilación cuando las personas usan go1.13 y antes para compilar código con try?

Tengo entendido que la versión del lenguaje en sí estará controlada por la directiva de versión de lenguaje go en el archivo go.mod para cada pieza de código que se compila.

La documentación actual de go.mod describe la directiva de versión de idioma go la siguiente manera:

La versión de idioma esperada, establecida por la directiva go , determina
qué funciones de idioma están disponibles al compilar el módulo.
Las funciones de idioma disponibles en esa versión estarán disponibles para su uso.
Funciones de idioma eliminadas en versiones anteriores o añadidas en versiones posteriores,
no estará disponible. Tenga en cuenta que la versión del idioma no afecta
etiquetas de compilación, que están determinadas por la versión de Go que se está utilizando.

Si hipotéticamente algo como un nuevo try integrado aterriza en algo como Go 1.15, entonces en ese momento alguien cuyo archivo go.mod lee go 1.12 no tendría acceso a ese nuevo try incorporado incluso si se compilan con la cadena de herramientas Go 1.15. Entiendo que el plan actual es que tendrían que cambiar la versión de idioma de Go declarada en su go.mod de go 1.12 para leer go 1.15 si quieren usar el nuevo Go Función de idioma 1.15 de try .

Por otro lado, si tiene un código que usa try y ese código vive en un módulo cuyo archivo go.mod declara su versión de lenguaje Go como go 1.15 , pero luego alguien intenta construya eso con la cadena de herramientas Go 1.12, en ese punto la cadena de herramientas Go 1.12 fallará con un error de compilación. La cadena de herramientas de Go 1.12 no sabe nada acerca try , pero sabe lo suficiente como para imprimir un mensaje adicional de que el código que no se pudo compilar afirmaba requerir Go 1.15 según lo que hay en el archivo go.mod . Puede intentar este experimento ahora mismo utilizando la cadena de herramientas Go 1.12 actual y ver el mensaje de error resultante:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

Hay una discusión mucho más larga en el documento de propuesta de transiciones Go2 .

Dicho esto, los detalles exactos de eso podrían discutirse mejor en otro lugar (por ejemplo, quizás en #30791, o este hilo reciente de golang-nuts ).

@griesemer , lo siento si me perdí una solicitud más específica para un formato, pero me encantaría compartir algunos resultados y tener acceso (un posible permiso) al código fuente de algunas empresas.
A continuación se muestra un ejemplo real para un proyecto pequeño, creo que los resultados adjuntos son una buena muestra, si es así, probablemente podamos compartir alguna tabla con resultados similares:

Total = Número de líneas de código
$find /path/to/repo -name '*.go' -exec cat {} \; | wc -l
Errs = número de líneas con err := (esto probablemente omite err = y myerr := , pero creo que en la mayoría de los casos cubre)
$find /path/to/repo -name '*.go' -exec cat {} \; | grep "err :=" | wc -l
tryhard = número de líneas tryhard encontradas

el primer caso que probé para estudiar devolvió:
totales = 5106
Errores = 111
esforzarse = 16

base de código más grande
totales = 131777
Errores = 3289
esforzarse = 265

Si este formato es aceptable, háganos saber cómo desea obtener los resultados, supongo que simplemente arrojarlo aquí no sería el formato correcto.
Además, probablemente sería rápido intentar contar las líneas, ocasiones de err := (y probablemente err = , solo 4 en la base de código que traté de aprender)

Gracias.

De @griesemer en https://github.com/golang/go/issues/32437#issuecomment -503276339

Le insto a que mire este código para ver un ejemplo.

Con respecto a ese código, noté que el archivo de salida creado aquí nunca parece estar cerrado. Además, es importante verificar los errores al cerrar los archivos en los que ha escrito, porque esa puede ser la única vez que se le informa que hubo un problema con una escritura.

No menciono esto como un informe de error (¿aunque tal vez debería serlo?), sino como una oportunidad para ver si try tiene algún efecto sobre cómo solucionarlo. Enumeraré todas las formas que se me ocurren para solucionarlo y consideraré si la adición de try ayudaría o perjudicaría. Aquí hay algunas maneras:

  1. Agregue llamadas explícitas a outf.Close() justo antes de que se devuelva cualquier error.
  2. Nombre el valor de retorno y agregue un aplazamiento para cerrar el archivo, registrando el error si aún no está presente. p.ej
func foo() (err error) {
    outf := try(os.Create())
    defer func() {
        cerr := outf.Close()
        if err == nil {
            err = cerr
        }
    }()

    ...
}
  1. El patrón de "doble cierre" donde uno hace defer outf.Close() para garantizar la limpieza de recursos y try(outf.Close()) antes de regresar para garantizar que no haya errores.
  2. Refactorice para que una función de ayuda tome el archivo abierto en lugar de una ruta para que la persona que llama pueda asegurarse de que el archivo se cierre correctamente. p.ej
func foo() error {
    outf := try(os.Create())
    if err := helper(outf); err != nil {
        outf.Close()
        return err
    }
    try(outf.Close())
    return nil
}

Creo que en todos los casos, excepto en el caso número 1, try es, en el peor de los casos, neutral y generalmente positivo. Y consideraría que el número 1 es la opción menos aceptable dado el tamaño y la cantidad de posibilidades de error en esa función, por lo que agregar try reduciría el atractivo de una opción negativa.

Espero que este análisis haya sido útil.

Si hipotéticamente algo como un nuevo try integrado aterriza en algo como Go 1.15, entonces en ese momento alguien cuyo archivo go.mod lee go 1.12 no tendría acceso

@thepudds gracias por explicar. Pero no uso módulos. Así que tu explicación está muy por encima de mi cabeza.

Alex

@alexbrainman

¿Cómo lidiaríamos con los errores de compilación cuando las personas usan go1.13 y antes para compilar código con try?

Si try aterrizara hipotéticamente en algo como Go 1.15, entonces la respuesta muy breve a su pregunta es que alguien que use Go 1.13 para compilar código con try vería un error de compilación como este:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

(Al menos hasta donde yo entiendo lo dicho sobre la propuesta de transición).

@alexbrainman Gracias por sus comentarios.

Una gran cantidad de comentarios en este hilo son del tipo "esto no se parece a Go", o "Go no funciona así", o "No espero que esto suceda aquí". Eso es todo correcto, _existing_ Go no funciona así.

Este es quizás el primer cambio de idioma sugerido que afecta la sensación del idioma de manera más sustancial. Somos conscientes de eso, por eso lo mantuvimos tan mínimo. (Me cuesta imaginar el alboroto que podría causar una propuesta genérica concreta, hablando de un cambio de idioma).

Pero volviendo a tu punto: los programadores se acostumbran a cómo funciona y se siente un lenguaje de programación. Si algo he aprendido en el transcurso de unos 35 años de programación es que uno se acostumbra a casi cualquier lenguaje, y sucede muy rápido. Después de haber aprendido Pascal original como mi primer lenguaje de alto nivel, era _inconcebible_ que un lenguaje de programación no pusiera en mayúsculas todas sus palabras clave. Pero solo tomó una semana más o menos acostumbrarse al "mar de palabras" que era C donde "uno no podía ver la estructura del código porque está todo en minúsculas". Después de esos días iniciales con C, el código de Pascal parecía terriblemente ruidoso, y todo el código real parecía enterrado en un lío de palabras clave gritando. Avance rápido a Go, cuando introdujimos las mayúsculas para marcar los identificadores exportados, fue un cambio impactante con respecto al enfoque anterior, si no recuerdo mal, basado en palabras clave (esto fue antes de que Go fuera público). Ahora creemos que es una de las mejores decisiones de diseño (con la idea concreta que en realidad proviene de fuera del Go Team). O considere el siguiente experimento mental: Imagine Go no tenía una declaración defer y ahora alguien presenta un caso sólido a favor de defer . defer no tiene una semántica como cualquier otra cosa en el idioma, el nuevo idioma no se siente así antes defer Ir más. Sin embargo, después de haber vivido con él durante una década, parece totalmente "Go-like".

El punto es que la reacción inicial hacia un cambio de idioma casi no tiene sentido sin probar el mecanismo en código real y recopilar comentarios concretos. Por supuesto, el código de manejo de errores existente está bien y se ve más claro que el reemplazo que usa try ; hemos sido entrenados para pensar en esas declaraciones if durante una década. Y, por supuesto, el código try se ve extraño y tiene una semántica "rara", nunca lo hemos usado antes y no lo reconocemos de inmediato como parte del lenguaje.

Es por eso que le pedimos a las personas que realmente se comprometan con el cambio al experimentarlo en su propio código; es decir, escribirlo realmente, o ejecutar tryhard sobre el código existente, y considerar el resultado. Recomiendo dejarlo reposar por un tiempo, tal vez una semana más o menos. Míralo de nuevo y vuelve a informar.

Finalmente, estoy de acuerdo con su evaluación de que la mayoría de las personas no conocen esta propuesta o no se han comprometido con ella. Está bastante claro que esta discusión está dominada por quizás una docena de personas. Pero aún es pronto, esta propuesta solo lleva dos semanas y no se ha tomado ninguna decisión. Hay mucho tiempo para que más y diferentes personas se comprometan con esto.

https://github.com/golang/go/issues/32437#issuecomment -503297387 prácticamente dice que si está envolviendo errores en más de una forma en una sola función, aparentemente lo está haciendo mal. Mientras tanto, tengo un montón de código que se ve así:

        if err := gen.Execute(tmp, s); err != nil {
                return fmt.Errorf("template error: %v", err)
        }

        if err := tmp.Close(); err != nil {
                return fmt.Errorf("cannot write temp file: %v", err)
        }
        closed = true

        if err := os.Rename(tmp.Name(), *genOutput); err != nil {
                return fmt.Errorf("cannot finalize file: %v", err)
        }
        removed = true

( closed y removed son utilizados por diferidos para limpiar, según corresponda)

Realmente no creo que a todos estos se les deba dar el mismo contexto que describe la misión de nivel superior de esta función. Realmente no creo que el usuario deba ver

processing path/to/dir: template: gen:42:17: executing "gen" at <.Broken>: can't evaluate field Broken in type main.state

cuando la plantilla se estropea, creo que es responsabilidad de mi controlador de errores que la plantilla Ejecute llame para agregar "plantilla en ejecución" o algo más. (Ese no es el mejor contexto, pero quería copiar y pegar código real en lugar de un ejemplo inventado).

No creo que el usuario deba ver

processing path/to/dir: rename /tmp/blahDs3x42aD commands.gen.go: No such file or directory

sin alguna pista de _por qué_ mi programa está tratando de hacer que suceda ese cambio de nombre, cuál es la semántica, cuál es la intención. Creo que agregar ese poco de "no se puede finalizar el archivo:" realmente ayuda.

Si estos ejemplos no te convencen lo suficiente, imagina esta salida de error de una aplicación de línea de comandos:

processing path/to/dir: open /some/path/here: No such file or directory

¿Que significa eso? Quiero agregar una razón por la cual la aplicación intentó crear un archivo allí (ni siquiera sabía que era una creación, ¡no solo os.Open! Es ENOENT porque no existe una ruta intermedia). Esto no es algo que deba agregarse a _todos_ los retornos de error de esta función.

Entonces, ¿qué me estoy perdiendo? ¿Estoy "sosteniéndolo mal"? ¿Se supone que debo insertar cada una de esas cosas en una pequeña función separada que usa un aplazamiento para envolver todos sus errores?

@guybrand Gracias por estos números . Sería bueno tener algunas ideas sobre por qué los números tryhard son lo que son. ¿Quizás hay una gran cantidad de decoración de errores específicos? Si es así, eso es genial y las declaraciones if son la elección correcta.

Mejoraré la herramienta cuando llegue a ella.

Gracias, @zeebo por tu análisis . No conozco este código específicamente, pero parece que outf es parte de un loadCmdReader (línea 173) que luego se pasa a la línea 204. Tal vez esa sea la razón por la que outf no está cerrado. (Lo siento, no escribí este código).

@tv42 De los ejemplos en su https://github.com/golang/go/issues/32437#issuecomment -503340426, suponiendo que no lo está haciendo "mal", parece que usa una instrucción if es la manera de manejar estos casos si todos requieren respuestas diferentes. try no ayudará, y defer solo lo hará más difícil (cualquier otra propuesta de cambio de idioma en este hilo que intente simplificar la escritura de este código está muy cerca del if declaración de que no vale la pena introducir un nuevo mecanismo). Consulte también las preguntas frecuentes de la propuesta detallada.

@griesemer Entonces todo lo que puedo pensar es que tú y @rsc no están de acuerdo. O que, de hecho, "lo estoy haciendo mal" y me gustaría tener una conversación al respecto.

Es un objetivo explícito, tanto en ese entonces como hoy, que cualquier solución haga más probable que los usuarios agreguen el contexto apropiado a los errores. Y pensamos que try puede hacer eso, o no lo hubiéramos propuesto.

La publicación de @tv42 @rsc trata sobre la estructura general de manejo de errores de un buen código, con lo que estoy de acuerdo. Si tiene una pieza de código existente que no se ajusta exactamente a este patrón y está satisfecho con el código, déjelo en paz.

aplaza

El cambio principal del borrador de verificación/manejo de Gophercon a esta propuesta fue eliminar handle a favor de reutilizar defer . Ahora el contexto de error se agregaría mediante un código como esta llamada diferida (consulte mi comentario anterior sobre el contexto de error):

func CopyFile(src, dst string) (err error) {
    defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

La viabilidad de defer como mecanismo de anotación de errores en este ejemplo depende de algunas cosas.

  1. _Resultados de error con nombre._ Ha habido mucha preocupación acerca de agregar resultados de error con nombre. Es cierto que hemos desaconsejado que en el pasado no sea necesario para fines de documentación, pero esa es una convención que elegimos en ausencia de un factor decisivo más fuerte. E incluso en el pasado, un factor decisivo más fuerte, como la referencia a resultados específicos en la documentación, superaba la convención general para resultados sin nombre. Ahora hay un segundo factor decisivo más fuerte, a saber, querer referirse al error en un aplazamiento. Eso parece que no debería ser más objetable que nombrar los resultados para su uso en la documentación. Varias personas han reaccionado bastante negativamente a esto y, sinceramente, no entiendo por qué. Casi parece que las personas están combinando devoluciones sin listas de expresiones (las llamadas "devoluciones desnudas") con resultados con nombre. Es cierto que los retornos sin listas de expresiones pueden generar confusión en funciones más grandes. Evitar esa confusión al evitar esos retornos en funciones largas a menudo tiene sentido. Pintar resultados nombrados con el mismo pincel no lo hace.

  2. _Expresiones de dirección._ Algunas personas han expresado su preocupación de que el uso de este patrón requerirá que los desarrolladores de Go entiendan las expresiones de dirección. Almacenar cualquier valor con métodos de puntero en una interfaz ya requiere eso, por lo que esto no parece ser un inconveniente importante.

  3. _Defer sí mismo._ Algunas personas han expresado su preocupación sobre el uso de defer como concepto de lenguaje, nuevamente porque los nuevos usuarios pueden no estar familiarizados con él. Al igual que con las expresiones de dirección, diferir es un concepto básico del lenguaje que debe aprenderse con el tiempo. Los modismos estándar en torno a cosas como defer f.Close() y defer l.mu.Unlock() son tan comunes que es difícil justificar evitar diferir como un rincón oscuro del idioma.

  4. _Rendimiento._ Hemos discutido durante años trabajar en hacer que los patrones de aplazamiento comunes, como un aplazamiento en la parte superior de una función, tengan cero gastos generales en comparación con insertar esa llamada a mano en cada retorno. Creemos que sabemos cómo hacerlo y lo exploraremos para el próximo lanzamiento de Go. Aunque no sea así, la sobrecarga actual de aproximadamente 50 ns no debería ser prohibitiva para la mayoría de las llamadas que necesitan agregar un contexto de error. Y las pocas llamadas sensibles al rendimiento pueden continuar usando declaraciones if hasta que defer sea más rápido.

Las primeras tres preocupaciones equivalen a objeciones a la reutilización de las características existentes del lenguaje. Pero reutilizar las características existentes del lenguaje es exactamente el avance de esta propuesta sobre la verificación/manejo: hay menos para agregar al lenguaje central, menos piezas nuevas para aprender y menos interacciones sorprendentes.

Aún así, apreciamos que el uso de defer de esta manera es nuevo y que debemos dar tiempo a las personas para evaluar si defer funciona lo suficientemente bien en la práctica para los modismos de manejo de errores que necesitan.

Desde que comenzamos esta discusión en agosto pasado, he estado haciendo el ejercicio mental de "¿cómo se vería este código con check/handle?" y, más recientemente, "¿con probar/aplazar?" cada vez que escribo código nuevo. Por lo general, la respuesta significa que escribo un código diferente y mejor, con el contexto agregado en un lugar (el aplazamiento) en lugar de en cada retorno u omitido por completo.

Dada la idea de usar un controlador diferido para tomar medidas sobre los errores, hay una variedad de patrones que podríamos habilitar con un paquete de biblioteca simple. Presenté el n.º 32676 para pensar más al respecto, pero al usar la API del paquete en ese problema, nuestro código se vería así:

func CopyFile(src, dst string) (err error) {
    defer errd.Add(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

Si estábamos depurando CopyFile y queríamos ver cualquier error devuelto y el seguimiento de la pila (similar a querer insertar una impresión de depuración), podríamos usar:

func CopyFile(src, dst string) (err error) {
    defer errd.Trace(&err)
    defer errd.Add(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

y así.

El uso de diferir de esta manera termina siendo bastante poderoso, y conserva la ventaja de verificar/manejar que puede escribir "hacer esto en cualquier error" una vez en la parte superior de la función y luego no preocuparse por el resto de el cuerpo. Esto mejora la legibilidad de la misma manera que las primeras salidas rápidas .

¿Funcionará esto en la práctica? Queremos averiguarlo.

Habiendo hecho el experimento mental de cómo se vería el aplazamiento en mi propio código durante unos meses, creo que es probable que funcione. Pero, por supuesto, llegar a usarlo en código real no siempre es lo mismo. Tendremos que experimentar para averiguarlo.

Las personas pueden experimentar con este enfoque hoy al continuar escribiendo declaraciones if err != nil pero copiando los ayudantes diferidos y usándolos según corresponda. Si está dispuesto a hacer esto, háganos saber lo que aprende.

@tv42 , estoy de acuerdo con @griesemer. Si encuentra que se necesita contexto adicional para suavizar una conexión, como que el cambio de nombre es un paso de "finalización", no hay nada de malo en usar declaraciones if para agregar contexto adicional. En muchas funciones, sin embargo, hay poca necesidad de tal contexto adicional.

@guybrand , los números de Tryhard son excelentes, pero aún mejores serían las descripciones de por qué los ejemplos específicos no se convirtieron y, además, habría sido inapropiado reescribirlos para poder convertirlos. El ejemplo y la explicación de @tv42 son una instancia de esto.

@griesemer sobre su preocupación por el aplazamiento. Iba por ese emit o en la propuesta inicial handle . El emit/handle se llamaría si el err no es nulo. Y se iniciará en ese momento en lugar de al final de la función. El aplazamiento se llama al final. emit/handle TERMINARÍA la función en función de si err es nulo o no. Es por eso que diferir no funcionaría.

algunos datos:

de un proyecto LOC de ~ 70k que he promocionado para eliminar religiosamente los "devoluciones de errores desnudos", todavía tenemos 612 devoluciones de errores desnudos. principalmente se trata de un caso en el que se registra un error, pero el mensaje solo es importante internamente (el mensaje para el usuario está predefinido). Sin embargo, try() tendrá un ahorro mayor que solo dos líneas por cada retorno simple, porque con errores predefinidos podemos diferir un controlador y usar try en más lugares.

Más interesante aún, en el directorio de proveedores, de ~ 620k + LOC, solo tenemos 1600 devoluciones de errores desnudos. Las bibliotecas que elegimos tienden a decorar los errores incluso más religiosamente que nosotros.

@rsc si, más tarde, se agregan controladores a try , ¿habrá un paquete de errores/errc con funciones como func Wrap(msg string) func(error) error para que pueda hacer try(f(), errc.Wrap("f failed")) ?

@damienfamed75 Gracias por tus explicaciones . Entonces, se llamará a emit cuando try encuentre un error, y se llamará con ese error. Eso parece bastante claro.

También está diciendo que emit terminaría la función si hay un error, y no si el error se manejó de alguna manera. Si no finaliza la función, ¿dónde continúa el código? Presumiblemente con el regreso de try (de lo contrario, no entiendo el emit que no finaliza la función). ¿No sería más fácil y claro en ese caso usar if en lugar de try ? El uso de emit o handle oscurecería enormemente el flujo de control en esos casos, especialmente porque la cláusula emit puede estar en una parte completamente diferente (presumiblemente anterior) en la función. (En ese sentido, ¿se puede tener más de un emit ? Si no, ¿por qué no? ¿Qué sucede si no hay un emit ? Muchas de las mismas preguntas que plagaron el check original handle borrador de diseño.)

Solo si uno quiere regresar de una función sin mucho trabajo adicional además de la decoración de errores, o con siempre el mismo trabajo, tiene sentido usar try y algún tipo de controlador. Y ese mecanismo controlador, que se ejecuta antes de que regrese una función, ya existe en defer .

@guybrand (y @griesemer) con respecto a su segundo patrón no reconocido, consulte https://github.com/griesemer/tryhard/issues/2

@daved

¿Cómo se derivará el valor de los datos cuando gran parte del proceso público esté impulsado por el sentimiento?

Tal vez otros puedan tener una experiencia como la mía reportada aquí . Esperaba hojear algunas instancias de try insertadas por tryhard , encontrar que se parecían más o menos a lo que ya existía en este hilo y seguir adelante. En cambio, me sorprendió encontrar un caso en el que try condujo a un código claramente mejor, de una manera que no se había discutido antes.

Así que al menos hay esperanza. :)

Para las personas que prueban tryhard , si aún no lo han hecho, les animo no solo a ver qué cambios hizo la herramienta, sino también a grep para las instancias restantes de err != nil y ver lo que dejó solo, y por qué.

(Y también tenga en cuenta que hay un par de problemas y relaciones públicas en https://github.com/griesemer/tryhard/).

@rsc aquí está mi idea de por qué personalmente no me gusta el patrón defer HandleFunc(&err, ...) . No es porque lo asocie con devoluciones desnudas ni nada, simplemente se siente demasiado "inteligente".

Hubo una propuesta de manejo de errores hace unos meses (¿quizás un año?), Sin embargo, ahora la he perdido de vista. Olvidé lo que estaba solicitando, sin embargo, alguien respondió con algo como:

func myFunction() (i int, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("wrapping the error: %s", err)
        }
    }()

    // ...
    return 0, err

    // ...
    return someInt, nil
}

Fue interesante ver por decir lo menos. Era la primera vez que veía que se usaba defer para el manejo de errores, y ahora se muestra aquí. Lo veo como "inteligente" y "hacky", y, al menos en el ejemplo que menciono, no se siente como Go. Sin embargo, envolverlo en una llamada de función adecuada con algo como fmt.HandleErrorf ayuda a que se sienta mucho mejor. Sin embargo, todavía me siento negativamente hacia eso.

Otra razón por la que puedo ver que a la gente no le gusta es que cuando uno escribe return ..., err , parece que se debe devolver err . Pero no se devuelve, sino que se modifica el valor antes de enviarlo. He dicho antes que return siempre me ha parecido una operación "sagrada" en Go, y alentar el código que modifica un valor devuelto antes de devolverlo simplemente se siente mal.

OK, números y datos son entonces. :)

Ejecuté Tryhard en las fuentes de varios servicios de nuestra plataforma de microservicios y lo comparé con los resultados de loccount y grep 'if err'. Obtuve los siguientes resultados en el orden loccount / grep 'if err' | wc / tryhard:

1382 / 64 / 14
108554 / 66 / 5
58401 / 22 / 5
2052/247/39
12024 / 1655 / 1

Algunos de nuestros microservicios manejan muchos errores y otros solo hacen poco, pero desafortunadamente, Tryhard solo pudo mejorar automáticamente el código, en el mejor de los casos, en el 22 % de los casos, en el peor de los casos, en menos del 1 %. Ahora, no vamos a reescribir manualmente nuestro manejo de errores, por lo que una herramienta como Tryhard será esencial para introducir try() en nuestra base de código. Agradezco que esta sea una herramienta preliminar simple, pero me sorprendió la poca frecuencia con la que pudo ayudar.

Pero creo que ahora, con el número en la mano, puedo decir que para nuestro uso, try() realmente no está resolviendo ningún problema, o al menos no hasta que tryhard sea mucho mejor.

También encontré en nuestras bases de código que el caso de uso de $ if err != nil { return err } de try() es realmente muy raro, a diferencia del compilador go, donde es común. Con el debido respeto, creo que los diseñadores de Go, que miran el código fuente del compilador de Go con mucha más frecuencia que otras bases de código, están sobreestimando la utilidad de try() debido a esto.

@beoran tryhard es muy rudimentario en este momento. ¿Tiene alguna idea de las razones más comunes por las que try sería raro en su base de código? Por ejemplo, porque decoras los errores? ¿Porque haces otro trabajo extra antes de volver? ¿Algo más?

@rsc , @griesemer

En cuanto a los ejemplos , di dos muestras repetitivas aquí que TryHard se perdió, una probablemente permanecerá como "if Err :=", la otra puede resolverse

en cuanto a la decoración de errores , dos patrones recurrentes que veo en el código son (puse los dos en un fragmento de código):

if v, err := someFunction(vars...) ; err != nil {
        return fmt.Errorf("extra data to help with where did error occur and params are %s , %d , err : %v",
            strParam, intParam, err)
    } else if v2, err := passToAnotherFunc(v,vars ...);err != nil {
        extraData := DoSomethingAccordingTo(v2,err)
        return formatError(err,extraData)
    } else {

    }

Y muchas veces, formatError es un estándar para la aplicación o alguna vez entre repositorios, la mayoría de las veces se repite el formato DbError (una función en todas las aplicaciones, que se usa en docenas de ubicaciones), en algunos casos (sin entrar en "es esto un patrón correcto") guardando algunos datos para registrar (fallando la consulta sql, no le gustaría dejar pasar la pila) y algún otro texto para el error.

En otras palabras, si quiero "hacer algo inteligente con datos adicionales, como registrar el error A y generar el error B, además de mi mención de estas dos opciones para extender el manejo de errores
Esta es otra opción para "más que solo devolver el error y dejar que 'alguien más' o 'alguna otra función' lo maneje"

Lo que significa que probablemente haya más uso de try() en "bibliotecas" que en "programas ejecutables", tal vez intente ejecutar la comparación Total/Errs/tryHard diferenciando libs de ejecutables ("aplicaciones").

Me encontré exactamente en la situación descrita en https://github.com/golang/go/issues/32437#issuecomment -503297387
En algún nivel, envuelvo los errores individualmente, no cambiaré esto con try , está bien con if err!=nil .
En otro nivel, solo return err es una molestia agregar el mismo contexto para todos los retornos, luego usaré try y defer .
Incluso ya hago esto con un registrador específico que uso al comienzo de la función en caso de error. Para mí try y la decoración por función ya es goish.

@thepudds

Si try iba a aterrizar hipotéticamente en algo como Go 1.15, entonces la respuesta muy breve a su pregunta es que alguien que usa Go 1.13

Go 1.13 aún no se ha lanzado, por lo que no puedo usarlo. Y dado que mi proyecto no usa módulos Go, no podré actualizar a Go 1.13. (Creo que Go 1.13 requerirá que todos usen módulos Go)

para compilar código con try vería un error de compilación como este:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

(Al menos hasta donde yo entiendo lo dicho sobre la propuesta de transición).

Eso es todo hipotético. Es difícil para mí comentar sobre cosas ficticias. Y, tal vez te guste ese error, pero lo encuentro confuso e inútil.

Si try no está definido, lo buscaría. Y no encontraré nada. ¿Que debería hacer entonces?

Y el note: module requires Go 1.15 es la peor ayuda en esta situación. ¿Por qué module ? ¿Por qué Go 1.15 ?

@griesemer

Este es quizás el primer cambio de idioma sugerido que afecta la sensación del idioma de manera más sustancial. Somos conscientes de eso, por eso lo mantuvimos tan mínimo. (Me cuesta imaginar el alboroto que podría causar una propuesta genérica concreta, hablando de un cambio de idioma).

Prefiero que dediques tiempo a los genéricos, en lugar de intentarlo. Tal vez haya un beneficio en tener genéricos en Go.

Pero volviendo a tu punto: los programadores se acostumbran a cómo funciona y se siente un lenguaje de programación. ...

Estoy de acuerdo con todos tus puntos. Pero estamos hablando de reemplazar una forma particular de la declaración if con una llamada a la función try. Esto está en el lenguaje que se enorgullece de la simplicidad y la ortogonalidad. Puedo acostumbrarme a todo, pero ¿cuál es el punto? ¿Para guardar un par de líneas de código?

O considere el siguiente experimento mental: Imagine Go no tenía una declaración defer y ahora alguien presenta un caso sólido a favor de defer . defer no tiene una semántica como cualquier otra cosa en el idioma, el nuevo idioma no se siente así antes defer Ir más. Sin embargo, después de haber vivido con él durante una década, parece totalmente "Go-like".

Después de muchos años, sigo siendo engañado por el cuerpo de defer y cerré las variables. Pero defer paga su precio con creces cuando se trata de la gestión de recursos. No me puedo imaginar Go sin defer . Pero no estoy preparado para pagar un precio similar por try , porque no veo beneficios aquí.

Es por eso que le pedimos a las personas que realmente se comprometan con el cambio al experimentarlo en su propio código; es decir, escribirlo realmente, o ejecutar tryhard sobre el código existente, y considerar el resultado. Recomiendo dejarlo reposar por un tiempo, tal vez una semana más o menos. Míralo de nuevo y vuelve a informar.

Intenté cambiar un pequeño proyecto mío (alrededor de 1200 líneas de código). Y se parece a tu cambio en https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go No veo mi opinión cambio sobre esto después de una semana. Mi mente siempre está ocupada con algo, y lo olvidaré.

... Pero aún es pronto, esta propuesta solo lleva dos semanas, ...

Y puedo ver que ya hay 504 mensajes sobre esta propuesta solo en este hilo. Si estuviera interesado en impulsar este cambio, me llevaría días, si no semanas, leer y comprender todo esto. No envidio tu trabajo.

Gracias por tomarse el tiempo para responder a mi mensaje. Lo siento, si no respondo a este hilo, es demasiado grande para que yo lo controle, si el mensaje está dirigido a mí o no.

Alex

@griesemer Gracias por la maravillosa propuesta y Tryhard parece ser más útil de lo que esperaba. También voy a querer apreciar.

@rsc gracias por la respuesta y la herramienta bien articuladas.

He estado siguiendo este hilo por un tiempo y los siguientes comentarios de @beoran me dan escalofríos.

Ocultar la variable de error y el retorno no ayuda a que las cosas sean más fáciles de entender.

He manejado varios bad written code antes y puedo testificar que es la peor pesadilla para todos los desarrolladores.

El hecho de que la documentación diga usar A me gusta no significa que se siga, el hecho es que si es posible usar AA , AB entonces no hay límite en cuanto a cómo puede ser usado.

To my surprise, people already think the code below is cool ... creo que it's an abomination con todo respeto disculpas a quien se haya ofendido.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

Espera hasta que marques AsCommit y verás

func AsCommit() error(){
    return try(try(try(tail()).find()).auth())
}

La locura continúa y, sinceramente, no quiero creer que esta sea la definición de @roppike simplicity is complicated (Humor)

Basado en el ejemplo de @rsc

// Example 1
headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// Example 2 
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// Example 3 
try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

Estoy a favor de Example 2 con un poco else Sin embargo, tenga en cuenta que este podría no ser el mejor enfoque.

  • Es fácil ver claramente el error.
  • Lo menos posible para mutar en el abomination que los demás pueden dar a luz
  • try no se comporta como una función normal. darle una sintaxis similar a una función es poco. go usa if y si puedo cambiarlo a try tree := r.LookupTree(treeOid) else { se siente más natural
  • Los errores pueden ser muy, muy costosos, necesitan la mayor visibilidad posible y creo que esa es la razón por la que go no admitía los tradicionales try y catch
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)

parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()

try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid) else { 
    // Heal the world 
   // I may return with return keyword 
   // I may not return but set some values to 0 
   // I may remember I need to log only this 
   // I may send a mail to let the cute monkeys know the server is on fire 
}

Una vez más quiero disculparme por ser un poco egoísta.

@josharian No puedo divulgar demasiado aquí, sin embargo, las razones son bastante diversas. Como usted dice, decoramos los errores, y también hacemos un procesamiento diferente, y también, un caso de uso importante es que los registramos, donde el mensaje de registro difiere para cada error que puede devolver una función, o porque usamos el if err := foo() ; err != nil { /* various handling*/ ; return err } formulario u otras razones.

Lo que quiero enfatizar es esto: el caso de uso simple para el cual está diseñado try() ocurre muy raramente en nuestra base de código. Entonces, para nosotros no hay mucho que ganar agregando 'try()' al lenguaje.

EDITAR: Si se va a implementar try(), entonces creo que el siguiente paso debería ser hacer que Tryhard sea mucho mejor, de modo que pueda usarse ampliamente para actualizar las bases de código existentes.

@griesemer Intentaré abordar todas sus inquietudes una por una desde su última respuesta .
Primero preguntó si el controlador no regresa o sale de la función de alguna manera, entonces qué sucedería. Sí, puede haber instancias en las que la cláusula emit / handle no regrese o salga de una función, sino que continúe donde se quedó. Por ejemplo, en el caso de que estemos tratando de encontrar un delimitador o algo simple usando un lector y lleguemos a EOF , es posible que no queramos devolver un error cuando lo alcancemos. Así que construí este ejemplo rápido de cómo podría verse:

func findDelimiter(r io.Reader) ([]byte, error) {
    emit err {
        // if this doesn't return then continue from where we left off
        // at the try function that was called last.
        if err != io.EOF {
            return nil, err
        }
    }

    bufReader := bufio.NewReader(r)

    token := try(bufReader.ReadSlice('|'))

    return token, nil
}

O incluso podría simplificarse aún más a esto:

func findDelimiter(r io.Reader) ([]byte, error) {
    emit err != io.EOF {
        return nil, err
    }

    bufReader := bufio.NewReader(r)

    token := try(bufReader.ReadSlice('|'))

    return token, nil
}

La segunda preocupación fue sobre la interrupción del flujo de control. Y sí, interrumpiría el flujo, pero para ser justos, la mayoría de las propuestas interrumpen un poco el flujo para tener una función central de manejo de errores y tal. Esto no es diferente, creo.
A continuación, preguntó si usamos emit / handle más de una vez en lo que digo que está redefinido.
Si usa emit más de una vez, sobrescribirá el último y así sucesivamente. Si no tiene ninguno, try tendrá un controlador predeterminado que solo devuelve valores nulos y el error. Eso significa que este ejemplo aquí:

func writeStuff(filename string) (io.ReadCloser, error) {
    emit err {
        return nil, err
    }

    f := try(os.Open(filename))

    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Haría lo mismo que este ejemplo:

func writeStuff(filename string) (io.ReadCloser, error) {
    // when not defining a handler then try's default handler kicks in to
    // return nil valued then error as usual.

    f := try(os.Open(filename))

    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Su última pregunta fue sobre la declaración de una función de controlador que se llama en un defer con una referencia a un error . Este diseño no funciona de la misma manera que esta propuesta porque un defer no puede detener inmediatamente una función dada una condición en sí misma.

Creo que abordé todo en su respuesta y espero que esto aclare mi propuesta un poco más. Si hay más inquietudes, háganmelo saber porque creo que toda esta discusión con todos es bastante divertida para reflexionar sobre nuevas ideas. Sigan con el gran trabajo de todos!

@velovix , re https://github.com/golang/go/issues/32437#issuecomment -503314834:

Nuevamente, esto significa que try apunta a reaccionar a los errores, pero no a agregarles contexto. Esa es una distinción que me ha aludido a mí y quizás a otros. Esto tiene sentido porque la forma en que una función agrega contexto a un error no es de particular interés para un lector, pero la forma en que una función reacciona a los errores es importante. Deberíamos hacer que las partes menos interesantes de nuestro código sean menos detalladas, y eso es lo que hace try .

Esta es una muy buena manera de decirlo. Gracias.

@olekukonko , re https://github.com/golang/go/issues/32437#issuecomment -503508478:

To my surprise, people already think the code below is cool ... creo que it's an abomination con todo respeto disculpas a quien se haya ofendido.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

Grepping https://swtch.com/try.html , esa expresión se ha producido tres veces en este hilo.
@goodwine lo mencionó como un código incorrecto, estuve de acuerdo y @velovix dijo: "a pesar de su fealdad... es mejor que lo que se ve a menudo en los lenguajes de prueba... porque aún puede saber qué partes del código pueden desviar controlar el flujo debido a un error y cuál no puede".

Nadie dijo que fuera "genial" o algo para presentar como un gran código. Una vez más, siempre es posible escribir un código incorrecto .

Yo también diría re

Los errores pueden ser muy, muy costosos, necesitan la mayor visibilidad posible.

Los errores en Go están destinados a no ser costosos. Son sucesos cotidianos, ordinarios y destinados a ser ligeros. (Esto contrasta con algunas implementaciones de excepciones en particular. Una vez tuvimos un servidor que dedicaba demasiado tiempo de su CPU a preparar y descartar objetos de excepción que contenían seguimientos de pila para llamadas fallidas de "apertura de archivos" en un bucle que verificaba una lista de conocidos ubicaciones para un archivo dado.)

@alexbrainman , lamento la confusión sobre lo que sucede si las versiones anteriores del código de compilación de Go contienen try. La respuesta corta es que es como cualquier otra vez que cambiamos el idioma: el compilador anterior rechazará el nuevo código con un mensaje en su mayoría inútil (en este caso, "indefinido: intente"). El mensaje no es útil porque el compilador anterior no conoce la nueva sintaxis y realmente no puede ser más útil. En ese momento, la gente probablemente haría una búsqueda en la web de "ir a prueba indefinida" y averiguaría sobre la nueva característica.

En el ejemplo de @thepudds , el código que usa try tiene un go.mod que contiene la línea 'go 1.15', lo que significa que el autor del módulo dice que el código está escrito contra la versión del lenguaje Go. Esto sirve como una señal para que los comandos go más antiguos sugieran después de un error de compilación que tal vez el mensaje inútil se deba a tener una versión demasiado antigua de Go. Esto es explícitamente un intento de hacer que el mensaje sea un poco más útil sin obligar a los usuarios a recurrir a búsquedas en la web. Si ayuda, bien; si no, las búsquedas en la web parecen bastante efectivas de todos modos.

@guybrand , re https://github.com/golang/go/issues/32437#issuecomment -503287670 y con disculpas por llegar demasiado tarde a su reunión:

Un problema en general con las funciones que devuelven tipos que no son del todo errores es que, para las que no son interfaces, la conversión a error no preserva la nulidad. Entonces, por ejemplo, si tiene su propio tipo concreto personalizado *MyError (por ejemplo, un puntero a una estructura) y usa err == nil como señal de éxito, eso es genial hasta que tenga

func f() (int, *MyError)
func g() (int, error) { x, err := f(); return x, err }

Si f devuelve un *MyError nulo, g devuelve el mismo valor como un error no nulo, que probablemente no sea lo que se pretendía. Si *MyError es una interfaz en lugar de un puntero de estructura, entonces la conversión conserva la nulidad, pero aún así es una sutileza.

Para probar, podría pensar que, dado que probar solo se activaría para valores no nulos, no hay problema. Por ejemplo, esto está bien en cuanto a devolver un error no nulo cuando f falla, y también está bien en cuanto a devolver un error nulo cuando f tiene éxito:

func g() (int, error) {
    return try(f()), nil
}

Así que en realidad está bien, pero es posible que veas esto y pienses en reescribirlo para

func g() (int, error) {
    return f()
}

que parece que debería ser lo mismo pero no lo es.

Hay suficientes otros detalles de la propuesta de prueba que necesitan un examen cuidadoso y una evaluación en la experiencia real, por lo que parecía que sería mejor posponer decidir sobre esta sutileza en particular.

Gracias a todos por todos los comentarios hasta ahora . En este punto, parece que hemos identificado los principales beneficios, preocupaciones y posibles implicaciones buenas y malas de try . Para progresar, es necesario evaluarlos más a fondo analizando qué significaría try para las bases de código reales. La discusión en este punto está dando vueltas y repitiendo esos mismos puntos.

La experiencia es ahora más valiosa que la discusión continua. Queremos alentar a las personas a que se tomen el tiempo de experimentar cómo se vería try en sus propias bases de código y escribir y vincular informes de experiencia en la página de comentarios .

Para darles a todos algo de tiempo para respirar y experimentar, pausaremos esta conversación y bloquearemos el problema durante la próxima semana y media.

El bloqueo comenzará alrededor de 1p PDT/4p EDT (dentro de aproximadamente 3 horas a partir de ahora) para que las personas tengan la oportunidad de enviar una publicación pendiente. Reabriremos el tema para más discusión el 1 de julio.

Tenga la seguridad de que no tenemos la intención de apresurar ninguna característica nueva del lenguaje sin tomarnos el tiempo para entenderlas bien y asegurarnos de que están resolviendo problemas reales en código real. Nos tomaremos el tiempo necesario para hacerlo bien, tal como lo hemos hecho en el pasado.

Esa página wiki está repleta de respuestas para verificar/manejar. Te sugiero que comiences una nueva página.

En cualquier caso, no tendré tiempo para continuar con la jardinería en la wiki.

@networkimprov , gracias por tu ayuda con la jardinería. Creé una nueva sección superior en https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback. Creo que debería ser mejor que una página completamente nueva.

También me perdí el billete de 1p PDT / 4p EDT de Robert para el candado, así que lo bloqueé brevemente un poco antes de tiempo. Está abierto de nuevo, por un poco más de tiempo.

Estaba planeando escribir esto, y solo quería completarlo antes de que se bloquee.

Espero que el equipo de go no vea las críticas y sienta que es indicativo del sentimiento mayoritario. Siempre existe la tendencia de que la minoría vocal abrume la conversación, y siento que eso podría haber sucedido aquí. Cuando todos se van por la tangente, desalienta a otros que solo quieren hablar sobre la propuesta TAL CUAL.

Entonces, me gustaría articular mi posición positiva por lo que vale.

Tengo un código que ya usa diferir para decorar/anotar errores, incluso para escupir rastros de pila, exactamente por este motivo.

Ver:
https://github.com/ugorji/go-ndb/blob/master/ndb/ndb.go#L331
https://github.com/ugorji/go-serverapp/blob/master/app/baseapp.go#L129
https://github.com/ugorji/go-serverapp/blob/master/app/webrouter.go#L180

que todos llaman errorutil.OnError(*error)

https://github.com/ugorji/go-common/blob/master/errorutil/errors.go#L193

Esto está en la línea de los ayudantes diferidos que Russ/Robert mencionaron anteriormente.

Es un patrón que ya uso, FWIW. No es magia. Es completamente como en mi humilde opinión.

También lo uso con parámetros con nombre, y funciona excelentemente.

Digo esto para cuestionar la noción de que cualquier cosa recomendada aquí es mágica.

En segundo lugar, quería agregar algunos comentarios sobre try(...) como función.
Tiene una clara ventaja sobre una palabra clave, ya que puede extenderse para tomar parámetros.

Hay 2 modos de extensión que se han discutido aquí:

  • extender tratar de tomar una etiqueta para saltar a
  • extender tratar de tomar un controlador de la forma func(error) error

Para cada uno de ellos, se necesita que trate como función tomar un solo parámetro, pudiendo extenderse más adelante para tomar un segundo parámetro si es necesario.

No se ha tomado la decisión de si es necesario extender el intento y, de ser así, qué dirección tomar. En consecuencia, la primera dirección es tratar de eliminar la mayor parte del tartamudeo "if err != nil { return err }" que siempre he odiado pero que tomé como el costo de hacer negocios en marcha.

Personalmente, me alegro de que try sea una función, que puedo llamar en línea, por ejemplo, puedo escribir

var u User = db.loadUser(try(strconv.Atoi(stringId)))

Opuesto a:

var id int // i have to define this on its own if err is already defined in an enclosing block
id, err = strconv.Atoi(stringId)
if err != nil {
  return
}
var u User = db.loadUser(id)

Como puede ver, acabo de reducir 6 líneas a 1. Y 5 de esas líneas son realmente repetitivas.
Esto es algo con lo que me he enfrentado muchas veces, y he escrito muchos códigos y paquetes de go. Puede consultar mi github para ver algunos de los que he publicado en línea, o mi biblioteca de códecs de go.

Finalmente, muchos de los comentarios aquí no han mostrado realmente problemas con la propuesta, sino que han planteado su propia forma preferida de resolver el problema.

Personalmente, estoy encantado de que try(...) esté llegando. Y aprecio las razones por las que try como función es la solución preferida. Claramente me gusta que se use aplazar aquí, ya que solo tiene sentido.

Recordemos uno de los principios básicos de go: conceptos ortogonales que se pueden combinar bien. Esta propuesta aprovecha un montón de conceptos ortogonales de go (aplazamiento, parámetros de devolución con nombre, funciones integradas para hacer lo que no es posible a través del código de usuario, etc.) para proporcionar el beneficio clave que
Los usuarios de go han solicitado universalmente durante años, es decir, reducir/eliminar el estándar if err != nil { return err }. Las encuestas de usuarios de Go muestran que este es un problema real. El equipo de go es consciente de que es un problema real. Me alegro de que las voces fuertes de unos pocos no estén sesgando demasiado la posición del equipo de go.

Tenía una pregunta sobre probar como un goto implícito si err != nil.

Si decidimos que esa es la dirección, ¿será difícil adaptar "intentar hacer un retorno" a "intentar hacer un ir a",
dado que goto tiene una semántica definida que no puede pasar por variables no asignadas?

Gracias por tu nota, @ugorji.

Tenía una pregunta sobre probar como un goto implícito si err != nil.

Si decidimos que esa es la dirección, ¿será difícil adaptar "intentar hacer un retorno" a "intentar hacer un ir a",
dado que goto tiene una semántica definida que no puede pasar por variables no asignadas?

Sí, exactamente correcto. Hay alguna discusión sobre #26058.
Creo que 'try-goto' tiene al menos tres faltas:
(1) tienes que responder variables no asignadas,
(2) pierde la información de la pila sobre qué intento falló, que, por el contrario, aún puede capturar en el caso de devolución + aplazamiento, y
(3) a todo el mundo le encanta odiar en goto.

Sí, try es el camino a seguir.
Intenté agregar try una vez y me gustó.
Parche: https://github.com/ascheglov/go/pull/1
Tema en Reddit: https://www.reddit.com/r/golang/comments/6vt3el/the_try_keyword_proofofconcept/

@griesemer

Continuando desde https://github.com/golang/go/issues/32825#issuecomment -507120860...

De acuerdo con la premisa de que el abuso de try se mitigará mediante la revisión del código, la investigación de antecedentes y/o los estándares de la comunidad, puedo ver la sabiduría de evitar cambiar el lenguaje para restringir la flexibilidad de try . No veo la sabiduría de proporcionar instalaciones adicionales que alienten fuertemente las manifestaciones más difíciles/desagradables de consumir.

Al desglosar esto un poco, parece que se expresan dos formas de flujo de control de ruta de error: Manual y Automático. Con respecto a la envoltura de errores, parece que se expresan tres formas: directa, indirecta y de transferencia. Esto da como resultado seis "modos" totales de manejo de errores.

Los modos Directo Manual y Directo Automático parecen agradables:

wrap := func(err error) error {
  return fmt.Errorf("failed to process %s: %v", filename, err)
}

f, err := os.Open(filename)
if err != nil {
    return nil, wrap(err)
}
defer f.Close()

info, err := f.Stat()
if err != nil {
    return nil, wrap(err)
}
// in errors, named better, and optimized
WrapfFunc := func(format string, args ...interface{}) func(error) error {
  return func(err error) error {
    if err == nil {
      return nil
    }
    s := fmt.Sprintf(format, args...)
    return errors.Errorf(s+": %w", err)
  }
}

```ir
envolver := errores.WrapfFunc("Error al procesar %s", nombre de archivo)

f, err := os.Open(nombre de archivo)
intentar (envolver (err))
aplazar f.Cerrar()

info, err := f.Stat()
intentar (envolver (err))

Manual Pass-through, and Automatic Pass-through modes are also simple enough to be agreeable (despite often being a code smell):
```go
f, err := os.Open(filename)
if err != nil {
    return nil, err
}
defer f.Close()

info, err := f.Stat()
if err != nil {
    return nil, err
}
f := try(os.Open(filename))
defer f.Close()

info := try(f.Stat())

Sin embargo, los modos Manual Indirecto y Automático Indirecto son bastante desagradables debido a la alta probabilidad de errores sutiles:

defer errd.Wrap(&err, "failed to do X for %s", filename)

var f *os.File
f, err = os.Open(filename)
if err != nil {
    return
}
defer f.Close()

var info os.FileInfo
info, err = f.Stat()
if err != nil {
    return
}
defer errd.Wrap(&err, "failed to do X for %s", filename)

f := try(os.Open(filename))
defer f.Close()

info := try(f.Stat())

Nuevamente, puedo entender que no se los prohíba, sino que facilitar/bendecir los modos indirectos es donde esto todavía me genera claras señales de alerta. Lo suficiente, en este momento, para que yo permanezca enfáticamente escéptico de toda la premisa.

Try no debe ser una función para evitar ese maldito

info := try(try(os.Open(filename)).Stat())

fuga de archivos

Me refiero a que la instrucción try no permitirá el encadenamiento. Y se ve mejor como un bono. Sin embargo, hay problemas de compatibilidad.

@sirkon Dado que try es especial, el lenguaje podría no permitir try anidados si eso es importante, incluso si try parece una función. Nuevamente, si este es el único obstáculo para try , eso podría solucionarse fácilmente de varias maneras ( go vet o restricción de idioma). Pasemos de esto, lo hemos escuchado muchas veces ahora. Gracias.

Pasemos de esto, lo hemos escuchado muchas veces antes.

"Esto es tan aburrido, vamos a pasar de esto"

Hay otro buen análogo:

- ¡Tu teoría contradice los hechos!
- ¡Peor para los hechos!

por Hegel

Quiero decir que estás resolviendo un problema que de hecho no existe. Y la manera fea en eso.

Echemos un vistazo a dónde aparece realmente este problema: manejar los efectos secundarios del mundo exterior, eso es todo. Y esto en realidad es una de las partes más fáciles lógicamente en la ingeniería de software. Y lo más importante en eso. No puedo entender por qué necesitamos una simplificación para lo más fácil que nos costará menos confiabilidad.

En mi opinión, el problema más difícil de este tipo es la preservación de la consistencia de los datos en sistemas distribuidos (y no tan distribuidos, de hecho). Y el manejo de errores no era un problema con el que estaba luchando en Go al resolverlos. La falta de comprensión de cortes y mapas, la falta de suma/algebraica/varianza/cualquier tipo era MUCHO más molesto.

Dado que el debate aquí parece continuar sin cesar, permítanme repetir de nuevo:

La experiencia es ahora más valiosa que la discusión continua. Queremos alentar a las personas a que se tomen el tiempo de experimentar cómo se vería try en sus propias bases de código y escribir y vincular informes de experiencia en la página de comentarios.

Si la experiencia concreta proporciona evidencia significativa a favor o en contra de esta propuesta, nos gustaría escuchar eso aquí. Manías personales, escenarios hipotéticos, diseños alternativos, etc. podemos reconocer, pero son menos accionables.

Gracias.

No quiero ser grosero aquí, y agradezco toda su moderación, pero la comunidad ha hablado muy enfáticamente sobre el cambio del manejo de errores. Cambiar cosas o agregar código nuevo molestará a _todas_ las personas que prefieren el sistema actual. No puedes hacer felices a todos, así que concentrémonos en el 88% que podemos hacer felices (número derivado de la proporción de votos a continuación).

En el momento de escribir este artículo, el hilo "déjalo en paz" tiene 1322 votos a favor y 158 en contra. Este hilo está en 158 arriba y 255 abajo. Si ese no es el final directo de este hilo sobre el manejo de errores, entonces deberíamos tener una muy buena razón para seguir insistiendo en el tema.

Es posible hacer siempre lo que su comunidad pide a gritos y destruir su producto al mismo tiempo.

Como mínimo, creo que esta propuesta específica debe considerarse fallida.

Afortunadamente, go no está diseñado por un comité. Necesitamos confiar en que los custodios del idioma que todos amamos continuarán tomando la mejor decisión con todos los datos disponibles y no tomarán una decisión basada en la opinión popular de las masas. Recuerde: ellos también usan go, al igual que nosotros. Sienten los puntos de dolor, al igual que nosotros.

Si tienes una posición, tómate el tiempo para defenderla como el equipo de Go defiende sus propuestas. De lo contrario, simplemente está ahogando la conversación con sentimientos de vuelo nocturno que no son procesables y no llevan adelante las conversaciones. Y hace que sea más difícil para las personas que quieren participar, ya que dichas personas pueden querer esperar hasta que el ruido se apague.

Cuando comenzó el proceso de propuesta, Russ hizo un gran esfuerzo por evangelizar la necesidad de informes de experiencia como una forma de influir en una propuesta o hacer que se escuche su solicitud. Tratemos al menos de honrar eso.

El equipo de go ha tenido en cuenta todos los comentarios procesables. Todavía no nos han fallado. Vea los documentos detallados producidos para alias, módulos, etc. Al menos démosles la misma atención y dediquemos tiempo a pensar en nuestras objeciones, responder a su posición sobre sus objeciones y hacer que sea más difícil ignorar su objeción.

El beneficio de Go siempre ha sido que es un lenguaje pequeño y simple con construcciones ortogonales diseñado por un pequeño grupo de personas que pensarían en el espacio críticamente antes de comprometerse con una posición. Ayudémoslos en lo que podamos, en lugar de simplemente decir "mira, el voto popular dice que no", donde muchas personas que votan ni siquiera tienen mucha experiencia en go o no entienden completamente. He leído carteles en serie que admitieron que no conocen algunos conceptos fundamentales de este lenguaje ciertamente pequeño y simple. Eso hace que sea difícil tomar en serio sus comentarios.

De todos modos, apesta que esté haciendo esto aquí. Siéntase libre de eliminar este comentario. No me ofenderé. ¡Pero alguien tiene que decir esto sin rodeos!

Todo esto de la segunda propuesta me parece muy similar a los influencers digitales que organizan un mitin. Los concursos de popularidad no evalúan los méritos técnicos.

La gente puede estar en silencio, pero todavía esperan Go 2. Personalmente, espero con ansias este y el resto de Go 2. Go 1 es un gran lenguaje y se adapta bien a diferentes tipos de programas. Espero que Go 2 amplíe eso.

Finalmente, también revertiré mi preferencia por tener try como declaración. Ahora apoyo la propuesta tal como está. Después de tantos años bajo la promesa de compatibilidad "Go 1", la gente piensa que Go ha sido tallado en piedra. Debido a esa suposición problemática, no cambiar la sintaxis del idioma en este caso me parece un compromiso mucho mejor ahora. Editar: también espero ver los informes de experiencia para verificar los hechos.

PD: Me pregunto qué tipo de oposición ocurrirá cuando se propongan genéricos.

Tenemos alrededor de una docena de herramientas escritas de una vez en nuestra empresa. Ejecuté la herramienta Tryhard contra nuestra base de código y encontré 933 candidatos potenciales para Try(). Personalmente, creo que la función try() es una idea brillante porque resuelve más que un simple problema de código repetitivo.

Obliga a la función/método llamado y llamado a devolver el error como el último parámetro. Esto no estará permitido:

var file= try(parse())

func parse()(err, result) {
}

Hace cumplir una forma de lidiar con los errores en lugar de declarar la variable de error y permitir vagamente el patrón err!=nil err==nil, lo que dificulta la legibilidad, aumenta el riesgo de código propenso a errores en IMO:

func Foo() (err error) {
    var file, ferr = os.Open("file1.txt")
    if ferr == nil {
               defer file.Close()
        var parsed, perr = parseFile(file)
        if perr != nil {
            return
        }
        fmt.Printf("%s", parsed)
    }
    return nil
}

Con try(), el código es más legible, consistente y seguro en mi opinión:

func Foo() (err error) {
    var file = try(os.Open("file.txt"))
        defer file.Close()
    var parsed = try(parseFile(file))
    fmt.Printf(parsed)
    return
}

Realicé algunos experimentos similares a los que hizo @lpar en todos los repositorios Go no archivados de Heroku (públicos y privados).

Los resultados están en esta esencia: https://gist.github.com/freeformz/55abbe5da61a28ab94dbb662bfc7f763

cc @davecheney

@ubikenobi Su función más segura ~is~ se estaba filtrando.

Además, nunca he visto un valor devuelto después de un error. Sin embargo, podría imaginar que tiene sentido cuando una función tiene que ver con el error y los otros valores devueltos no dependen del error en sí (tal vez lo que lleva a dos retornos de error con el segundo "protegiendo" los valores anteriores).

Por último, aunque no es común, err == nil proporciona una prueba legítima para algunos retornos anticipados.

@David

Gracias por señalar sobre la fuga, olvidé agregar defer.Close() en ambos ejemplos. (actualizado ahora).

Rara vez veo err return en ese orden también, pero aún así es bueno poder detectarlos en tiempo de compilación si son errores que por diseño.

Veo el caso err==nil como una excepción que una norma en la mayoría de los casos. Puede ser útil en algunos casos, como mencionaste, pero lo que no me gusta es que los desarrolladores elijan de manera inconsistente sin una razón válida. Afortunadamente, en nuestra base de código, la gran mayoría de las sentencias son err!=nil, que pueden beneficiarse fácilmente de la función try().

  • Ejecuté tryhard contra una gran API de Go que mantengo con un equipo de otros cuatro ingenieros a tiempo completo. En 45580 líneas de código Go, tryhard identificó 301 errores para reescribir (por lo tanto, sería un cambio de +301/-903), o reescribiría alrededor del 2% del código suponiendo que cada error ocupa aproximadamente 3 líneas. Teniendo en cuenta los comentarios, los espacios en blanco, las importaciones, etc., eso me parece sustancial.
  • He estado usando la herramienta de línea de Tryhard para explorar cómo try cambiaría mi trabajo y, subjetivamente, me resulta muy agradable. El verbo intentar me parece más claro de que algo podría salir mal en la función de llamada y lo logra de manera compacta. Estoy muy acostumbrado a escribir if err != nil , y realmente no me importa, pero tampoco me importaría cambiar. Escribir y refactorizar la variable vacía que precede al error (es decir, hacer que la porción/mapa/variable vacía devuelva) repetidamente es probablemente más tedioso que el err en sí mismo.
  • Es un poco difícil seguir todos los hilos de discusión, pero tengo curiosidad por saber qué significa esto para envolver errores. Sería bueno si try fuera variable si quisiera agregar un contexto opcional como try(json.Unmarshal(b, &accountBalance), "failed to decode bank account info for user %s", user) . Editar: este punto probablemente esté fuera de tema; Sin embargo, al mirar las reescrituras que no son de prueba, aquí es donde sucede esto.
  • ¡Realmente aprecio el pensamiento y el cuidado que se está poniendo en esto! La compatibilidad con versiones anteriores y la estabilidad son muy importantes para nosotros y el esfuerzo de Go 2 hasta la fecha ha sido muy fluido para mantener proyectos. ¡Gracias!

¿No debería hacerse esto en una fuente que haya sido examinada por Gophers experimentados para garantizar que los reemplazos sean racionales? ¿Cuánto de esa reescritura del "2%" debería haberse reescrito con un manejo explícito? Si no sabemos eso, entonces LOC sigue siendo una métrica relativamente inútil.

*Es exactamente por eso que mi publicación de esta mañana se centró en los "modos" de manejo de errores. Es más fácil y más sustantivo discutir los modos de manejo de errores que try facilita y luego lidiar con los peligros potenciales del código que probablemente escribiremos que ejecutar un contador de línea bastante arbitrario.

@kingishb ¿Cuántos de los puntos _try_ encontrados están en funciones públicas de paquetes no principales? Por lo general, las funciones públicas deberían devolver errores nativos del paquete (es decir, envueltos o decorados)...

@networkimprov Esa es una fórmula demasiado simplista para mi sensibilidad. Donde eso suena cierto es en términos de superficies API que devuelven errores inspeccionables. Normalmente es apropiado agregar contexto a un mensaje de error en función de la relevancia del contexto, no de su posición en la pila de llamadas.

Es probable que muchos falsos positivos sobrevivan en las métricas actuales. ¿Y qué pasa con los errores que ocurren debido a las siguientes prácticas sugeridas (https://blog.golang.org/errors-are-values)? try probablemente reduciría el uso de tales prácticas y, en ese sentido, son objetivos principales para el reemplazo (probablemente uno de los únicos casos de uso realmente intrigante para mí). Entonces, nuevamente, parece inútil eliminar la fuente existente sin mucha más diligencia debida.

Gracias @ubikenobi , @freeformz y @kingishb por recopilar sus datos, ¡muchas gracias! Aparte, si ejecuta tryhard con la opción -err="" , también intentará trabajar con un código en el que la variable de error se llame algo diferente a err (como e ). Esto puede generar algunos casos más, según el código base (pero también posiblemente aumente la posibilidad de falsos positivos).

@griesemer en caso de que esté buscando más puntos de datos. Corrí tryhard contra dos de nuestros microservicios, con estos resultados:

reloj v 1.82 / tryhard
13280 líneas de código Go / 148 identificadas para intento (1%)

Otro servicio:
9768 líneas de código Go / 50 identificadas para intento (0,5 %)

Posteriormente tryhard inspeccionó un conjunto más amplio de varios microservicios:

314343 Líneas de código Go / 1563 identificadas para intento (0,5 %)

Haciendo una inspección rápida. Los tipos de paquetes que try podrían optimizar son típicamente adaptadores/envoltorios de servicios que devuelven de forma transparente el error (GRPC) devuelto por el servicio envuelto.

Espero que esto ayude.

Es una idea absolutamente mala.

  • ¿Cuándo aparece err var para defer ? ¿Qué hay de "explícito mejor que implícito"?
  • Usamos una regla simple: debe encontrar rápidamente un lugar exacto donde haya devuelto el error. Cada error está envuelto con contexto para comprender qué y dónde sale mal. defer creará una gran cantidad de código feo y difícil de entender.
  • @davecheney escribió una excelente publicación sobre errores y la propuesta está totalmente en contra de todo lo que se incluye en esta publicación.
  • Por último, si usa os.Exit , sus errores no se verificarán.

Acabo de ejecutar tryhard en un paquete (con el proveedor) y reportó 2478 con el número de códigos bajando de 873934 a 851178 pero no estoy seguro cómo interpretar eso porque no sé cuánto de eso se debe al sobreenvoltorio (con stdlib que carece de soporte para el envoltorio de errores de seguimiento de pila) o cuánto de ese código se trata incluso de manejo de errores.

Lo que sí sé, sin embargo, es que solo esta semana perdí una cantidad vergonzosa de tiempo debido a copias como if err != nil { return nil } y errores que parecen error: cannot process ....file: cannot parse ...file: cannot open ...file .

\ No le daría demasiada importancia a la cantidad de votos a menos que piense que solo hay ~3000 desarrolladores de Go por ahí. El alto número de votos en la otra no propuesta se debe simplemente al hecho de que el problema llegó a la cima de HN y Reddit: la comunidad de Go no es exactamente conocida por su falta de dogma y/o decir que no, así que no. -uno debería estar sorprendido por el conteo de votos.

Tampoco me tomaría demasiado en serio los intentos de apelar a la autoridad, porque se sabe que estas mismas autoridades rechazan nuevas ideas y propuestas incluso después de señalar su propia ignorancia y/o malentendidos.
\

Ejecutamos tryhard -err="" en nuestro servicio más grande (±163 000 líneas de código, incluidas las pruebas): encontró 566 ocurrencias. Sospecho que sería aún más en la práctica, ya que parte del código se escribió con if err != nil en mente, por lo que se diseñó en torno a él (me viene a la mente el artículo de Rob Pike "los errores son valores" sobre cómo evitar la repetición).

@griesemer Agregué un nuevo archivo a la esencia. Se generó con -err="". Lo revisé y hay algunos cambios. También actualicé Tryhard esta mañana, por lo que también se usó la versión más nueva.

@griesemer Creo que tryhard sería más útil si pudiera contar:

a) el número de sitios de llamadas que arrojan un error
b) el número de controladores de if err != nil [&& ...] de declaración única (candidatos para on err #32611)
c) el número de los que devuelven algo (candidatos a defer #32676)
d) el número de los que devuelven err (candidatos a try() )
e) el número de aquellos que están en funciones exportadas de paquetes no principales (probable falso positivo)

Comparar LoC total con instancias de return err parece carecer de contexto, en mi opinión.

@networkimprov De acuerdo: se han planteado sugerencias similares antes. Intentaré encontrar algo de tiempo en los próximos días para mejorar esto.

Aquí están las estadísticas de ejecutar Tryhard sobre nuestra base de código interna (solo nuestro código, no dependencias):

Antes:

  • 882 archivos .go
  • 352434 ubicación
  • 329909 ubicación no vacía

Después de intentarlo:

  • 2701 reemplazos (promedio de 3.1 reemplazos/archivo)
  • 345364 loc (-2,0%)
  • 322838 loc no vacío (-2.1%)

Editar: ahora que @griesemer actualizó Tryhard para incluir estadísticas de resumen, aquí hay un par más:

  • El 39,2 % de las declaraciones de if son if <err> != nil
  • El 69,6 % de estos son candidatos try

Mirando a través de los reemplazos que encontró Tryhard, ciertamente hay tipos de código en los que el uso de try sería muy frecuente, y otros tipos en los que rara vez se usarían.

También noté algunos lugares que Tryhard no pudo transformar, pero que se beneficiarían enormemente de Try. Por ejemplo, aquí hay un código que tenemos para decodificar mensajes de acuerdo con un protocolo de conexión simple (editado por simplicidad/claridad):

func (req *Request) Decode(r Reader) error {
    typ, err := readByte(r)
    if err != nil {
        return err
    }
    req.Type = typ
    req.Body, err = readString(r)
    if err != nil {
        return unexpected(err)
    }

    req.ID, err = readID(r)
    if err != nil {
        return unexpected(err)
    }
    n, err := binary.ReadUvarint(r)
    if err != nil {
        return unexpected(err)
    }
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err = readID(r)
        if err != nil {
            return unexpected(err)
        }
    }
    return nil
}

// unexpected turns any io.EOF into an io.ErrUnexpectedEOF.
func unexpected(err error) error {
    if err == io.EOF {
        return io.ErrUnexpectedEOF
    }
    return err
}

Sin try , solo escribimos unexpected en los puntos de retorno donde se necesita, ya que no hay una gran mejora al manejarlo en un solo lugar. Sin embargo, con try , podemos aplicar la transformación de error unexpected con un aplazamiento y luego acortar drásticamente el código, haciéndolo más claro y más fácil de hojear:

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

@cespare ¡Fantástico reportaje!

El fragmento completamente reducido es generalmente mejor, pero los paréntesis son incluso peores de lo que esperaba, y el try dentro del bucle es tan malo como esperaba.

Una palabra clave es mucho más legible y es un poco surrealista que ese sea un punto en el que muchos otros difieren. Lo siguiente es legible y no me preocupan las sutilezas debido a que solo se devuelve un valor (aunque aún podría aparecer en funciones más largas y/o en aquellas con mucho anidamiento):

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = wrapEOF(err) }()

    req.Type = try readByte(r)
    req.Body = try readString(r)
    req.ID = try readID(r)

    n := try binary.ReadUvarint(r)
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err = readID(r)
        try err
    }
    return nil
}

*Para ser justos, resaltar el código ayudaría mucho, pero parece un pintalabios barato.

¿Entiendes que la mayor ventaja que obtienes en caso de un código realmente malo?

Si usa unexpected() o devuelve el error tal como está, no sabe nada sobre su código y su aplicación.

try no puede ayudarlo a escribir mejor código, pero puede producir más código incorrecto.

@cespare Un decodificador también puede ser una estructura con un tipo de error dentro, con los métodos verificando err == nil antes de cada operación y devolviendo un booleano ok.

Debido a que este es el proceso que usamos para los códecs, try es absolutamente inútil porque uno puede crear fácilmente un idioma no mágico, más corto y más sucinto para manejar errores para este caso específico.

@makhov Por "código realmente malo", supongo que te refieres al código que no envuelve los errores.

Si es así, puede tomar un código que se vea así:

a, b, c, err := someFn()
if err != nil {
  return ..., errors.Wrap(err, ...)
}

Y conviértalo en un código semánticamente idéntico[1] que se vea así:

a, b, c, err := someFn()
try(errors.Wrap(err, ...))

La propuesta no dice que deba usar aplazar para envolver errores, solo explica por qué la palabra clave handle de la iteración anterior de la propuesta no es necesaria, ya que se puede implementar en términos de aplazamiento sin ningún cambio de idioma.

(Su otro comentario también parece estar basado en ejemplos o pseudocódigo en la propuesta, a diferencia del núcleo de lo que se propone)

Ejecuté tryhard en mi código base con 54K LOC, se encontraron 1116 instancias.
Vi la diferencia, y debo decir que tengo muy poca construcción que se beneficiaría enormemente de probar, porque casi todo mi uso del tipo de construcción if err != nil es un bloque simple de un solo nivel que solo devuelve el error con contexto agregado. Creo que solo encontré un par de instancias en las que try realmente cambiaría la construcción del código.

En otras palabras, mi opinión es que try en su forma actual me da:

  • menos escritura (una reducción de la friolera de ~30 caracteres por aparición, indicada por los "**" a continuación)
-       **if err := **json.NewEncoder(&buf).Encode(in)**; err != nil {**
-               **return err**
-       **}**
+       try(json.NewEncoder(&buf).Encode(in))

mientras me presenta estos problemas:

  • Otra forma más de manejar los errores
  • falta una señal visual para la división de la ruta de ejecución

Como escribí anteriormente en este hilo, puedo vivir con try , pero después de probarlo en mi código, creo que personalmente preferiría que no se introdujera esto en el lenguaje. mi $.02

característica inútil, ahorra escribir, pero no es gran cosa.
Prefiero elegir la forma antigua.
escriba más manejador de errores para que el programa sea fácil de solucionar problemas.

Solo algunos pensamientos...

Esa expresión es útil en go, pero es solo eso: una expresión que debes
enseñar a los recién llegados. Un nuevo programador de go tiene que aprender eso, de lo contrario
puede incluso tener la tentación de refactorizar el manejo de errores "ocultos". También el
el código no es más corto usando ese idioma (todo lo contrario) a menos que lo olvides
para contar los métodos.

Ahora imaginemos que se implementa try, ¿qué tan útil será esa expresión para
ese caso de uso? Considerando:

  • Try mantiene la implementación más cerca en lugar de repartirse entre los métodos.
  • Los programadores leerán y escribirán código con intento mucho más a menudo que eso.
    lenguaje específico (que rara vez se usa, excepto para cada tarea específica). A
    el idioma más usado se vuelve más natural y legible a menos que haya una clara
    desventaja, que claramente no es el caso aquí si comparamos ambos con un
    mente abierta.

Así que tal vez esa expresión se considere reemplazada por try.

Em ter, 2 de jul de 2019 18:06, como [email protected] escreveu:

@cespare https://github.com/cespare Un decodificador también puede ser una estructura con
un tipo de error dentro de él, con los métodos buscando err == nil antes
cada operación y devolviendo un ok booleano.

Debido a que este es el proceso que usamos para los códecs, intentarlo es absolutamente inútil.
porque uno puede hacer fácilmente un idioma no mágico, más corto y más sucinto
para el manejo de errores para este caso específico.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAT5WM3YDDRZXVXOLDQXKH3P5O7L5A5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODZCRHXA#issuecomment-4
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AAT5WMYXLLO74CIM6H4Y2RLP5O7L5ANCNFSM4HTGCZ7Q
.

La verbosidad en el manejo de errores es algo bueno en mi opinión. En otras palabras, no veo un caso de uso sólido para probar.

Estoy abierto a esta idea, pero creo que debería incluir algún mecanismo para determinar dónde ocurrió la división de la ejecución. Xerror/Is estaría bien para algunos casos (por ejemplo, si el error es un ErrNotExists, puede inferir que sucedió en un Open), pero para otros, incluidos los errores heredados en las bibliotecas, no hay sustituto.

¿Podría incluirse una recuperación similar incorporada para proporcionar información de contexto sobre dónde cambió el flujo de control? Posiblemente, para mantenerlo barato, se use una función separada en lugar de try().

O tal vez una depuración. ¿Intentar con la misma sintaxis que try() pero con la información de depuración agregada? De esta manera, try() podría ser igual de útil con el código que usa errores antiguos, sin obligarlo a recurrir al manejo de errores antiguos.

La alternativa sería que try() se ajustara y agregara contexto, pero en la mayoría de los casos esto reduciría el rendimiento sin ningún propósito, de ahí la sugerencia de funciones adicionales.

Editar: después de escribir esto, se me ocurrió que el compilador podría determinar qué variante de try() usar en función de si alguna declaración diferida usa esta función de provisión de contexto similar a "recuperar". Sin embargo, no estoy seguro de la complejidad de esto.

@lestrrat No diría mi opinión en este comentario, pero si existe la posibilidad de explicarle cómo "intentar" puede afectarnos bien, sería que se pueden escribir dos o más tokens en la declaración if. Entonces, si escribe 200 condiciones en una declaración if, podrá reducir muchas líneas.

if try(foo()) == 1 && try(bar()) == 2 {
  // err
}
n1, err := foo()
if err != nil {
  // err
}
n2, err := bar()
if err != nil {
  // err
}
if n1 == 1 && n2 == 2 {
  // err
}

Sin embargo, @mattn esa es la cuestión, _teóricamente_ tienes toda la razón. Estoy seguro de que podemos encontrar casos en los que try encajaría maravillosamente.

Acabo de proporcionar datos que en la vida real, al menos _yo_ encontré casi ninguna ocurrencia de tales construcciones que se beneficiarían de la traducción para probar en _mi código_.

Es posible que escriba el código de manera diferente al resto del mundo, pero pensé que valía la pena que alguien dijera eso, según la traducción de PoC, que algunos de nosotros en realidad no ganamos mucho con la introducción de try al idioma.

Aparte, todavía no usaría su estilo en mi código. yo lo escribiria como

n1 := try(foo())
n2 := try(bar())
if n1 == 1 && n2 == 2 {
   return errors.New(`boo`)
}

así que todavía estaría ahorrando aproximadamente la misma cantidad de tipeo por instancia de esos n1/n2/....n(n)s

¿Por qué tener una palabra clave (o función)?

Si el contexto de llamada espera n+1 valores, entonces todo es como antes.

Si el contexto de llamada espera n valores, se activa el comportamiento de prueba.

(Esto es particularmente útil en el caso n=1, que es de donde proviene todo el desorden).

Mi ide ya resalta los valores de retorno ignorados; sería trivial ofrecer señales visuales para esto si es necesario.

@balasanjay Sí, los errores de ajuste son el caso. Pero también tenemos registro, diferentes reacciones en diferentes errores (¿qué deberíamos hacer con las variables de error, por ejemplo, sql.NoRows ?), código legible, etc. Escribimos defer f.Close() inmediatamente después de abrir un archivo para que quede claro para los lectores. Verificamos los errores inmediatamente por la misma razón.

Más importante aún, esta propuesta viola la regla "los errores son valores ". Así es como está diseñado Go. Y esta propuesta va directamente en contra de la regla.

try(errors.Wrap(err, ...)) es otra pieza de código terrible porque contradice tanto esta propuesta como el diseño actual de Go.

Tiendo a estar de acuerdo con @lestrrat
Como suele ser habitual, foo() y bar() son en realidad:
AlgunaFunciónConBuenNombre(Parm1, Parms2)

entonces la sintaxis de @mattn sugerida en realidad sería:

if  try(SomeFunctionWithGoodName(Parm1, Parms2)) == 1 && try(package.SomeOtherFunction(Parm1, Parms2,Parm3))) == 2 {


} 

La legibilidad por lo general será un desastre.

considere un valor de retorno:
someRetVal, err := SomeFunctionWithGoodName(Parm1, Parms2)
se usa con más frecuencia que solo comparar con una const como 1 o 2 y no empeora, pero requiere una función de asignación doble:

if  a := try(SomeFunctionWithGoodName(Parm1, Parms2)) && b:= try(package.SomeOtherFunction(Parm1, Parms2,Parm3))) {


} 

En cuanto a todos los casos de uso ("cuánto me ayudó Tryhard"):

  1. Creo que vería una gran diferencia entre las bibliotecas y el ejecutable, sería interesante ver de otros si también obtienen esta diferencia.
  2. mi sugerencia es no comparar el % de ahorro en las líneas del código, sino la cantidad de errores en el código frente a la cantidad refactorizada.
    (mi opinión sobre esto fue
    $find /path/to/repo -name '*.go' -exec cat {} \; | grep "err :=" | wc -l
    )

@makhov

esta propuesta viola la regla "los errores son valores"

Realmente no. Los errores siguen siendo valores en esta propuesta. try() simplemente simplifica el flujo de control al ser un atajo para if err != nil { return ...,err } . El tipo error ya es de alguna manera "especial" al ser un tipo de interfaz integrado. Esta propuesta solo agrega una función integrada que complementa el tipo error . No hay nada extraordinario aquí.

@ngrilly ¿Simplificando? ¿Cómo?

func (req *Request) Decode(r Reader) error {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

¿Cómo debo entender que el error se devolvió dentro del bucle? ¿Por qué se asigna a err var, no a foo ?
¿Es más sencillo tenerlo en cuenta y no guardarlo en código?

@daved

los paréntesis son incluso peores de lo que esperaba [...] Una palabra clave es mucho más legible y es un poco surrealista que ese sea un punto en el que muchos otros difieren.

Elegir entre una palabra clave y una función integrada es principalmente una cuestión estética y sintáctica. Sinceramente, no entiendo por qué esto es tan importante para tus ojos.

PD: La función incorporada tiene la ventaja de ser compatible con versiones anteriores, ser extensible con otros parámetros en el futuro y evitar los problemas relacionados con la precedencia del operador. La palabra clave tiene la ventaja de... ser una palabra clave y señalar que try es "especial".

@makhov

¿Simplificando?

Está bien. La palabra correcta es "acortamiento".

try() acorta nuestro código reemplazando el patrón if err != nil { return ..., err } por una llamada a la función incorporada try() .

Es exactamente como cuando identifica un patrón recurrente en su código y lo extrae en una nueva función.

Ya tenemos funciones integradas como append(), que podríamos reemplazar escribiendo el código "in extenso" nosotros mismos cada vez que necesitemos agregar algo a un segmento. Pero debido a que lo hacemos todo el tiempo, se integró en el lenguaje. try() no es diferente.

¿Cómo debo entender que el error se devolvió dentro del bucle?

El try() en el ciclo actúa exactamente como el try() en el resto de la función, fuera del ciclo. Si readID() devuelve un error, entonces la función devuelve el error (después de haber decorado si).

¿Por qué está asignado a err var, no a foo?

No veo la variable foo en su ejemplo de código...

@makhov Creo que el fragmento está incompleto ya que no se nombra el error devuelto (rápidamente volví a leer la propuesta pero no pude ver si el nombre de la variable err es el nombre predeterminado si no se establece ninguno).

Tener que cambiar el nombre de los parámetros devueltos es uno de los puntos que no les gusta a las personas que rechazan esta propuesta.

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

@pierrec , ¿tal vez podríamos tener una función como recover() para recuperar el error si no está en el parámetro con nombre?
defer func() {err = unexpected(tryError())}

@makhov Puedes hacerlo más explícito:

func (req *Request) Decode(r Reader) error {
    req.Type, err := readByte(r)
        try(err) // or add annotation like try(annotate(err, ...))
    req.Body, err := readString(r)
        try(err)
    req.ID, err := readID(r)
        try(err)

    n, err := binary.ReadUvarint(r)
        try(err)
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err := readID(r)
                try(err)
    }
    return nil
}

@pierrec Ok, cambiémoslo:

func (req *Request) Decode(r Reader) error {
        var errOne, errTwo error
    defer func() { err = unexpected(???) }()

    req.Type = try(readByte(r))
    …
}

@reusee ¿Y por qué es mejor que esto?

func (req *Request) Decode(r Reader) error {
    req.Type, err := readByte(r)
        if err != nil { return err }
        …
}

¿En qué momento decidimos todos que la brevedad es mejor que la legibilidad?

@flibustenet Gracias por entender el problema. Se ve mucho mejor, pero todavía no estoy seguro de que necesitemos compatibilidad con versiones anteriores rotas para esta pequeña "mejora". Es muy molesto si tengo una aplicación que deja de construir sobre la nueva versión de Go:

package main

func main() {
    // ...
   try("a", "b")
    // ...
}

func try(a, b string) {
    // ...
}

@makhov Estoy de acuerdo en que esto debe aclararse: ¿el compilador falla cuando no puede descifrar la variable? Pensé que lo haría.
¿Quizás la propuesta necesita aclarar este punto? ¿O me lo perdí en el documento?

@flibustenet sí, esa es una forma de usar try() pero me parece que no es una forma idiomática de usar try.

@cespare Por lo que escribió, parece que la modificación de los valores devueltos en diferir es una característica de try pero ya puede hacerlo.

https://play.golang.com/p/ZMauFmt9ezJ

(Perdón si malinterpreté lo que dijiste)

@jan-g Con respecto a https://github.com/golang/go/issues/32437#issuecomment -507961463: La idea de manejar errores de manera invisible ha surgido varias veces. El problema con un enfoque tan implícito es que agregar un retorno de error a una función llamada puede hacer que la función que llama se comporte de manera silenciosa e invisible de manera diferente. Absolutamente queremos ser explícitos cuando se verifican los errores. Un enfoque implícito también va en contra del principio general en Go de que todo es explícito.

@griesemer

Probé tryhand en uno de mis proyectos (https://github.com/komuw/meli) y no hubo ningún cambio.

gobin github.com/griesemer/tryhard
     Installed github.com/griesemer/[email protected] to ~/go/bin/tryhard

```bash
~/ir/bin/tryhard -err "" -r
0

most of my err handling looks like;
```Go
import "github.com/pkg/errors"

func CreateDockerVolume(volName string) (string, error) {
    volume, err := VolumeCreate(volName)
    if err != nil {
        return "", errors.Wrapf(err, "unable to create docker volume %v", volName)
    }
    return volume.Name, nil
}

@komuw En primer lugar, asegúrese de proporcionar un argumento de nombre de archivo o directorio a tryhard , como en

tryhard -err="" -r .  // <<< note the dot
tryhard -err="" -r filename

Además, el código como el que tiene en su comentario no se reescribirá, ya que maneja un error específico en el bloque if . Lea la documentación de tryhard para saber cuándo se aplica. Gracias.

func CreateDockerVolume(volName string) (string, error) {
    volume, err := VolumeCreate(volName)
    if err != nil {
        return "", errors.Wrapf(err, "unable to create docker volume %v", volName)
    }
    return volume.Name, nil
}

Este es un ejemplo algo interesante. Mi primera reacción al mirarlo fue preguntar si esto produciría cadenas de error entrecortadas como:

unable to create docker volume: VolumeName: could not create volume VolumeName: actual problem

La respuesta es que no, porque la función VolumeCreate (de un repositorio diferente) es:

func (cli *Client) VolumeCreate(ctx context.Context, options volumetypes.VolumeCreateBody) (types.Volume, error) {
        var volume types.Volume
        resp, err := cli.post(ctx, "/volumes/create", nil, options, nil)
        defer ensureReaderClosed(resp)
        if err != nil {
                return volume, err
        }
        err = json.NewDecoder(resp.body).Decode(&volume)
        return volume, err
}

En otras palabras, la decoración adicional del error es útil porque la función subyacente no decoró su error. Esa función subyacente se puede simplificar ligeramente con try .

Quizás la función VolumeCreate realmente debería estar decorando sus errores. En ese caso, sin embargo, no me queda claro que la función CreateDockerVolume deba agregar decoración adicional, ya que no tiene información nueva que proporcionar.

@neild
Incluso si VolumeCreate decorara los errores, todavía necesitaríamos CreateDockerVolume para agregar su decoración, ya que se puede llamar a VolumeCreate desde varias otras funciones, y si algo falla (y con suerte registrado) le gustaría saber qué falló, que en este caso es CreateDockerVolume ,
Sin embargo, Considering VolumeCreate es parte de la interfaz APIclient.

Lo mismo ocurre con otras bibliotecas: os.Open bien puede decorar el nombre del archivo, el motivo del error, etc., pero
func ReadConfigFile(...
func WriteDataFile(...
etc. - llamar a os.Open son las partes que fallan que le gustaría ver para registrar, rastrear y manejar sus errores, especialmente, pero no solo en el entorno de producción.

@neild gracias.

No quiero descarrilar este hilo, pero...

Quizás la función VolumeCreate realmente debería estar decorando sus errores.
En ese caso, sin embargo, no me queda claro que el
Función CreateDockerVolume
debe agregar decoración adicional,

El problema es que, como autor de la función CreateDockerVolume , es posible que no
saber si el autor de VolumeCreate había decorado sus errores, así que
no es necesario para decorar la mía.
E incluso si supiera que lo habían hecho, podrían decidir des-decorar sus
función en una versión posterior. Y dado que ese cambio no es un cambio de API, ellos
lo lanzaría como un parche/versión menor y ahora mi función que era
dependiente de su función tener errores decorados no tiene todos los
información que necesito.
Por lo general, me encuentro decorando/envolviendo incluso si la biblioteca en la que estoy
la llamada ya terminó.

Se me ocurrió mientras hablaba de try con un compañero de trabajo. Tal vez try solo debería habilitarse para la biblioteca estándar en 1.14. @crawshaw y @jimmyfrasche hicieron un recorrido rápido por algunos casos y brindaron cierta perspectiva, pero en realidad sería valioso volver a escribir el código de la biblioteca estándar usando try tanto como sea posible.

Eso le da tiempo al equipo de Go para volver a escribir un proyecto no trivial usándolo, y la comunidad puede tener un informe de experiencia sobre cómo funciona. Sabríamos con qué frecuencia se usa, con qué frecuencia debe combinarse con un defer , si cambia la legibilidad del código, qué tan útil es tryhard , etc.

Va un poco en contra del espíritu de la biblioteca estándar, lo que le permite usar algo que el código Go normal no puede, pero nos brinda un campo de juego para ver cómo try afecta una base de código existente.

Disculpas si alguien más ha pensado en esto ya; Revisé las diversas discusiones y no vi una propuesta similar.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 le da una idea bastante buena de cómo se vería eso.

Y olvidé decir: participé en su encuesta y voté por un mejor manejo de errores, no por esto.

Quise decir que me gustaría ver un procesamiento de errores más estricto e imposible de olvidar.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 le da una idea bastante buena de cómo se vería eso.

Para resumir:

  1. 1 línea reemplaza universalmente 4 líneas (2 líneas para aquellos que usan if ... { return err } )
  2. La evaluación de los resultados devueltos puede optimizarse, aunque solo en la ruta de error.

Aproximadamente 6,000 reemplazos en total de lo que parece ser solo un cambio cosmético: no expondrá los errores existentes, quizás no introduzca otros nuevos (corríjame si me equivoco con alguno).

¿Me molestaría, en calidad de mantenedor, en hacer algo como esto con mi propio código? No a menos que yo mismo escriba la herramienta de reemplazo. Lo que lo hace todo correcto para el repositorio golang/go .

PD Un descargo de responsabilidad interesante en CL:

... Some transformations may be incorrect due to the limitations of the tool (see https://github.com/griesemer/tryhard)...

Como xerrors , ¿qué tal si da el primer paso para usarlo como un paquete de terceros?

Por ejemplo, intente usar el paquete a continuación.

https://github.com/junpayment/gotry

  • Puede ser corto para su caso de uso porque lo hice.

Sin embargo, creo que intentarlo en sí mismo es una gran idea, así que creo que también hay un enfoque que en realidad lo usa con menos influencia.

===

Aparte, hay dos cosas que me preocupa probar.

1. Existe la opinión de que se puede omitir la línea, pero parece que no se considera la cláusula de aplazamiento (o controlador).

Por ejemplo, cuando el manejo de errores es en detalle.

foo, err: = Foo ()
if err! = nil {
  if err.Error () = "AAA" {
    some action for AAA
  } else if err.Error () = "BBB" {
    some action for BBB
  } else if err.Error () = "CCC" {
    some action for CCC
  } else {
    return err
  }
}

Si simplemente reemplaza esto con try, será de la siguiente manera.

handler: = func (err error) {
  if err.Error () = "AAA" {
    some action for AAA
  } else if err.Error () = "BBB" {
    some action for BBB
  } else if err.Error () = "CCC" {
    some action for CCC
  } else {
    return err
  }
}
foo: = try (Foo (), handler)

2. Puede haber otros paquetes defectuosos que implementaron accidentalmente la interfaz de error.

type Bad struct {}
func (bad * Bad) Error () {
  return "i really do not intend to be an error"
}

@junpayment Gracias por su paquete de gotry . Supongo que es una forma de hacerse una idea de try , pero será un poco molesto tener que teclear todos los Try resulta de un interface{} en uso real.

Con respecto a tus dos preguntas:
1) No estoy seguro de a dónde vas con esto. ¿Está sugiriendo que try debería aceptar un controlador como en su ejemplo? (¿y como teníamos en una versión interna anterior de try ?)
2) No me preocupan demasiado las funciones que implementan accidentalmente la interfaz de error. Este problema no es nuevo y no parece haber causado problemas serios hasta donde sabemos.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 le da una idea bastante buena de cómo se vería eso.

Gracias por hacer este ejercicio. Sin embargo, esto me confirma lo que sospechaba, el código fuente de go tiene muchos lugares donde try() sería útil porque el error simplemente se transmite. Sin embargo, como puedo ver en los experimentos con tryhard que otros y yo mismo enviamos anteriormente, para muchas otras bases de código try() no sería muy útil porque en el código de la aplicación los errores tienden a manejarse, no acaba de pasar

Creo que eso es algo que los diseñadores de Go deberían tener en cuenta, el compilador de Go y el tiempo de ejecución son un código Go "único" en cierto modo, diferente del código de la aplicación Go. Por lo tanto, creo que try() debe mejorarse para que también sea útil en otros casos en los que el error realmente debe manejarse y en los que no es realmente deseable manejar el error con una declaración diferida.

@griesemer

Será un poco molesto tener que escribir y afirmar todos los resultados de prueba de una interfaz{} en uso real.

Estás bien. Este método requiere que la persona que llama emita el tipo.

No estoy seguro de a dónde vas con esto. ¿Está sugiriendo que try debería aceptar un controlador como en su ejemplo? (¿y como lo hicimos en una versión interna anterior de try?)

Cometí un error. Debería haberse explicado usando defer en lugar de handler. Lo siento.

Lo que quería decir es que hay un caso en el que no contribuye a la cantidad de código como resultado del proceso de manejo de errores que se omite y debe describirse en el aplazamiento de todos modos.

Se espera que el impacto sea más pronunciado cuando desee manejar los errores en detalle.

Entonces, en lugar de reducir la cantidad de líneas de código, podemos entender la propuesta, que organiza las ubicaciones de manejo de errores.

No estoy demasiado preocupado por las funciones que implementan accidentalmente la interfaz de error. Este problema no es nuevo y no parece haber causado problemas serios hasta donde sabemos.

Exactamente es un caso raro.

@beoran Hice un análisis inicial de Go Corpus (https://github.com/rsc/corpus). Creo que tryhard en su estado actual podría eliminar el 41,7 % de todos los cheques de err != nil del corpus. Si excluyo el patrón "_test.go", este número sube al 51,1% ( tryhard solo opera en funciones que devuelven errores, y tiende a no encontrar muchos de ellos en las pruebas). Advertencia, tome estos números con un grano de sal, obtuve el denominador (es decir, la cantidad de lugares en el código que realizamos err != nil verificaciones) usando una versión pirateada de tryhard , e idealmente esperaríamos hasta que tryhard informara estas estadísticas.

Además, si tryhard tuviera reconocimiento de tipos, teóricamente podría realizar transformaciones como esta:

// Before.
a, err := foo()
if err != nil {
  return 0, nil, errors.Wrapf(err, "some message %v", b)
}

// After.
a, err := foo()
try(errors.Wrapf(err, "some message %v", b))

Esto aprovecha los errores. El comportamiento de Wrap de devolver nil cuando el argumento de error pasado es nil . (github.com/pkg/errors tampoco es único en este sentido, la biblioteca interna que utilizo para corregir errores también conserva nil errores, y también funcionaría con este patrón, al igual que la mayoría de las bibliotecas de manejo de errores post- try , me imagino). La nueva generación de bibliotecas de soporte probablemente también nombraría a estos ayudantes de propagación de forma ligeramente diferente.

Dado que esto se aplicaría al 50% de las comprobaciones de err != nil que no son de prueba, antes de cualquier evolución de la biblioteca para admitir el patrón, no parece que el compilador Go y el tiempo de ejecución sean únicos, como usted sugiere .

Sobre el ejemplo con CreateDockerVolume https://github.com/golang/go/issues/32437#issuecomment -508199875
Encontré exactamente el mismo tipo de uso. En lib envuelvo el error con contexto en cada error, en el uso de lib me gustaría usar try y agregar contexto en defer para toda la función.

Traté de imitar esto agregando una función de controlador de errores al principio, funciona bien:

func MyLib() error {
    return errors.New("Error from my lib")
}
func MyOtherLib() error {
    return errors.New("Error from my otherLib")
}

func Caller(a, b int) error {
    eh := func(err error) error {
        return fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, err)
    }

    err := MyLib()
    if err != nil {
        return eh(err)
    }

    err = MyOtherLib()
    if err != nil {
        return eh(err)
    }

    return nil
}

Eso se verá bien e idiomático con try+defer

func Caller(a, b int) (err error) {
    defer fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, &err)

    try(MyLib())
    try(MyOtherLib())

    return nil
}

@griesemer

El documento de diseño actualmente tiene las siguientes declaraciones:

Si la función envolvente declara otros parámetros de resultado con nombre, esos parámetros de resultado conservan el valor que tengan. Si la función declara otros parámetros de resultado sin nombre, asumen sus correspondientes valores cero (que es lo mismo que mantener el valor que ya tienen).

Esto implica que este programa imprimiría 1, en lugar de 0: https://play.golang.org/p/KenN56iNVg7.

Como me señalaron en Twitter, esto hace que try se comporte como un retorno desnudo, donde los valores que se devuelven son implícitos; para averiguar qué valores reales se devuelven, es posible que deba mirar el código a una distancia significativa de la llamada a try en sí.

Dado que esta propiedad de retornos desnudos (no localidad) generalmente no es del agrado, ¿qué piensa sobre tener try siempre devolviendo los valores cero de los argumentos sin error (si es que regresa)?

Algunas consideraciones:

Esto podría hacer que algunos patrones que impliquen el uso de valores devueltos con nombre no puedan usar try . Por ejemplo, para implementaciones de io.Writer , que necesitan devolver un recuento de bytes escritos, incluso en la situación de escritura parcial. Dicho esto, parece que try es propenso a errores en este caso de todos modos (por ejemplo n += try(wrappedWriter.Write(...)) no establece n en el número correcto en caso de que se devuelva un error). Me parece bien que try se vuelva inutilizable para este tipo de casos de uso, ya que los escenarios en los que necesitamos ambos valores y un error son bastante raros, según mi experiencia.

Si hay una función con muchos usos de try , esto podría conducir a una sobrecarga de código, donde hay muchos lugares en una función que necesitan poner a cero las variables de salida. Primero, el compilador es bastante bueno para optimizar escrituras innecesarias en estos días. Y segundo, si resulta necesario, parece una optimización sencilla tener todos los bloques $ try generados por goto en una etiqueta común compartida en toda la función, que pone a cero los valores de salida sin errores.

Además, como estoy seguro de que sabe, tryhard ya está implementado de esta manera, por lo que, como beneficio adicional, hará que tryhard sea más correcto retroactivamente.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 le da una idea bastante buena de cómo se vería eso.

Gracias por hacer este ejercicio. Sin embargo, esto me confirma lo que sospechaba, el código fuente de go tiene muchos lugares donde try() sería útil porque el error simplemente se transmite. Sin embargo, como puedo ver en los experimentos con tryhard que otros y yo mismo enviamos anteriormente, para muchas otras bases de código try() no sería muy útil porque en el código de la aplicación los errores tienden a manejarse, no acaba de pasar

Yo interpretaría esto de otra manera.

No hemos tenido genéricos, por lo que será difícil encontrar código en la naturaleza que se beneficie directamente de los genéricos basados ​​en el código escrito. Eso no significa que los genéricos no serían útiles.

Para mí, hay 2 patrones que he usado en el código para el manejo de errores.

  1. use panics dentro del paquete, y recupere el panic y devuelva un resultado de error en los pocos métodos exportados
  2. use selectivamente un controlador diferido en algunos métodos para poder decorar errores con información de PC de número de línea/archivo de pila rica y más contexto

Estos patrones no están muy extendidos pero funcionan. 1) se usa en la biblioteca estándar en sus funciones no exportadas y 2) se usa ampliamente en mi base de código durante los últimos años porque pensé que era una buena manera de usar las características ortogonales para hacer una decoración de errores simplificada, y la propuesta recomienda y ha bendecido el acercamiento. El hecho de que no estén muy extendidos no significa que no sean buenos. Pero como con todo, las pautas del equipo de Go que lo recomiendan conducirán a que se usen más en la práctica, en el futuro .

Un último punto a tener en cuenta es que los errores de decoración en cada línea de su código pueden ser demasiado. Habrá algunos lugares en los que tenga sentido decorar los errores y otros en los que no. Debido a que antes no teníamos grandes pautas, la gente decidió que tenía sentido decorar siempre los errores. Pero es posible que no agregue mucho valor decorar siempre cada vez que un archivo no se abrió, ya que puede ser suficiente dentro del paquete para tener un error como "no se puede abrir el archivo: conf.json", en lugar de: "no se puede para obtener el nombre de usuario: no se puede obtener la conexión db: no se puede cargar el archivo del sistema: no se puede abrir el archivo: conf.json".

Con la combinación de los valores de error y el manejo de errores conciso, ahora estamos obteniendo mejores pautas sobre cómo manejar los errores. La preferencia parece ser:

  • un error será simple, por ejemplo, "no se puede abrir el archivo: conf.json"
  • se puede adjuntar un marco de error que incluya el contexto: GetUserName --> GetConnection --> LoadSystemFile.
  • Si se agrega al contexto, puede envolver ese error un poco, por ejemplo, MyAppError{error}

Tiendo a sentir que seguimos pasando por alto los objetivos de la propuesta de prueba y las cosas de alto nivel que intenta resolver:

  1. reduzca el texto estándar de if err != nil { return err } para los lugares donde tiene sentido propagar el error para que se maneje más arriba en la pila
  2. Permitir el uso simplificado de valores devueltos donde err == nil
  3. permita que la solución se amplíe más adelante para permitir, por ejemplo, más decoración de errores en el sitio, saltar al controlador de errores, usar goto en lugar de semántica de retorno, etc.
  4. Permita que el manejo de errores no abarrote la lógica del código base, es decir, déjelo un poco a un lado con una especie de controlador de errores.

Mucha gente todavía tiene 1). Mucha gente ha trabajado alrededor de 1) porque antes no existían mejores pautas. Pero eso no quiere decir que, después de que comiencen a usarlo, su reacción negativa no cambiaría para volverse más positiva.

Mucha gente puede usar 2). Puede haber desacuerdo sobre cuánto, pero di un ejemplo en el que hace que mi código sea mucho más fácil.

var u user = try(db.LoadUser(try(strconv.ParseInt(stringId)))

En Java, donde las excepciones son la norma, tendríamos:

User u = db.LoadUser(Integer.parseInt(stringId)))

Nadie miraría este código y diría que tenemos que hacerlo en 2 líneas, es decir.

int id = Integer.parseInt(stringId)
User u = db.LoadUser(id))

No deberíamos tener que hacer eso aquí, bajo la pauta de que try no DEBE llamarse en línea y DEBE estar siempre en su propia línea .

Además, hoy en día, la mayoría del código hará cosas como:

var u user
var err error
var id int
id, err = strconv.ParseInt(stringId)
if err != nil {
  return u, errors.Wrap("cannot load userid from string: %s: %v", stringId, err)
}
u, err = db.LoadUser(id)
if err != nil {
  return u, errors.Wrap("cannot load user given user id: %d: %v", id, err)
}
// now work with u

Ahora, alguien que lea esto tiene que analizar estas 10 líneas, que en Java habría sido 1 línea, y que podría ser 1 línea con la propuesta aquí. Visualmente, tengo que intentar mentalmente ver qué líneas aquí son realmente pertinentes cuando leo este código. La plantilla hace que este código sea más difícil de leer y asimilar.

Recuerdo en mi vida pasada trabajar en/con programación orientada a aspectos en Java. Allí, el objetivo era

Esto permite que comportamientos que no son fundamentales para la lógica empresarial (como el registro) se agreguen a un programa sin saturar el código, el núcleo de la funcionalidad. (citando de wikipedia https://en.wikipedia.org/wiki/Aspect-oriented_programming).
El manejo de errores no es fundamental para la lógica empresarial, pero es fundamental para la corrección. La idea es la misma: no debemos saturar nuestro código con cosas que no son fundamentales para la lógica comercial porque " pero el manejo de errores es muy importante ". Sí lo es, y sí podemos ponerlo a un lado.

Con respecto a 4), muchas propuestas han sugerido controladores de errores, que es un código al lado que maneja los errores pero no abarrota la lógica comercial. La propuesta inicial tiene la palabra clave handle para ello, y la gente ha sugerido otras cosas. Esta propuesta dice que podemos aprovechar el mecanismo de aplazamiento y hacerlo más rápido, lo que antes era su talón de Aquiles. Lo sé, he hecho ruido sobre el rendimiento del mecanismo de aplazamiento muchas veces al equipo Go.

Tenga en cuenta que tryhard no marcará este código como algo que se puede simplificar. Pero con try y nuevas pautas, es posible que la gente quiera simplificar este código a una sola línea y dejar que el marco de error capture el contexto requerido.

El contexto, que se ha usado muy bien en lenguajes basados ​​en excepciones, capturará que uno intentó que ocurriera un error al cargar un usuario porque la identificación del usuario no existía, o porque la identificación de la cadena no estaba en un formato en el que una identificación entera podría ser analizado de él.

Combine eso con Error Formatter, y ahora podemos inspeccionar detalladamente el cuadro de error y el error en sí mismo y formatear el mensaje correctamente para los usuarios, sin el estilo a: b: c: d: e: underlying error difícil de leer que muchas personas han hecho y que nosotros no. tenía grandes pautas para.

Recuerde que todas estas propuestas juntas nos brindan la solución que queremos: manejo de errores conciso sin repeticiones innecesarias, al tiempo que brinda mejores diagnósticos y un mejor formateo de errores para los usuarios. Estos son conceptos ortogonales pero juntos se vuelven extremadamente poderosos.

Finalmente, dado el punto 3) anterior, es difícil usar una palabra clave para resolver esto. Por definición, una palabra clave no permite que la extensión en el futuro pase un controlador por nombre, ni permita la decoración de errores en el acto, ni admita la semántica de ir a (en lugar de la semántica de retorno). Con una palabra clave, primero debemos tener en mente la solución completa. Y una palabra clave no es compatible con versiones anteriores. El equipo de go declaró cuando estaba comenzando Go 2, que querían tratar de mantener la compatibilidad con versiones anteriores tanto como fuera posible. La función try mantiene eso, y si vemos más tarde que no se necesita una extensión, un simple gofix puede modificar fácilmente el código para cambiar la función try a una palabra clave.

¡Mis 2 centavos otra vez!

El 4/7/19, Sanjay Menakuru [email protected] escribió:

@griesemer

[ ... ]
Como me señalaron en Twitter, esto hace que try se comporte como un desnudo
return, donde los valores que se devuelven son implícitos; para averiguar qué
se devuelven los valores reales, es posible que deba mirar el código en un
distancia significativa de la llamada a try en sí.

Dado que esta propiedad de rendimientos desnudos (no localidad) es generalmente
No me gustó, ¿cuáles son sus pensamientos sobre tener try siempre devolver el cero
valores de los argumentos que no son de error (si es que regresa)?

Solo se permiten devoluciones desnudas cuando se nombran los argumentos de devolución. Eso
parece que try sigue una regla diferente?

Me gusta la idea general de reutilizar defer para solucionar el problema. Sin embargo, me pregunto si la palabra clave try es la forma correcta de hacerlo. ¿Qué pasaría si pudiéramos reutilizar un patrón ya existente? Algo que todo el mundo ya sabe de las importaciones:

Manejo explícito

res, err := doSomething()
if err != nil {
    return err
}

Ignorando explícitamente

res, _ := doSomething()

Manejo diferido

Comportamiento similar a lo que va a hacer try .

res, . := doSomething()

@piotrkowalczuk
Esta puede ser una sintaxis más agradable, pero no sé qué tan fácil sería adaptar Go para que sea legal, tanto en Go como en los resaltadores de sintaxis.

@balasanjay (y @lootch): Según su comentario aquí , sí, el programa https://play.golang.org/p/KenN56iNVg7 imprimirá 1.

Dado que try solo se preocupa por el resultado del error, deja todo lo demás en paz. Podría establecer otros valores de retorno en sus valores cero, pero obviamente no está claro por qué sería mejor. Por un lado, podría causar más trabajo cuando se nombran los valores de los resultados porque es posible que deban establecerse en cero; sin embargo, la persona que llama (probablemente) los ignorará si hubo un error. Pero esta es una decisión de diseño que podría cambiarse si hay buenas razones para ello.

[editar: tenga en cuenta que esta pregunta (sobre si borrar los resultados que no son de error al encontrar un error) no es específica de la propuesta try . Cualquiera de las alternativas propuestas que no requiera un return explícito tendrá que responder a la misma pregunta.]

Con respecto a su ejemplo de un escritor n += try(wrappedWriter.Write(...)) : Sí, en una situación en la que necesita incrementar n incluso en caso de error, no se puede usar try , incluso si try no pone a cero los valores de resultados que no son errores. Esto se debe a que try solo devuelve algo si no hay ningún error: try se comporta limpiamente como una función (pero una función que puede no regresar a la persona que llama, sino a la persona que llama). Vea el uso de temporales en la implementación de try .

Pero en casos como su ejemplo, también habría que tener cuidado con una instrucción if y asegurarse de incorporar el recuento de bytes devuelto en n .

Pero tal vez estoy malinterpretando su preocupación.

@griesemer : estoy sugiriendo que es mejor establecer los otros valores de retorno en sus valores cero, porque entonces está claro lo que hará try con solo inspeccionar el sitio de llamadas. A) no hará nada, o b) regresará de la función con valores cero y el argumento para probar.

Como se especifica, try retendrá los valores de los valores devueltos con nombre que no son errores y, por lo tanto, sería necesario inspeccionar toda la función para tener claro qué valores devuelve try .

Este es el mismo problema con una devolución simple (tener que escanear toda la función para ver qué valor se devuelve), y presumiblemente fue el motivo de la presentación de https://github.com/golang/go/issues/21291. Esto, para mí, implica que try en una función grande con valores de retorno con nombre tendría que desaconsejarse bajo la misma base que los retornos simples (https://github.com/golang/go/wiki/CodeReviewComments #parámetros-resultados-nombrados). En su lugar, sugiero que se especifique try para devolver siempre los valores cero del argumento sin error.

desconcertado y me siento mal por el equipo go últimamente. try es una solución limpia y comprensible para el problema específico que intenta resolver: verbosidad en el manejo de errores.

la propuesta dice: después de un año de discusión, estamos agregando esto incorporado. úselo si desea un código menos detallado, de lo contrario, continúe haciendo lo que hace. ¡la reacción es una resistencia no totalmente justificada para una característica opcional para la cual los miembros del equipo han mostrado claras ventajas!

Alentaría aún más al equipo de Go a que haga de try una variante incorporada si es fácil de hacer.

try(outf.Seek(linkstart, 0))
try(io.Copy(outf, exef))

se convierte

try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))

la siguiente cosa detallada podría ser esas llamadas sucesivas a try .

Estoy de acuerdo con nvictor en su mayor parte, excepto por los parámetros variados para try . Todavía creo que debería tener un lugar para un controlador y la propuesta variada puede estar empujando el límite de legibilidad para mí.

@nvictor Go es un lenguaje al que no le gustan las características no ortogonales. Eso significa que si, en el futuro, descubrimos una mejor solución de manejo de errores que no sea try , será mucho más complicado cambiar (si no se rechaza rotundamente porque nuestro actual solución es "suficientemente buena").

Creo que hay una solución mejor que try , y prefiero tomarlo con calma y encontrar esa solución que conformarme con esta.

Sin embargo, no estaría enojado si esto se agregara. No es una mala solución, solo creo que podemos encontrar una mejor.

En mi opinión, quiero probar un código de bloque, ahora try como una función de error de manejo

Al leer esta discusión (y las discusiones en Reddit), no siempre sentí que todos estaban en la misma página.

Por lo tanto, escribí una pequeña publicación de blog que demuestra cómo se puede usar try : https://faiface.github.io/post/how-to-use-try/.

Traté de mostrar múltiples aspectos de esta propuesta para que todos puedan ver lo que puede hacer y formar una opinión más informada (aunque sea negativa).

Si me perdí algo importante, ¡házmelo saber!

@faiface estoy bastante seguro de que puedes reemplazar

if err != nil {
    return resps, err
}

con try(err) .

Aparte de eso, ¡excelente artículo!

@DmitriyMV ¡Cierto! Pero supongo que lo mantendré como está, para que haya al menos un ejemplo del clásico if err != nil , aunque no muy bueno.

Tengo dos preocupaciones:

  • las devoluciones con nombre han sido muy confusas, y esto las alienta con un caso de uso nuevo e importante
  • esto desalentará agregar contexto a los errores

En mi experiencia, agregar contexto a los errores inmediatamente después de cada sitio de llamada es fundamental para tener un código que se pueda depurar fácilmente. Y las devoluciones con nombre han causado confusión para casi todos los desarrolladores de Go que conozco en algún momento.

Una preocupación estilística menor es que es desafortunado cuántas líneas de código ahora se incluirán en try(actualThing()) . Puedo imaginar ver la mayoría de las líneas en un código base envuelto en try() . Eso se siente desafortunado.

Creo que estas preocupaciones se abordarían con un ajuste:

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

check() se comportaría de manera muy similar a try() , pero eliminaría el comportamiento de pasar los valores de retorno de la función de forma genérica y, en su lugar, proporcionaría la capacidad de agregar contexto. Todavía desencadenaría un regreso.

Esto conservaría muchas de las ventajas de try() :

  • es un incorporado
  • sigue el flujo de control existente WRT para diferir
  • se alinea bien con la práctica existente de agregar contexto a los errores
  • se alinea con las propuestas y bibliotecas actuales para corregir errores, como errors.Wrap(err, "context message")
  • da como resultado un sitio de llamada limpio: no hay texto estándar en la línea a, b, err := myFunc()
  • Todavía es posible describir errores con defer fmt.HandleError(&err, "msg") , pero no es necesario alentarlo.
  • la firma de check es un poco más simple, porque no necesita devolver un número arbitrario de argumentos de la función que está envolviendo.

Esto es bueno, creo que el equipo go realmente debería tomar este. Esto es mejor que intentarlo, más claro !!!

@buchanae Me interesaría saber lo que piensas sobre la publicación de mi blog porque argumentaste que try desalentará agregar contexto a los errores, mientras que diría que al menos en mi artículo es incluso más fácil de lo habitual.

Solo voy a tirar esto por ahí en la etapa actual. Lo pensaré un poco más, pero pensé que publicaría aquí para ver lo que piensas. ¿Quizás debería abrir un nuevo número para esto? También publiqué esto en #32811

Entonces, ¿qué hay de hacer algún tipo de macro C genérica en su lugar para abrirse a una mayor flexibilidad?

Me gusta esto:

define returnIf(err error, desc string, args ...interface{}) {
    if (err != nil) {
        return fmt.Errorf("%s: %s: %+v", desc, err, args)
    }
}

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    :returnIf(err, "Error opening src", src)
    defer r.Close()

    w, err := os.Create(dst)
    :returnIf(err, "Error Creating dst", dst)
    defer w.Close()

    ...
}

Esencialmente, returnIf será reemplazado/alineado por lo definido anteriormente. La flexibilidad que existe es que depende de usted lo que hace. Depurar esto puede ser un poco extraño, a menos que el editor lo reemplace en el editor de alguna manera agradable. Esto también lo hace menos mágico, ya que puedes leer claramente la definición. Y también, esto le permite tener una línea que podría regresar en caso de error. Y capaz de tener diferentes mensajes de error dependiendo de dónde sucedió (contexto).

Editar: también se agregaron dos puntos delante de la macro para sugerir que tal vez eso se pueda hacer para aclarar que es una macro y no una llamada de función.

@nvictor

animaría aún más al equipo de go a hacer try un variadic incorporado

¿Qué devolvería try(foo(), bar()) si foo y bar no devolvieran lo mismo?

Solo voy a tirar esto por ahí en la etapa actual. Lo pensaré un poco más, pero pensé que publicaría aquí para ver lo que piensas. ¿Quizás debería abrir un nuevo número para esto? También publiqué esto en #32811

Entonces, ¿qué hay de hacer algún tipo de macro C genérica en su lugar para abrirse a una mayor flexibilidad?

@Chillance , en mi humilde opinión, creo que un sistema macro higiénico como Rust (y muchos otros lenguajes) le daría a la gente la oportunidad de jugar con ideas como try o genéricos y luego, una vez que se gana experiencia, las mejores ideas pueden convertirse parte de la lengua y las bibliotecas. Pero también creo que hay muy pocas posibilidades de que se agregue algo así a Go.

@jonbodner actualmente hay una propuesta para agregar macros higiénicas en Go. No hay sintaxis propuesta ni nada todavía, sin embargo, no ha habido mucho _en contra_ de la idea de agregar macros higiénicas. #32620

@Allenyn , con respecto a la sugerencia anterior de @buchanae que acaba de citar :

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

Por lo que he visto de la discusión, creo que sería un resultado poco probable aquí que la semántica de fmt se incorpore a una función integrada. (Vea, por ejemplo, la respuesta de @josharian ).

Dicho esto, no es realmente necesario, incluso porque permitir una función de controlador puede eludir la extracción de la semántica fmt directamente en una función integrada. Uno de esos enfoques fue propuesto por @eihigh el primer día de discusión aquí, que es similar en espíritu a la sugerencia de @buchanae , y que sugirió ajustar el try integrado para que tenga la siguiente firma:

func try(error, optional func(error) error)

Debido a que esta alternativa try no devuelve nada, esa firma implica:

  • no se puede anidar dentro de otra llamada de función
  • debe estar al principio de la línea

No quiero activar el cambio de nombre, pero esa forma de try podría leerse mejor con un nombre alternativo como check . Uno podría imaginar ayudantes de biblioteca estándar que podrían hacer conveniente la anotación en el lugar opcional, mientras que defer podría seguir siendo una opción para la anotación uniforme cuando se desee.

Hubo algunas propuestas relacionadas creadas más tarde en #32811 ( catch como elemento integrado) y #32611 ( on palabra clave para permitir on err, <statement> ). Esos pueden ser buenos lugares para discutir más a fondo, o para agregar un pulgar hacia arriba o hacia abajo, o para sugerir posibles ajustes a esas propuestas.

@jonbodner actualmente hay una propuesta para agregar macros higiénicas en Go. No hay sintaxis propuesta ni nada todavía, sin embargo, no ha habido mucho _en contra_ de la idea de agregar macros higiénicas. #32620

Es genial que haya una propuesta, pero sospecho que el equipo central de Go no tiene la intención de agregar macros. Sin embargo, estaría feliz de estar equivocado acerca de esto, ya que terminaría con todos los argumentos sobre los cambios que actualmente requieren modificaciones en el núcleo del lenguaje. Para citar a un títere famoso, "Hazlo. O no lo hagas. No hay intento".

@jonbodner No creo que agregar macros higiénicas termine el argumento. Todo lo contrario. Una crítica común es que try "oculta" la devolución. Las macros serían estrictamente peores desde este punto de vista, porque cualquier cosa sería posible en una macro. E incluso si Go permitiera macros higiénicas definidas por el usuario, todavía tendríamos que debatir si try debería ser una macro integrada predeclarada en el bloque del universo o no. Sería lógico que los que se oponen a try se opongan aún más a las macros higiénicas ;-)

@ngrilly hay varias formas de asegurarse de que las macros sobresalgan y sean fáciles de ver. La forma en que Rust lo hace es que las macros siempre van precedidas de ! (es decir try!(...) y println!(...) ).

Yo diría que si se adoptaran macros higiénicas y fáciles de ver, y no parecieran llamadas de funciones normales, encajarían mucho mejor. Deberíamos optar por soluciones más generales en lugar de solucionar problemas individuales.

@thepudds Estoy de acuerdo en que agregar un parámetro opcional de tipo func(error) error podría ser útil (esta posibilidad se analiza en la propuesta, con algunos problemas que deberían resolverse), pero no veo el sentido de try sin devolver nada. El try propuesto por el equipo de Go es una herramienta más general.

@deanveloper Sí, el ! al final de las macros en Rust es inteligente. Recuerda a los identificadores exportados que comienzan con una letra mayúscula en Go :-)

Estaría de acuerdo en tener macros higiénicas en Go si y solo si podemos preservar la velocidad de compilación y resolver problemas complejos relacionados con las herramientas (las herramientas de refactorización necesitarían expandir las macros para comprender la semántica del código, pero deben generar código con las macros sin expandir) . Es dificil. Mientras tanto, ¿tal vez try podría cambiarse de nombre a try! ? ;-)

Una idea ligera: si el cuerpo de una construcción if/for contiene una sola declaración, no se necesitan llaves siempre que esta declaración esté en la misma línea que if o for . Ejemplo:

fd, err := os.Open("foo")
if err != nil return err

Tenga en cuenta que en la actualidad un tipo error es solo un tipo de interfaz normal. El compilador no lo trata como algo especial. try cambia eso. Si el compilador puede tratar error como especial, preferiría un /bin/sh inspirado || :

fd, err := os.Open("foo") || return err

El significado de dicho código sería bastante obvio para la mayoría de los programadores, no hay un flujo de control oculto y, como en la actualidad este código es ilegal, no se daña ningún código en funcionamiento.

Aunque puedo imaginar que algunos de ustedes están retrocediendo horrorizados.

@bakul En if err != nil return err , ¿cómo sabe dónde termina la expresión err != nil y dónde comienza la instrucción return err ? Su idea sería un cambio importante en la gramática del idioma, mucho más grande que lo que se propone con try .

Su segunda idea se ve como catch |err| return err en Zig . Personalmente, no estoy "retrocediendo con horror" y diría ¿por qué no? Pero se debe tener en cuenta que Zig también tiene una palabra clave try , que es un atajo para catch |err| return err , y casi equivalente a lo que el equipo de Go propone aquí como una función integrada. Entonces, ¿tal vez try es suficiente y no necesitamos la palabra clave catch ? ;-)

@ngrilly , actualmente <expr> <statement> no es válido, así que no creo que este cambio haga que la gramática sea más ambigua, pero puede ser un poco más frágil.

Esto generaría exactamente el mismo código que la propuesta de prueba pero a) el retorno es explícito aquí b) no es posible anidar como con probar y c) esta sería una sintaxis familiar para los usuarios de shell (que superan con creces a los usuarios de zig). No hay catch aquí.

Mencioné esto como una alternativa, pero para ser sincero, estoy perfectamente de acuerdo con lo que decidan los diseñadores de lenguaje principales.

He subido una versión ligeramente mejorada de tryhard . Ahora informa información más detallada sobre los archivos de entrada. Por ejemplo, al ejecutarse contra la punta del repositorio de Go, informa ahora:

$ tryhard $HOME/go/src
...
--- stats ---
  55620 (100.0% of   55620) function declarations
  14936 ( 26.9% of   55620) functions returning an error
 116539 (100.0% of  116539) statements
  27327 ( 23.4% of  116539) if statements
   7636 ( 27.9% of   27327) if <err> != nil statements
    119 (  1.6% of    7636) <err> name is different from "err" (use -l flag to list file positions)
   6037 ( 79.1% of    7636) return ..., <err> blocks in if <err> != nil statements
   1599 ( 20.9% of    7636) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
     17 (  0.2% of    7636) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
   5907 ( 77.4% of    7636) try candidates (use -l flag to list file positions)

Hay más por hacer, pero esto da una imagen más clara. Específicamente, el 28 % de todas las declaraciones de if parecen ser para verificación de errores; esto confirma que hay una cantidad significativa de código repetitivo. De esas verificaciones de errores, el 77 % sería susceptible de try .

$ esforzarse.
--- estadísticas ---
2930 (100.0% de 2930) declaraciones de funciones
1408 (48,1% de 2930) funciones que devuelven un error
10497 (100,0% de 10497) declaraciones
2265 (21,6% de 10497) declaraciones if
1383 (61,1% de 2265) si!= sentencias nulas
0 (0,0% de 1383)el nombre es diferente de "err" (use la bandera -l
para listar las posiciones de los archivos)
645 (46,6% de 1383) devuelve...,bloques en si!= cero
declaraciones
738 (53,4 % de 1383) controlador de errores más complejo en if!= cero
declaraciones; evitar el uso de try (use el indicador -l para enumerar las posiciones de los archivos)
1 (0.1% de 1383) bloques else no vacíos en if!= cero
declaraciones; evitar el uso de try (use el indicador -l para enumerar las posiciones de los archivos)
638 (46,1 % de 1383) prueban candidatos (utilice el indicador -l para listar el archivo)
posiciones)
$ ir proveedor mod
$ proveedor de Tryhard
--- estadísticas ---
37757 (100.0% de 37757) declaraciones de función
12557 (33,3% de 37757) funciones que devuelven un error
88919 (100,0% de 88919) declaraciones
20143 (22,7% de 88919) declaraciones si
6555 (32.5% de 20143) si!= sentencias nulas
109 (1,7% de 6555)el nombre es diferente de "err" (use la bandera -l
para listar las posiciones de los archivos)
5545 (84,6% de 6555) devuelve...,bloques en si!= cero
declaraciones
1010 (15,4 % de 6555) controlador de errores más complejo en if!= cero
declaraciones; evitar el uso de try (use el indicador -l para enumerar las posiciones de los archivos)
12 (0.2% de 6555) bloques else no vacíos en if!= cero
declaraciones; evitar el uso de try (use el indicador -l para enumerar las posiciones de los archivos)
5427 (82,8 % de 6555) prueban candidatos (utilice el indicador -l para listar el archivo)
posiciones)

Entonces, es por eso que agregué dos puntos en el ejemplo de macro, para que sobresalga y no parezca una llamada de función. No tiene que ser dos puntos, por supuesto. Es solo un ejemplo. Además, una macro no oculta nada. Solo mira lo que hace la macro, y listo. Como si fuera una función, pero estará en línea. Es como si hiciera una búsqueda y reemplazara con la pieza de código de la macro en sus funciones donde se realizó el uso de la macro. Naturalmente, si la gente crea macros de macros y comienza a complicar las cosas, bueno, culpese a sí mismo por hacer que el código sea más complicado. :)

@mirtchovski

$ tryhard .
--- stats ---
   2930 (100.0% of    2930) function declarations
   1408 ( 48.1% of    2930) functions returning an error
  10497 (100.0% of   10497) statements
   2265 ( 21.6% of   10497) if statements
   1383 ( 61.1% of    2265) if <err> != nil statements
      0 (  0.0% of    1383) <err> name is different from "err" (use -l flag to list file positions)
    645 ( 46.6% of    1383) return ..., <err> blocks in if <err> != nil statements
    738 ( 53.4% of    1383) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
      1 (  0.1% of    1383) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
    638 ( 46.1% of    1383) try candidates (use -l flag to list file
positions)
$ go mod vendor
$ tryhard vendor
--- stats ---
  37757 (100.0% of   37757) function declarations
  12557 ( 33.3% of   37757) functions returning an error
  88919 (100.0% of   88919) statements
  20143 ( 22.7% of   88919) if statements
   6555 ( 32.5% of   20143) if <err> != nil statements
    109 (  1.7% of    6555) <err> name is different from "err" (use -l flag to list file positions)
   5545 ( 84.6% of    6555) return ..., <err> blocks in if <err> != nil statements
   1010 ( 15.4% of    6555) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
     12 (  0.2% of    6555) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
   5427 ( 82.8% of    6555) try candidates (use -l flag to list file
positions)
$

@av86743 ,

lo siento, no consideré que "Las respuestas por correo electrónico no son compatibles con Markdown"

Algunas personas han comentado que no es justo contar el código de proveedor en los resultados de tryhard . Por ejemplo, en la biblioteca estándar, el código suministrado incluye los paquetes syscall generados que contienen muchas comprobaciones de errores y que pueden distorsionar la imagen general. La versión más reciente de tryhard ahora excluye las rutas de archivo que contienen "vendor" de forma predeterminada (esto también se puede controlar con el nuevo indicador -ignore ). Aplicado a la biblioteca estándar en la punta:

tryhard $HOME/go/src
/Users/gri/go/src/cmd/go/testdata/src/badpkg/x.go:1:1: expected 'package', found pkg
/Users/gri/go/src/cmd/go/testdata/src/notest/hello.go:6:1: expected declaration, found Hello
/Users/gri/go/src/cmd/go/testdata/src/syntaxerror/x_test.go:3:11: expected identifier
--- stats ---
  45424 (100.0% of   45424) func declarations
   8346 ( 18.4% of   45424) func declarations returning an error
  71401 (100.0% of   71401) statements
  16666 ( 23.3% of   71401) if statements
   4812 ( 28.9% of   16666) if <err> != nil statements
     86 (  1.8% of    4812) <err> name is different from "err" (-l flag lists details)
   3463 ( 72.0% of    4812) return ..., <err> blocks in if <err> != nil statements
   1349 ( 28.0% of    4812) complex error handler in if <err> != nil statements; cannot use try (-l flag lists details)
     17 (  0.4% of    4812) non-empty else blocks in if <err> != nil statements; cannot use try (-l flag lists details)
   3345 ( 69.5% of    4812) try candidates (-l flag lists details)

Ahora, el 29 % (28,9 %) de todas las declaraciones de if parecen ser para verificación de errores (un poco más que antes), y de ellas, alrededor del 70 % parecen ser candidatas para try (un poco más menos que antes).

El cambio https://golang.org/cl/185177 menciona este problema: src: apply tryhard -err="" -ignore="vendor" -r $GOROOT/src

@griesemer contó "controladores de errores complejos" pero no "controladores de errores de declaración única".

Si la mayoría de los controladores "complejos" son una declaración única, entonces on err #32611 produciría tantos ahorros repetitivos como try() : 2 líneas frente a 3 líneas x 70 %. Y on err agrega el beneficio de un patrón consistente para la gran mayoría de los errores.

@nvictor

try es una solución limpia y comprensible para el problema específico que está tratando de resolver:
verbosidad en el manejo de errores.

La verbosidad en el manejo de errores no es _un problema_, es la fortaleza de Go.

la propuesta dice: después de un año de discusión, estamos agregando esto incorporado. úselo si desea un código menos detallado, de lo contrario, continúe haciendo lo que hace. ¡la reacción es una resistencia no totalmente justificada para una característica opcional para la cual los miembros del equipo han mostrado claras ventajas!

Su _opt-in_ al momento de escribir es un _must_ para todos los lectores, incluido usted del futuro.

claras ventajas

Si enturbiar el flujo de control puede llamarse 'una ventaja', entonces sí.

try , por el bien de los hábitos de los expatriados de Java y C++, presenta magia que todos los Gophers deben entender. Mientras tanto, ahorre unas pocas líneas minoritarias para escribir en algunos lugares (como han demostrado las ejecuciones tryhard ).

Yo diría que mi manera más simple onErr macro ahorraría más líneas escribiendo, y para la mayoría:

x, err = fa()
onErr break

r, err := fb(x)
onErr return 0, nil, err

if r, err := fc(x); onErr && triesleft > 0 {
  triesleft--
  continue retry
}

_(Tenga en cuenta que estoy en el campo de 'dejar if err!= nil paz' ​​y se publicó una propuesta anterior para mostrar una solución más simple que puede hacer felices a más llorones)._

Editar:

Alentaría aún más al equipo de Go a que haga try una variante integrada si es fácil de hacer.
try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))

~Corto para escribir, largo para leer, propenso a errores o malentendidos, escamoso y peligroso en la etapa de mantenimiento.~

Me equivoqué. En realidad, la variante try sería mucho mejor que los nidos, como podríamos escribir por líneas:

try( outf.Seek(linkstart, 0),
 io.Copy(outf, exef),
)

y tiene un retorno try(…) después del primer error.

No creo que este manejo de error implícito (azúcar de sintaxis) como try sea bueno, porque no puede manejar múltiples errores de manera intuitiva, especialmente cuando necesita ejecutar múltiples funciones secuencialmente.

Sugeriría algo como Elixir's con declaración: https://www.openmymind.net/Elixirs-With-Statement/

Algo como esto a continuación en golang:

switch a, b, err1 := go_func_01(),
       apple, banana, err2 := go_func_02(),
       fans, dissman, err3 := go_func_03()
{
   normal_func()
else
   err1 -> handle_err1()
   err2 -> handle_err2()
   _ -> handle_other_errs()
}

¿Es este tipo de violación de "Go prefiere menos funciones" y "agregar funciones a Go no lo haría mejor sino más grande"? No estoy seguro...

Solo quiero decir que, personalmente, estoy perfectamente satisfecho con la forma antigua.

if err != nil {
    return …, err
}

Y definitivamente no quiero leer el código escrito por otros usando el try ... La razón puede ser doble:

  1. a veces es difícil adivinar lo que hay dentro a primera vista
  2. try s se pueden anidar, es decir, try( ... try( ... try ( ... ) ... ) ... ) , difíciles de leer

Si cree que escribir código a la antigua para pasar errores es tedioso, ¿por qué no simplemente copiar y pegar, ya que siempre hacen el mismo trabajo?

Bueno, podría pensar que no siempre queremos hacer el mismo trabajo, pero luego tendrá que escribir su función de "controlador". Así que tal vez no pierdas nada si sigues escribiendo a la antigua usanza.

¿No es el rendimiento de diferir un problema con esta solución propuesta? He comparado funciones con y sin diferir y hubo un impacto significativo en el rendimiento. Acabo de buscar en Google a otra persona que haya hecho un punto de referencia de este tipo y encontré un costo de 16x. No recuerdo que el mío fuera tan malo, pero 4 veces más lento me suena. ¿Cómo se puede considerar una solución general viable algo que podría duplicar o empeorar el tiempo de ejecución de muchas funciones?

@ eric-hawthorne El rendimiento diferido es un tema aparte. Try no requiere inherentemente aplazar y no elimina la capacidad de manejar errores sin él.

@fabian-f Pero esta propuesta podría alentar el reemplazo del código en el que alguien está decorando los errores por separado para cada error en línea dentro del alcance del bloque if err != nil. Eso sería una diferencia de rendimiento significativa.

@ eric-hawthorne Citando el documento de diseño:

P: ¿Usar aplazar para envolver errores no va a ser lento?

R: Actualmente, una sentencia diferida es relativamente costosa en comparación con el flujo de control ordinario. Sin embargo, creemos que es posible hacer que los casos de uso común de aplazamiento para el manejo de errores sean comparables en rendimiento con el enfoque "manual" actual. Consulte también CL 171758, que se espera que mejore el rendimiento de diferir en alrededor de un 30 %.

Aquí hubo una charla interesante de Rust vinculada en Reddit. La parte más relevante comienza a las 47:55

Probé con fuerza en mi repositorio público más grande, https://github.com/dpinela/mflg , y obtuve lo siguiente:

--- stats ---
    309 (100.0% of     309) func declarations
     36 ( 11.7% of     309) func declarations returning an error
    305 (100.0% of     305) statements
     73 ( 23.9% of     305) if statements
     29 ( 39.7% of      73) if <err> != nil statements
      0 (  0.0% of      29) <err> name is different from "err"
     19 ( 65.5% of      29) return ..., <err> blocks in if <err> != nil statements
     10 ( 34.5% of      29) complex error handler in if <err> != nil statements; cannot use try
      0 (  0.0% of      29) non-empty else blocks in if <err> != nil statements; cannot use try
     15 ( 51.7% of      29) try candidates

La mayor parte del código en ese repositorio está administrando el estado del editor interno y no realiza ninguna E/S, por lo que tiene pocas verificaciones de errores; por lo tanto, los lugares donde se puede usar try son relativamente limitados. Seguí adelante y reescribí manualmente el código para usar try siempre que sea posible; git diff --stat devuelve lo siguiente:

 application.go                  | 42 +++++++++++-------------------------------
 internal/atomicwrite/write.go   | 35 ++++++++++++++---------------------
 internal/clipboard/clipboard.go | 17 +++--------------
 internal/config/config.go       | 15 +++++++--------
 internal/termesc/term.go        |  5 +----
 render.go                       |  8 ++------
 6 files changed, 38 insertions(+), 84 deletions(-)

(Diferencia completa aquí ).

De los 10 controladores que Tryhard informa como "complejos", 5 son falsos negativos en internal/atomicwrite/write.go; estaban usando pkg/errors.WithMessage para envolver el error. El envoltorio era exactamente el mismo para todos ellos, así que reescribí esa función para usar controladores de prueba y diferidos. Terminé con esta diferencia (+14, -21 líneas):

@@ -20,21 +20,20 @@ const (
 // The file is created with mode 0644 if it doesn't already exist; if it does, its permissions will be
 // preserved if possible.
 // If some of the directories on the path don't already exist, they are created with mode 0755.
-func Write(filename string, contentWriter func(io.Writer) error) error {
+func Write(filename string, contentWriter func(io.Writer) error) (err error) {
+       defer func() { err = errors.WithMessage(err, errString(filename)) }()
+
        dir := filepath.Dir(filename)
-       if err := os.MkdirAll(dir, defaultDirPerms); err != nil {
-               return errors.WithMessage(err, errString(filename))
-       }
-       tf, err := ioutil.TempFile(dir, "mflg-atomic-write")
-       if err != nil {
-               return errors.WithMessage(err, errString(filename))
-       }
+       try(os.MkdirAll(dir, defaultDirPerms))
+       tf := try(ioutil.TempFile(dir, "mflg-atomic-write"))
        name := tf.Name()
-       if err = contentWriter(tf); err != nil {
-               os.Remove(name)
-               tf.Close()
-               return errors.WithMessage(err, errString(filename))
-       }
+       defer func() {
+               if err != nil {
+                       tf.Close()
+                       os.Remove(name)
+               }
+       }()
+       try(contentWriter(tf))
        // Keep existing file's permissions, when possible. This may race with a chmod() on the file.
        perms := defaultPerms
        if info, err := os.Stat(filename); err == nil {
@@ -42,14 +41,8 @@ func Write(filename string, contentWriter func(io.Writer) error) error {
        }
        // It's better to save a file with the default TempFile permissions than not save at all, so if this fails we just carry on.
        tf.Chmod(perms)
-       if err = tf.Close(); err != nil {
-               os.Remove(name)
-               return errors.WithMessage(err, errString(filename))
-       }
-       if err = os.Rename(name, filename); err != nil {
-               os.Remove(name)
-               return errors.WithMessage(err, errString(filename))
-       }
+       try(tf.Close())
+       try(os.Rename(name, filename))
        return nil
 }

Observe el primer aplazamiento, que anota el error: pude ajustarlo cómodamente en una línea gracias a que WithMessage devolvió cero por un error nulo. Parece que este tipo de envoltorio funciona tan bien con este enfoque como los sugeridos en la propuesta.

Dos de los otros controladores "complejos" estaban en implementaciones de ReadFrom y WriteTo:

var line string
line, err = br.ReadString('\n')
b.lines = append(b.lines, line)
if err != nil {
  if err == io.EOF {
    err = nil
  }
  return
}
func (b *Buffer) WriteTo(w io.Writer) (int64, error) {
    var n int64
    for _, line := range b.lines {
        nw, err := w.Write([]byte(line))
        n += int64(nw)
        if err != nil {
            return n, err
        }
    }
    return n, nil
}

Estos realmente no eran susceptibles de probar, así que los dejé solos.

Otros dos eran códigos como este, donde estoy devolviendo un error completamente diferente al que verifiqué (no solo envolviéndolo). Los dejé sin cambios también:

n, err := strconv.ParseInt(s[1:], 16, 32)
if err != nil {
    return Color{}, errors.WithMessage(err, fmt.Sprintf("color: parse %q", s))
}

El último estaba en una función para cargar un archivo de configuración, que siempre devuelve una configuración (distinta de cero) incluso si hay un error. Solo tenía esta verificación de error, por lo que no se benefició mucho de probar:

-func Load() (*Config, error) {
-       c := Config{
+func Load() (c *Config, err error) {
+       defer func() { err = errors.WithMessage(err, "error loading config file") }()
+
+       c = &Config{
                TabWidth:    4,
                ScrollSpeed: 1,
                Lang:        make(map[string]LangConfig),
        }
-       f, err := basedir.Config.Open(filepath.Join("mflg", "config.toml"))
-       if err != nil {
-               return &c, errors.WithMessage(err, "error loading config file")
-       }
+       f := try(basedir.Config.Open(filepath.Join("mflg", "config.toml")))
        defer f.Close()
-       _, err = toml.DecodeReader(f, &c)
+       _, err = toml.DecodeReader(f, c)
        if c.TextStyle.Comment == (Style{}) {
                c.TextStyle.Comment = Style{Foreground: &color.Color{R: 0, G: 200, B: 0}}
        }
        if c.TextStyle.String == (Style{}) {
                c.TextStyle.String = Style{Foreground: &color.Color{R: 0, G: 0, B: 200}}
        }
-       return &c, errors.WithMessage(err, "error loading config file")
+       return c, err
 }

De hecho, confiar en el comportamiento de try de mantener los valores de los parámetros de retorno, como un retorno desnudo, se siente, en mi opinión, un poco más difícil de seguir; a menos que agregue más controles de error, me quedaría con if err != nil en este caso particular.

TL; DR: try solo es útil en un porcentaje bastante pequeño (por número de líneas) de este código, pero donde ayuda, realmente ayuda.

(Noob aquí). Otra idea para argumentos múltiples. Qué tal si:

package trytest

import "fmt"

func errorInner() (string, error) {
   return "", fmt.Errorf("inner error")
}

func errorOuter() (string, error) {
   tryreturn errorInner()
   return "", nil
}

func errorOuterWithArg() (string, error) {
   var toProcess string
   tryreturn toProcess, _ = errorOuter()
   return toProcess + "", nil
}

func errorOuterWithArgStretch() (bool, string, error) {
   var toProcess string
   tryreturn false, ( toProcess,_ = errorOuterWithArg() )
   return true, toProcess + "", nil
}

es decir, tryreturn activa la devolución de todos los valores si hay un error en el último
valor, de lo contrario la ejecución continúa.

Los principios con los que estoy de acuerdo:
-

  • El manejo de errores de una llamada de función merece su propia línea. Go es deliberadamente explícito en el flujo de control, y creo que empaquetar eso en una expresión está en desacuerdo con su claridad.
  • Sería beneficioso tener un método de manejo de errores que se ajuste a una línea. (E idealmente requiere solo una palabra o algunos caracteres repetitivos antes del manejo real del error). 3 líneas de manejo de errores para cada llamada de función es un punto de fricción en el lenguaje que merece algo de amor y atención.
  • Cualquier elemento integrado que devuelva (como el try propuesto) debe ser al menos una declaración, e idealmente debe contener la palabra retorno. Nuevamente, creo que el flujo de control en Go debería ser explícito.
  • Los errores de Go son más útiles cuando tienen contexto adicional incluido (casi siempre agrego contexto a mis errores). Una solución para este problema también debería ser compatible con el código de manejo de errores de adición de contexto.

Sintaxis que apoyo:
-

  • una instrucción reterr _x_ (azúcar sintáctica para if err != nil { return _x_ } , explícitamente nombrada para indicar que regresará)

Entonces, los casos comunes podrían ser una buena línea corta y explícita:

func foo() error {
    a, err := bar()
    reterr err

    b, err := baz(a)
    reterr fmt.Errorf("getting the baz of %v: %v", a, err)

    return nil
}

En lugar de las 3 líneas ahora son:

func foo() error {
    a, err := bar()
    if err != nil {
        return err
    }

    b, err := baz()
    if err != nil {
        return fmt.Errorf("getting the baz of %v: %v", a, err)
    }

    return nil
}

Cosas con las que no estoy de acuerdo:



    • "Este es un cambio demasiado pequeño para que valga la pena cambiar el idioma"

      No estoy de acuerdo, este es un cambio de calidad de vida que elimina la mayor fuente de fricción que tengo al escribir código Go. Cuando llamar a una función requiere 4 líneas

  • "Sería mejor esperar a una solución más general"
    No estoy de acuerdo, creo que este problema es digno de su propia solución dedicada. La versión generalizada de este problema es reducir el código repetitivo, y la respuesta generalizada son las macros, lo que va en contra de la ética Go del código explícito. Si Go no va a proporcionar una función general de macros, entonces debería proporcionar algunas macros específicas y muy utilizadas como reterr (todas las personas que escriben Go se beneficiarían de reterr).

@Qhesz No es muy diferente con try:

func foo() error {
    a, err := bar()
    try(err)

    b, err := baz(a)
    try(wrap(err, "getting the baz of %v", a))

    return nil
}

@reusee Agradezco esa sugerencia, no me di cuenta de que podría usarse así. Sin embargo, me parece un poco irritante, estoy tratando de señalar por qué.

Creo que "intentar" es una palabra extraña para usar de esa manera. "try(action())" tiene sentido en inglés, mientras que "try(value)" no lo tiene. Estaría más bien si fuera una palabra diferente.

También try(wrap(...)) evalúa wrap(...) primero, ¿verdad? ¿Cuánto de eso crees que se optimiza con el compilador? (¿En comparación con solo ejecutar if err != nil ?)

También #32611 es una propuesta vagamente similar, y los comentarios tienen algunas opiniones esclarecedoras tanto del equipo central de Go como de los miembros de la comunidad, en particular en torno a las diferencias entre palabras clave y funciones integradas.

@Qhesz Estoy de acuerdo contigo sobre el nombre. Tal vez check sea más apropiado ya que "verificar (acción ())" o "verificar (err)" se lee bien.

@reusee Lo cual es un poco irónico, ya que el borrador del diseño original usaba check .

El 6/7/19, mirtchovski [email protected] escribió:

$ esforzarse.
--- estadísticas ---
2930 (100.0% de 2930) declaraciones de funciones
1408 (48,1% de 2930) funciones que devuelven un error
[ ... ]

No puedo evitar ser travieso aquí: es que "funciones que devuelven un
error como último argumento"?

Lucio.

Pensamiento final sobre mi pregunta anterior, aún prefiero la sintaxis try(err, wrap("getting the baz of %v: %v", a, err)) , con wrap() solo ejecutado si err no es nulo. En lugar de try(wrap(err, "getting the baz of %v", a)) .

@Qhesz Una posible implementación de wrap podría ser:

func wrap(err error, format string, args ...interface{}) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}

Si el compilador puede alinear wrap , entonces no hay diferencia de rendimiento entre la cláusula wrap y if err != nil .

@reusee Creo que quisiste decir if err == nil ;)

@Qhesz Una posible implementación de wrap podría ser:

func wrap(err error, format string, args ...interface{}) error {
  if err == nil {
      return nil
  }
  return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}

Si el compilador puede alinear wrap , entonces no hay diferencia de rendimiento entre la cláusula wrap y if err != nil .

%w no es válido go verbo

(Supongo que quiso decir %v...)

Entonces, aunque sería preferible escribir una palabra clave, entiendo que una función integrada es la forma preferida de implementarla.

Creo que estaría de acuerdo con esta propuesta si

  • era check en lugar de try
  • alguna parte de las herramientas de Go impuso que solo podría usarse como una declaración (es decir, trátela como una 'declaración' integrada, no como una 'función' integrada. Es solo una función integrada por razones prácticas, está tratando de ser una declaración sin ser implementado por el idioma.) Por ejemplo, si no devolvió nada, entonces nunca fue válido dentro de una expresión, como panic() .
  • ~tal vez algún indicador de que es una macro e influye en el flujo de control, algo que lo diferencia de una llamada de función. (por ejemplo check!(...) como lo hace Rust, pero no tengo una opinión sólida sobre la sintaxis específica) ~ Cambié de opinión

Entonces eso sería genial, lo usaría en cada llamada de función que hago.

Y disculpas menores al hilo, solo ahora encontré los comentarios anteriores que describen más o menos lo que acabo de decir.

@deanveloper arreglado, gracias.

@olekukonko @Qhesz %w se agregó recientemente en la sugerencia: https://tip.golang.org/pkg/fmt/#Errorf

Me disculpo por no haber leído todo en este tema, pero me gustaría mencionar algo que no he visto.

Veo dos casos separados en los que el manejo de errores de Go1 puede ser molesto: código "bueno" que es correcto pero un poco repetitivo; y código "malo" que está mal, pero en su mayoría funciona.

En el primer caso, realmente debería haber algo de lógica en el bloque if-err, y pasar a una construcción de estilo try desalienta esta buena práctica al dificultar la adición de lógica adicional.

En el segundo caso, el código incorrecto suele tener la forma:

..., _ := might_error()

o solo

might_error()

Cuando esto ocurre, normalmente es porque el autor no cree que sea lo suficientemente importante como para dedicar tiempo al manejo de errores, y solo espera que todo funcione. Este caso podría mejorarse con algo muy cercano al esfuerzo cero, como:

..., XXX := might_error()

donde XXX es un símbolo que significa "cualquier cosa aquí debería detener la ejecución de alguna manera". Esto dejaría en claro que este no es un código listo para la producción: el autor está al tanto de un caso de error, pero no ha invertido el tiempo para decidir qué hacer.

Por supuesto, esto no excluye una solución de tipo returnif handle(err) .

Estoy en contra de intentarlo, en general, con elogios a los colaboradores por el diseño agradablemente minimalista. No soy un gran experto en Go, pero fui uno de los primeros en adoptar y tengo código en producción aquí y allá. Trabajo en el grupo Serverless en AWS y parece que lanzaremos un servicio basado en Go a finales de este año, cuyo primer registro fue escrito sustancialmente por mí. Soy un tipo muy viejo, mi camino a seguir pasó por C, Perl, Java y Ruby. Mis problemas han aparecido antes en el muy útil resumen del debate, pero sigo pensando que vale la pena reiterarlos.

  1. Go es un lenguaje pequeño y simple y, por lo tanto, ha logrado una legibilidad sin igual. Reflexivamente, estoy en contra de agregarle algo a menos que el beneficio sea realmente cualitativamente sustancial. Por lo general, uno no se da cuenta de una pendiente resbaladiza hasta que está en ella, así que no demos el primer paso.
  2. Me afectó bastante el argumento anterior sobre facilitar la depuración. Me gusta el ritmo visual, en código de infraestructura de bajo nivel, de pequeñas estrofas de código del tipo “Haz A. Comprueba si funcionó. Haz B. Verifica si funcionó... etc. "Porque las líneas de "Verificar" son donde colocas el printf o el punto de interrupción. Tal vez todos los demás sean más inteligentes, pero termino usando ese idioma de punto de interrupción con regularidad.
  3. Asumiendo valores devueltos con nombre, "intentar" es aproximadamente equivalente a if err != nil { return } (¿creo?) Personalmente, me gustan los valores devueltos con nombre y, dadas las ventajas de los decoradores de errores, sospecho que la proporción de valores devueltos de error con nombre va a aumentar monótonamente; lo que debilita los beneficios de probar.
  4. Inicialmente me gustó la propuesta de que gofmt bendiga la frase de una línea en la línea anterior, pero en general, los IDE sin duda adoptarán este idioma de visualización de todos modos, y la frase de una línea sacrificaría el beneficio de depuración aquí.
  5. Parece bastante probable que algunas formas de anidamiento de expresiones que contengan "intentar" abran la puerta para que los complicados de nuestra profesión causen el mismo tipo de estragos que tienen con los flujos de Java y los divisores, etc. Go ha tenido más éxito que la mayoría de los otros idiomas en negar a los inteligentes entre nosotros oportunidades para demostrar sus habilidades.

Nuevamente, felicitaciones a la comunidad por la propuesta limpia y agradable y la discusión constructiva.

He pasado una cantidad significativa de tiempo saltando y leyendo bibliotecas desconocidas o fragmentos de código en los últimos años. A pesar del tedio, if err != nil proporciona un idioma muy fácil de leer, aunque verticalmente detallado. El espíritu de lo que try() está tratando de lograr es noble, y creo que hay algo que hacer, pero esta característica se siente mal priorizada y que la propuesta está viendo la luz del día demasiado pronto (es decir, debería venir después xerr y los genéricos han tenido la oportunidad de marinar en una versión estable durante 6-12 meses).

La introducción try() parece ser una propuesta noble y valiosa (por ejemplo, el 29 % - ~40 % de las declaraciones de if son para if err != nil verificación). En la superficie, parece que la reducción del modelo asociado con el manejo de errores mejorará las experiencias de los desarrolladores. La compensación de la introducción de try() viene en forma de carga cognitiva de los casos especiales semi-sutiles. Una de las mayores virtudes de Go es que es simple y se requiere muy poca carga cognitiva para hacer algo (en comparación con C++, donde la especificación del lenguaje es amplia y matizada). Reducir una métrica cuantitativa (LdC de if err != nil ) a cambio de aumentar la métrica cuantitativa de la complejidad mental es una píldora difícil de tragar (es decir, el impuesto mental sobre el recurso más preciado que tenemos, la capacidad intelectual).

En particular, los nuevos casos especiales para la forma en que try() se maneja con go , defer , y las variables de retorno nombradas hacen que try() sea lo suficientemente mágico como para hacer que el código sea menos explícito de tal manera que todos los autores o lectores de código Go tendrán que conocer estos nuevos casos especiales para poder leer o escribir correctamente Go y tal carga no existía anteriormente. Me gusta que haya casos especiales explícitos para estas situaciones, especialmente en comparación con la introducción de algún tipo de comportamiento indefinido, pero el hecho de que deban existir en primer lugar indica que esto está incompleto en este momento. Si los casos especiales fueran para algo más que el manejo de errores, podría ser aceptable, pero si ya estamos hablando de algo que podría afectar hasta el 40% de todas las LoC, estos casos especiales deberán ser capacitados en toda la comunidad y eso eleva el costo de la carga cognitiva de esta propuesta a un nivel lo suficientemente alto como para merecer preocupación.

Hay otro ejemplo en Go donde las reglas de casos especiales ya son una pendiente cognitiva resbaladiza, a saber, variables fijadas y no fijadas. La necesidad de anclar variables no es difícil de entender en la práctica, pero se pasa por alto porque hay un comportamiento implícito aquí y esto provoca una falta de coincidencia entre el autor, el lector y lo que sucede con el ejecutable compilado en tiempo de ejecución. Incluso con linters como scopelint muchos desarrolladores todavía no parecen entender este problema (o peor aún, lo saben pero lo extrañan porque se les olvida). Algunos de los errores de tiempo de ejecución más inesperados y difíciles de diagnosticar de programas en funcionamiento provienen de este problema en particular (por ejemplo, N objetos se completan con el mismo valor en lugar de iterar sobre un segmento y obtener los distintos valores esperados). El dominio de error de try() es diferente de las variables ancladas, pero como resultado, habrá un impacto en la forma en que las personas escriben código.

IMNSHO, las propuestas xerr y genéricos necesitan tiempo para hornearse en la producción durante 6-12 meses antes de intentar conquistar el estándar de if err != nil . Los genéricos probablemente allanarán el camino para un manejo de errores más completo y una nueva forma idiomática de manejo de errores. Una vez que comienza a surgir el manejo de errores idiomáticos con genéricos, entonces, y solo entonces, tiene sentido volver a examinar una discusión sobre try() o lo que sea.

No pretendo saber cómo afectarán los genéricos al manejo de errores, pero me parece seguro que los genéricos se usarán para crear tipos enriquecidos que casi seguramente se usarán en el manejo de errores. Una vez que los genéricos hayan permeado las bibliotecas y se hayan agregado al manejo de errores, puede haber una manera obvia de reutilizar los try() para mejorar la experiencia del desarrollador con respecto al manejo de errores.

Los puntos de preocupación que tengo son:

  1. try() no es complicado de forma aislada, pero es una sobrecarga cognitiva donde antes no existía.
  2. Al incorporar err != nil en el comportamiento asumido de try() , el lenguaje impide el uso de err como una forma de comunicar el estado de la pila.
  3. Estéticamente try() se siente como una inteligencia forzada, pero no lo suficientemente inteligente como para satisfacer la prueba explícita y obvia que disfruta la mayoría del lenguaje Go. Como la mayoría de las cosas que involucran criterios subjetivos, esta es una cuestión de gusto y experiencia personal y es difícil de cuantificar.
  4. El manejo de errores con declaraciones switch / case y el envoltorio de error parecen no haber sido tocados por esta propuesta, y es una oportunidad perdida, lo que me lleva a creer que esta propuesta está a un paso de convertir un desconocido-desconocido en conocido. -conocido (o en el peor de los casos, un conocido-desconocido).

Por último, la propuesta try() se siente como una nueva ruptura en la presa que estaba frenando una avalancha de matices específicos del lenguaje como el que escapamos al dejar atrás C++.

TL;DR: no es tanto una respuesta #nevertry como lo es, "no ahora, todavía no, y consideremos esto nuevamente en el futuro después de que xerr y los genéricos maduren en el ecosistema. "

El #32968 vinculado anteriormente no es exactamente una contrapropuesta completa, pero se basa en mi desacuerdo con la peligrosa capacidad de anidar que posee la macro try . A diferencia de #32946, esta es una propuesta seria, que espero carezca de fallas serias (puedes verla, evaluarla y comentarla, por supuesto). Extracto:

  • _La macro check no es de una sola línea: ayuda más donde hay muchas repeticiones
    las comprobaciones que utilicen la misma expresión deben realizarse muy próximas._
  • _Su versión implícita ya compila en playground._

Restricciones de diseño (cumplidas)

Es integrado, no anida en una sola línea, permite muchos más flujos que try y no tiene expectativas sobre la forma de un código interno. No fomenta las devoluciones desnudas.

ejemplo de uso

// built-in 'check' macro signature: 
func check(Condition bool) {}

check(err != nil) // explicit catch: label.
{
    ucred, err := getUserCredentials(user)
    remote, err := connectToApi(remoteUri)
    err, session, usertoken := remote.Auth(user, ucred)
    udata, err := session.getCalendar(usertoken)

  catch:               // sad path
    ucred.Clear()      // cleanup passwords
    remote.Close()     // do not leak sockets
    return nil, 0, err // dress before leaving
}
// happy path

// implicit catch: label is above last statement
check(x < 4) 
  {
    x, y = transformA(x, z)
    y, z = transformB(x, y)
    x, y = transformC(y, z)
    break // if x was < 4 after any of above
  }

Espero que esto ayude, ¡Disfrútalo!

He leído todo lo que he podido para comprender mejor este hilo. Estoy a favor de dejar las cosas tal y como están.

mis razones:

  1. Yo, y nadie a quien le he enseñado a Go, nunca ha entendido el manejo de errores.
  2. Me doy cuenta de que nunca me salto una trampa de error porque es muy fácil hacerlo en ese mismo momento.

Además, tal vez no entienda bien la propuesta, pero por lo general, la construcción try en otros idiomas da como resultado varias líneas de código que pueden generar potencialmente un error, por lo que requieren tipos de error. Agregar complejidad y, a menudo, algún tipo de arquitectura de error inicial y esfuerzo de diseño.

En esos casos (y lo he hecho yo mismo), se agregan múltiples bloques de prueba. que alarga el código y eclipsa la implementación.

Si la implementación de Go de try difiere de la de otros idiomas, surgirá aún más confusión.

Mi sugerencia es dejar el manejo de errores como está.

Sé que mucha gente ha opinado, pero me gustaría agregar mi crítica de la especificación tal como está.

La parte de la especificación que más me preocupa son estas dos solicitudes:

Por lo tanto, sugerimos no permitir try como la función llamada en una instrucción go.
...
Por lo tanto, sugerimos no permitir try como la función llamada en una declaración diferida también.

Esta sería la primera función integrada en la que esto es cierto (incluso puede defer y go a panic ) editar porque no es necesario descartar el resultado. La creación de una nueva función incorporada que requiere que el compilador dé una consideración especial al flujo de control parece una gran tarea y rompe la coherencia semántica de go. Cualquier otro token de flujo de control en go no es una función.

Un contraargumento a mi queja es que ser capaz de defer y go a panic es probablemente un accidente y no muy útil. Sin embargo, mi punto es que la coherencia semántica de las funciones en go se rompe con esta propuesta, no que sea importante que defer y go siempre tengan sentido de usar. Probablemente hay muchas funciones no integradas con las que nunca tendría sentido usar defer o go , pero no hay una razón explícita, semánticamente, por la que no puedan serlo. ¿Por qué este builtin llega a eximirse del contrato semántico de funciones en go?

Sé que @griesemer no quiere opiniones estéticas sobre esta propuesta en la discusión, pero sí creo que una de las razones por las que la gente encuentra esta propuesta estéticamente repugnante es que pueden sentir que no cuadra como una función.

La propuesta dice:

Proponemos agregar una nueva función integrada llamada probar con firma (pseudocódigo)

func try(expr) (T1, T2, … Tn)

Excepto que esto no es una función (que la propuesta básicamente admite). Es, efectivamente, una macro única integrada en la especificación del idioma (si se aceptara). Hay algunos problemas con esta firma.

  1. ¿Qué significa que una función acepte una expresión genérica como argumento, sin mencionar una expresión llamada? Cada vez que se usa la palabra "expresión" en la especificación, significa algo así como una función no llamada. ¿Cómo es que se puede pensar que una función "llamada" es una expresión, cuando en todos los demás contextos sus valores de retorno son lo que está semánticamente activo? Es decir, pensamos en una función llamada como sus valores de retorno. Las excepciones, de manera reveladora, son go y defer , que son tokens sin procesar y no funciones integradas.

  2. Además, esta propuesta tiene su propia firma de función incorrecta, o al menos no tiene sentido, la firma real es:

func try(R1, R2, ... Rn) ((R|T)1, (R|T)2, ... (R|T)(n-1), ?Rn) 
// where T is the return params of the function that try is being called from
// where `R` is a return value from a function, `Rn` must be an error
// try will return the R values if Rn is nil and not return Tn at all
// if Rn is not nil then the T values will be returned as well as Rn at the end 
  1. La propuesta no incluye lo que sucede en situaciones en las que se llama try con argumentos. Qué sucede si se llama a try con argumentos:
try(arg1, arg2,..., err)

Creo que la razón por la que esto no se aborda es porque try está tratando de aceptar un argumento expr que en realidad representa una cantidad de argumentos de retorno de una función más algo más, lo que ilustra aún más el hecho. que esta propuesta rompe la coherencia semántica de lo que es una función.

Mi queja final contra esta propuesta es que rompe aún más el significado semántico de las funciones integradas. No soy indiferente a la idea de que las funciones integradas a veces necesitan estar exentas de las reglas semánticas de las funciones "normales" (como no poder asignarlas a variables, etc.), pero esta propuesta crea un gran conjunto de exenciones de la " reglas normales" que parecen gobernar las funciones dentro de golang.

Esta propuesta efectivamente convierte a try en algo nuevo que go no ha tenido, no es exactamente un token ni una función, son ambas cosas, lo que parece un mal precedente para establecer en términos de crear coherencia semántica en todo el idioma.

Si vamos a agregar una nueva cosa de flujo de control, creo que tiene más sentido convertirlo en un token sin formato como goto , et al. Sé que se supone que no debemos lanzar propuestas en esta discusión, pero a modo de breve ejemplo, creo que algo como esto tiene mucho más sentido:

f, err := os.Open("/dev/stdout")
throw err

Si bien esto agrega una línea adicional de código, creo que soluciona todos los problemas que planteé y también elimina toda la deficiencia de firmas de funciones "alternativas" con try .

edit1 : nota sobre las excepciones a los casos defer y go en los que no se puede usar la función incorporada, porque los resultados se ignorarán, mientras que con try ni siquiera se puede hacer dijo que la función tiene resultados.

@nathanjsweet la propuesta que buscas es la #32611 :-)

@nathanjsweet Parte de lo que dices resulta no ser el caso. El idioma no permite usar defer o go con las funciones predeclaradas append cap complex imag len make new real . Tampoco permite defer o go con las funciones definidas por especificaciones unsafe.Alignof unsafe.Offsetof unsafe.Sizeof .

Gracias @nathanjsweet por tu extenso comentario - @ianlancetaylor ya señaló que tus argumentos son técnicamente incorrectos. Permítanme ampliar un poco:

1) Mencionas que la parte de la especificación que no permite try con go y defer te preocupa más porque try sería el primero integrado donde esto es cierto. Esto no es correcto. El compilador ya no permite, por ejemplo, defer append(a, 1) . Lo mismo ocurre con otros elementos integrados que producen un resultado que luego se deja caer al suelo. Esta misma restricción también se aplicaría a try (excepto cuando try no devuelva un resultado). (La razón por la que incluso hemos mencionado estas restricciones en el documento de diseño es para ser lo más exhaustivos posible; son realmente irrelevantes en la práctica. Además, si lee el documento de diseño con precisión, no dice que no podamos hacer try trabajar con go o defer - simplemente sugiere que no lo permitamos, principalmente como una medida práctica. Es una "gran petición" - para usar sus palabras - para hacer try funciona con go y defer aunque es prácticamente inútil).

2) Usted sugiere que algunas personas encuentran try "estéticamente repugnante" porque técnicamente no es una función, y luego se concentra en las reglas especiales para la firma. Considere new , make , append , unsafe.Offsetof : todos tienen reglas especializadas que no podemos expresar con una función Go ordinaria. Mire unsafe.Offsetof que tiene exactamente el tipo de requisito sintáctico para su argumento (¡debe ser un campo de estructura!) que requerimos del argumento para try (debe ser un valor único de tipo error o una llamada de función que devuelve error como último resultado). No expresamos esas firmas formalmente en la especificación, para ninguna de estas funciones integradas porque no encajan en el formalismo existente; si lo hicieran, no tendrían que ser funciones integradas. En cambio, expresamos sus reglas en prosa. Es por eso que son elementos integrados que _son_ la escotilla de escape en Go, por diseño, desde el primer día. Tenga en cuenta también que el documento de diseño es muy explícito al respecto.

3) La propuesta también aborda lo que sucede cuando se llama a try con argumentos (más de uno): no está permitido. El documento de diseño establece explícitamente que try acepta una (una) expresión de argumento entrante.

4) Está afirmando que "esta propuesta rompe el significado semántico de las funciones integradas". En ninguna parte Go restringe lo que puede hacer un dispositivo integrado y lo que no puede hacer. Tenemos completa libertad aquí.

Gracias.

@griesemer

Tenga en cuenta también que el documento de diseño es muy explícito al respecto.

¿Puedes señalar esto? Me sorprendió leer esto.

Está afirmando que "esta propuesta rompe el significado semántico de las funciones integradas". En ninguna parte Go restringe lo que puede hacer un dispositivo integrado y lo que no puede hacer. Tenemos completa libertad aquí.

Creo que este es un punto justo. Sin embargo, creo que existe lo que se detalla en los documentos de diseño y lo que se siente como "ir" (que es algo de lo que Rob Pike habla mucho). Creo que es justo para mí decir que la propuesta try amplía las formas en que las funciones integradas rompen las reglas por las que esperamos que se comporten las funciones, y reconozco que entiendo por qué esto es necesario para otras funciones integradas. , pero creo que en este caso la expansión de romper las reglas es:

  1. Contra-intuitivo en algunos aspectos. Esta es la primera función que cambia la lógica del flujo de control de una manera que no deshace la pila (como lo hacen panic y os.Exit )
  2. Una nueva excepción a cómo funcionan las convenciones de llamada de una función. Usted dio el ejemplo de unsafe.Offsetof como un caso en el que existe un requisito sintáctico para una llamada de función (realmente me sorprende que esto provoque un error en tiempo de compilación, pero ese es otro problema), pero el requisito sintáctico , en este caso, es un requisito sintáctico diferente al que usted indicó. unsafe.Offsetof requiere un argumento, mientras que try requiere una expresión que se vería, en cualquier otro contexto, como un valor devuelto por una función (es decir try(os.Open("/dev/stdout")) ) y podría asumirse con seguridad en cualquier otro contexto para devolver solo un valor (a menos que la expresión se vea como try(os.Open("/dev/stdout")...) ).

@nathanjdulce escribió:

Tenga en cuenta también que el documento de diseño es muy explícito al respecto.

¿Puedes señalar esto? Me sorprendió leer esto.

Está en la sección "Conclusiones" de la propuesta:

En Go, los elementos integrados son el mecanismo de escape de lenguaje elegido para operaciones que son irregulares de alguna manera pero que no justifican una sintaxis especial.

Me sorprende que te lo hayas perdido ;-)

@ngrilly No me refiero a esta propuesta, me refiero a la especificación del lenguaje go. Tuve la impresión de que @griesemer estaba diciendo que la especificación del lenguaje go llama a las funciones integradas como el mecanismo específicamente útil para romper la convención sintáctica.

@nathanjdulce

Contra-intuitivo en algunos aspectos. Esta es la primera función que cambia la lógica del flujo de control de una manera que no deshace la pila (como panic y os.Exit do)

No creo que os.Exit desenrolle la pila en ningún sentido útil. Termina el programa inmediatamente sin ejecutar ninguna función diferida. Me parece que os.Exit es el extraño aquí, ya que tanto panic como try ejecutan funciones diferidas y viajan hacia arriba en la pila.

Estoy de acuerdo en que os.Exit es el extraño, pero tiene que ser así. os.Exit detiene todas las rutinas; no tendría sentido ejecutar solo las funciones diferidas de solo la gorutina que llama a os.Exit . Debería ejecutar todas las funciones diferidas o ninguna. Y es mucho más fácil ejecutar ninguno.

Ejecutó tryhard en nuestro código base y esto es lo que obtuvimos:

--- stats ---
  15298 (100.0% of   15298) func declarations
   3026 ( 19.8% of   15298) func declarations returning an error
  33941 (100.0% of   33941) statements
   7765 ( 22.9% of   33941) if statements
   3747 ( 48.3% of    7765) if <err> != nil statements
    131 (  3.5% of    3747) <err> name is different from "err"
   1847 ( 49.3% of    3747) return ..., <err> blocks in if <err> != nil statements
   1900 ( 50.7% of    3747) complex error handler in if <err> != nil statements; cannot use try
     19 (  0.5% of    3747) non-empty else blocks in if <err> != nil statements; cannot use try
   1789 ( 47.7% of    3747) try candidates

Primero, quiero aclarar que debido a que Go (antes de 1.13) carece de contexto en los errores, implementamos nuestro propio tipo de error que implementa la interfaz error , algunas funciones se declaran como devolviendo foo.Error en lugar de error , y parece que este analizador no lo detectó, por lo que estos resultados no son "justos".

Yo estaba en el campo de "¡sí! Hagámoslo", y creo que será un experimento interesante para 1.13 o 1.14 betas , pero estoy preocupado por el _" 47.7% ... probar candidatos"_. Ahora significa que hay 2 formas de hacer las cosas, que no me gustan. Sin embargo, también hay 2 formas de crear un puntero ( new(Foo) vs &Foo{} ), así como 2 formas de crear una división o un mapa con make([]Foo) y []Foo{} .

Ahora estoy en el campo de "vamos a _probar_ esto" :^) y ver qué piensa la comunidad. Tal vez cambiemos nuestros patrones de codificación para que sean perezosos y dejemos de agregar contexto, pero tal vez esté bien si los errores obtienen un mejor contexto del xerrors impl que viene de todos modos.

¡Gracias, @Goodwine por proporcionar más datos concretos!

(Aparte, anoche hice un pequeño cambio en tryhard , por lo que divide el recuento del "controlador de errores complejo" en dos recuentos: controladores complejos y devuelve el formulario return ..., expr donde el último el valor del resultado no es <err> . Esto debería proporcionar información adicional).

¿Qué hay de enmendar la propuesta para que sea variada en lugar de este extraño argumento de expresión?

Eso resolvería muchos problemas. En el caso de que la gente quisiera simplemente devolver el error, lo único que cambiaría es la variable explícita ... . P.EJ:

try(os.Open("/dev/stdout")...)

sin embargo, las personas que desean una situación más flexible pueden hacer algo como:

f, err := os.Open("/dev/stdout")
try(WrapErrorf(err, "whatever wrap does: %v"))

Una cosa que hace esta idea es hacer que la palabra try sea menos apropiada, pero mantiene la compatibilidad con versiones anteriores.

@nathanjdulce escribió:

No me refiero a esta propuesta, me refiero a la especificación del lenguaje go.

Aquí están los extractos que estaba buscando en la especificación de idioma:

En la sección "Declaraciones de expresión":

Las siguientes funciones integradas no están permitidas en el contexto de la instrucción: append cap complex imag len make new real unsafe.Alignof unsafe.Offsetof unsafe.Sizeof

En las secciones "Estados de cuenta Go" y "Estados de cuenta diferidos":

Las llamadas de funciones integradas están restringidas en cuanto a declaraciones de expresión.

En la sección "Funciones integradas":

Las funciones integradas no tienen tipos de Go estándar, por lo que solo pueden aparecer en expresiones de llamada; no se pueden utilizar como valores de función.

@nathanjdulce escribió:

Tuve la impresión de que @griesemer estaba diciendo que la especificación del lenguaje go llama a las funciones integradas como el mecanismo específicamente útil para romper la convención sintáctica .

Las funciones integradas no rompen las convenciones sintácticas de Go (paréntesis, comas entre argumentos, etc.). Usan la misma sintaxis que las funciones definidas por el usuario, pero permiten cosas que no se pueden hacer en las funciones definidas por el usuario.

@nathanjsweet Eso ya se consideró (de hecho, fue un descuido) pero hace que try no sea extensible. Consulte https://go-review.googlesource.com/c/proposal/+/181878 .

De manera más general, creo que está enfocando su crítica en lo incorrecto: las reglas especiales para el argumento try realmente no son un problema: prácticamente todas las funciones integradas tienen reglas especiales.

@griesemer gracias por trabajar en esto y tomarse el tiempo para responder a las inquietudes de la comunidad. Estoy seguro de que has respondido a muchas de las mismas preguntas en este momento. Me doy cuenta de que es realmente difícil resolver estos problemas y mantener la compatibilidad con versiones anteriores al mismo tiempo. ¡Gracias!

@nathanjsweet Con respecto a su comentario aquí :

Consulte la sección Conclusión , que habla de forma destacada sobre el papel de los elementos integrados en Go.

Con respecto a sus comentarios sobre try extendiendo los elementos integrados de diferentes maneras: Sí, el requisito que unsafe.Offsetof pone en su argumento es diferente al de try . Pero ambos esperan sintácticamente una expresión. Ambos tienen algunas restricciones adicionales en esa expresión. El requisito de try encaja tan fácilmente en la sintaxis de Go que no es necesario ajustar ninguna de las herramientas de análisis de front-end. Entiendo que te parezca inusual, pero eso no es lo mismo que una razón técnica en contra.

@griesemer el último _tryhard_ cuenta "controladores de errores complejos" pero no "controladores de errores de declaración única". ¿Se podría agregar eso?

@networkimprov ¿Qué es un controlador de errores de declaración única? ¿Un bloque if que contiene una sola declaración de no devolución?

@griesemer , un controlador de errores de declaración única es un bloque if err != nil que contiene _cualquier_ declaración única, incluido un retorno.

@networkimprov Listo. Los "controladores complejos" ahora se dividen en "instrucción única y luego bifurcación" y "complejo luego bifurcación".

Dicho esto, tenga en cuenta que estos recuentos pueden ser engañosos: por ejemplo, estos recuentos incluyen cualquier instrucción if que verifique cualquier variable contra cero (si -err="" que ahora es el valor predeterminado para tryhard ). Debería arreglar esto. En resumen, como es tryhard sobrestima en gran medida el número de oportunidades de controlador de declaración única o complejas. Para ver un ejemplo, consulte archive/tar/common.go , línea 701.

@networkimprov tryhard ahora brinda recuentos más precisos sobre por qué una verificación de errores no es un candidato try . El número total de recuentos de try no ha cambiado, pero el número de oportunidades para más controladores únicos y complejos ahora es más preciso (y aproximadamente un 50 % más pequeño que antes, porque antes cualquier then complejo Se consideró la rama de una instrucción if siempre que el if contuviera una verificación <varname> != nil , ya sea que implicara la verificación de errores o no).

Si alguien quiere probar try de una manera un poco más práctica, he creado un área de juegos WASM aquí con una implementación de prototipo:

https://ccbrown.github.io/wasm-go-playground/experimental/try-builtin/

Y si alguien está realmente interesado en compilar código localmente con try, tengo una bifurcación Go con lo que creo que es una implementación completamente funcional/actualizada aquí: https://github.com/ccbrown/go/pull/1

me gusta 'intentar'. Considero que administrar el estado local de err y usar := vs = con err, junto con las importaciones asociadas, es una distracción habitual. Además, no veo esto como la creación de dos formas de hacer lo mismo, más como dos casos, uno en el que desea transmitir un error sin actuar en consecuencia, el otro en el que desea manejarlo explícitamente en la función de llamada p.ej. Inicio sesión.

Corrí tryhard contra un pequeño proyecto interno en el que trabajé hace más de un año. El directorio en cuestión tiene el código para 3 servidores ("microservicios", supongo), un rastreador que se ejecuta periódicamente como un trabajo cron y algunas herramientas de línea de comandos. También tiene pruebas unitarias bastante completas. (FWIW, las diversas piezas han estado funcionando sin problemas durante más de un año, y ha resultado sencillo depurar y resolver cualquier problema que surja)

Aquí están las estadísticas:

--- stats ---
    370 (100.0% of     370) func declarations
    115 ( 31.1% of     370) func declarations returning an error
   1159 (100.0% of    1159) statements
    258 ( 22.3% of    1159) if statements
    123 ( 47.7% of     258) if <err> != nil statements
     64 ( 52.0% of     123) try candidates
      0 (  0.0% of     123) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     54 ( 43.9% of     123) { return ... zero values ..., expr }
      2 (  1.6% of     123) single statement then branch
      3 (  2.4% of     123) complex then branch; cannot use try
      1 (  0.8% of     123) non-empty else branch; cannot use try

Algunos comentarios:
1) El 50 % de todas las instrucciones if en este código base estaban comprobando errores, y try podría reemplazar ~la mitad de ellas. Esto significa que una cuarta parte de todas las instrucciones if en este (pequeño) código base son una versión escrita de try .

2) Debo señalar que esto es sorprendentemente alto para mí, porque unas semanas antes de comenzar con este proyecto, leí sobre una familia de funciones auxiliares internas ( status.Annotate ) que anotan un mensaje de error pero conservan el código de estado de gRPC. Por ejemplo, si llama a una RPC y devuelve un error con un código de estado asociado de PERMISSION_DENIED, el error devuelto por esta función auxiliar aún tendría un código de estado asociado de PERMISSION_DENIED (y teóricamente, si ese código de estado asociado se propagó todo el hasta un controlador de RPC, entonces el RPC fallaría con ese código de estado asociado). Había resuelto usar estas funciones para todo en este nuevo proyecto. Pero aparentemente, para el 50% de todos los errores, simplemente propagué un error sin anotar. (Antes de ejecutar tryhard , había pronosticado un 10%).

3) status.Annotate conserva nil errores (es decir status.Annotatef(err, "some message: %v", x) devolverá nil iff err == nil ). Revisé a todos los candidatos que no fueron de prueba de la primera categoría, y parece que todos serían susceptibles de la siguiente reescritura:

```
// Before
enc, err := keymaster.NewEncrypter(encKeyring)                                                     
if err != nil {                                                                                    
  return status.Annotate(err, "failed to create encrypter")                                        
}

// After
enc, err := keymaster.NewEncrypter(encKeyring)                                                                                                                                                                  
try(status.Annotate(err, "failed to create encrypter"))
```

To be clear, I'm not saying this transformation is always necessarily a good idea, but it seemed worth mentioning since it boosts the count significantly to a bit under half of all `if` statements.

4) La anotación de error basada en defer parece algo ortogonal a try , para ser honesto, ya que funcionará con y sin try . Pero mientras revisaba el código de este proyecto, dado que estaba observando de cerca el manejo de errores, noté varias instancias en las que los errores generados por el destinatario tendrían más sentido. Como ejemplo, noté varias instancias de código que llama a clientes gRPC de esta manera:

```
resp, err := s.someClient.SomeMethod(ctx, req)
if err != nil {
  return ..., status.Annotate(err, "failed to call SomeMethod")
}
```

This is actually a bit redundant in retrospect, since gRPC already prefixes its errors with something like "/Service.Method to [ip]:port : ".

There was also code that called standard library functions using the same pattern:

```
hreq, err := http.NewRequest("GET", targetURL, nil)
if err != nil {
  return status.Annotate(err, "http.NewRequest failed")
}
```

In retrospect, this code demonstrates two issues: first, `http.NewRequest` isn't calling a gRPC API, so using `status.Annotate` was unnecessary, and second, assuming the standard library also return errors with callee context, this particular use of error annotation was unnecessary (although I am fairly certain the standard library does not consistently follow this pattern).

En cualquier caso, pensé que era un ejercicio interesante volver a este proyecto y observar detenidamente cómo manejaba los errores.

Una cosa, @griesemer : ¿ tryhard tiene el denominador correcto para los "candidatos que no son de prueba"?
Editar: respondido a continuación, leí mal las estadísticas.

EDITAR: lo que se suponía que era un comentario se convirtió en una propuesta, que se nos pidió explícitamente que no hiciéramos aquí. Muevo mi comentario a una esencia .

@balasanjay Gracias por su comentario basado en hechos; eso es muy útil

Con respecto a su pregunta sobre tryhard : los "candidatos que no se prueban" (mejor sugerencia de título bienvenida) son simplemente la cantidad de casos en los que la declaración if cumplió con todos los criterios para una "verificación de error" (es decir, , teníamos lo que parecía una asignación a una variable de error <err> , seguida de una verificación de if <err> != nil en la fuente), pero donde no podemos usar fácilmente try debido a el código en los bloques if . Específicamente, en el orden de aparición en la salida de "candidatos que no son de prueba", estas son instrucciones if que tienen una instrucción return que devuelven algo más que <err> al final, instrucciones if con una única instrucción más compleja return (u otra), instrucciones if con varias instrucciones en la rama "entonces" y instrucciones if con sucursal no vacía de else . Algunas de estas declaraciones de if pueden tener varias de estas condiciones satisfechas simultáneamente, por lo que estos números no solo se suman. Su objetivo es dar una idea de lo que salió mal para que try se pueda utilizar.

Hice los ajustes más recientes a esto hoy (usted ejecutó la última versión). Antes del último cambio, algunas de estas condiciones se contaban incluso si no había una verificación de errores involucrada, lo que parecía tener menos sentido porque parecía que try no se podía usar en muchos más casos cuando en realidad try no tenía sentido en esos casos en primer lugar.

Sin embargo, lo más importante es que, para una base de código dada, la cantidad total de try candidatos no ha cambiado con estos refinamientos, ya que las condiciones relevantes para try siguen siendo las mismas.

Si tiene una sugerencia mejor sobre cómo y/o qué medir, me encantaría escucharla. He realizado varios ajustes en función de los comentarios de la comunidad. Gracias.

@subfuzion Gracias por tu comentario, pero no estamos buscando propuestas alternativas. Consulte https://github.com/golang/go/issues/32437#issuecomment -501878888. Gracias.

En aras de ser contados, independientemente del resultado:

Soy de la opinión, junto con mi equipo, que si bien el marco try propuesto por Rob es una idea razonable e interesante, no alcanza el nivel en el que sería apropiado como elemento integrado. Un paquete de biblioteca estándar sería un enfoque mucho más apropiado hasta que los patrones de uso se establezcan en la práctica. Si try entrara en el idioma de esa manera, lo usaríamos en varios lugares diferentes.

En una nota más general, vale la pena preservar la combinación de Go de un lenguaje central muy estable y una biblioteca estándar muy rica. Cuanto más lento avance el equipo lingüístico en los cambios lingüísticos principales, mejor. La tubería x -> stdlib sigue siendo un enfoque sólido para este tipo de cosas.

@griesemer Ah, lo siento. Leí mal las estadísticas, está usando el contador "if err! = declaraciones nulas" (123) como denominador, no el contador "probar candidatos" (64) como denominador. Voy a hacer esa pregunta.

¡Gracias!

Los patrones de uso de @mattpalmer se han establecido durante aproximadamente una década. Son estos patrones de uso exactos los que influyeron directamente en el diseño de try . ¿A qué patrones de uso te refieres?

@griesemer Lo siento, es mi culpa: lo que comenzó en mi mente explicando lo que me molestaba sobre try se convirtió en su propia propuesta para demostrar mi punto de no agregarlo. Eso iba claramente en contra de las reglas básicas establecidas (sin mencionar que, a diferencia de esta propuesta para una nueva función integrada, introduce un nuevo operador). ¿Sería útil eliminar el comentario para mantener la conversación fluida (o eso se considera de mala forma)?

@subfuzion No me preocuparía por eso. Es una sugerencia controvertida y hay muchas propuestas. Muchos son extravagantes

Repetimos ese diseño varias veces y solicitamos comentarios de muchas personas antes de sentirnos lo suficientemente cómodos como para publicarlo y recomendar avanzarlo a la fase de experimento real, pero aún no lo hemos hecho. Tiene sentido volver a la mesa de dibujo si el experimento falla, o si la retroalimentación nos dice de antemano que claramente fallará.

@griesemer , ¿puede dar más detalles sobre las métricas específicas que utilizará el equipo para establecer el éxito o el fracaso del experimento?

@yo y

Le pregunté esto a @rsc hace un tiempo (https://github.com/golang/go/issues/32437#issuecomment-503245958):

@rsc
No habrá escasez de lugares donde se pueda colocar esta conveniencia. ¿Qué métrica se busca que demuestre la sustancia del mecanismo aparte de eso? ¿Existe una lista de casos de manejo de errores clasificados? ¿Cómo se derivará el valor de los datos cuando gran parte del proceso público esté impulsado por el sentimiento?

La respuesta fue intencionada, pero poco inspiradora y carente de sustancia (https://github.com/golang/go/issues/32437#issuecomment-503295558):

La decisión se basa en qué tan bien funciona esto en programas reales. Si las personas nos muestran que intentar no es efectivo en la mayor parte de su código, eso es información importante. El proceso es impulsado por ese tipo de datos. No es impulsado por el sentimiento.

Se ofreció un sentimiento adicional (https://github.com/golang/go/issues/32437#issuecomment-503408184):

Me sorprendió encontrar un caso en el que try condujo a un código claramente mejor, de una manera que no se había discutido antes.

Eventualmente, respondí mi propia pregunta "¿Existe una lista de casos de manejo de errores clasificados?". Habrá efectivamente 6 modos de manejo de errores: directo manual, transferencia manual, indirecta manual, directa automática, transferencia automática, indirecta automática. Actualmente, solo es común usar 2 de esos modos. Los modos indirectos, que tienen una cantidad significativa de esfuerzo puesto en su facilitación, parecen fuertemente prohibitivos para la mayoría de los Gophers veteranos y esa preocupación aparentemente está siendo ignorada. (https://github.com/golang/go/issues/32437#issuecomment-507332843).

Además, sugerí que las transformaciones automatizadas se examinaran antes de la transformación para tratar de garantizar el valor de los resultados (https://github.com/golang/go/issues/32437#issuecomment-507497656). Con el tiempo, afortunadamente, más de los resultados que se ofrecen parecen tener mejores retrospectivas, pero esto aún no aborda el impacto de los métodos indirectos de manera sobria y concertada. Después de todo (en mi opinión), así como los usuarios deben ser tratados como hostiles, los desarrolladores deben ser tratados como vagos.

También se señaló la falla del enfoque actual para perder candidatos valiosos (https://github.com/golang/go/issues/32437#issuecomment-507505243).

Creo que vale la pena ser ruidoso acerca de que este proceso es generalmente deficiente y notablemente sordo.

@iand La respuesta dada por @rsc sigue siendo válida. No estoy seguro de qué parte de esa respuesta es "falta de sustancia" o qué se necesita para ser "inspirador". Pero déjame intentar agregar más "sustancia":

El propósito del proceso de evaluación de la propuesta es identificar en última instancia "si un cambio ha generado los beneficios esperados o ha generado costos inesperados" (paso 5 del proceso).

Hemos superado el paso 1: El equipo de Go ha seleccionado propuestas concretas que parece que vale la pena aceptar; esta propuesta es una de ellas. No lo hubiéramos seleccionado si no lo hubiéramos pensado mucho y considerado que valía la pena. Específicamente, creemos que hay una cantidad significativa de repeticiones en el código Go relacionadas únicamente con el manejo de errores. La propuesta tampoco surge de la nada: hemos estado discutiendo esto durante más de un año en varias formas.

Actualmente estamos en el paso 2, por lo que todavía estamos bastante lejos de una decisión final. El paso 2 es para recopilar comentarios e inquietudes, que parecen haber muchos. Pero para ser claros aquí: hasta ahora solo hubo un comentario que señalaba una deficiencia _técnica_ con el diseño, que corregimos. También hubo bastantes comentarios con datos concretos basados ​​en código real que indicaban que try hecho reduciría el modelo estándar y simplificaría el código; y hubo algunos comentarios, también basados ​​en datos de código real, que mostraron que try no ayudaría mucho. Estos comentarios concretos, basados ​​en datos reales o que señalan deficiencias técnicas, son prácticos y muy útiles. Absolutamente tendremos esto en cuenta.

Y luego estaba la gran cantidad de comentarios que son esencialmente sentimientos personales. Esto es menos procesable. Esto no quiere decir que lo estemos ignorando. Pero el hecho de que nos apeguemos al proceso no significa que seamos "sordos".

Con respecto a estos comentarios: quizás haya dos, quizás tres docenas de opositores vocales a esta propuesta, ya sabes quién eres. Están dominando esta discusión con publicaciones frecuentes, a veces varias al día. Hay poca información nueva que se puede obtener de esto. El mayor número de publicaciones tampoco refleja un sentimiento "más fuerte" por parte de la comunidad; simplemente significa que estas personas son más vocales que otras.

@iand La respuesta dada por @rsc sigue siendo válida. No estoy seguro de qué parte de esa respuesta es "falta de sustancia" o qué se necesita para ser "inspirador". Pero déjame intentar agregar más "sustancia":

@griesemer Estoy seguro de que no fue intencional, pero me gustaría señalar que ninguna de las palabras que citó fueron mías, sino de un comentarista posterior.

Aparte de eso, espero que, además de reducir el modelo estándar y simplificar el éxito de try , se juzgue si nos permite escribir un código mejor y más claro.

@iand De hecho, eso fue solo un descuido mío. Mis disculpas.

Creemos que try nos permite escribir un código más legible, y gran parte de la evidencia que hemos recibido del código real y nuestros propios experimentos con tryhard muestran limpiezas significativas. Pero la legibilidad es más subjetiva y más difícil de cuantificar.

@griesemer

¿A qué patrones de uso te refieres?

Me refiero a los patrones de uso que se desarrollarán alrededor try a lo largo del tiempo, no al patrón existente de comprobación de errores para el manejo de errores. El potencial de mal uso y abuso es un gran desconocido, especialmente con la afluencia continua de programadores que han usado versiones semánticamente diferentes de try-catch en otros lenguajes.

Todo esto y las consideraciones sobre la estabilidad a largo plazo del lenguaje central me llevan a pensar que la introducción de esta característica al nivel de los paquetes x o la biblioteca estándar (ya sea como paquete errors/try o como errors.Try() ) sería preferible a introducirlo como una función integrada.

@mattparlmer Corríjame si me equivoco, pero creo que esta propuesta tendría que estar en el tiempo de ejecución de Go para usar g, m (necesario para anular el flujo de ejecución).

@fabian-f

@mattparlmer Corríjame si me equivoco, pero creo que esta propuesta tendría que estar en el tiempo de ejecución de Go para usar g, m (necesario para anular el flujo de ejecución).

Ese no es el caso; como señala el documento de diseño , se puede implementar como una transformación de árbol de sintaxis en tiempo de compilación.

Eso es posible porque la semántica de try se puede expresar completamente en términos de if y return ; realmente no "anula el flujo de ejecución" más de lo que lo hacen if y return .

Aquí hay un informe de tryhard del código base Go de 300k líneas de mi empresa:

Ejecución inicial:

--- stats ---
  13879 (100.0% of   13879) func declarations
   4381 ( 31.6% of   13879) func declarations returning an error
  38435 (100.0% of   38435) statements
   8028 ( 20.9% of   38435) if statements
   4496 ( 56.0% of    8028) if <err> != nil statements
    453 ( 10.1% of    4496) try candidates
      4 (  0.1% of    4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   3066 ( 68.2% of    4496) { return ... zero values ..., expr }
    356 (  7.9% of    4496) single statement then branch
    345 (  7.7% of    4496) complex then branch; cannot use try
     63 (  1.4% of    4496) non-empty else branch; cannot use try

Tenemos una convención de usar el paquete errgo de juju (https://godoc.org/github.com/juju/errgo) para enmascarar errores y agregarles información de seguimiento de pila, lo que evitaría que ocurran la mayoría de las reescrituras. Eso significa que es poco probable que adoptemos try , por la misma razón por la que generalmente evitamos las devoluciones de errores desnudos.

Dado que parece que podría ser una métrica útil, eliminé las llamadas errgo.Mask() (que devuelven el error sin anotación) y volví a ejecutar tryhard . Esta es una estimación de cuántas comprobaciones de error podrían reescribirse si no usáramos errgo:

--- stats ---
  13879 (100.0% of   13879) func declarations
   4381 ( 31.6% of   13879) func declarations returning an error
  38435 (100.0% of   38435) statements
   8028 ( 20.9% of   38435) if statements
   4496 ( 56.0% of    8028) if <err> != nil statements
   3114 ( 69.3% of    4496) try candidates
      7 (  0.2% of    4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
    381 (  8.5% of    4496) { return ... zero values ..., expr }
    358 (  8.0% of    4496) single statement then branch
    345 (  7.7% of    4496) complex then branch; cannot use try
     63 (  1.4% of    4496) non-empty else branch; cannot use try

Por lo tanto, supongo que ~70 % de las devoluciones de errores serían compatibles con try .

Por último, mi principal preocupación con la propuesta no parece quedar reflejada en ninguno de los comentarios que leí ni en los resúmenes de las discusiones:

Esta propuesta aumenta significativamente el costo relativo de anotar errores.

Actualmente, el costo marginal de agregar algo de contexto a un error es muy bajo; es apenas más que escribir la cadena de formato. Si se adoptara esta propuesta, me preocupa que los ingenieros prefieran cada vez más la estética que ofrece try , tanto porque hace que su código "se vea más elegante" (que lamento decir que es una consideración para algunas personas, en mi experiencia), y ahora requiere un bloque adicional para agregar contexto. Podrían justificarlo basándose en un argumento de "legibilidad", cómo agregar contexto amplía el método en otras 3 líneas y distrae al lector del punto principal. Creo que las bases del código corporativo son diferentes a la biblioteca estándar de Go en el sentido de que hacer que sea más fácil hacer lo correcto probablemente tenga un impacto medible en la calidad del código resultante, las revisiones del código son de calidad variable y las prácticas del equipo varían independientemente unas de otras. . De todos modos, como dijiste antes, siempre podríamos no adoptar try para nuestro código base.

Gracias por la consideración

@mattparlmer

Todo esto y las consideraciones sobre la estabilidad a largo plazo del lenguaje central me llevan a pensar que la introducción de esta característica al nivel de los paquetes x o la biblioteca estándar (ya sea como paquete errors/try o como errors.Try() ) sería preferible a introducirlo como una función integrada.

try no se puede implementar como una función de biblioteca; no hay forma de que una función regrese de su llamador (la habilitación se ha propuesto como #32473) y, como la mayoría de los otros integrados, tampoco hay forma de expresar la firma de try en Go. Incluso con los genéricos, es poco probable que eso sea posible; consulte las preguntas frecuentes sobre el documento de diseño , cerca del final.

Además, implementar try como una función de biblioteca requeriría que tuviera un nombre más detallado, lo que anula en parte el sentido de usarlo.

Sin embargo, se puede implementar, y se ha implementado dos veces, como un preprocesador de código fuente: consulte https://github.com/rhysd/trygo y https://github.com/lunixbochs/og.

Parece que ~60% del código base de tegola podría hacer uso de esta función.

Aquí está la salida de Tryhard para el proyecto tegola: (http://github.com/go-spatial/tegola)

--- try candidates ---
      1  tegola/atlas/atlas.go:84
      2  tegola/atlas/map.go:232
      3  tegola/atlas/map.go:238
      4  tegola/atlas/map.go:248
      5  tegola/atlas/map.go:253
      6  tegola/basic/geometry_math.go:248
      7  tegola/basic/geometry_math.go:251
      8  tegola/basic/geometry_math.go:268
      9  tegola/basic/geometry_math.go:276
     10  tegola/basic/json_marshal.go:33
     11  tegola/basic/json_marshal.go:153
     12  tegola/basic/json_marshal.go:276
     13  tegola/cache/azblob/azblob.go:54
     14  tegola/cache/azblob/azblob.go:61
     15  tegola/cache/azblob/azblob.go:67
     16  tegola/cache/azblob/azblob.go:74
     17  tegola/cache/azblob/azblob.go:80
     18  tegola/cache/azblob/azblob.go:105
     19  tegola/cache/azblob/azblob.go:109
     20  tegola/cache/azblob/azblob.go:204
     21  tegola/cache/azblob/azblob.go:259
     22  tegola/cache/file/file.go:42
     23  tegola/cache/file/file.go:56
     24  tegola/cache/file/file.go:110
     25  tegola/cache/file/file.go:116
     26  tegola/cache/file/file.go:129
     27  tegola/cache/redis/redis.go:41
     28  tegola/cache/redis/redis.go:46
     29  tegola/cache/redis/redis.go:51
     30  tegola/cache/redis/redis.go:56
     31  tegola/cache/redis/redis.go:70
     32  tegola/cache/redis/redis.go:79
     33  tegola/cache/redis/redis.go:84
     34  tegola/cache/s3/s3.go:85
     35  tegola/cache/s3/s3.go:102
     36  tegola/cache/s3/s3.go:112
     37  tegola/cache/s3/s3.go:118
     38  tegola/cache/s3/s3.go:123
     39  tegola/cache/s3/s3.go:138
     40  tegola/cache/s3/s3.go:164
     41  tegola/cache/s3/s3.go:172
     42  tegola/cache/s3/s3.go:179
     43  tegola/cache/s3/s3.go:284
     44  tegola/cache/s3/s3.go:340
     45  tegola/cmd/tegola/cmd/cache/format.go:97
     46  tegola/cmd/tegola/cmd/cache/seed_purge.go:94
     47  tegola/cmd/tegola/cmd/cache/seed_purge.go:103
     48  tegola/cmd/tegola/cmd/cache/seed_purge.go:170
     49  tegola/cmd/tegola/cmd/cache/tile_list.go:51
     50  tegola/cmd/tegola/cmd/cache/tile_list.go:64
     51  tegola/cmd/tegola/cmd/cache/tile_name.go:35
     52  tegola/cmd/tegola/cmd/cache/tile_name.go:43
     53  tegola/cmd/tegola/cmd/root.go:58
     54  tegola/cmd/tegola/cmd/root.go:61
     55  tegola/cmd/xyz2svg/cmd/draw.go:62
     56  tegola/cmd/xyz2svg/cmd/draw.go:70
     57  tegola/cmd/xyz2svg/cmd/draw.go:214
     58  tegola/config/config.go:96
     59  tegola/internal/env/parse.go:30
     60  tegola/internal/env/parse.go:69
     61  tegola/internal/env/parse.go:116
     62  tegola/internal/env/parse.go:174
     63  tegola/internal/env/parse.go:221
     64  tegola/internal/env/types.go:67
     65  tegola/internal/env/types.go:86
     66  tegola/internal/env/types.go:105
     67  tegola/internal/env/types.go:124
     68  tegola/internal/env/types.go:143
     69  tegola/maths/makevalid/main.go:189
     70  tegola/maths/makevalid/main.go:207
     71  tegola/maths/makevalid/main.go:221
     72  tegola/maths/makevalid/main.go:295
     73  tegola/maths/makevalid/main.go:504
     74  tegola/maths/makevalid/makevalid.go:77
     75  tegola/maths/makevalid/makevalid.go:89
     76  tegola/maths/makevalid/makevalid.go:118
     77  tegola/maths/makevalid/makevalid_test.go:93
     78  tegola/maths/makevalid/makevalid_test.go:163
     79  tegola/maths/makevalid/plyg/ring.go:518
     80  tegola/maths/triangle.go:1023
     81  tegola/mvt/layer.go:73
     82  tegola/mvt/layer.go:79
     83  tegola/mvt/vector_tile/vector_tile.pb.go:64
     84  tegola/provider/gpkg/gpkg.go:138
     85  tegola/provider/gpkg/gpkg.go:223
     86  tegola/provider/gpkg/gpkg_register.go:46
     87  tegola/provider/gpkg/gpkg_register.go:51
     88  tegola/provider/gpkg/gpkg_register.go:186
     89  tegola/provider/gpkg/gpkg_register.go:227
     90  tegola/provider/gpkg/gpkg_register.go:240
     91  tegola/provider/gpkg/gpkg_register.go:245
     92  tegola/provider/gpkg/gpkg_register.go:256
     93  tegola/provider/gpkg/gpkg_register.go:377
     94  tegola/provider/postgis/postgis.go:112
     95  tegola/provider/postgis/postgis.go:117
     96  tegola/provider/postgis/postgis.go:122
     97  tegola/provider/postgis/postgis.go:127
     98  tegola/provider/postgis/postgis.go:136
     99  tegola/provider/postgis/postgis.go:142
    100  tegola/provider/postgis/postgis.go:148
    101  tegola/provider/postgis/postgis.go:153
    102  tegola/provider/postgis/postgis.go:158
    103  tegola/provider/postgis/postgis.go:163
    104  tegola/provider/postgis/postgis.go:181
    105  tegola/provider/postgis/postgis.go:198
    106  tegola/provider/postgis/postgis.go:264
    107  tegola/provider/postgis/postgis.go:441
    108  tegola/provider/postgis/postgis.go:446
    109  tegola/provider/postgis/postgis.go:529
    110  tegola/provider/postgis/postgis.go:559
    111  tegola/provider/postgis/postgis.go:603
    112  tegola/provider/postgis/util.go:31
    113  tegola/provider/postgis/util.go:36
    114  tegola/provider/postgis/util.go:200
    115  tegola/server/bindata/bindata.go:89
    116  tegola/server/bindata/bindata.go:109
    117  tegola/server/bindata/bindata.go:129
    118  tegola/server/bindata/bindata.go:149
    119  tegola/server/bindata/bindata.go:169
    120  tegola/server/bindata/bindata.go:189
    121  tegola/server/bindata/bindata.go:209
    122  tegola/server/bindata/bindata.go:229
    123  tegola/server/bindata/bindata.go:370
    124  tegola/server/bindata/bindata.go:374
    125  tegola/server/bindata/bindata.go:378
    126  tegola/server/bindata/bindata.go:382
    127  tegola/server/bindata/bindata.go:386
    128  tegola/server/bindata/bindata.go:402
    129  tegola/server/middleware_gzip.go:71
    130  tegola/server/middleware_gzip.go:78
    131  tegola/server/server_test.go:85

--- <err> name is different from "err" ---
      1  tegola/basic/json_marshal.go:276

--- { return ... zero values ..., expr } ---
      1  tegola/basic/geometry_math.go:214
      2  tegola/basic/geometry_math.go:222
      3  tegola/basic/geometry_math.go:230
      4  tegola/cache/azblob/azblob.go:131
      5  tegola/cache/azblob/azblob.go:140
      6  tegola/cache/azblob/azblob.go:149
      7  tegola/cache/azblob/azblob.go:171
      8  tegola/cache/file/file.go:47
      9  tegola/cache/s3/s3.go:92
     10  tegola/cmd/internal/register/maps.go:108
     11  tegola/cmd/tegola/cmd/cache/flags.go:20
     12  tegola/cmd/tegola/cmd/cache/tile_name.go:51
     13  tegola/cmd/tegola/cmd/cache/worker.go:112
     14  tegola/cmd/tegola/cmd/cache/worker.go:123
     15  tegola/cmd/tegola/cmd/root.go:73
     16  tegola/cmd/tegola/cmd/root.go:78
     17  tegola/cmd/xyz2svg/cmd/root.go:60
     18  tegola/provider/gpkg/gpkg.go:90
     19  tegola/provider/gpkg/gpkg.go:95
     20  tegola/provider/gpkg/gpkg_register.go:264
     21  tegola/provider/gpkg/gpkg_register.go:297
     22  tegola/provider/gpkg/gpkg_register.go:302
     23  tegola/provider/gpkg/gpkg_register.go:313
     24  tegola/provider/gpkg/gpkg_register.go:328
     25  tegola/provider/postgis/postgis.go:193
     26  tegola/provider/postgis/postgis.go:208
     27  tegola/provider/postgis/postgis.go:222
     28  tegola/provider/postgis/postgis.go:228
     29  tegola/provider/postgis/postgis.go:234
     30  tegola/provider/postgis/postgis.go:243
     31  tegola/provider/postgis/postgis.go:249
     32  tegola/provider/postgis/postgis.go:255
     33  tegola/provider/postgis/postgis.go:304
     34  tegola/provider/postgis/postgis.go:315
     35  tegola/provider/postgis/postgis.go:319
     36  tegola/provider/postgis/postgis.go:364
     37  tegola/provider/postgis/postgis.go:456
     38  tegola/provider/postgis/postgis.go:520
     39  tegola/provider/postgis/postgis.go:534
     40  tegola/provider/postgis/postgis.go:565
     41  tegola/provider/postgis/util.go:108
     42  tegola/provider/postgis/util.go:113
     43  tegola/server/bindata/bindata.go:29
     44  tegola/server/bindata/bindata.go:245
     45  tegola/server/bindata/bindata.go:271
     46  tegola/server/bindata/bindata.go:396

--- single statement then branch ---
      1  tegola/cache/azblob/azblob.go:241
      2  tegola/cache/file/file.go:87
      3  tegola/cache/s3/s3.go:321
      4  tegola/cmd/internal/register/caches.go:18
      5  tegola/cmd/internal/register/providers.go:43
      6  tegola/cmd/internal/register/providers.go:62
      7  tegola/cmd/internal/register/providers.go:75
      8  tegola/config/config.go:192
      9  tegola/config/config.go:207
     10  tegola/config/config.go:217
     11  tegola/internal/env/dict.go:43
     12  tegola/internal/env/dict.go:121
     13  tegola/internal/env/dict.go:197
     14  tegola/internal/env/dict.go:273
     15  tegola/internal/env/dict.go:348
     16  tegola/internal/env/parse.go:79
     17  tegola/internal/env/parse.go:126
     18  tegola/internal/env/parse.go:184
     19  tegola/internal/env/parse.go:231
     20  tegola/maths/makevalid/plyg/ring.go:541
     21  tegola/maths/maths.go:239
     22  tegola/maths/validate/validate.go:49
     23  tegola/maths/validate/validate.go:53
     24  tegola/maths/validate/validate.go:59
     25  tegola/maths/validate/validate.go:69
     26  tegola/mvt/feature.go:94
     27  tegola/mvt/feature.go:99
     28  tegola/mvt/feature.go:592
     29  tegola/mvt/feature.go:603
     30  tegola/mvt/layer.go:90
     31  tegola/mvt/tile.go:48
     32  tegola/provider/postgis/postgis.go:570
     33  tegola/provider/postgis/postgis.go:586
     34  tegola/tile.go:172

--- complex then branch; cannot use try ---
      1  tegola/cache/azblob/azblob.go:226
      2  tegola/cache/file/file.go:78
      3  tegola/cache/file/file.go:122
      4  tegola/cache/s3/s3.go:195
      5  tegola/cache/s3/s3.go:206
      6  tegola/cache/s3/s3.go:219
      7  tegola/cache/s3/s3.go:307
      8  tegola/provider/gpkg/gpkg.go:39
      9  tegola/provider/gpkg/gpkg.go:45
     10  tegola/provider/gpkg/gpkg.go:131
     11  tegola/provider/gpkg/gpkg.go:154
     12  tegola/provider/gpkg/gpkg_register.go:171
     13  tegola/provider/gpkg/gpkg_register.go:195

--- stats ---
   1294 (100.0% of    1294) func declarations
    246 ( 19.0% of    1294) func declarations returning an error
   2693 (100.0% of    2693) statements
    551 ( 20.5% of    2693) if statements
    238 ( 43.2% of     551) if <err> != nil statements
    131 ( 55.0% of     238) try candidates
      1 (  0.4% of     238) <err> name is different from "err"
--- non-try candidates ---
     46 ( 19.3% of     238) { return ... zero values ..., expr }
     34 ( 14.3% of     238) single statement then branch
     13 (  5.5% of     238) complex then branch; cannot use try
      0 (  0.0% of     238) non-empty else branch; cannot use try

Y el proyecto complementario: (http://github.com/go-spatial/geom)

--- try candidates ---
      1  geom/bbox.go:202
      2  geom/encoding/geojson/geojson.go:152
      3  geom/encoding/geojson/geojson.go:157
      4  geom/encoding/wkb/internal/tcase/symbol/symbol.go:73
      5  geom/encoding/wkb/internal/tcase/tcase.go:161
      6  geom/encoding/wkb/internal/tcase/tcase.go:172
      7  geom/encoding/wkb/wkb.go:50
      8  geom/encoding/wkb/wkb.go:110
      9  geom/encoding/wkt/internal/token/token.go:176
     10  geom/encoding/wkt/internal/token/token.go:252
     11  geom/internal/parsing/parsing.go:44
     12  geom/internal/parsing/parsing.go:85
     13  geom/internal/rtreego/rtree_test.go:110
     14  geom/multi_line_string.go:34
     15  geom/multi_polygon.go:35
     16  geom/planar/clip/linestring.go:82
     17  geom/planar/clip/linestring.go:181
     18  geom/planar/clip/point.go:23
     19  geom/planar/intersect/xsweep.go:106
     20  geom/planar/makevalid/makevalid.go:92
     21  geom/planar/makevalid/makevalid.go:191
     22  geom/planar/makevalid/setdiff/polygoncleaner.go:283
     23  geom/planar/makevalid/setdiff/polygoncleaner.go:345
     24  geom/planar/makevalid/setdiff/polygoncleaner.go:543
     25  geom/planar/makevalid/setdiff/polygoncleaner.go:554
     26  geom/planar/makevalid/setdiff/polygoncleaner.go:572
     27  geom/planar/makevalid/setdiff/polygoncleaner.go:578
     28  geom/planar/simplify/douglaspeucker.go:84
     29  geom/planar/simplify/douglaspeucker.go:88
     30  geom/planar/simplify.go:13
     31  geom/planar/triangulate/constraineddelaunay/triangle.go:186
     32  geom/planar/triangulate/constraineddelaunay/triangulator.go:134
     33  geom/planar/triangulate/constraineddelaunay/triangulator.go:138
     34  geom/planar/triangulate/constraineddelaunay/triangulator.go:142
     35  geom/planar/triangulate/constraineddelaunay/triangulator.go:173
     36  geom/planar/triangulate/constraineddelaunay/triangulator.go:176
     37  geom/planar/triangulate/constraineddelaunay/triangulator.go:203
     38  geom/planar/triangulate/constraineddelaunay/triangulator.go:248
     39  geom/planar/triangulate/constraineddelaunay/triangulator.go:396
     40  geom/planar/triangulate/constraineddelaunay/triangulator.go:466
     41  geom/planar/triangulate/constraineddelaunay/triangulator.go:553
     42  geom/planar/triangulate/constraineddelaunay/triangulator.go:583
     43  geom/planar/triangulate/constraineddelaunay/triangulator.go:667
     44  geom/planar/triangulate/constraineddelaunay/triangulator.go:672
     45  geom/planar/triangulate/constraineddelaunay/triangulator.go:677
     46  geom/planar/triangulate/constraineddelaunay/triangulator.go:814
     47  geom/planar/triangulate/constraineddelaunay/triangulator.go:818
     48  geom/planar/triangulate/constraineddelaunay/triangulator.go:823
     49  geom/planar/triangulate/constraineddelaunay/triangulator.go:865
     50  geom/planar/triangulate/constraineddelaunay/triangulator.go:870
     51  geom/planar/triangulate/constraineddelaunay/triangulator.go:875
     52  geom/planar/triangulate/constraineddelaunay/triangulator.go:897
     53  geom/planar/triangulate/constraineddelaunay/triangulator.go:901
     54  geom/planar/triangulate/constraineddelaunay/triangulator.go:907
     55  geom/planar/triangulate/constraineddelaunay/triangulator.go:1107
     56  geom/planar/triangulate/constraineddelaunay/triangulator.go:1146
     57  geom/planar/triangulate/constraineddelaunay/triangulator.go:1157
     58  geom/planar/triangulate/constraineddelaunay/triangulator.go:1202
     59  geom/planar/triangulate/constraineddelaunay/triangulator.go:1206
     60  geom/planar/triangulate/constraineddelaunay/triangulator.go:1216
     61  geom/planar/triangulate/delaunaytriangulationbuilder.go:66
     62  geom/planar/triangulate/incrementaldelaunaytriangulator.go:46
     63  geom/planar/triangulate/incrementaldelaunaytriangulator.go:78
     64  geom/planar/triangulate/quadedge/lastfoundquadedgelocator.go:65
     65  geom/planar/triangulate/quadedge/quadedgesubdivision.go:976
     66  geom/slippy/tile.go:133

--- { return ... zero values ..., expr } ---
      1  geom/internal/parsing/parsing.go:125
      2  geom/planar/triangulate/constraineddelaunay/triangulator.go:428
      3  geom/planar/triangulate/constraineddelaunay/triangulator.go:447
      4  geom/planar/triangulate/constraineddelaunay/triangulator.go:460

--- single statement then branch ---
      1  geom/bbox.go:259
      2  geom/encoding/wkb/internal/decode/decode.go:29
      3  geom/encoding/wkb/internal/decode/decode.go:55
      4  geom/encoding/wkb/internal/decode/decode.go:63
      5  geom/encoding/wkb/internal/decode/decode.go:70
      6  geom/encoding/wkb/internal/decode/decode.go:79
      7  geom/encoding/wkb/internal/decode/decode.go:84
      8  geom/encoding/wkb/internal/decode/decode.go:93
      9  geom/encoding/wkb/internal/decode/decode.go:99
     10  geom/encoding/wkb/internal/decode/decode.go:105
     11  geom/encoding/wkb/internal/decode/decode.go:114
     12  geom/encoding/wkb/internal/decode/decode.go:119
     13  geom/encoding/wkb/internal/decode/decode.go:135
     14  geom/encoding/wkb/internal/decode/decode.go:140
     15  geom/encoding/wkb/internal/decode/decode.go:149
     16  geom/encoding/wkb/internal/decode/decode.go:155
     17  geom/encoding/wkb/internal/decode/decode.go:161
     18  geom/encoding/wkb/internal/decode/decode.go:170
     19  geom/encoding/wkb/internal/decode/decode.go:176
     20  geom/encoding/wkb/internal/tcase/token/token.go:162
     21  geom/encoding/wkt/internal/token/token.go:136

--- complex then branch; cannot use try ---
      1  geom/encoding/wkb/internal/tcase/tcase.go:74
      2  geom/encoding/wkt/internal/symbol/symbol.go:125
      3  geom/planar/intersect/xsweep.go:165
      4  geom/planar/makevalid/makevalid.go:85
      5  geom/planar/makevalid/makevalid.go:172
      6  geom/planar/makevalid/triangulate.go:19
      7  geom/planar/makevalid/triangulate.go:28
      8  geom/planar/makevalid/triangulate.go:36
      9  geom/planar/makevalid/triangulate.go:58
     10  geom/planar/triangulate/constraineddelaunay/triangulator.go:358
     11  geom/planar/triangulate/constraineddelaunay/triangulator.go:373
     12  geom/planar/triangulate/constraineddelaunay/triangulator.go:453
     13  geom/planar/triangulate/constraineddelaunay/triangulator.go:1237
     14  geom/planar/triangulate/constraineddelaunay/triangulator.go:1243
     15  geom/planar/triangulate/constraineddelaunay/triangulator.go:1249

--- stats ---
    820 (100.0% of     820) func declarations
    146 ( 17.8% of     820) func declarations returning an error
   1715 (100.0% of    1715) statements
    391 ( 22.8% of    1715) if statements
    111 ( 28.4% of     391) if <err> != nil statements
     66 ( 59.5% of     111) try candidates
      0 (  0.0% of     111) <err> name is different from "err"
--- non-try candidates ---
      4 (  3.6% of     111) { return ... zero values ..., expr }
     21 ( 18.9% of     111) single statement then branch
     15 ( 13.5% of     111) complex then branch; cannot use try
      0 (  0.0% of     111) non-empty else branch; cannot use try

Sobre el tema de los costos inesperados, vuelvo a publicar esto desde #32611...

Veo tres clases de costo:

  1. el costo de la especificación, que se elabora en el documento de diseño.
  2. el costo de las herramientas (es decir, la revisión del software), también explorado en el documento de diseño.
  3. el costo para el ecosistema, que la comunidad ha detallado detalladamente arriba y en #32825.

Re nos. 1 y 2, los costos de try() son modestos.

Para simplificar demasiado no. 3, la mayoría de los comentaristas creen que try() dañaría nuestro código y/o el ecosistema de código del que dependemos y, por lo tanto, reduciría nuestra productividad y calidad del producto. Esta percepción generalizada y bien razonada no debe menospreciarse como "no fáctica" o "estética".

El costo para el ecosistema es mucho más importante que el costo para las especificaciones o las herramientas.

@griesemer es evidentemente injusto afirmar que "tres docenas de opositores vocales" son la mayor parte de la oposición. Cientos de personas han comentado aquí y en #32825. Me dijiste el 12 de junio: "Reconozco que alrededor de 2/3 de los encuestados no están contentos con la propuesta". Desde entonces, más de 2000 personas han votado "dejar en paz a err != nil " con un 90 % de aprobación.

@gdey , ¿podría modificar su publicación para incluir solo _estadísticas y candidatos sin intento_?

@robfig , @gdey Gracias por proporcionar estos datos, especialmente la comparación antes/después.

@griesemer
Ciertamente ha agregado algo de sustancia aclarando que mis preocupaciones (y las de otros) pueden abordarse directamente. Mi pregunta, entonces, es si el equipo de Go ve el probable abuso de los modos indirectos (es decir, devoluciones desnudas y/o mutación de error de alcance posterior a la función a través de aplazamiento) como un costo que vale la pena discutir durante el paso 5, y que vale la pena potencialmente tomando medidas para su mitigación. El estado de ánimo actual es que el equipo de Go considera que este aspecto tan desconcertante de la propuesta es una característica inteligente/novedosa (esta preocupación no se aborda en la evaluación de las transformaciones automatizadas y parece que se fomenta/respalda activamente. - errd , en una conversación, etc.).

editar para agregar... La preocupación con el equipo de Go fomentando lo que los Gophers veteranos ven como prohibitivo es lo que quise decir con respecto a la sordera.
... La indirección es un costo que a muchos de nosotros nos preocupa profundamente como una cuestión de dolor experiencial. Puede que no sea algo que se pueda comparar fácilmente (si es que es razonable), pero es falso considerar esta preocupación como sentimental en sí misma. Más bien, ignorar la sabiduría de la experiencia compartida en favor de números simples sin un juicio contextual sólido es el tipo de sentimiento contra el que estamos tratando de trabajar.

@networkimprov Disculpas por no ser lo suficientemente claro. Lo que dije fue:

Tal vez haya dos, tal vez tres docenas de opositores vocales a esta propuesta: usted sabe quién es. Están dominando esta discusión con publicaciones frecuentes, a veces varias al día.

Estaba hablando de comentarios reales (como en "publicaciones frecuentes"), no de emojis. Solo hay un número relativamente pequeño de personas que publican aquí _repetidamente_, lo que creo que sigue siendo correcto. Tampoco estaba hablando de #32825; Estaba hablando de esta propuesta.

En cuanto a los emojis, la situación prácticamente no ha cambiado desde hace un mes: 1/3 de los emojis indican una opinión favorable y 2/3 indican una opinión negativa.

@griesemer

Recordé algo mientras escribía mi comentario anterior: mientras que el documento de diseño dice que try se puede implementar como una transformación de árbol de sintaxis sencilla, y en muchos casos ese es obviamente el caso, hay algunos casos en los que no ver una forma sencilla de hacerlo. Por ejemplo, supongamos que tenemos lo siguiente:

switch x {
case rand.Int():
  a()
case 5, try(strconv.Atoi(y)):
  b()
}

Dado el orden de evaluación de switch , no veo cómo sacar de forma trivial el strconv.Atoi(y) de la cláusula case conservando la semántica prevista; lo mejor que se me ocurrió es reescribir switch como la cadena equivalente de declaraciones if / else , así:

if x == rand.Int() {
  a()
} else if x == 5 {
  b()
} else if _v, _err := strconv.Atoi(y); _err != nil {
  return _err
} else if x == _v {
  b()
}

(Hay otras situaciones en las que esto puede surgir, pero este es uno de los ejemplos más simples y el primero que me viene a la mente).

De hecho, antes de publicar esta propuesta, había estado trabajando en un traductor AST para implementar el operador check del borrador del diseño y me encontré con este problema. Sin embargo, estaba usando una versión pirateada de los paquetes stdlib go/* ; ¿Quizás el front-end del compilador está estructurado de una manera que lo hace más fácil? ¿O me he perdido algo y realmente hay una forma sencilla de hacerlo?

Ver también https://github.com/rhysd/trygo; de acuerdo con README, no implementa las expresiones try , y observa esencialmente la misma preocupación que planteo aquí; Sospecho que puede ser por eso que el autor no implementó esa función.

El código de @daved Professional no se está desarrollando en el vacío: existen convenciones locales, recomendaciones de estilo, revisiones de código, etc. (lo he dicho antes). Por lo tanto, no veo por qué el abuso sería "probable" (es posible, pero eso es cierto para cualquier construcción de lenguaje).

Tenga en cuenta que usar defer para decorar errores es posible con o sin try . Sin duda, hay buenas razones para que una función que contiene muchas verificaciones de errores, todas las cuales decoran los errores de la misma manera, haga esa decoración una vez, por ejemplo, usando defer . O tal vez use una función de envoltura que haga la decoración. O cualquier otro mecanismo que se ajuste a la factura y las recomendaciones de codificación locales. Después de todo, "los errores son solo valores" y tiene mucho sentido escribir y factorizar el código que se ocupa de los errores.

Las devoluciones desnudas pueden ser problemáticas cuando se usan de manera indisciplinada. Eso no significa que sean malos en general. Por ejemplo, si los resultados de una función son válidos solo si no hubo error, parece perfectamente correcto usar un retorno desnudo en caso de un error, siempre que seamos disciplinados con la configuración del error (ya que los otros valores de retorno no lo hacen). importa en este caso). try asegura exactamente eso. No veo ningún "abuso" aquí.

@dpinela El compilador ya traduce una instrucción switch como la suya como una secuencia de if-else-if , por lo que no veo ningún problema aquí. Además, el "árbol de sintaxis" que utiliza el compilador no es el árbol de sintaxis "go/ast". La representación interna del compilador permite un código mucho más flexible que no necesariamente se puede volver a traducir a Go.

@griesemer
Sí, por supuesto, todo lo que dices tiene fundamento. El área gris, sin embargo, no es tan simple como lo está enmarcando. Las devoluciones desnudas normalmente son tratadas con mucha cautela por aquellos de nosotros que enseñamos a otros (nosotros, que nos esforzamos por hacer crecer/promover la comunidad). Aprecio que el stdlib lo tenga todo lleno. Pero, cuando se enseña a otros, siempre se enfatizan los retornos explícitos. Deje que el individuo alcance su propia madurez para recurrir al enfoque más "fantasioso", pero alentarlo desde el principio seguramente sería fomentar un código difícil de leer (es decir, malos hábitos). Esto, nuevamente, es la sordera que estoy tratando de sacar a la luz.

Personalmente, no deseo prohibir las devoluciones desnudas o la manipulación diferida del valor. Cuando son realmente adecuados, me alegro de que estas capacidades estén disponibles (aunque otros usuarios experimentados pueden adoptar una postura más rígida). No obstante, alentar la aplicación de estas características menos comunes y generalmente frágiles de una manera tan generalizada es completamente la dirección opuesta que imaginé que tomaría Go. ¿Es el cambio pronunciado en el carácter de evitar la magia y las formas precarias de direccionamiento indirecto un cambio intencionado? ¿Deberíamos también comenzar a enfatizar el uso de DIC y otros mecanismos difíciles de depurar?

ps Su tiempo es muy apreciado. Su equipo y el idioma tienen mi respeto y cuidado. No deseo ningún dolor para nadie por hablar; Espero que escuchen la naturaleza de mi/nuestra preocupación y traten de ver las cosas desde nuestra perspectiva de "primera línea".

Agregando algunos comentarios a mi voto negativo.

Para la propuesta específica en cuestión:

1) Preferiría en gran medida que esto fuera una palabra clave en lugar de una función integrada por razones previamente articuladas de flujo de control y legibilidad del código.

2) Semánticamente, "intentar" es un pararrayos. Y, a menos que se produzca una excepción, sería mejor cambiar el nombre de "intentar" a algo como guard o ensure .

3) Además de estos dos puntos, creo que esta es la mejor propuesta que he visto para este tipo de cosas.

Un par de comentarios más que articulan mi objeción a cualquier adición de un concepto de try/guard/ensure frente a dejar solo if err != nil :

1) Esto va en contra de uno de los mandatos originales de golang (al menos como lo percibí) de ser explícito, fácil de leer/comprender, con muy poca "magia".

2) Esto fomentará la pereza en el momento preciso en que se requiere pensar: "¿qué es lo mejor que puede hacer mi código en caso de este error?". Hay muchos errores que pueden surgir al hacer cosas "repetidas", como abrir archivos, transferir datos a través de una red, etc. Si bien puede comenzar con un montón de "intentos" que ignoran escenarios de fallas no comunes, eventualmente muchos de estos " trys" desaparecerá, ya que es posible que deba implementar sus propias tareas de retroceso/reintento, registro/rastreo y/o limpieza. Los "eventos de baja probabilidad" están garantizados a escala.

Aquí hay algunas estadísticas más crudas tryhard . Esto solo está ligeramente validado, así que siéntete libre de señalar errores. ;-)

Primeros 20 "Paquetes Populares" en godoc.org

Estos son los repositorios que corresponden a los primeros 20 paquetes populares en https://godoc.org , ordenados por porcentaje de prueba de candidatos. Esto está usando la configuración predeterminada tryhard , que en teoría debería excluir los directorios vendor .

El valor medio de los candidatos de prueba en estos 20 repositorios es del 58 %.

| proyecto | ubicación | si stmts | si != cero (% de si) | probar candidatos (% de if != nil) |
|---------|-----|---------------|----------------- -----|---------------
| github.com/google/uuid | 1714 | 12 | 16,7% | 0,0% |
| github.com/pkg/errores | 1886 | 10 | 0,0% | 0,0% |
| github.com/aws/aws-sdk-go | 1911309 | 32015 | 9,4% | 8,9% |
| github.com/jinzhu/gorm | 15246 | 44 | 11,4% | 20,0% |
| github.com/robfig/cron | 1911 | 20 | 35,0% | 28,6% |
| github.com/gorila/websocket | 6959 | 212 | 32,5% | 39,1% |
| github.com/dgrijalva/jwt-go | 3270 | 118 | 29,7% | 40,0% |
| github.com/gomodule/redigo | 7119 | 187 | 34,8% | 41,5% |
| github.com/unixpickle/kahoot-hack | 1743 | 52 | 75,0% | 43,6% |
| github.com/lib/pq | 13396 | 239 | 30,1% | 55,6% |
| github.com/sirupsen/logrus | 5063 | 29 | 17,2% | 60,0% |
| github.com/prometheus/client_golang | 17791 | 194 | 49,0% | 62,1% |
| github.com/go-redis/redis | 21182 | 326 | 42,6% | 73,4% |
| github.com/mongodb/mongo-go-controlador | 86605 | 2097 | 37,8% | 73,9% |
| github.com/uber-go/zap | 15363 | 84 | 36,9% | 74,2% |
| github.com/golang/protobuf | 42959 | 685 | 22,9% | 77,1% |
| github.com/ginebra-gonic/ginebra | 14574 | 96 | 53,1% | 86,3% |
| github.com/go-pg/pg | 26369 | 831 | 37,7% | 86,9% |
| https://github.com/Shopify/sarama | 36427 | 1369 | 68,2% | 91,0% |
| github.com/stretchr/testificar | 13496 | 32 | 43,8% | 92,9% |

La columna " if stmts " solo cuenta declaraciones de if en funciones que devuelven un error, que es como lo informa tryhard , y que con suerte explica por qué es tan bajo para algo como gorm .

10 varios Proyectos Go "grandes"

Dado que los paquetes populares en godoc.org tienden a ser paquetes de biblioteca, también quería verificar las estadísticas de algunos proyectos más grandes.

Estos son misceláneos. grandes proyectos que resultaron ser lo más importante para mí (es decir, no hay una lógica real detrás de estos 10). Esto se ordena de nuevo por porcentaje de candidato de prueba.

El valor medio de los candidatos de prueba en estos 10 repositorios es del 59 %.

| proyecto | ubicación | si stmts | si != cero (% de si) | probar candidatos (% de if != nil) |
|---------|-----|---------------|----------------- -----|---------------------------------|
| github.com/juju/juju | 1026473 | 26904 | 51,9% | 17,5% |
| github.com/go-kit/kit | 38949 | 467 | 57,0% | 51,9% |
| github.com/boltdb/bolt | 12426 | 228 | 46,1% | 53,3% |
| github.com/hashicorp/cónsul | 249369 | 5477 | 47,6% | 54,5% |
| github.com/docker/docker | 251152 | 8690 | 48,7% | 56,8% |
| github.com/istio/istio | 429636 | 7564 | 40,4% | 61,9% |
| github.com/gohugoio/hugo | 94875 | 1853 | 42,4% | 64,8% |
| github.com/etcd-io/etcd | 209603 | 4657 | 38,3% | 65,5% |
| github.com/kubernetes/kubernetes | 1789172 | 40289 | 43,3% | 66,5% |
| github.com/cucarachadb/cucaracha | 1038529 | 22018 | 39,9% | 74,0% |


Estas dos tablas, por supuesto, solo representan una muestra de proyectos de código abierto, y solo los que son razonablemente conocidos. He visto a personas teorizar que las bases de códigos privados mostrarían una mayor diversidad, y hay al menos alguna evidencia de eso basada en algunos de los números que varias personas han estado publicando.

@thepudds , eso no se parece al _tryhard_ más reciente, que da "candidatos sin prueba".

@networkimprov Puedo confirmar que al menos para gorm estos son resultados de los últimos tryhard . Los "candidatos que no son de prueba" simplemente no se informan en las tablas anteriores.

@daved Primero, permítanme asegurarles que los escuchamos alto y claro. Aunque todavía estamos en las primeras etapas del proceso y muchas cosas pueden cambiar. No nos apresuremos.

Entiendo (y aprecio) que uno podría querer elegir un enfoque más conservador al enseñar Go. Gracias.

@griesemer FYI, aquí están los resultados de ejecutar la última versión de Tryhard en 233k líneas de código en las que he estado involucrado, muchas de ellas no de código abierto:

--- stats ---
   8760 (100.0% of    8760) functions (function literals are ignored)
   2942 ( 33.6% of    8760) functions returning an error
  22991 (100.0% of   22991) statements in functions returning an error
   5548 ( 24.1% of   22991) if statements
   2929 ( 52.8% of    5548) if <err> != nil statements
    163 (  5.6% of    2929) try candidates
      0 (  0.0% of    2929) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   2213 ( 75.6% of    2929) { return ... zero values ..., expr }
    167 (  5.7% of    2929) single statement then branch
    253 (  8.6% of    2929) complex then branch; cannot use try
     14 (  0.5% of    2929) non-empty else branch; cannot use try

Gran parte del código utiliza un modismo similar a:

 if err != nil {
     return ... zero values ..., errors.Wrap(err)
 }

Sería interesante si tryhard pudiera identificar cuándo todas esas expresiones en una función usan una expresión idéntica, es decir, cuándo sería posible reescribir la función con un único controlador común defer .

Estas son las estadísticas de una pequeña herramienta auxiliar de GCP para automatizar la creación de usuarios y proyectos:

$ tryhard -r .
--- stats ---
    129 (100.0% of     129) functions (function literals are ignored)
     75 ( 58.1% of     129) functions returning an error
    725 (100.0% of     725) statements in functions returning an error
    164 ( 22.6% of     725) if statements
     93 ( 56.7% of     164) if <err> != nil statements
     64 ( 68.8% of      93) try candidates
      0 (  0.0% of      93) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     17 ( 18.3% of      93) { return ... zero values ..., expr }
      7 (  7.5% of      93) single statement then branch
      1 (  1.1% of      93) complex then branch; cannot use try
      0 (  0.0% of      93) non-empty else branch; cannot use try

Después de esto, seguí adelante y revisé todos los lugares en el código que todavía están tratando con una variable err para ver si podía encontrar algún patrón significativo.

Recaudar err s

En un par de lugares, no queremos detener la ejecución en el primer error y, en cambio, poder ver todos los errores que ocurrieron una vez al final de la ejecución. Tal vez haya una forma diferente de hacer esto que se integre bien con try o se agregue alguna forma de soporte para errores múltiples a Go.

var errs []error
for _, p := range toDelete {
    fmt.Println("delete:", p.ProjectID)
    if err := s.DeleteProject(ctx, p.ProjectID); err != nil {
        errs = append(errs, err)
    }
}

Responsabilidad de decoración de errores

Después de haber leído este comentario nuevamente, de repente hubo muchos casos potenciales de try que me llamaron la atención. Todos son similares en el sentido de que la función que llama está decorando el error de una función llamada con información que la función llamada ya podría haber agregado al error:

func run() error {
    key := "MY_ENV_VAR"
    client, err := ClientFromEnvironment(key)
    if err != nil {
        // "github.com/pkg/errors"
        return errors.Wrap(err, key)
    }
    // do something with `client`
}

func ClientFromEnvironment(key string) (*http.Client, error) {
    filename, ok := os.LookupEnv(key)
    if !ok {
        return nil, errors.New("environment variable not set")
    }
    return ClientFromFile(filename)
}

Citando la parte importante del blog Go aquí nuevamente aquí para mayor claridad:

Es responsabilidad de la implementación del error resumir el contexto. El error devuelto por os.Open formatea como "abrir /etc/passwd: permiso denegado", no solo "permiso denegado". Al error devuelto por nuestro Sqrt le falta información sobre el argumento no válido.

Con esto en mente, el código anterior ahora se convierte en:

func run() error {
    key := "MY_ENV_VAR"
    client := try(ClientFromEnvironment(key))
    // do something with `client`
}

func ClientFromEnvironment(key string) (*http.Client, error) {
    filename, ok := os.LookupEnv(key)
    if !ok {
        return nil, fmt.Errorf("environment variable not set: %s", key)
    }
    return ClientFromFile(filename)
}

A primera vista, esto parece un cambio menor, pero en mi opinión, podría significar que try en realidad está incentivando a impulsar un manejo de errores mejor y más consistente en la cadena de funciones y más cerca de la fuente o el paquete.

Notas finales

En general, creo que el valor que aporta try a largo plazo es mayor que los posibles problemas que veo actualmente, que son:

  1. Una palabra clave podría "sentirse" mejor ya que try está cambiando el flujo de control.
  2. El uso try significa que ya no puede poner un tope de depuración en el caso return err .

Dado que el equipo de Go ya conoce esas preocupaciones, tengo curiosidad por ver cómo se desarrollarán en "el mundo real". Gracias por su tiempo en leer y responder a todos nuestros mensajes.

Actualizar

Se corrigió una firma de función que no devolvía un error antes. ¡Gracias @magical por detectar eso!

func main() {
    key := "MY_ENV_VAR"
    client := try(ClientFromEnvironment(key))
    // do something with `client`
}

@mrkanister Nitpicking, pero en realidad no puede usar try en este ejemplo porque main no devuelve error .

Este es un comentario de agradecimiento;
gracias @griesemer por la jardinería y todo lo que ha estado haciendo en este tema y en otros lugares.

En caso de que tenga muchas líneas como estas (de https://github.com/golang/go/issues/32437#issuecomment-509974901):

if !ok {
    return nil, fmt.Errorf("environment variable not set: %s", key)
}

Puede usar una función auxiliar que solo devuelva un error no nulo si alguna condición es verdadera:

try(condErrorf(!ok, "environment variable not set: %s", key))

Una vez que se identifiquen los patrones comunes, creo que será posible manejar muchos de ellos con solo unos pocos ayudantes, primero a nivel de paquete, y tal vez eventualmente llegando a la biblioteca estándar. Tryhard es genial, está haciendo un trabajo maravilloso y brinda mucha información interesante, pero hay mucho más.

Compacto de una sola línea si

Como una adición a la propuesta if de una sola línea de @zeebo y otros, la declaración if podría tener una forma compacta que elimine el != nil y las llaves:

if err return err
if err return errors.Wrap(err, "foo: failed to boo")
if err return fmt.Errorf("foo: failed to boo: %v", err)

Creo que esto es simple, ligero y legible. Hay dos partes:

  1. Haga que las declaraciones if comprueben implícitamente los valores de error para nil (o quizás las interfaces de manera más general). En mi humilde opinión, esto mejora la legibilidad al reducir la densidad y el comportamiento es bastante obvio.
  2. Agregue soporte para if variable return ... . Dado que return está tan cerca del lado izquierdo, parece que todavía es bastante fácil hojear el código; la dificultad adicional de hacerlo es uno de los principales argumentos en contra de los ifs (?) de una sola línea. Go también tiene un precedente para simplificar la sintaxis, por ejemplo, eliminando los paréntesis de su declaración if.

Estilo actual:

a, err := BusinessLogic(state)
if err != nil {
   return nil, err
}

Una línea si:

a, err := BusinessLogic(state)
if err != nil { return nil, err }

Compacto de una línea si:

a, err := BusinessLogic(state)
if err return nil, err
a, err := BusinessLogic(state)
if err return nil, errors.Wrap(err, "some context")
func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err return nil, errors.WithMessage(err, "load config dir")

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err return nil, errors.WithMessage(err, "execute main template")

    buf, err := format.Source(b.Bytes())
    if err return nil, errors.WithMessage(err, "format main template")

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err return nil, errors.WithMessagef(err, "write file %s", target)

    // ...
}

@eug48 ver #32611

Aquí están las estadísticas de Tryhard para un monorepo (líneas de código de acceso, excluyendo el código del proveedor: 2,282,731):

--- stats ---
 117551 (100.0% of  117551) functions (function literals are ignored)
  35726 ( 30.4% of  117551) functions returning an error
 263725 (100.0% of  263725) statements in functions returning an error
  50690 ( 19.2% of  263725) if statements
  25042 ( 49.4% of   50690) if <err> != nil statements
  12091 ( 48.3% of   25042) try candidates
     36 (  0.1% of   25042) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   3561 ( 14.2% of   25042) { return ... zero values ..., expr }
   3304 ( 13.2% of   25042) single statement then branch
   4966 ( 19.8% of   25042) complex then branch; cannot use try
    296 (  1.2% of   25042) non-empty else branch; cannot use try

Dado que la gente todavía está proponiendo alternativas, me gustaría saber con más detalle qué funcionalidad es la que la comunidad Go más amplia realmente quiere de cualquier nueva función de manejo de errores propuesta.

He preparado una encuesta que enumera un montón de características diferentes, piezas de funcionalidad de manejo de errores que he visto proponer a la gente. Cuidadosamente he omitido cualquier denominación o sintaxis propuesta y, por supuesto, traté de hacer que la encuesta fuera neutral en lugar de favorecer mis propias opiniones.

Si a la gente le gustaría participar, aquí está el enlace, abreviado para compartir:

https://forms.gle/gaCBgxKRE4RMCz7c7

Todos los que participen deberían poder ver los resultados resumidos. ¿Quizás esto podría ayudar a centrar la discusión?

if err := os.Setenv("GO111MODULE", "on"); err != nil {
    return err
}

El controlador de aplazamiento que agrega contexto no funciona en este caso, ¿o sí? De lo contrario, sería bueno hacerlo más visible, si es posible, ya que sucede bastante rápido, especialmente porque ese es el recurso estándar hasta ahora.

Ah, y presente try , también encontré muchos casos de uso aquí.

--- stats ---
    929 (100.0% of     929) functions (function literals are ignored)
    230 ( 24.8% of     929) functions returning an error
   1480 (100.0% of    1480) statements in functions returning an error
    320 ( 21.6% of    1480) if statements
    206 ( 64.4% of     320) if <err> != nil statements
    109 ( 52.9% of     206) try candidates
      2 (  1.0% of     206) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     53 ( 25.7% of     206) { return ... zero values ..., expr }
     18 (  8.7% of     206) single statement then branch
     17 (  8.3% of     206) complex then branch; cannot use try
      2 (  1.0% of     206) non-empty else branch; cannot use try

@lpar Le invitamos a discutir alternativas , pero no lo haga en este número. Se trata de la propuesta de try . El mejor lugar sería en realidad una de las listas de correo, por ejemplo, go-nuts. El rastreador de problemas es realmente mejor para rastrear y discutir un problema específico en lugar de una discusión general. Gracias.

@fabstu El controlador defer funcionará bien en su ejemplo, tanto con como sin try . Expandiendo su código con la función de cierre:

func f() (err error) {
    defer func() {
       if err != nil {
          err = decorate(err, "msg") // here you can modify the result error as you please
       }
    }()
    ...
    if err := os.Setenv("GO111MODULE", "on"); err != nil {
        return err
    }
    ...
}

(tenga en cuenta que el resultado err lo establecerá el return err ; y el err utilizado por el return es el declarado localmente con el if - estas son solo las reglas normales de alcance en acción).

O bien, usando un try , que eliminará la necesidad de la variable local err :

func f() (err error) {
    defer func() {
       if err != nil {
          err = decorate(err, "msg") // here you can modify the result error as you please
       }
    }()
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

Y lo más probable es que quieras usar una de las funciones errors/errd propuestas :

func f() (err error) {
    defer errd.Wrap(&err, ... )
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

Y si no necesita envolverlo, solo será:

func f() error {
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

@fastu Y finalmente, puedes usar errors/errd también sin try y luego obtienes:

func f() (err error) {
    defer errd.Wrap(&err, ... )
    ...
    if err := os.Setenv("GO111MODULE", "on"); err != nil {
        return err
    }
    ...
}

Cuanto más lo pienso más me gusta esta propuesta.
Lo único que me molesta es usar el retorno con nombre en todas partes. ¿Es finalmente una buena práctica y debería usarla (nunca lo intenté)?

De todos modos, antes de cambiar todo mi código, ¿funcionará así?

func f() error {
  var err error
  defer errd.Wrap(&err,...)
  try(...)
}

@flibustenet Los parámetros de resultado con nombre en sí mismos no son una mala práctica en absoluto; la preocupación habitual con los resultados con nombre es que habilitan naked returns ; es decir, uno puede simplemente escribir return sin necesidad de especificar los resultados reales _con return _. En general (¡pero no siempre!), esta práctica hace que sea más difícil leer y razonar sobre el código porque uno no puede simplemente mirar la instrucción return y concluir cuál es el resultado. Uno tiene que escanear el código para los parámetros de resultado. Uno puede fallar al establecer un valor de resultado, y así sucesivamente. Entonces, en algunas bases de código, simplemente se desaconsejan las devoluciones desnudas.

Pero, como mencioné antes , si los resultados no son válidos en caso de un error, está perfectamente bien establecer el error e ignorar el resto. Un retorno desnudo en tales casos está perfectamente bien siempre que el resultado del error se establezca de manera consistente. try garantizará exactamente eso.

Finalmente, los parámetros de resultado con nombre solo son necesarios si desea aumentar el retorno de error con defer . El documento de diseño también analiza brevemente la posibilidad de proporcionar otra función integrada para acceder al resultado del error. Eso eliminaría por completo la necesidad de devoluciones con nombre.

Con respecto a su ejemplo de código: esto no funcionará como se esperaba porque try _siempre_ establece el _error de resultado_ (que no tiene nombre en este caso). Pero está declarando una variable local diferente err y el errd.Wrap opera en esa. No se establecerá por try .

Informe de experiencia rápida: estoy escribiendo un controlador de solicitud HTTP que se ve así:

func Handler(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        id := chi.URLParam(r, "id")

        var err error
        // starts as bad request, then it's an internal server error after we parse inputs
        var statusCode = http.StatusBadRequest

        defer func() {
            if err != nil {
                wrap := xerrors.Errorf("handler fail: %w", err)
                logger.With(zap.Error(wrap)).Error("error")
                http.Error(w, wrap.Error(), statusCode)
            }
        }()
        var c Thingie
        err = unmarshalBody(r, &c)
        if err != nil {
            return
        }
        statusCode = http.StatusInternalServerError
        s, err := DoThing(ctx, c)
        if err != nil {
            return
        }
        d, err := DoThingWithResult(ctx, id, s)
        if err != nil {
            return
        }
        data, err := json.Marshal(detail)
        if err != nil {
            return
        }
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        _, err = w.Write(data)
        if err != nil {
            return
        }
}

A primera vista, parece que este es un candidato ideal para try ya que hay mucho manejo de errores donde no hay nada que hacer excepto devolver un mensaje, todo lo cual se puede hacer en diferido. Pero no puede usar try porque un controlador de solicitudes no devuelve error . Para usarlo, tendría que envolver el cuerpo en un cierre con la firma func() error . Eso se siente...poco elegante y sospecho que el código que se ve así es un patrón algo común.

@jonbodner

Esto funciona (https://play.golang.org/p/NaBZe-QShpu):

package main

import (
    "errors"
    "fmt"

    "golang.org/x/xerrors"
)

func main() {
    var err error
    defer func() {
        filterCheck(recover())
        if err != nil {
            wrap := xerrors.Errorf("app fail (at count %d): %w", ct, err)
            fmt.Println(wrap)
        }
    }()

    check(retNoErr())

    n, err := intNoErr()
    check(err)

    n, err = intErr()
    check(err)

    check(retNoErr())

    check(retErr())

    fmt.Println(n)
}

func check(err error) {
    if err != nil {
        panic(struct{}{})
    }
}

func filterCheck(r interface{}) {
    if r != nil {
        if _, ok := r.(struct{}); !ok {
            panic(r)
        }
    }
}

var ct int

func intNoErr() (int, error) {
    ct++
    return 0, nil
}

func retNoErr() error {
    ct++
    return nil
}

func intErr() (int, error) {
    ct++
    return 0, errors.New("oops")
}

func retErr() error {
    ct++
    return errors.New("oops")
}

¡Ah, el primer voto negativo! Bueno. Deja que el pragmatismo fluya a través de ti.

Ejecuté tryhard en algunas de mis bases de código. Desafortunadamente, algunos de mis paquetes tienen 0 candidatos de prueba a pesar de ser bastante grandes porque los métodos en ellos usan una implementación de error personalizada. Por ejemplo, cuando construyo servidores, me gusta que mis métodos de capa de lógica empresarial solo emitan SanitizedError s en lugar de error s para garantizar en el momento de la compilación que cosas como las rutas del sistema de archivos o la información del sistema no filtrarse a los usuarios en mensajes de error.

Por ejemplo, un método que usa este patrón podría verse así:

func (a *App) GetFriendsOfUser(userId model.Id) ([]*model.User, SanitizedError) {
    if user, err := a.GetUserById(userId); err != nil {
        // (*App).GetUserById returns (*model.User, SanitizedError)
        // This could be a try() candidate.
        return err
    } else if user == nil {
        return NewUserError("The specified user doesn't exist.")
    }

    friends, err := a.Store.GetFriendsOfUser(userId)
    // (*Store).GetFriendsOfUser returns ([]*model.User, error)
    // This could be a SQL error or a network error or who knows what.
    return friends, NewInternalError(err)
}

¿Hay alguna razón por la que no podamos relajar la propuesta actual para que funcione siempre que el último valor de retorno tanto de la función envolvente como de la expresión de la función de prueba implemente el error y sean del mismo tipo? Esto aún evitaría cualquier confusión concreta de la interfaz nil ->, pero permitiría probar en situaciones como la anterior.

Gracias, @jonbodner , por tu ejemplo . Escribiría ese código de la siguiente manera (a pesar de los errores de traducción):

func Handler(w http.ResponseWriter, r *http.Request) {
    statusCode, err := internalHandler(w, r)
    if err != nil {
        wrap := xerrors.Errorf("handler fail: %w", err)
        logger.With(zap.Error(wrap)).Error("error")
        http.Error(w, wrap.Error(), statusCode)
    }
}

func internalHandler(w http.ResponseWriter, r *http.Request) (statusCode int, err error) {
    ctx := r.Context()
    id := chi.URLParam(r, "id")

    // starts as bad request, then it's an internal server error after we parse inputs
    statusCode = http.StatusBadRequest
    var c Thingie
    try(unmarshalBody(r, &c))

    statusCode = http.StatusInternalServerError
    s := try(DoThing(ctx, c))
    d := try(DoThingWithResult(ctx, id, s))
    data := try(json.Marshal(detail))

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    try(w.Write(data))

    return
}

Utiliza dos funciones, pero es mucho más corto (29 líneas frente a 40 líneas), y usé un espacio agradable, y este código no necesita un defer . El defer en particular, junto con el código de estado que se cambia en el camino hacia abajo y se usa en el defer hace que el código original sea más difícil de seguir de lo necesario. El nuevo código, si bien usa resultados con nombre y un retorno simple (puede reemplazarlo fácilmente con return statusCode, nil si lo desea) es más simple porque separa claramente el manejo de errores de la "lógica empresarial".

Simplemente vuelva a publicar mi comentario en otro número https://github.com/golang/go/issues/32853#issuecomment -510340544

Creo que si podemos proporcionar otro parámetro funcname , sería genial, de lo contrario, todavía no sabemos qué función devuelve el error.

func foo() error {
    handler := func(err error, funcname string) error {
        return fmt.Errorf("%s: %v", funcname, err) // wrap something
        //return nil // or dismiss
    }

    a, b := try(bar1(), handler) 
    c, d := try(bar2(), handler) 
}

@ccbrown Me pregunto si su ejemplo sería susceptible al mismo tratamiento que el anterior ; es decir, si tuviera sentido factorizar el código de modo que los errores internos se envuelvan una vez (mediante una función de inclusión) antes de que se apaguen (en lugar de envolverlos en todas partes). Me parecería (sin saber mucho sobre su sistema) que eso sería preferible ya que centralizaría el ajuste de errores en un solo lugar en lugar de en todas partes.

Pero con respecto a su pregunta: tendría que pensar en hacer que try acepte un tipo de error más general (y devuelva uno también). No veo ningún problema en este momento (excepto que es más complicado de explicar), pero puede haber un problema después de todo.

En ese sentido, nos hemos preguntado desde el principio si try podría generalizarse para que no solo funcione para los tipos error , sino para cualquier tipo, y la prueba err != nil entonces se convierte en x != zero donde x es el equivalente de err (el último resultado), y zero el valor cero respectivo para el tipo de x . Desafortunadamente, esto no funciona para el caso común de booleanos (y casi cualquier otro tipo básico), porque el valor cero de bool es false y ok != false es exactamente lo contrario de lo que nos gustaría probar.

@lunny La versión propuesta de try no acepta una función de controlador.

@griesemer Ay. ¡Lo que es una lástima! De lo contrario, puedo eliminar github.com/pkg/errors y todo errors.Wrap .

@ccbrown Me pregunto si su ejemplo sería susceptible al mismo tratamiento que el anterior; es decir, si tuviera sentido factorizar el código de modo que los errores internos se envuelvan una vez (mediante una función de inclusión) antes de que se apaguen (en lugar de envolverlos en todas partes). Me parecería (sin saber mucho sobre su sistema) que eso sería preferible ya que centralizaría el ajuste de errores en un solo lugar en lugar de en todas partes.

@griesemer Devolver error en lugar de una función adjunta haría posible olvidar clasificar cada error como un error interno, error de usuario, error de autorización, etc. Tal como está, el compilador detecta eso y usa try no valdría la pena cambiar esas verificaciones en tiempo de compilación por verificaciones en tiempo de ejecución.

Me gustaría decir que me gusta el diseño de try , pero todavía hay una instrucción $#$ if $#$ en el controlador defer mientras usa try . No creo que sea más simple que las instrucciones if sin el controlador try y defer . Tal vez solo usar try sería mucho mejor.

@ccbrown Lo tengo. En retrospectiva, creo que su relajación sugerida no debería ser un problema. Creo que podríamos relajar try para trabajar con cualquier tipo de interfaz (y tipo de resultado coincidente), en realidad, no solo error , siempre que la prueba relevante siga siendo x != nil . Algo sobre lo que pensar. Esto podría hacerse antes o de forma retroactiva, ya que creo que sería un cambio compatible con versiones anteriores.

El ejemplo de @jonbodner , y la forma en que @griesemer lo reescribió , es exactamente el tipo de código que tengo donde realmente me gustaría usar try .

¿A nadie le molesta este tipo de uso de try:

data := try(json.Marshal(detalle))

Independientemente del hecho de que el error de serialización puede resultar en encontrar la línea correcta en el código escrito, me siento incómodo sabiendo que se trata de un error simple que se devuelve sin incluir el número de línea o la información de la persona que llama. Conocer el archivo de origen, el nombre de la función y el número de línea suele ser lo que incluyo cuando manejo errores. Tal vez estoy malinterpretando algo sin embargo.

@griesemer No estaba planeando discutir alternativas aquí. El hecho de que todos sigan sugiriendo alternativas es exactamente la razón por la que creo que una encuesta para averiguar qué es lo que la gente realmente quiere sería una buena idea. Acabo de publicar sobre esto aquí para tratar de atrapar a la mayor cantidad posible de personas interesadas en la posibilidad de mejorar el manejo de errores de Go.

@trende-jp Realmente depende del contexto de esta línea de código; por sí solo, no se puede juzgar de ninguna manera significativa. Si esta es la única llamada a json.Marshal y desea aumentar el error, una declaración if puede ser mejor. Si hay muchas llamadas json.Marshal , se podría agregar contexto al error con defer ; o quizás envolviendo todas estas llamadas dentro de un cierre local que devuelve el error. Hay multitud de formas en que esto podría tenerse en cuenta si es necesario (es decir, si hay muchas llamadas de este tipo en la misma función). "Los errores son valores" también es cierto aquí: use el código para que el manejo de errores sea manejable.

try no va a resolver todos sus problemas de manejo de errores, esa no es la intención. Es simplemente otra herramienta en la caja de herramientas. Y tampoco es una maquinaria realmente nueva, es una forma de azúcar sintáctico para un patrón que hemos observado con frecuencia en el transcurso de casi una década. Tenemos alguna evidencia de que funcionaría muy bien en algún código, y que tampoco sería de mucha ayuda en otro código.

@trende-jp

¿No se puede resolver con defer ?

defer fmt.HandleErrorf(&err, "decoding %q", path)

Los números de línea en los mensajes de error también se pueden resolver como lo he mostrado en mi blog: Cómo usar 'try' .

@trende-jp @faiface Además del número de línea, puede almacenar la cadena del decorador en una variable. Esto le permitiría aislar la llamada de función específica que está fallando.

Realmente creo que esto no debería ser una función integrada .

Se ha mencionado varias veces que panic() y recover() también alteran el flujo de control. Muy bien, no agreguemos más.

@networkimprov escribió https://github.com/golang/go/issues/32437#issuecomment -498960081:

No se lee como Go.

No podría estar mas de acuerdo.

En todo caso, creo que cualquier mecanismo para abordar el problema raíz (y no estoy seguro de que haya uno), debería ser activado por una palabra clave (¿o un símbolo de clave?).

¿Cómo te sentirías si go func() fuera go(func()) ?

¿Qué tal usar bang(!) en lugar de la función try ? Esto podría hacer posible la cadena de funciones:

func foo() {
    f := os.Open!("")
    defer f.Close()
    // etc
}

func bar() {
    count := mustErr!().Read!()
}

@sylr

¿Cómo te sentirías si go func() fuera go(func()) ?

Vamos, eso sería bastante aceptable.

@sylr Gracias, pero no estamos solicitando propuestas alternativas en este hilo. Vea también esto sobre mantenerse enfocado.

Con respecto a su comentario : Go es un lenguaje pragmático; usar un lenguaje incorporado aquí es una opción pragmática. Tiene varias ventajas sobre el uso de una palabra clave como se explica detalladamente en el documento de diseño . Tenga en cuenta que try es simplemente azúcar sintáctico para un patrón común (en contraste con go que implementa una función principal de Go y no se puede implementar con otros mecanismos de Go), como append , copy , etc. Usar un integrado es una buena elección.

(Pero como dije antes, si _eso_ es lo único que evita que try sea aceptable, podemos considerar convertirlo en una palabra clave).

Estaba reflexionando sobre una parte de mi propio código y cómo se vería con try :

slurp, err := ioutil.ReadFile(path)
if err != nil {
    return err
}
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)

Podría convertirse:

return ioutil.WriteFile(path, append(copyrightText, try(ioutil.ReadFile(path))...), 0666)

No estoy seguro si esto es mejor. Parece hacer que el código sea mucho más difícil de leer. Pero puede que solo sea cuestión de acostumbrarse.

@gbbr Tienes una opción aquí. Podrías escribirlo como:

slurp := try(ioutil.ReadFile(path))
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)

lo que aún le ahorra una gran cantidad de repeticiones pero lo hace mucho más claro. Esto no es inherente a try . El hecho de que pueda comprimir todo en una sola expresión no significa que deba hacerlo. Eso se aplica en general.

@griesemer Este ejemplo _es_ inherente para probar, no puede anidar código que puede fallar hoy; está obligado a manejar errores con flujo de control. Me gustaría aclarar algo de https://github.com/golang/go/issues/32825#issuecomment -507099786 / https://github.com/golang/go/issues/32825#issuecomment -507136111 a lo que respondió https://github.com/golang/go/issues/32825#issuecomment -507358397. Más tarde, el mismo problema se discutió nuevamente en https://github.com/golang/go/issues/32825#issuecomment -508813236 y https://github.com/golang/go/issues/32825#issuecomment -508937177 - el último de lo cual declaro:

Me alegro de que hayas leído mi argumento central en contra de try: la implementación no es lo suficientemente restrictiva. Creo que la implementación debe coincidir con todos los ejemplos de uso de propuestas que sean concisos y fáciles de leer.

_O_ la propuesta debe contener ejemplos que coincidan con la implementación para que todas las personas que la consideren puedan estar expuestas a lo que inevitablemente aparecerá en el código de Go. Junto con todos los casos de esquina que podemos enfrentar al solucionar problemas de software menos que idealmente escrito, que ocurre en cualquier idioma / entorno. Debería responder preguntas como cómo se verán los seguimientos de pila con múltiples niveles de anidamiento, ¿son fácilmente reconocibles las ubicaciones de los errores? ¿Qué pasa con los valores del método, los literales de funciones anónimas? ¿Qué tipo de seguimiento de pila produce lo siguiente si falla la línea que contiene las llamadas a fn()?

fn := func(n int) (int, error) { ... }
return try(func() (int, error) { 
    mu.Lock()
    defer mu.Unlock()
    return try(try(fn(111111)) + try(fn(101010)) + try(func() (int, error) {
       // yea...
    })(2))
}(try(fn(1)))

Soy muy consciente de que se escribirá mucho código razonable, pero ahora proporcionamos una herramienta que nunca antes había existido: la capacidad de escribir código potencialmente sin un flujo de control claro. Así que quiero justificar por qué lo permitimos en primer lugar, nunca quiero perder mi tiempo depurando este tipo de código. Porque sé que lo haré, la experiencia me ha enseñado que alguien lo hará si se lo permites. Ese alguien es a menudo un yo desinformado.

Go proporciona las formas menos posibles para que otros desarrolladores y yo perdamos el tiempo al limitarnos a usar las mismas construcciones mundanas. No quiero perder eso sin un beneficio abrumador. No creo que "porque try se implementa como una función" sea un beneficio abrumador. ¿Puede proporcionar una razón por la que es?

Tener un seguimiento de pila que muestre dónde falla lo anterior sería útil, ¿tal vez agregar un literal compuesto con campos que llamen a esa función a la mezcla? Lo pido porque sé cómo se ven los seguimientos de la pila hoy en día para este tipo de problema. Go no proporciona información de columna fácilmente digerible en la información de la pila, solo la dirección de entrada de la función hexadecimal. Varias cosas me preocupan sobre esto, como la consistencia del seguimiento de la pila entre arquitecturas, por ejemplo, considere este código:

package main
import "fmt"
func dopanic(b bool) int { if b { panic("panic") }; return 1 }
type bar struct { a, b, d int; b *bar }
func main() {
    fmt.Println(&bar{
        a: 1,
        c: 1,
        d: 1,
        b: &bar{
            a: 1,
            c: 1,
            d: dopanic(true) + dopanic(false),
        },
    })
}

Observe cómo el primer patio de juegos falla en el dopánico de la izquierda, el segundo en el de la derecha, pero ambos imprimen un seguimiento de pila idéntico:
https://play.golang.org/p/SYs1r4hBS7O
https://play.golang.org/p/YMKkflcQuav

panic: panic

goroutine 1 [running]:
main.dopanic(...)
    /tmp/sandbox709874298/prog.go:7
main.main()
    /tmp/sandbox709874298/prog.go:27 +0x40

Habría esperado que el segundo fuera +0x41 o algún desplazamiento después de 0x40, que podría usarse para determinar la llamada real que falló dentro del pánico. Incluso si obtuviéramos las compensaciones hexadecimales correctas, no podré determinar dónde ocurrió la falla sin una depuración adicional. Hoy en día, este es un caso límite, algo que la gente rara vez enfrentará. Si lanza una versión anidable de try, se convertirá en la norma, ya que incluso la propuesta incluye una strconv try() + try() que muestra que es posible y aceptable usar try de esta manera.

1) Dada la información anterior, ¿qué cambios en los seguimientos de la pila planea hacer (si los hay) para que pueda saber dónde falló mi código?

2) ¿Se permite el anidamiento de prueba porque cree que debería serlo? Si es así, ¿cuáles cree que son los beneficios de intentar anidar y cómo evitará el abuso? Creo que Tryhard debe ajustarse para realizar intentos anidados donde lo considere aceptable para que las personas puedan tomar una decisión más informada sobre cómo afecta su código, ya que actualmente solo obtenemos ejemplos de uso mejores/más estrictos. Esto nos dará una idea de qué tipo de limitaciones de vet se impondrán, a partir de ahora ha dicho que el veterinario será la defensa contra los intentos irrazonables, pero ¿cómo se materializará eso?

3) ¿Intenta anidar porque resulta ser una consecuencia de la implementación? Si es así, ¿no parece esto un argumento muy débil para el cambio de idioma más notable desde que se lanzó Go?

Creo que este cambio necesita más consideración en torno al intento de anidamiento. Cada vez que pienso en ello, surge un nuevo punto de dolor en alguna parte, me preocupa mucho que todos los aspectos negativos potenciales no surjan hasta que se revele en la naturaleza. El anidamiento también proporciona una manera fácil de filtrar recursos, como se menciona en https://github.com/golang/go/issues/32825#issuecomment -506882164, que hoy no es posible. Creo que la historia del "veterinario" necesita un plan mucho más concreto con ejemplos de cómo proporcionará retroalimentación si se usará como defensa contra los ejemplos dañinos de try() que he dado aquí, o la implementación debería proporcionar errores de tiempo de compilación. para uso fuera de sus mejores prácticas ideales.

editar: pregunté en gophers sobre la arquitectura de play.golang.org y alguien mencionó que se compila a través de NaCl, por lo que probablemente solo sea una consecuencia / error de eso. Pero podría ver que esto es un problema en otro arco, creo que muchos de los problemas que podrían surgir de la introducción de múltiples retornos por línea simplemente no se han explorado completamente ya que la mayoría de los usos se centran en el uso de una sola línea sensato y limpio.

Oh no, por favor no agregues esta 'magia' al idioma.
Estos no se ven ni se sienten como el resto del idioma.
Ya veo código como este que aparece en todas partes.

a, b := try( f() )
if a != 0 && b != "" {
...
}
...

en lugar de

a,b,err := f()
if err != nil {
...
}
...

o

a,b,_:= f()

El patrón call if err.... era un poco antinatural al principio para mí, pero ahora estoy acostumbrado
Me siento más fácil de manejar los errores, ya que pueden llegar al flujo de ejecución en lugar de escribir contenedores/controladores que tendrán que realizar un seguimiento de algún tipo de estado para actuar una vez activados.
Y si decido ignorar los errores para salvar la vida de mi teclado, sé que algún día entraré en pánico.

incluso cambié mis hábitos en vbscript a:

on error resume next
a = f()
if er.number <> 0 then
    ...
end if
...

me gusta esta propuesta

Todas las inquietudes que tenía (p. ej., idealmente debería ser una palabra clave y no una función integrada) se abordan en el documento detallado.

No es 100% perfecto, pero es una solución lo suficientemente buena que a) resuelve un problema real yb) lo hace teniendo en cuenta una gran cantidad de compatibilidad con versiones anteriores y otros problemas.

Claro que hace algo de 'magia', pero también defer . La única diferencia es la palabra clave frente a la incorporada, y la elección de evitar una palabra clave aquí tiene sentido.

Siento que todos los comentarios importantes contra la propuesta try() ya se expresaron. Pero déjame intentar resumir:

1) try() mueve la complejidad del código vertical a horizontal
2) Las llamadas try() anidadas son tan difíciles de leer como los operadores ternarios
3) Introduce un flujo de control de 'retorno' invisible que no es visualmente distintivo (en comparación con los bloques sangrados que comienzan con la palabra clave return )
4) Empeora la práctica de ajuste de errores (contexto de función en lugar de una acción específica)
5) Divide la comunidad #golang y el estilo de código (anti-gofmt)
6) Hará que los desarrolladores reescriban try() a if-err-nil y viceversa con frecuencia (tryhard vs. agregar lógica de limpieza/logs adicionales/mejor contexto de error)

@VojtechVitek Creo que los puntos que hace son subjetivos y solo pueden evaluarse una vez que las personas comienzan a usarlos en serio.

Sin embargo, creo que hay un punto técnico que no se ha discutido mucho. El patrón de usar defer para envolver/decorar errores tiene implicaciones de rendimiento más allá del costo simple de defer en sí mismo, ya que las funciones que usan defer no se pueden alinear.

Esto significa que la adopción try con ajuste de error impone dos costos potenciales en comparación con la devolución de un error ajustado directamente después de una comprobación err != nil :

  1. un aplazamiento para todos los caminos a través de la función, incluso los exitosos
  2. pérdida de inline

Aunque hay algunas mejoras de rendimiento impresionantes por defer el costo sigue siendo distinto de cero.

try tiene mucho potencial, por lo que sería bueno si el equipo de Go pudiera revisar el diseño para permitir que se realice algún tipo de ajuste en el punto de falla en lugar de hacerlo de forma preventiva a través defer .

la historia del veterinario necesita un plan mucho más concreto

La historia del veterinario es un cuento de hadas. Solo funcionará para los tipos conocidos y será inútil en los personalizados.

Hola a todos,

Nuestro objetivo con propuestas como esta es tener una discusión en toda la comunidad sobre las implicaciones, las compensaciones y cómo proceder, y luego usar esa discusión para ayudar a decidir el camino a seguir.

En base a la abrumadora respuesta de la comunidad y la extensa discusión aquí, marcamos esta propuesta como rechazada antes de lo previsto .

En cuanto a los comentarios técnicos, esta discusión ha identificado de manera útil algunas consideraciones importantes que pasamos por alto, sobre todo las implicaciones para agregar impresiones de depuración y analizar la cobertura del código.

Más importante aún, hemos escuchado claramente a muchas personas que argumentaron que esta propuesta no estaba enfocada en un problema que valiera la pena. Todavía creemos que el manejo de errores en Go no es perfecto y se puede mejorar significativamente, pero está claro que nosotros, como comunidad, debemos hablar más sobre qué aspectos específicos del manejo de errores son problemas que debemos abordar.

En cuanto a discutir el problema a resolver, tratamos de exponer nuestra visión del problema en agosto pasado en la " Resumen del problema de manejo de errores de Go 2 ", pero en retrospectiva no llamamos suficiente atención a esa parte y no alentamos lo suficiente. discusión sobre si el problema específico era el correcto. La propuesta try puede ser una buena solución al problema descrito allí, pero para muchos de ustedes simplemente no es un problema que resolver. En el futuro, debemos hacer un mejor trabajo llamando la atención sobre estas primeras declaraciones de problemas y asegurándonos de que haya un acuerdo generalizado sobre el problema que debe resolverse.

(También es posible que la declaración del problema de manejo de errores se haya eclipsado por completo al publicar un borrador de diseño genérico el mismo día).

En el tema más amplio de qué mejorar en el manejo de errores de Go, nos encantaría ver informes de experiencia sobre qué aspectos del manejo de errores en Go son más problemáticos para usted en sus propias bases de código y entornos de trabajo y cuánto impacto tendría una buena solución. tener en su propio desarrollo. Si escribe un informe de este tipo, publique un enlace en la página Go2ErrorHandlingFeedback .

Gracias a todos los que participaron en esta discusión, aquí y en otros lugares. Como Russ Cox ha señalado anteriormente, las discusiones de toda la comunidad como esta son de código abierto en su máxima expresión . Realmente apreciamos la ayuda de todos para examinar esta propuesta específica y, de manera más general, para analizar las mejores formas de mejorar el estado del manejo de errores en Go.

Robert Griesemer, por el Comité de Revisión de Propuestas.

Gracias, Go Team, por el trabajo realizado en la propuesta de prueba. Y gracias a los comentaristas que tuvieron problemas y propusieron alternativas. A veces, estas cosas cobran vida propia. Gracias Go Team por escuchar y responder apropiadamente.

¡Hurra!

¡Gracias a todos por aclarar esto para que podamos tener el mejor resultado posible!

La llamada es para obtener una lista de problemas y experiencias negativas con el manejo de errores de Go. Sin embargo,
Los equipos y yo estamos muy contentos con xerrors.As, xerrors.Is y xerrors.Errorf en producción. Estas nuevas incorporaciones cambian por completo el manejo de errores de una manera maravillosa para nosotros ahora que hemos adoptado los cambios por completo. Por el momento, no hemos encontrado ningún problema o necesidad que no se aborde.

@griesemer Solo quería agradecerles (y probablemente a muchos otros que trabajaron con ustedes) por su paciencia y esfuerzos.

¡bien!

@griesemer Gracias a ti y a todos los demás en el equipo de Go por escuchar incansablemente todos los comentarios y soportar todas nuestras variadas opiniones.

Entonces, ¿tal vez ahora es un buen momento para cerrar este hilo y pasar a cosas futuras?

@griesemer @rsc y @all , genial, gracias a todos. para mí, es una gran discusión/identificación/clarificación. la mejora de alguna parte como el problema de 'error' en go, necesita una discusión más abierta (en la propuesta y los comentarios...) para identificar/clarificar los problemas principales primero.

ps, x/xerrors es bueno por ahora.

(podría tener sentido bloquear este hilo también...)

Gracias al equipo y a la comunidad por participar en esto. Me encanta cuántas personas se preocupan por Go.

Realmente espero que la comunidad vea primero el esfuerzo y la habilidad que se dedicó a la propuesta de prueba en primer lugar, y luego el espíritu de compromiso que siguió y que nos ayudó a tomar esta decisión. El futuro de Go es muy brillante si podemos seguir así, especialmente si todos podemos mantener actitudes positivas.

func M() (Datos, error){
a, err1 := A()
b, err2 := B()
devuelve b, cero
} => (si err1 != nil){ devuelve a, err1}.
(si err2 != nil){ devuelve b, err2}

De acuerdo... Me gustó esta propuesta, pero me encanta la forma en que la comunidad y el equipo de Go reaccionaron y se involucraron en una discusión constructiva, aunque a veces fue un poco difícil.

Sin embargo, tengo 2 preguntas con respecto a este resultado:
1/ ¿Sigue siendo el "manejo de errores" un área de investigación?
2/ ¿Se vuelven a priorizar las mejoras diferidas?

Esto demuestra una vez más que la comunidad Go está siendo escuchada y capaz de discutir propuestas controvertidas de cambio de idioma. Al igual que los cambios que se hacen en el idioma, los cambios que no son una mejora. ¡Gracias, equipo y comunidad de Go, por el arduo trabajo y el debate civilizado en torno a esta propuesta!

¡Excelente!

impresionante, bastante útil

Tal vez estoy demasiado apegado a Go, pero creo que aquí se mostró un punto, como
Russ describió: hay un punto en el que la comunidad no es solo un
pollo sin cabeza, es una fuerza a tener en cuenta y ser
aprovechado para su propio bien.

Con el debido agradecimiento a la coordinación brindada por el Go Team, podemos
todos estén orgullosos de que hayamos llegado a una conclusión, una con la que podamos vivir y
Volverá a visitar, sin duda, cuando las condiciones sean más maduras.

Esperemos que el dolor sentido aquí nos sirva bien en el futuro
(¿no sería triste, de lo contrario?).

Lucio.

No estoy de acuerdo con la decisión. Sin embargo, apoyo absolutamente el enfoque que ha emprendido el equipo de go. Tener una discusión en toda la comunidad y considerar los comentarios de los desarrolladores es lo que significaba que era el código abierto.

Me pregunto si también vendrán las optimizaciones diferidas. Me gusta anotar errores con él y xerrors juntos bastante y es demasiado costoso en este momento.

@pierrec Creo que necesitamos una comprensión más clara de qué cambios en el manejo de errores serían útiles. Algunos de los cambios en los valores de error estarán en la próxima versión 1.13 (https://tip.golang.org/doc/go1.13#errors), y adquiriremos experiencia con ellos. En el curso de esta discusión, hemos visto muchas, muchas, muchas propuestas de manejo de errores sintácticos, y sería útil si la gente pudiera votar y comentar sobre cualquiera que parezca particularmente útil. De manera más general, como dijo @griesemer , los informes de experiencia serían útiles.

También sería útil comprender mejor hasta qué punto la sintaxis de manejo de errores es problemática para las personas nuevas en el idioma, aunque eso será difícil de determinar.

Hay un trabajo activo para mejorar el rendimiento de defer en https://golang.org/cl/183677 y, a menos que se encuentre algún obstáculo importante, espero que llegue a la versión 1.14.

@griesemer @ianlancetaylor @rsc ¿Aún planea abordar la verbosidad en el manejo de errores, con otra propuesta que resuelva algunos o todos los problemas planteados aquí?

Entonces, tarde para la fiesta, ya que esto ya se ha rechazado, pero para futuras discusiones sobre el tema, ¿qué pasa con una sintaxis de retorno condicional similar a la ternaria? (No vi nada similar a esto en mi escaneo del tema o al revisar la vista que Russ Cox publicó en Twitter). Ejemplo:

f, err := Foo()
return err != nil ? nil, err

Devuelve nil, err si err no es nil, continúa la ejecución si err es nil. El formulario de declaración sería

return <boolean expression> ? <return values>

y esto sería azúcar sintáctico para:

if <boolean expression> {
    return <return values>
}

El principal beneficio es que es más flexible que una palabra clave check o una función integrada try , porque puede desencadenar más que errores (p. ej. return err != nil || f == nil ? nil, fmt.Errorf("failed to get Foo") , en más que simplemente el error no es nulo (por ejemplo, return err != nil && err != io.EOF ? nil, err ), etc., mientras que sigue siendo bastante intuitivo de entender cuando se lee (especialmente para aquellos acostumbrados a leer operadores ternarios en otros idiomas).

También asegura que el manejo de errores _todavía se lleva a cabo en la ubicación de la llamada_, en lugar de que ocurra automáticamente en función de alguna declaración diferida. Una de las mayores quejas que tuve con la propuesta original es que intenta, de alguna manera, hacer que el _manejo_ real de los errores sea un proceso implícito que simplemente ocurre automáticamente cuando el error no es nulo, sin una indicación clara de que el flujo de control regresará si la llamada a la función devuelve un error no nulo. El _punto_ completo de Go que usa retornos de error explícitos en lugar de un sistema similar a una excepción es alentar a los desarrolladores a verificar y manejar sus errores de manera explícita e intencional, en lugar de simplemente dejar que se propaguen en la pila para que, en teoría, se manejen en algún punto más alto. arriba. Al menos una declaración de retorno explícita, aunque condicional, anota claramente lo que está sucediendo.

@ngrilly Como dijo @griesemer , creo que debemos comprender mejor qué aspectos del manejo de errores encuentran más problemáticos los programadores de Go.

Hablando personalmente, no creo que valga la pena hacer una propuesta que elimine una pequeña cantidad de verbosidad. Después de todo, el lenguaje funciona bastante bien hoy. Todo cambio tiene un costo. Si vamos a hacer un cambio, necesitamos un beneficio significativo. Creo que esta propuesta brindó un beneficio significativo en la reducción de la verbosidad, pero claramente hay un segmento significativo de programadores de Go que sienten que los costos adicionales que impuso fueron demasiado altos. No sé si hay un término medio aquí. Y no sé si vale la pena abordar el problema.

@kaedys Este problema cerrado y extremadamente detallado definitivamente no es el lugar adecuado para discutir sintaxis alternativas específicas para el manejo de errores.

@ianlancetaylor

Creo que esta propuesta brindó un beneficio significativo en la reducción de la verbosidad, pero claramente hay un segmento significativo de programadores de Go que sienten que los costos adicionales que impuso fueron demasiado altos.

Me temo que hay un sesgo de autoselección. Go es conocido por su manejo detallado de errores y su falta de genéricos. Esto naturalmente atrae a los desarrolladores que no se preocupan por estos dos problemas. Mientras tanto, otros desarrolladores continúan usando sus lenguajes actuales (Java, C++, C#, Python, Ruby, etc.) y/o cambian a lenguajes más modernos (Rust, TypeScript, Kotlin, Swift, Elixir, etc.) debido a esto. . Conozco a muchos desarrolladores que evitan Go principalmente por este motivo.

También creo que hay un sesgo de confirmación en juego. Se han utilizado Gophers para defender el manejo detallado de errores y la falta de manejo de errores cuando la gente critica a Go. Esto hace que sea más difícil evaluar objetivamente una propuesta como try.

Steve Klabnik publicó un interesante comentario en Reddit hace unos días. Estaba en contra de introducir ? en Rust, porque eran "dos formas de escribir lo mismo" y era "demasiado implícito". Pero ahora, después de haber escrito más de unas pocas líneas de código, ? es una de sus funciones favoritas.

@ngrilly Estoy de acuerdo con tus comentarios. Esos sesgos son muy difíciles de evitar. Lo que sería muy útil es una comprensión más clara de cuántas personas evitan Go debido al manejo detallado de errores. Estoy seguro de que el número no es cero, pero es difícil de medir.

Dicho esto, también es cierto que try introdujo un nuevo cambio en el flujo de control que era difícil de ver, y que aunque try estaba destinado a ayudar con el manejo de errores, no ayudó con la anotación de errores. .

Gracias por la cita de Steve Klabnik. Si bien aprecio y estoy de acuerdo con el sentimiento, vale la pena considerar que, como lenguaje, Rust parece algo más dispuesto a confiar en detalles sintácticos que Go.

Como partidario de esta propuesta, naturalmente estoy decepcionado de que ahora se haya retirado, aunque creo que el equipo de Go ha hecho lo correcto dadas las circunstancias.

Una cosa que ahora parece bastante clara es que la mayoría de los usuarios de Go no consideran que la verbosidad del manejo de errores sea un problema y creo que eso es algo con lo que el resto de nosotros tendremos que vivir, incluso si desanima a los nuevos usuarios potenciales. .

He perdido la cuenta de la cantidad de propuestas alternativas que he leído y, aunque algunas son bastante buenas, no he visto ninguna que pensara que valía la pena adoptar si try muerde el polvo. Así que la posibilidad de que surja una propuesta de término medio ahora me parece remota.

En una nota más positiva, la discusión actual ha señalado formas en las que todos los errores potenciales en una función se pueden decorar de la misma manera y en el mismo lugar (usando defer o incluso goto ) que no había considerado anteriormente y espero que el equipo de Go al menos considere cambiar go fmt para permitir que las declaraciones únicas if se escriban en una línea que al menos hará el manejo de errores _look_ más compacto, incluso si en realidad no elimina ningún texto estándar.

@pierrec

1/ ¿Sigue siendo el "manejo de errores" un área de investigación?

Lo ha sido, por más de 50 años! No parece haber una teoría general o incluso una guía práctica para el manejo de errores consistente y sistemático. En la tierra de Go (como en otros idiomas) existe incluso confusión sobre lo que es un error. Por ejemplo, un EOF puede ser una condición excepcional cuando intenta leer un archivo, pero ¿por qué es un error? Si eso es un error real o no, realmente depende del contexto. Y hay otros problemas similares.

Quizás se necesita una discusión de mayor nivel (aunque no aquí).

Gracias @griesemer @rsc y todos los demás involucrados en proponer. Muchos otros lo han dicho anteriormente, y vale la pena repetir que se agradecen sus esfuerzos por analizar el problema, redactar la propuesta y discutirla de buena fe. Gracias.

@ianlancetaylor

Gracias por la cita de Steve Klabnik. Si bien aprecio y estoy de acuerdo con el sentimiento, vale la pena considerar que, como lenguaje, Rust parece algo más dispuesto a confiar en detalles sintácticos que Go.

En general, estoy de acuerdo con que Rust confía más que Go en los detalles sintácticos, pero no creo que esto se aplique a esta discusión específica sobre la verbosidad del manejo de errores.

Los errores son valores en Rust como lo son en Go. Puede manejarlos usando un flujo de control estándar, como en Go. En las primeras versiones de Rust, era la única forma de manejar errores, como en Go. Luego introdujeron la macro try! , que es sorprendentemente similar a la propuesta de función integrada try . Eventualmente agregaron el operador ? , que es una variación sintáctica y una generalización de la macro try! , pero esto no es necesario para demostrar la utilidad de try , y el hecho que la comunidad de Rust no se arrepienta de haberlo agregado.

Soy muy consciente de las enormes diferencias entre Go y Rust, pero sobre el tema de la verbosidad en el manejo de errores, creo que su experiencia es transponible a Go. Vale la pena leer los RFC y las discusiones relacionadas con try! y ? . Me ha sorprendido mucho lo similares que son los problemas y los argumentos a favor y en contra de los cambios de lenguaje.

@griesemer , anunció la decisión de rechazar la propuesta try , en su forma actual, pero no dijo qué planea hacer el equipo de Go a continuación.

¿Todavía planea abordar la verbosidad del manejo de errores, con otra propuesta que resolvería los problemas planteados en esta discusión (depuración de impresiones, cobertura de código, mejor decoración de errores, etc.)?

En general, estoy de acuerdo con que Rust confía más que Go en los detalles sintácticos, pero no creo que esto se aplique a esta discusión específica sobre la verbosidad del manejo de errores.

Dado que otros todavía están agregando sus dos centavos, supongo que todavía hay espacio para que yo haga lo mismo.

Aunque he estado programando desde 1987, solo he estado trabajando con Go durante aproximadamente un año. Hace unos 18 meses, cuando estaba buscando un nuevo lenguaje para satisfacer ciertas necesidades, miré tanto a Go como a Rust. Me decidí por Go porque sentí que el código Go era mucho más fácil de aprender y usar, y que el código Go era mucho más legible porque Go parece preferir palabras para transmitir significado en lugar de símbolos concisos.

Por lo tanto, por mi parte, no estaría feliz de ver que Go se vuelve más como Rust , incluido el uso de signos de exclamación ( ! ) y signos de interrogación ( ? ) para implicar significado.

De manera similar, creo que la introducción de macros cambiaría la naturaleza de Go y daría como resultado miles de dialectos de Go, como es el caso de Ruby. Así que espero que nunca se agreguen macros Go, aunque supongo que hay pocas posibilidades de que eso suceda, afortunadamente en mi opinión.

jmtcw

@ianlancetaylor

Lo que sería muy útil es una comprensión más clara de cuántas personas evitan Go debido al manejo detallado de errores. Estoy seguro de que el número no es cero, pero es difícil de medir.

No es una "medida" per se, pero esta discusión de Hacker News proporciona decenas de comentarios de desarrolladores descontentos con el manejo de errores de Go debido a su verbosidad (y algunos comentarios explican su razonamiento y dan ejemplos de código): https://news.ycombinator. com/item?id=20454966.

En primer lugar, gracias a todos por los comentarios de apoyo sobre la decisión final, incluso si esa decisión no fue satisfactoria para muchos. Este fue realmente un esfuerzo de equipo, y estoy muy feliz de que todos hayamos logrado superar las intensas discusiones de una manera civil y respetuosa en general.

@ngrilly Hablando solo por mí, sigo pensando que sería bueno abordar la verbosidad del manejo de errores en algún momento. Dicho esto, acabamos de dedicar bastante tiempo y energía a esto durante el último medio año y especialmente los últimos 3 meses, y estábamos muy contentos con la propuesta, pero obviamente subestimamos la posible reacción hacia ella. Ahora tiene mucho sentido dar un paso atrás, digerir y destilar los comentarios, y luego decidir cuáles son los mejores pasos a seguir.

Además, de manera realista, dado que no tenemos recursos ilimitados, veo que pensar en el soporte de idiomas para el manejo de errores se queda en un segundo plano por un tiempo a favor de más progreso en otros frentes, sobre todo el trabajo en genéricos, al menos para el próximos meses. if err != nil puede ser molesto, pero no es motivo para una acción urgente.

Si desea continuar la discusión, me gustaría sugerir amablemente a todos que se vayan de aquí y continúen la discusión en otro lugar, en un tema separado (si hay una propuesta clara), o en otros foros más adecuados para una discusión abierta. Este tema está cerrado, después de todo. Gracias.

Me temo que hay un sesgo de autoselección.

Me gustaría acuñar un nuevo término aquí y ahora: "sesgo del creador". Si alguien está dispuesto a poner el trabajo, se le debe dar el beneficio de la duda.

Es muy fácil para la galería de cacahuetes gritar alto y ancho en foros no relacionados que no les gusta una solución propuesta para un problema. También es muy fácil para todos escribir un intento incompleto de 3 párrafos para una solución diferente (sin trabajo real presentado al margen). Si uno está de acuerdo con el statu quo, está bien. Punto justo. Presentar cualquier otra cosa como solución sin una propuesta completa le da -10k puntos.

No apoyo ni estoy en contra de intentarlo, pero confío en el juicio de Go Teams al respecto, hasta ahora su juicio ha brindado un excelente lenguaje, así que creo que lo que decidan funcionará para mí, intente o no intente, considero necesitamos entender como forasteros, que los mantenedores tienen una visibilidad más amplia sobre el asunto. sintaxis que podemos discutir todo el día. Me gustaría agradecer a todos los que han trabajado o están tratando de mejorar go en este momento por sus esfuerzos, estamos agradecidos y esperamos nuevas mejoras (sin retroceder) en las bibliotecas de idiomas y el tiempo de ejecución, si se considera alguno. útil por ustedes.

También es muy fácil para todos escribir un intento incompleto de 3 párrafos para una solución diferente (sin trabajo real presentado al margen).

Lo único que yo (y varios otros) queríamos que try fuera útil era un argumento opcional que permitiera devolver una versión envuelta del error en lugar del error sin cambios. No creo que eso haya necesitado una gran cantidad de trabajo de diseño.

Oh no.

Veo. Ir quiere hacer algo diferente a otros idiomas.

¿Quizás alguien debería bloquear este problema? La discusión es probablemente más adecuada en otro lugar.

Este problema ya es tan largo que bloquearlo parece inútil.

Todos, tengan en cuenta que este problema está cerrado y que los comentarios que hagan aquí serán ignorados para siempre. Si te parece bien, comenta.

En caso de que alguien odie la palabra probar que les permite pensar en el lenguaje Java, C*, aconsejo no usar 'intentar' sino otras palabras como 'ayuda' o 'debe' o 'verificarError'... (ignorarme)

En caso de que alguien odie la palabra probar que les permite pensar en el lenguaje Java, C*, aconsejo no usar 'intentar' sino otras palabras como 'ayuda' o 'debe' o 'verificarError'... (ignorarme)

Siempre habrá palabras clave superpuestas y conceptos que tienen pequeñas o grandes diferencias semánticas en idiomas que están razonablemente cerca uno del otro (como los idiomas de la familia C). Una característica del idioma no debe causar confusión dentro del propio idioma, siempre habrá diferencias entre los idiomas.

malo. esto es anti patron, irrespeto autor de esa propuesta

@alersenkevich Por favor, sea cortés. Consulte https://golang.org/conduct.

Creo que me alegro de la decisión de no ir más allá con esto. Para mí, esto se sintió como un truco rápido para resolver un pequeño problema con respecto a si err != nil está en varias líneas. No queremos inflar Go con palabras clave menores para resolver cosas menores como esta, ¿verdad? Es por eso que la propuesta con macros higiénicas https://github.com/golang/go/issues/32620 se siente mejor. Intenta ser una solución más genérica para abrir más flexibilidad con más cosas. La discusión sobre la sintaxis y el uso continúa allí, así que no piense solo si se trata de macros C/C++. El punto allí es discutir una mejor manera de hacer macros. Con él, podrías implementar tu propio intento.

Me encantaría recibir comentarios sobre una propuesta similar que aborde un problema con el manejo de errores actual https://github.com/golang/go/issues/33161.

Honestamente, esto debería reabrirse, de todas las propuestas de manejo de errores, esta es la más sensata.

@OneOfOne respetuosamente, no estoy de acuerdo con que esto deba reabrirse. Este hilo ha establecido que existen limitaciones reales con la sintaxis. Quizás tengas razón en que esta es la propuesta más "sana": pero yo creo que el statu quo es aún más cuerdo.

Estoy de acuerdo en que if err != nil se escribe con demasiada frecuencia en Go, pero tener una forma singular de regresar de una función mejora enormemente la legibilidad. Si bien generalmente puedo respaldar propuestas que reducen el código repetitivo, el costo nunca debería ser la legibilidad en mi humilde opinión.

Sé que muchos desarrolladores lamentan el error "manual" al verificar en go, pero, sinceramente, la brevedad a menudo está reñida con la legibilidad. Go tiene muchos patrones establecidos aquí y en otros lugares que fomentan una forma particular de hacer las cosas y, en mi experiencia, el resultado es un código confiable que envejece bien. Esto es fundamental: el código del mundo real debe leerse y comprenderse muchas veces a lo largo de su vida útil, pero solo se escribe una vez. La sobrecarga cognitiva es un costo real, incluso para desarrolladores experimentados.

En lugar de:

f := try(os.Open(filename))

Yo esperaría:

f := try os.Open(filename)

Todos, tengan en cuenta que este problema está cerrado y que los comentarios que hagan aquí serán ignorados para siempre. Si te parece bien, comenta.
—@ianlancetaylor

Sería bueno si pudiéramos usar try para un bloque de códigos junto con la forma actual de manejar errores.
Algo como esto:

// Generic Error Handler
handler := func(err error) error {
    return fmt.Errorf("We encounter an error: %v", err)  
}
a := "not Integer"
b := "not Integer"

try(handler){
    f := os.Open(filename)
    x := strconv.Atoi(a)
    y, err := strconv.Atoi(b) // <------ If you want a specific error handler
    if err != nil {
        panic("We cannot covert b to int")   
    }
}

El código anterior parece más limpio que el comentario inicial. Desearía poder proponer esto.

Hice una nueva propuesta #35179

val := prueba f() (err){
pánico (error)
}

Eso espero:

i, err := strconv.Atoi("1")
if err {
    println("ERROR")
} else {
    println(i)
}

o

i, err := strconv.Atoi("1")
if err {
    io.EOF:
        println("EOF")
    io.ErrShortWrite:
        println("ErrShortWrite")
} else {
    println(i)
}

@myroid No me importaría tener su segundo ejemplo un poco más genérico en forma de declaración switch-else :

```ir
yo, err := strconv.Atoi("1")
cambiar error != nil; error {
caso io.EOF:
println("EOF")
caso io.ErrShortWrite:
println("ErrEscrituraCorta")
} demás {
imprimir (yo)
}

@piotrkowalczuk Tu código se ve mucho mejor que el mío. Creo que el código puede ser más conciso.

i, err := strconv.Atoi("1")
switch err {
case io.EOF:
    println("EOF")
case io.ErrShortWrite:
    println("ErrShortWrite")
} else {
    println(i)
}

Esto no considera la opción habrá un ojo de diferente tipo

Tiene que haber
Error de caso! = cero

Para errores, el desarrollador no capturó explícitamente

El viernes 8 de noviembre de 2019 a las 12:06, Yang Fan, [email protected] escribió:

@piotrkowalczuk https://github.com/piotrkowalczuk Tu código se ve mucho
mejor que el mío. Creo que el código puede ser más conciso.

i, err := strconv.Atoi("1")switch err {caso io.EOF:
println("EOF")caso io.ErrShortWrite:
println("ErrEscrituraCorta")
} demás {
imprimir (yo)
}


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=ABNEY4VH4KS2OX4M5BVH673QSU24DA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEDPTTYY#issuecomment-259150
o darse de baja
https://github.com/notifications/unsubscribe-auth/ABNEY4W4XIIHGUGIW2KXRPTQSU24DANCNFSM4HTGCZ7Q
.

switch no necesita else , tiene default .

Abrí https://github.com/golang/go/issues/39890 que propone algo similar al guard de Swift que debería abordar algunas de las preocupaciones con esta propuesta:

  • flujo de control
  • localidad de manejo de errores
  • legibilidad

No ha ganado mucha tracción, pero podría ser de interés para quienes comentaron aquí.

¿Fue útil esta página
0 / 5 - 0 calificaciones