Go: propuesta: especificación: agregar tipo de resultado integrado (como Rust, OCaml)

Creado en 15 abr. 2017  ·  79Comentarios  ·  Fuente: golang/go

Esta es una propuesta para agregar un tipo de resultado para completar. Los tipos de resultados suelen contener un valor devuelto o un error, y podrían proporcionar una encapsulación de primera clase del patrón común (value, err) ubicuo en todos los programas Go.

Mis disculpas si se ha enviado algo como esto antes, pero espero que sea una descripción bastante completa de la idea.

Fondo

Se pueden encontrar algunos antecedentes sobre esta idea en la publicación Manejo de errores en Go , aunque donde esa publicación sugiere los genéricos de apalancamiento de implementación, propondré que no es necesario y que, de hecho, los tipos de resultados podrían (con cierto cuidado) ser adaptado para Go sin agregar genéricos y sin realizar cambios importantes en el lenguaje en sí.

Dicho esto, estoy autoaplicando la etiqueta "Go 2" no porque sea un cambio rotundo, sino porque espero que sea controvertido y, hasta cierto punto, vaya en contra del lenguaje.

El tipo de resultado de óxido proporciona algunos precedentes. Se puede encontrar una idea similar en muchos lenguajes funcionales, incluidos Either de Haskell, el resultado de OCaml y Either de Scala. Rust maneja los errores de manera bastante similar a Go: los errores son solo valores, su propagación se maneja en cada sitio de llamada en lugar de la acción espeluznante a distancia de las excepciones que usan saltos no locales, y es posible que se necesite algo de trabajo para convierta tipos de errores o envuelva errores en cadenas de errores .

Donde Rust usa tipos de suma (vea la propuesta de tipos de suma de Go 2 ) y genéricos para implementar tipos de resultado, como una característica del lenguaje central de caso especial, creo que un tipo de resultado de Go tampoco lo necesita, y simplemente puede aprovechar la magia del compilador de casos especiales. Esto implicaría una sintaxis especial y nodos AST especiales muy parecidos a los que usan actualmente los tipos de colección de Go.

Objetivos

Creo que la adición de un tipo de resultado para completar podría tener los siguientes resultados positivos:

  1. Reducir el texto repetitivo de manejo de errores: esta es una queja extremadamente común sobre Go. El "patrón" if err != nil { return nil, err } (o variaciones menores del mismo) se puede ver en todas partes en los programas Go. Este texto estándar no aporta ningún valor y solo sirve para alargar mucho más los programas.
  2. Permite al compilador razonar sobre los resultados: en Rust, los resultados no consumidos emiten una advertencia. Aunque existen herramientas de linting para que Go logre lo mismo, creo que sería mucho más valioso que esto fuera una característica de primera clase del compilador. También es razonablemente sencillo de implementar y no debería afectar negativamente al rendimiento del compilador.
  3. Error al manejar combinadores (esta es la parte que creo que va en contra del lenguaje): si hubiera un tipo para los resultados, podría admitir varios métodos para manejar, transformar y consumir resultados. Admito que este enfoque viene con una pequeña curva de aprendizaje y, como tal, puede afectar negativamente la claridad de los programas para las personas que no están familiarizadas con los modismos combinatorios. Aunque personalmente me encantan los combinadores para el manejo de errores, definitivamente puedo ver cuán culturalmente pueden ser un mal ajuste para Go.

Ejemplos de sintaxis

Primero una nota rápida: por favor, no permita que la idea se enrede demasiado en la sintaxis. La sintaxis es una cosa muy fácil de usar en bicicleta, y no creo que ninguno de estos ejemplos sirva como la única sintaxis verdadera, por lo que estoy dando varias alternativas.

En su lugar, prefiero que la gente preste atención a la "forma" general del problema y solo mire estos ejemplos para comprender mejor la idea.

Firma del tipo de resultado

Lo más simple que funciona: simplemente agregue "resultado" delante de la tupla de valor de retorno:

func f1(arg int) result(int, error) {

Más típico es una sintaxis "genérica", pero esto probablemente debería reservarse para si / cuando Go realmente agrega genéricos (una función de tipo de resultado podría adaptarse para aprovecharlos si eso alguna vez sucediera):

func f1(arg int) result<int, error> {

Al devolver resultados, necesitaremos una sintaxis para ajustar valores o errores en un tipo de resultado. Esto podría ser solo una invocación de método:

return result.Ok(value)

ir
devuelve resultado.Err (error)

If we allow "result" to be shadowed here, it should avoid breaking any code that already uses "result".

Perhaps "Go 2" could add syntax sugar similar to Rust (although it would be a breaking change, I think?):

```go
return Ok(value)

ir
return Err (valor)

### Propagating errors

Rust recently added a `?` operator for propagating errors (see [Rust RFC 243](https://github.com/rust-lang/rfcs/blob/master/text/0243-trait-based-exception-handling.md)). A similar syntax could enable replacing `if err != nil { return _, err }` boilerplate with a shorthand syntax that bubbles the error up the stack.

Here are some prospective examples. I have only done some cursory checking for syntactic ambiguity. Apologies if these are either ambiguous or breaking changes: I assume with a little work you can find a syntax for this which isn't at breaking change.

First, an example with present-day Go syntax:

```go
count, err = fd.Write(bytes)
if err != nil {
    return nil, err
}

Ahora, con una nueva sintaxis que consume un resultado y genera el error en la pila. Tenga en cuenta que estos ejemplos son solo para fines ilustrativos:

count := fd.Write!(bytes)

ir
recuento: = fd.Write (bytes)!

```go
count := fd.Write?(bytes)

ir
recuento: = fd.Write (bytes)?

```go
count := try(fd.Write(bytes))

NOTA: Rust anteriormente era compatible con este último, pero generalmente se ha alejado de él ya que no se puede encadenar.

En todos mis ejemplos posteriores, usaré esta sintaxis, pero tenga en cuenta que es solo un ejemplo, puede ser ambiguo o tener otros problemas, y ciertamente no estoy casado con él:

count := fd.Write(bytes)!

Compatibilidad al revés

Todas las propuestas de sintaxis utilizan una palabra clave result para identificar el tipo. Creo (pero ciertamente no estoy seguro) que se podrían desarrollar reglas de sombreado que permitirían que el código existente usara "resultado", por ejemplo, un nombre de variable para continuar funcionando como está sin problemas.

Idealmente, debería ser posible "actualizar" el código existente para usar tipos de resultados de una manera completamente transparente. Para hacer esto, podemos permitir que los resultados se consuman como una tupla de 2, es decir, dado:

func f1(arg int) result(int, error) {

Debería ser posible consumirlo como:

result := f1(42)

o:

(value, err) := f1(42)

Es decir, si el compilador ve una asignación de result(T, E) a (T, E) , debería forzar automáticamente. Esto debería permitir que las funciones cambien sin problemas al uso de tipos de resultados.

Combinadores

Comúnmente, el manejo de errores será mucho más complicado que if err != nil { return _, err } . Esta propuesta estaría lamentablemente incompleta si ese fuera el único caso en el que ayuda.

Los tipos de resultados son conocidos por ser una especie de cuchillo suizo de manejo de errores en lenguajes funcionales debido a los "combinadores" que admiten. En realidad, estos combinadores son solo un conjunto de métodos que nos permiten transformarnos y comportarnos de forma selectiva en función de un tipo de resultado, normalmente en "combinación" con un cierre.

Then() : encadenar llamadas de función que devuelven el mismo tipo de resultado

Digamos que tenemos un código que se parece a esto:

resp, err := doThing(a)
if err != nil {
    return nil, err
}
resp, err = doAnotherThing(b, resp.foo())
if err != nil {
    return nil, err
}
resp, err = FinishUp(c, resp.bar())
if err != nil {
    return nil, err
}

Con un tipo de resultado, podemos crear una función que tome un cierre como parámetro y solo llame al cierre si el resultado fue exitoso, de lo contrario cortocircuitando y devolviéndolo representa un error. Llamaremos a esta función Then (se describe de esta manera en la publicación del blog Manejo de errores en Go ), y se conoce como and_then en Rust). Con una función como esta, podemos reescribir el ejemplo anterior como algo como:

result := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })

if result.isError() {
    return result.Error()
}

o usando una de las sintaxis propuestas de arriba (elegiré ! como el operador mágico):

final_value := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })!

Esto reduce las 12 líneas de código en nuestro ejemplo original a tres, y nos deja con el valor final que realmente buscamos y el tipo de resultado desapareció de la imagen. En este caso, nunca tuvimos que darle un nombre al tipo de resultado.

Ahora bien, la sintaxis de cierre en ese caso se siente un poco difícil de manejar / similar a JavaScript. Probablemente podría beneficiarse de una sintaxis de cierre más ligera. Personalmente, me encantaría algo como esto:

final_value := doThing(a).
    Then(|resp| doAnotherThing(b, resp.foo())).
    Then(|resp| FinishUp(c, resp.bar()))!

... pero algo así probablemente merezca una propuesta por separado.

Map() y MapErr() : convertir entre valores de éxito y error

A menudo, al hacer el baile if err != nil { return nil, err } , querrá manejar el error o transformarlo en un tipo diferente. Algo como esto:

resp, err := doThing(a)
if err != nil {
    return nil, myerror.Wrap(err)
}

En este caso, podemos lograr lo mismo usando MapErr() (volveré a usar la sintaxis ! para devolver el error):

resp := doThing(a).
    MapErr(func(err) { myerror.Wrap(err) })!

Map hace lo mismo, solo transforma el valor de éxito en lugar del error.

¡Y más!

Hay muchos más combinadores de los que he mostrado aquí, pero creo que estos son los más interesantes. Para tener una mejor idea de cómo se ve un tipo de resultado con todas las funciones, le sugiero que consulte el de Rust:

https://doc.rust-lang.org/std/result/enum.Result.html

Go2 LanguageChange NeedsInvestigation Proposal

Comentario más útil

final_value := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })!

Creo que esta no sería la dirección correcta para Go. ()) })! , ¿en serio? El objetivo principal de Go debe ser la facilidad de aprendizaje, la legibilidad y la facilidad de uso. Esto no ayuda.

Todos 79 comentarios

Las propuestas de cambio de idioma no se están considerando actualmente durante el proceso de revisión de propuestas, ya que el idioma de Go 1.x está congelado (y este es Go2, como ha señalado). Solo le hago saber que no espere una decisión al respecto en el corto plazo.

final_value := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })!

Creo que esta no sería la dirección correcta para Go. ()) })! , ¿en serio? El objetivo principal de Go debe ser la facilidad de aprendizaje, la legibilidad y la facilidad de uso. Esto no ayuda.

Como alguien dijo en el hilo de reddit : definitivamente preferiría tipos de suma adecuados y genéricos en lugar de nuevos incorporados especiales.

Quizás no estaba claro en la publicación: ciertamente preferiría que un tipo de resultado se componga a partir de tipos de suma y genéricos.

Estaba intentando especificar esto de tal manera que la adición de ambos (que personalmente considero extremadamente improbable) no sería un impedimento para agregar esta función, y podría agregarse de tal manera que, cuando esté disponible, esta característica podría cambiar a ellos (incluso di un ejemplo de cómo se vería con una sintaxis genérica tradicional, y también vinculado al problema del tipo de suma Go).

No entiendo la conexión entre el tipo de resultado y los objetivos. Sus ideas sobre la propagación de errores y los combinadores parecen funcionar igual de bien con el soporte actual para múltiples parámetros de resultados.

@ianlancetaylor ¿ puede dar un ejemplo de cómo definir un combinador que funcione genéricamente en las tuplas de resultados actuales? Si es posible, tendría curiosidad por verlo, pero no creo que lo sea ( según esta publicación )

@tarcieri Esa publicación es significativamente diferente, ya que error no aparece en su uso sugerido de Result<A> . Este problema, a diferencia de la publicación, parece sugerir result<int, error> , lo que para mí implica que los combinadores propuestos reconocen especialmente error . Mis disculpas si no entiendo bien.

La intención no es acoplar result a error , sino que result lleve dos valores, similar al tipo Result en Rust o Either en Haskell. En ambos idiomas, por convención, el segundo valor suele ser un tipo error (aunque no tiene por qué serlo).

Este problema, a diferencia de la publicación, parece sugerir un resultado

La publicación sugiere:

type Result<A> struct {
    // fields
}

func (r Result<A>) Value() A {…}
func (r Result<A>) Error() error {…}

... entonces, por el contrario, esa publicación se especializa alrededor de error , mientras que esta propuesta acepta un tipo especificado por el usuario para el segundo valor.

Es cierto que cosas como result.Err() y result.MapErr() dan un guiño a que este valor siempre es un error .

@tarcieri ¿Qué pasa con una estructura? https://play.golang.org/p/mTqtaMbgIF

@griesemer, como se trata en la publicación Manejo de errores en Go , esa estructura no es genérica. Tendría que definir uno para cada combinación única de tipos de éxito y error que alguna vez quisiera usar.

@tarcieri Entendido. Pero si ese (no ser genérico, o tal vez no tener un tipo de suma) es el problema aquí, entonces deberíamos abordar esos problemas en su lugar. Manejar tipos de resultados solo es agregar más casos especiales.

Si Go tiene genéricos o no es ortogonal a si un tipo de resultado de primera clase es útil. Haría la implementación más cercana a algo que usted mismo implementa, pero como se explica en la propuesta, permitir que el compilador razone al respecto de una manera de primera clase le permite, por ejemplo, advertir sobre resultados no consumidos. Tener un solo tipo de resultado es también lo que hace que los combinadores de la propuesta sean componibles.

@tarcieri La composición como sugirió también sería posible con un tipo de estructura de resultado único.

No entiendo por qué no usarías un tipo de estructura incrustado o definido. ¿Por qué tener métodos y sintaxis especializados para verificar errores? Go ya tiene los medios para hacer todo esto. Parece que esto es solo agregar características que no definen el lenguaje Go, definen Rust. Sería un error implementar tales cambios.

No entiendo por qué no usarías un tipo de estructura incrustado o definido. ¿Por qué tener métodos y sintaxis especializados para verificar errores?

Para repetirme de nuevo: porque tener un tipo de resultado genérico requiere ... genéricos. Go no tiene genéricos. A menos que Go obtenga genéricos, necesita soporte para casos especiales del idioma.

¿Quizás estás sugiriendo algo como esto?

type Result struct {
    value interface{}
    err error
}

Sí, esto "funciona" ... a costa de la seguridad del tipo. Ahora, para consumir cualquier resultado, tenemos que hacer una afirmación de tipo para asegurarnos de que el interface{} es el que esperamos. Si no es así, ahora se ha convertido en un error de tiempo de ejecución (a diferencia de un error de tiempo de compilación como es actualmente).

Eso sería una regresión importante con respecto a lo que tiene Go ahora.

Para que esta función sea realmente útil, debe ser segura para los tipos. El sistema de tipos de Go no es lo suficientemente expresivo como para implementarlo de forma segura sin el soporte de un lenguaje de casos especiales. Necesitaría genéricos como mínimo e idealmente sumar tipos también.

Parece que esto es solo agregar características que no definen el lenguaje Go [...]. Sería un error implementar tales cambios.

Cubrí tanto en la propuesta original:

"Admito que este enfoque viene con un poco de curva de aprendizaje y, como tal, puede afectar negativamente la claridad de los programas para las personas que no están familiarizadas con los modismos de los combinadores. Aunque personalmente me encantan los combinadores para el manejo de errores, definitivamente puedo ver cuán culturalmente pueden ser una mala opción para Go ".

Siento que he confirmado mis sospechas y que una característica como esta no es fácil de entender por los desarrolladores de Go y va en contra de la naturaleza orientada a la simplicidad del lenguaje. Aprovecha los paradigmas de programación que, claramente, los desarrolladores de Go no parecen entender o no quieren, y en tal caso parece un error.

ellos definen óxido

Los tipos de resultados no son una característica específica de Rust. Se encuentran en muchos lenguajes funcionales (por ejemplo, de Haskell Either y de OCaml result ). Dicho esto, introducirlos en Go se siente como un puente demasiado lejos.

Gracias por compartir sus ideas, pero creo que los ejemplos utilizados anteriormente no son convincentes. Para mí, A es mejor que B:

A
`` `resp, err: = hacer (a)
if err! = nil {
retorno nulo, err
}
si resp, err = doAnotherThing (b, resp.foo ()); err! = nil {
volver err
}
si resp, err = FinishUp (c, resp.bar ()); err! = nil {
volver err
}


resultado: = hacer (a).
Entonces (func (resp) {doAnotherThing (b, resp.foo ())}).
Luego (func (resp) {FinishUp (c, resp.bar ())})

if result.isError () {
devuelve resultado.Error ()
}
''

  • A es más legible, en voz alta y mentalmente.
  • A no requiere formato / ajuste de línea
  • En A, las condiciones de error terminan la ejecución, explícitamente; sin necesidad de negación mental. B no es similar.
  • En B, la palabra clave "Entonces" no indica causalidad condicional. La palabra clave "si" sí, y ya está en el idioma.
  • En B, no quiero ralentizar mi rama de ejecución más probable empaquetándola en una lambda

No creo que A sea más legible. De hecho, las acciones no se notan en absoluto. En cambio, el primer vistazo revela que se están obteniendo y devolviendo un montón de errores.

Si B tuviera que formatearse de modo que los cuerpos de cierre estuvieran en nuevas líneas, ese habría sido el formato más legible.

Además, el último punto parece un poco tonto. Si el rendimiento de la llamada a la función es tan importante, entonces opte por una sintaxis más tradicional.

A De @ como creo que el flujo normal no debería tener sangría.

if err != nil {
    return err
}

resp, err = doAnotherThing(b, resp.foo());
if  err != nil {
    return err
}

resp, err = FinishUp(c, resp.bar());
if  err != nil {
    return err
}

Una observación interesante de este hilo: el ejemplo original que di, que la gente sigue copiando y pegando contenía algunos errores (el primer if devolvió nil, err por error, los dos siguientes solo devuelven err ). Estos errores no fueron deliberados por mi parte, pero creo que es un caso de estudio interesante.

Aunque esta clase de error en particular es del tipo que habría detectado el compilador de Go, creo que es interesante notar que con tanta repetición sintáctica, resulta muy fácil pasar por alto esos errores al copiar y pegar.

Esto no mejora la propuesta. Es una suposición de que no devolver varios valores es el resultado de un manejo de errores explícito. También podría haber cometido los mismos errores dentro de las funciones, simplemente no los habría visto debido a su encapsulación innecesaria.

No estoy de acuerdo, creo que ese es un punto fuerte de este tipo de propuesta. Si todo lo que hace un programa es devolver el error y no procesarlo, entonces está desperdiciando la sobrecarga cognitiva y el código y haciendo las cosas menos legibles. Agregar una característica como esta significaría que (en los proyectos que eligen usarla) el código que se ocupa de los errores en realidad está haciendo algo que vale la pena comprender.

Tendremos que aceptar estar en desacuerdo. Las fichas mágicas de la propuesta son fáciles de escribir, pero difíciles de entender. El hecho de que lo hayamos acortado no significa que lo hayamos simplificado.

Hacer las cosas menos legibles es subjetivo, así que aquí está mi opinión. Todo lo que veo en esta propuesta es un código más complejo y oscuro con funciones y símbolos mágicos (que son muy fáciles de pasar por alto). Y todo lo que hacen es esconder un código muy simple y fácil de entender en el caso A. Para mí, no agregan ningún valor, no acortan el código donde importa ni simplifican las cosas. No veo ningún valor en tratarlos a nivel de idioma.

El único problema que resuelve la propuesta, que pude ver con claridad, es el estándar en el manejo de errores. Si esa es la única razón, entonces no vale la pena para mí. El argumento sobre la repetición sintáctica en realidad va en contra de la propuesta. Es mucho más complejo en ese sentido: todos esos símbolos mágicos y corchetes que son tan fáciles de pasar por alto. El ejemplo A tiene un modelo estándar pero no causa errores lógicos. En ese contexto, no hay nada que ganar con esa propuesta, nuevamente, por lo que no es muy útil.

Dejemos las características de Rust a Rust.

Para aclarar, no me entusiasma agregar el sufijo ! como atajo, pero sí me gusta la idea de crear una sintaxis simple que simplifique

err = foo()
if err != nil {
  return err
}

Incluso si esa sintaxis es una palabra clave en lugar de un símbolo especial. Es mi mayor queja sobre el lenguaje (incluso más grande que Generics personalmente), y creo que la basura de ese patrón en el código hace que sea más difícil de leer y ruidoso.

También me encantaría ver algo que habilite el tipo de encadenamiento que trae @tarcieri , ya que lo encuentro más legible en código. Creo que la complejidad a la que alude @creker se equilibra con la mejor relación señal-ruido en el código.

No entiendo completamente cómo esta propuesta lograría sus objetivos declarados.

  1. Reducir el texto estándar de manejo de errores: la propuesta tiene un código Go hipotético:

    result := doThing(a).
    Then(func(resp) { doAnotherThing(b, resp.foo()) }).
    Then(func(resp) { FinishUp(c, resp.bar()) })
    
    if result.isError() {
    return result.Error()
    }
    

    No estoy realmente seguro de cómo se supone que funciona func(resp) { expr } sin cambios más extensos en la forma en que funcionan los literales de función. Creo que el código resultante terminaría pareciéndose más a esto:

    result := doThing(a).
    Then(func(resp T) result(T, error) { return doAnotherThing(b, resp.foo()) }).
    Then(func(resp T) result(T, error) { return FinishUp(c, resp.bar()) })
    
    if result.isError() {
    return result.Error()
    }
    

    En el código Go realista, también es bastante común que las expresiones intermedias sean más largas que esto y deban colocarse en sus propias líneas. Esto sucede de forma natural en el código Go real hoy; bajo esta propuesta, sería:

    result := doThing(a).
    Then(func(resp T) result(T, error) {
        return doAnotherThing(b, resp.foo())
    }).
    Then(func(resp T) result(T, error) {
        return FinishUp(c, resp.bar())
    })
    
    if result.isError() {
    return result.Error()
    }
    

    De cualquier manera, esto me parece bien , pero no genial, al igual que el código Go real que se encuentra arriba en la propuesta. Su combinador "Entonces" es esencialmente lo opuesto a "retorno". (Si está familiarizado con las mónadas, esto no le sorprenderá). Elimina el requisito de escribir una declaración "si", pero introduce el requisito de escribir una función. En general, no es sustancialmente mejor ni peor; es la misma lógica estándar con una nueva ortografía.

  2. Permite al compilador razonar sobre los resultados: si esta característica es deseable (y no estoy expresando ninguna opinión sobre eso aquí), no veo cómo esta propuesta la hace sustancialmente más o menos factible. Me parecen ortogonales.

  3. Error en el manejo de combinadores: este objetivo ciertamente se logra con la propuesta, pero no está del todo claro que valga la pena el costo de los cambios necesarios para lograrlo, en el contexto del lenguaje Go tal como está hoy. (Creo que este es el principal punto de discusión en la discusión hasta ahora).

En la mayoría de Go bien redactados, este tipo de texto repetitivo de manejo de errores constituye una pequeña fracción del código. Fue un porcentaje de líneas de un solo dígito en mi breve mirada a algunas bases de código de Go que considero que están bien escritas. Sí, a veces es apropiado, pero a menudo es una señal de que es necesario un rediseño. En particular, el simple hecho de devolver un error sin agregar ningún contexto ocurre con más frecuencia de lo que debería en la actualidad. Podría llamarse "antiidioma". Hay una discusión sobre lo que Go debería o podría hacer, en todo caso, para desalentar este anti-idioma, ya sea en el diseño del lenguaje, o en las bibliotecas, o en las herramientas, o puramente socialmente, o en alguna combinación de esos. . Me interesaría igualmente tener ese debate tanto si se aprueba esta propuesta como si no. De hecho, hacer que ese antiidioma sea ​​más fácil de expresar, como creo que es el objetivo de esta propuesta, podría generar incentivos equivocados.

Por el momento, esta propuesta se trata en gran medida como una cuestión de gustos. En mi opinión, lo que lo haría más convincente sería la evidencia que demuestre que su adopción reduciría la cantidad total de errores. Un buen primer paso podría ser convertir una parte representativa del corpus de Go para demostrar que algunos tipos de errores son imposibles o es poco probable que se expresen en el nuevo estilo; que x errores por línea en el código Go real en la naturaleza se solucionarían usando el nuevo estilo. (Parece mucho más difícil demostrar que el nuevo estilo no compensa ninguna mejora al hacer que otros tipos de errores sean más probables. Allí podríamos tener que conformarnos con argumentos abstractos sobre legibilidad y complejidad, como en los viejos tiempos antes de Go corpus saltó a la fama.)

Con evidencia de apoyo como esa en la mano, uno podría hacer un caso más sólido.

El simple hecho de devolver un error sin agregar ningún contexto ocurre con más frecuencia de lo que debería en la actualidad. Podría llamarse "antiidioma".

Me gustaría hacerme eco de este sentimiento. Esta

if err := foo(x); err != nil {
    return err
}

no debe simplificarse, debe desalentarse, a favor de, por ejemplo,

if err := foo(x); err != nil {
    return errors.Wrapf(err, "fooing %s", x)
}

@peterbourgon

Mi mayor problema con esto no es que el error se devuelva a ciegas. Es el hecho de que la acción: foo(x) ; no es tan visible, y en mi humilde opinión hace que todo sea un poco menos legible que las soluciones 'funcionales' alternativas, donde la acción en sí es un simple retorno en una nueva línea.

incluso si la asignación y la acción se mantienen separadas de la declaración if en sí, la declaración resultante aún pondría un acento en el resultado, en lugar de la acción. Eso es perfectamente válido, especialmente si el resultado es la parte importante. Pero si tiene un montón de declaraciones, donde cada una obtiene una tupla (resultado, error), verifica el error / devoluciones, luego procede a realizar otra acción mientras obtiene una nueva tupla, los resultados en sí mismos obviamente no son los personajes principales en el gráfico.

@urandom Creo que el resultado es un par de (val, error), así que creo que las comprobaciones de error / devoluciones son los personajes principales de la trama también.

¿Qué tal una palabra reservada (algo así como reterr ) para evitar todos los if err != nil { return err } ?

Así que esto

resp, err := doThing(a)
if err != nil {
    return nil, err
}
resp, err = doAnotherThing(b, resp.foo())
if err != nil {
    return nil, err
}
resp, err = FinishUp(c, resp.bar())
if err != nil {
    return nil, err
}

Se convertiría:

resp, _ := reterr doThing(a)
resp, _ = reterr doAnotherThing(b, resp.foo())
resp, _ = reterr FinishUp(c, resp.bar())

reterr básicamente comprobaría los valores de retorno de la función llamada y devolvería si alguno de ellos es un error y no es nulo (y devolvería nil en cualquier valor de retorno que no sea de error).

Suena cada vez más como # 18721

@tarcieri Solo usa algo del paquete reflect . Puedo simular algo como tu propuesta.
Pero creo que no vale la pena hacerlo.

https://play.golang.org/p/CC5txvAc0e

func main() {

    result := Do(func() (int, error) {
        return doThing(1000)
    }).Then(func(resp int) (int, error) {
        return doAnotherThing(200000, resp)
    }).Then(func(resp int) (int, error) {
        return finishUp(1000000, resp)
    })

    if result.err != nil {
        log.Fatal(result.err)
    }

    val := result.val.(int)
    fmt.Println(val)
}

@iporsut hay dos problemas con la reflexión que la convierten en una solución inadecuada para este problema en particular, aunque puede parecer que "resuelve" el problema en la superficie:

  1. Sin seguridad de tipos : con la reflexión no podemos determinar en el momento de la compilación si el cierre está adecuadamente mecanografiado. En cambio, nuestro programa se compilará independientemente de los tipos y, si no coinciden, encontraremos una falla en el tiempo de ejecución.
  2. Sobrecarga de rendimiento enorme : el enfoque que está sugiriendo no está muy lejos del ofrecido por go-linq . Afirman que el uso de la reflexión para este propósito es "5x-10x más lento". Ahora imagine esta cantidad de gastos generales en cada sitio de llamadas.

Para mí, cualquiera de estos problemas es un gran paso hacia atrás con respecto a lo que Go ya tiene, y en conjunto son completamente inútiles.

Me gusta Go y la forma en que maneja los errores. Sin embargo, tal vez podría ser más sencillo. Estas son algunas de mis ideas sobre el manejo de errores en Go.

Como está ahora:

resp, err := doThing(a)
if err != nil {
    return nil, err
}

resp, err = doAnotherThing(b, resp.foo())
if err != nil {
    return nil, err
}

resp, err = FinishUp(c, resp.bar())
if err != nil {
    return nil, err
}

A:

resp, _ := doThing(a) 
resp, _ = doAnotherThing(b, resp.foo())
resp, _ = FinishUp(c, resp.bar())
// return if error is omited, otherwise deal with it as usual (if err != nil { return err })
//However, this breaks semantics of Go and may mislead due to the usa of _ (__ or !_ could be used to avoid such misleading)

B:

resp, err := doThing(a)?
resp, err = doAnotherThing(b, resp.foo())?
resp, err = FinishUp(c, resp.bar())?
// ? indicates that it will return in case of error (more explicit)
// or any other indication could be used
// this approach is preferred for its explicitness

C:

resp, err := doThing(a)
return if err

resp, err = doAnotherThing(b, resp.foo())
return if err

resp, err = FinishUp(c, resp.bar())
return if err
// if err return err
// or if err return (similar to javascript return)
// this one is my favorite, almost no changes to the language, very readable and less SLOC

D:

resp, _ := return doThing(a)
resp, _ = return doAnotherThing(b, resp.foo())
resp, _ = return FinishUp(c, resp.bar())
// or 
resp = throw FinishUp(c, resp.bar())
// this one is also very readable (although maybe a litle less than option **C**) and even less SLOC than **C**
// at this point I'm not sure whether C or D is my favorite )) 

//This applies to all approaches above
// if the function that contains any of these options has no value to return, exit the function. E.g.:
func test() {
    resp, _ := return doThing(a) // or any of other approaches
    // exit function
}

func test() ([]byte, error) {
    resp, _ := return doThing(a) // or any of other approaches
    // return whatever is returned by doThing(a) (this function of course must return ([]byte, error))
}

Disculpe mi inglés y no estoy seguro de si tales cambios son posibles y si resultarán en gastos generales de rendimiento.

Si te gusta alguno de estos enfoques, dale me gusta siguiendo las siguientes reglas:

A = 👍
B = 😄
C = ❤️
D = 🎉

Y 👎 si no te gusta toda la idea))

De esta forma podemos tener algunas estadísticas y evitar comentarios innecesarios como "+1"

Elobrando sobre mis "propuestas" ...

// no need to explicitely define error in return statement, much like throw, try {} catch in java
func test() int {
     resp := throw doThing() // "returns" error if doThing returns (throws) an error
     return resp // yep, resp is int
}

func main() {
     resp, err := test() // the last variable is always error type
     if err != nil {
          os.Exit(0)
     }
}

Nuevamente, no estoy seguro de si algo así es posible))

Aquí hay otra opción loca, haz que la palabra error un poco más mágica. Se vuelve utilizable en el lado izquierdo de una tarea (o declaración corta) y funciona como una función mágica:

res, error() := doThing()
// Shorthand for
res, err := doThing()
if err != nil {
  return 0, ..., 0, err
}

Específicamente, el comportamiento de error() es el siguiente:

  1. Se trata como si tuviera el tipo error a los efectos de la asignación.
  2. Si se le asigna nil , no sucede nada.
  3. Si se le asigna un nil no sea error y al que se le asigna el valor asignado a error() .

Si desea aplicar alguna mutación al error, puede hacer lo siguiente:

res, error(func (e error) error { return fmt.Errorf("foo: %s", error)})
  := doThing()

En cuyo caso, el cierre se aplica al valor asignado antes de que vuelva la función.

Esto es un poco feo, en gran parte debido a la hinchazón sintáctica de tener que lidiar con cierres. La biblioteca estándar podría arreglar esto bien, por ejemplo, con error(errors.Wrapper("foo")) que generará el cierre de contenedor correcto para usted.

Como alternativa, si es muy probable que se pierda la sintaxis nulary error() , sugeriría error(return) como alternativa; el uso de la palabra clave reduce el riesgo de malas interpretaciones. Sin embargo, no se extiende bien al caso de cierre.

Todos los que escribieron Go se han encontrado con la desafortunada proliferación de errores repetidos en el manejo de errores que distraen del propósito central de su código. Por eso Rob Pike abordó el tema en 2015 . Como señala Martin Kühl , la propuesta de Rob para simplificar el manejo de errores:

nos deja tener que implementar mónadas artesanales únicas para cada interfaz para la que queremos manejar errores, lo que creo que sigue siendo tan detallado y repetitivo

Es por eso que todavía hoy hay tanto compromiso con este tema.

Idealmente podemos encontrar una solución que:

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

Propongo la introducción de una nueva palabra clave catch: que funciona de la siguiente manera:

En lugar del formulario actual:

res, err := doThing()
if err != nil {
  return 0, ..., 0, err
}

escribiríamos:

res, err := doThing() catch: 0, ..., 0, err

que se comportaría exactamente de la misma manera que el código de formulario actual anterior. Más específicamente, la función y las asignaciones a la izquierda de catch: se ejecutan primero. Entonces, si y solo si exactamente uno de los argumentos de retorno es de tipo error Y ese valor no es nulo, el catch: actúa como una instrucción return con los valores para la derecha. Si hay cero o más de un tipo error devuelto desde doThing() , es un error de sintaxis usar catch: . Si el valor de error devuelto por doThing() es nil , entonces todo desde catch: hasta el final de la declaración se ignora y no se evalúa.

Para dar un ejemplo más complejo de la publicación de blog reciente de Nemanja Mijailovic titulada Patrones de manejo de errores en Go :

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

  if err := binary.Read(r, binary.BigEndian, &p.Longitude); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil {
    return nil, err
  }

  if err := binary.Read(r, binary.BigEndian, &p.ElevationLoss); err != nil {
    return nil, err
  }

  return &p, nil
}

Esto se convierte en su lugar:

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

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

  return &p, nil
}

Ventajas:

  1. Cerca del mínimo estándar adicional para el manejo de errores.
  2. Mejora el enfoque en la intención principal del código con un mínimo de errores de manejo de equipaje en el lado izquierdo de la declaración y manejo de errores localizado en el lado derecho.
  3. Funciona en muchas situaciones diferentes, lo que le da al programador flexibilidad en el caso de múltiples valores de retorno (por ejemplo, si desea devolver un indicador del recuento de elementos que tuvieron éxito además del error).
  4. La sintaxis es simple y los usuarios de Go, tanto nuevos como antiguos, la entenderían y adoptarían fácilmente.
  5. Tiene éxito parcial en fomentar el manejo adecuado de errores al hacer que el código de error sea más conciso. Puede hacer que el código de error sea un poco menos probable de copiar y pegar y, por lo tanto, reducir la introducción de errores comunes de copiar y pegar.

Desventajas:

  1. Este enfoque no tiene éxito en fomentar el manejo adecuado de errores porque no hace nada para promover errores de envoltura antes de propagarlos. En mi mundo ideal, esta nueva sintaxis habría requerido que el error devuelto por catch: sea ​​un nuevo error o un error envuelto, pero no idéntico al error devuelto por la función a la izquierda de catch: . Go ha sido descrito como "obstinado" y tal rigor en el manejo de errores en aras de la claridad y confiabilidad habría encajado con eso. Sin embargo, me faltó la creatividad para incorporar ese objetivo.
  2. Algunos pueden argumentar que todo esto es azúcar sintáctico y no es necesario en el idioma. Un argumento en contra podría ser que el actual manejo de errores en Go son grasas trans sintácticas, y esta propuesta simplemente lo elimina. Para ser ampliamente adoptado, un lenguaje de programación debe ser agradable de usar. En gran medida, Go tiene éxito en eso, pero el texto estándar de manejo de errores es una excepción particularmente profusa.
  3. ¿Estamos "captando" el error de la función que llamamos, o estamos "lanzando" un error a quienquiera que nos haya llamado? ¿Es apropiado tener un catch: sin un lanzamiento explícito? La palabra reservada no tiene por qué ser necesariamente catch: . Otros pueden tener mejores ideas. Incluso podría ser un operador en lugar de una palabra reservada.

Todos los que escribieron Go se han encontrado con la desafortunada proliferación de errores repetidos en el manejo de errores que distraen del propósito central de su código.

Eso no es verdad. Yo programo bastante en Go y no tengo ningún problema con ningún error en el manejo del texto estándar. Escribir código de manejo de errores consume una fracción de tiempo tan microscópica en el desarrollo de un proyecto que apenas lo noto y, en mi humilde opinión, no justifica ningún cambio en el idioma.

Todos los que escribieron Go se han encontrado con la desafortunada proliferación de errores repetidos en el manejo de errores que distraen del propósito central de su código.

Eso no es verdad. Yo programo bastante en Go y no tengo ningún problema con ningún error en el manejo del texto estándar. Escribir código de manejo de errores consume una fracción de tiempo tan microscópica en el desarrollo de un proyecto que apenas lo noto y, en mi humilde opinión, no justifica ningún cambio en el idioma.

No dije nada sobre cuánto tiempo lleva escribir el código de manejo de errores. Solo dije que distrae del propósito central del código. Tal vez debería haber dicho "Todos los que han leído Go se han encontrado con la desafortunada proliferación del manejo de errores ...".

Entonces, @cznic , supongo que la pregunta para usted es si ha leído el código de Go que sintió que tenía una cantidad excesiva de errores de manejo repetitivo o qué distrajo del código que estaba tratando de entender.

A nadie le gustan mis propuestas 😅
De todos modos, deberíamos tener algo de sintaxis y votar por la mejor (algún sistema de encuesta) e incluir un enlace aquí o en el archivo Léame.

Tal vez debería haber dicho "Todos los que han leído Go se han encontrado con la desafortunada proliferación del manejo de errores ...".

Eso no es cierto. Prefiero lo explícito y la localidad adecuada del estado actual del manejo de errores. La propuesta, como cualquier otra que haya visto, hace que el código en mi humilde opinión sea menos legible y peor de mantener.

Entonces, @cznic , supongo que la pregunta para usted es si ha leído el código de Go que sintió que tenía una cantidad excesiva de errores de manejo repetitivo o qué distrajo del código que estaba tratando de entender.

No. Según mi experiencia, Go es un lenguaje de programación excepcionalmente legible. La mitad de ese crédito va a Gofmt, por supuesto.

Mi propia experiencia es que realmente comienza a arrastrarse cuando tienes un montón de declaraciones dependientes, cada una de las cuales puede arrojar un error, el manejo de errores se acumula y envejece rápidamente. Lo que podrían ser 5 líneas de código se convierte en 20.

@cznic
En mi experiencia, tener tantos errores de manejo repetitivo hace que el código sea mucho menos legible. Debido a que el manejo de errores en sí es en su mayoría idéntico (sin ningún ajuste de error que pueda ocurrir), produce una especie de efecto de cerca, donde si escanea rápidamente un fragmento de código, la mayoría de las veces termina viendo una gran cantidad de manejo de errores. Por lo tanto, el mayor problema, el código real, la parte más importante del programa, se esconde detrás de esta ilusión óptica, lo que hace que sea mucho más difícil ver de qué se trata un fragmento de código.

El manejo de errores no debería ser la parte principal de ningún código. Desafortunadamente, muy a menudo termina siendo exactamente eso.
Hay una razón por la que la composición de declaraciones en otros idiomas es tan popular.

Debido a que el manejo de errores en sí es en su mayoría idéntico (sin ningún error
envoltura que pueda ocurrir), produce una especie de efecto de cerca, donde si
escanea rápidamente un fragmento de código, la mayoría de las veces termina viendo una masa
de manejo de errores.

Ésta es una posición muy subjetiva. Es como argumentar que las declaraciones if
hacer que el código sea ilegible, o que las llaves de estilo K&R hagan las cosas ilegibles.

Desde mi punto de vista, lo explícito del manejo de errores de go se desvanece rápidamente
en el fondo de la familiaridad hasta que note que el patrón se rompe;
algo que el ojo humano hace muy bien; falta el manejo de errores,
las variables de error se asignan a _, etc.

Escribir a máquina es una carga, no se equivoque. Pero Go no optimiza para
autor del código, optimiza explícitamente para el lector.

El martes 16 de mayo de 2017 a las 5:45 p.m., Viktor Kojouharov < [email protected]

escribió:

@cznic https://github.com/cznic
En mi experiencia, tener tantos errores de manejo repetitivo hace que el código
mucho menos legible. Porque el manejo de errores en sí es mayormente idéntico
(sin ningún error de envoltura que pueda ocurrir), produce una especie de valla
efecto, donde si escanea rápidamente un fragmento de código, la mayoría de las veces termina
viendo una gran cantidad de manejo de errores. Por tanto, el mayor problema, el actual
El código, la parte más importante del programa, se esconde detrás de esta óptica.
ilusión, lo que hace que sea mucho más difícil ver realmente qué pieza de
se trata del código.

El manejo de errores no debería ser la parte principal de ningún código. Desafortunadamente, bastante
a menudo termina siendo exactamente eso.

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

si escanea rápidamente un fragmento de código, la mayoría de las veces termina viendo una masa
de manejo de errores.

Ésta es una posición muy subjetiva.

Altamente subjetivo pero ampliamente compartido.

Como dijo el propio Rob,

Un punto común de discusión entre los programadores de Go, especialmente los nuevos en el lenguaje, es cómo manejar los errores. La conversación a menudo se convierte en un lamento por la cantidad de veces que la secuencia

if err != nil {
    return err
}

aparece.

Para ser justos, Rob continuó diciendo que esta percepción sobre el manejo de errores de Go es "desafortunada, engañosa y fácil de corregir". Sin embargo, pasa la mayor parte de ese artículo explicando el método recomendado para corregir la percepción. Desafortunadamente, la prescripción de Rob es problemática en sí misma, como lo explicó tan bien Martin Kühl. Además de la crítica de Martin, la sugerencia de Rob también reduce la localidad que @cznic dice que valora en el manejo de errores de Go.

Tal vez la pregunta sea si tuvimos la capacidad de reemplazar

res, err := doThing()
if err != nil {
  return nil, err
}

con algo similar a:

res, err := doThing() catch: nil, err

¿Lo usaría o se quedaría con la versión de cuatro líneas? Independientemente de sus preferencias personales, ¿cree que una alternativa como esta sería ampliamente adoptada por la comunidad de Go y se volvería idiomática? Dada la subjetividad de cualquier argumento de que la versión más corta afecta negativamente a la legibilidad, mi experiencia con los programadores dice que gravitarían fuertemente hacia la versión de una sola línea.

Charla real: go 1 es fijo y no cambiará, especialmente de esta manera fundamental.

No tiene sentido proponer algún tipo de opción hasta que Go 2 implemente algún tipo de plantilla. En ese momento, todo cambia.

El 16 de mayo de 2017, a las 23:46, Billy Hinners [email protected] escribió:

si escanea rápidamente un fragmento de código, la mayoría de las veces termina viendo una masa
de manejo de errores.

Ésta es una posición muy subjetiva.

Altamente subjetivo pero ampliamente compartido.

Como dijo el propio Rob,

Un punto común de discusión entre los programadores de Go, especialmente los nuevos en el lenguaje, es cómo manejar los errores. La conversación a menudo se convierte en un lamento por la cantidad de veces que la secuencia

if err! = nil {
volver err
}
aparece.

Para ser justos, Rob continuó diciendo que esta percepción sobre el manejo de errores de Go es "desafortunada, engañosa y fácil de corregir". Sin embargo, pasa la mayor parte de ese artículo explicando el método recomendado para corregir la percepción. Desafortunadamente, la prescripción de Rob es problemática en sí misma, como lo explicó tan bien Martin Kühl. Además de la crítica de Martin, la sugerencia de Rob también reduce la localidad que @cznic dice que valora en el manejo de errores de Go.

Tal vez la pregunta sea si tuvimos la capacidad de reemplazar

res, err: = hacer ()
if err! = nil {
retorno nulo, err
}
con algo similar a:

res, err: = doThing () catch: nil, err

¿Lo usaría o se quedaría con la versión de cuatro líneas? Independientemente de sus preferencias personales, ¿cree que una alternativa como esta sería ampliamente adoptada por la comunidad de Go y se volvería idiomática? Dada la subjetividad de cualquier argumento de que la versión más corta afecta negativamente a la legibilidad, mi experiencia con los programadores dice que gravitarían fuertemente hacia la versión de una sola línea.

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

No tiene sentido proponer algún tipo de opción hasta que Go 2 implemente algún tipo de plantilla. En ese momento, todo cambia.

Supuse que estábamos hablando de Go 2 como lo implica el título de este hilo y con la plena convicción de que "Go 2" no es un eufemismo para "nunca". De hecho, dado que Go 1 está arreglado, probablemente deberíamos dedicar una porción mucho mayor de nuestras discusiones de Go a Go 2.

Dicho esto, creo que todos los que se quejan de la verbosidad de Go's
el manejo de errores no tiene el punto fundamental de que el propósito del error
El manejo en Go _no_ hace que el caso de no error sea breve y discreto
como sea posible. Más bien, el objetivo de la estrategia de manejo de errores de Go es forzar
que el autor del código considere, en todo momento, qué sucede cuando el
la función falla y, lo más importante, cómo limpiar, deshacer y recuperar
antes de volver a la persona que llama.

Todas las estrategias para ocultar errores en el manejo de la placa de la caldera me parecen ser
ignorando esto.

El martes 16 de mayo de 2017 a las 23:51, Dave Cheney [email protected] escribió:

Charla real: go 1 está arreglado y no cambiará, especialmente en este
forma fundamental.

No tiene sentido proponer algún tipo de opción hasta que Go 2 implemente
algunos para de tipo de plantillas. En ese momento, todo cambia.

El 16 de mayo de 2017, a las 23:46, Billy Hinners [email protected] escribió:

si escanea rápidamente un fragmento de código, la mayoría de las veces termina viendo un
masa
de manejo de errores.

Ésta es una posición muy subjetiva.

Altamente subjetivo pero ampliamente compartido.

Como dijo el propio Rob,

Un punto común de discusión entre los programadores de Go, especialmente los nuevos en
el idioma, es cómo manejar los errores. La conversación a menudo se convierte en un
lamentarse por la cantidad de veces que la secuencia

if err! = nil {
volver err
}

aparece.

Para ser justos, Rob continuó diciendo que esta percepción sobre el manejo de errores de Go es
"desafortunado, engañoso y fácil de corregir". Sin embargo, gasta la mayor parte de eso
artículo https://blog.golang.org/errors-are-values explicando su
método recomendado para corregir la percepción. Desafortunadamente, Rob's
la prescripción es problemática en sí misma, como se explica
https://www.innoq.com/en/blog/golang-errors-monads/ tan bien por Martin
Kühl. Además de la crítica de Martin, la sugerencia de Rob también reduce la
localidad que @cznic https://github.com/cznic dice que valora en Go
manejo de errores.

Tal vez la pregunta sea si tuvimos la capacidad de reemplazar

res, err: = hacer ()
if err! = nil {
retorno nulo, err
}

con algo similar a:

res, err: = doThing () catch: nil, err

¿Lo usaría o se quedaría con la versión de cuatro líneas?
Independientemente de sus preferencias personales, ¿cree que una alternativa como
esto sería ampliamente adoptado por la comunidad de Go y se volvería idiomático?
Dada la subjetividad de cualquier argumento de que la versión más corta
afecta la legibilidad, mi experiencia con los programadores dice que
gravitan fuertemente hacia la versión de una sola línea.

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

Más bien, el objetivo de la estrategia de manejo de errores de Go es forzar
que el autor del código considere, en todo momento, qué sucede cuando el
la función falla y, lo más importante, cómo limpiar, deshacer y recuperar
antes de volver a la persona que llama.

Bueno, entonces Go no logró ese objetivo. De forma predeterminada, Go le permite ignorar los errores devueltos y, en muchos casos, ni siquiera lo sabría hasta que algo en algún lugar no funcione como debería. Por el contrario, las excepciones de la comunidad Go muy odiadas (eso es solo un ejemplo para probar el punto) te obligan a considerarlas porque, de lo contrario, la aplicación se bloqueará. Eso a menudo nos lleva a tener problemas para capturar todo e ignorarlo, pero eso es culpa del programador.

Básicamente, el manejo de errores en Go es opcional. Se trata más de una convención hablada en la que todos los errores deben manejarse. El objetivo se alcanzaría si realmente lo obligara a manejar los errores. Por ejemplo, con advertencias o errores en tiempo de compilación.

Con eso en mente, ocultar la placa de la caldera no haría daño a nadie. La convención hablada se mantendría y los programadores aún optarían por el manejo de errores como está ahora.

El objetivo de la estrategia de manejo de errores de Go es forzar
que el autor del código considere, en todo momento, qué sucede cuando el
la función falla y, lo más importante, cómo limpiar, deshacer y recuperar
antes de volver a la persona que llama.

Ese es un objetivo indiscutiblemente noble. Sin embargo, es un objetivo que debe equilibrarse con la legibilidad del flujo principal y la intención del código.

Como programador de Go, puedo decirle que no encuentro la verbosidad de
Manejo de errores de Go para dañar su legibilidad. No veo que haya nada
intercambiar, porque no siento ninguna molestia _leyendo_ el código escrito por otros
Vaya programadores.

El miércoles 17 de mayo de 2017 a las 12:10 a. M., Billy Hinners [email protected]
escribió:

El objetivo de la estrategia de manejo de errores de Go es forzar
que el autor del código considere, en todo momento, qué sucede cuando el
la función falla y, lo más importante, cómo limpiar, deshacer y recuperar
antes de volver a la persona que llama.

Ese es un objetivo indiscutiblemente noble. Sin embargo, es un objetivo que debe ser
equilibrado con la legibilidad del flujo principal y la intención del código.

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

@davecheney , mientras que estoy de acuerdo con usted en que el manejo de errores debe ser explícito y no posponerse para más adelante (lo que, por supuesto, puede hacer con _), también existe la estrategia de "burbujear" los errores para tratarlos en una función, de para agregar información adicional o eliminar (antes de enviarla al cliente). Mi problema personal es que tengo que escribir las mismas 4 líneas de código una y otra vez

Por ejemplo:

getNewToken (id int64) (Token, error) {

user := &User{ID:id}

u, err := user.Get();
if err != nil {
    return Token{}, err
}

token, err := token.New(u);
if err != nil {
    return Token{}, err
}
return token, nil

}
No estoy manejando el error aquí, solo lo estoy devolviendo. y cuando leo este tipo de código, tengo que omitir el "manejo" de errores, y es difícil encontrar el propósito principal del código

y el código anterior podría ser fácilmente reemplazado por algo como eso:

getNewToken (id int64) (Token, error) {

user := &User{ID:id}

u, err := throw user.Get(); //throw should also wrap the error

token, err := throw token.New(u);

return token, nil

}
Un código como ese es más legible y menos innecesario (en mi humilde opinión). Y el error podría y debería manejarse en la función donde se usa esta función.

Como programador de Go, puedo decirle que no encuentro que la verbosidad del manejo de errores de Go perjudique su legibilidad.

Estoy de acuerdo.

En una nota no relacionada:

También me parece que un tipo de "resultado" es una propuesta demasiado específica; tal vez los tipos sean en realidad solo tipos enumerados de dos variantes. Si hubiera un concepto de enumeraciones, se podría crear un paquete de resultados u opciones a partir del árbol y experimentar con él antes de comprometerse a agregarlo al lenguaje y sin agregar mucha sintaxis o métodos adicionales que realmente no se pueden reutilizar y solo son buenos. para tipos de resultados. No sé si las enumeraciones serían útiles en Go o no, pero si puede argumentar el caso más general, probablemente también hará que su caso sea más sólido para el tipo de resultado más específico (sospecho; tal vez me equivoque).

func getNewToken (id int64) (Token, error) {
usuario: = & Usuario {ID: id}

u, err := user.Get()
if err != nil {
    return Token{}, err
}

return token.New(u)

}

Parece equivalente.

El miércoles 17 de mayo de 2017 a las 12:34 a. M., Kiura [email protected] escribió:

@davecheney https://github.com/davecheney , mientras que estoy de acuerdo contigo
que el manejo de errores debe ser explícito y no posponerse para más adelante (lo que
usted, por supuesto, puede hacerlo con _), también existe la estrategia de "burbujear"
errores para tratarlos en una función, de agregar información adicional o
remove (antes de enviarlo al cliente). Mi problema personal es que tengo
escribir las mismas 4 líneas de código una y otra vez

Por ejemplo:

getNewToken (id int64) (Token, error) {

usuario: = & Usuario {ID: id}

u, err: = usuario.Get ();
if err! = nil {
Devolver Token {}, err
}

token, err: = token.Nuevo (u);
if err! = nil {
Devolver Token {}, err
}
token de devolución, nulo

}
No estoy manejando el error aquí, solo lo estoy devolviendo. y cuando leo
este tipo de código, tengo que omitir el "manejo" de errores, y es difícil de encontrar
el propósito principal del código

y el código anterior podría ser fácilmente reemplazado por algo como eso:

getNewToken (id int64) (Token, error) {

usuario: = & Usuario {ID: id}

u, err: = lanzar usuario.Get (); // throw también debería envolver el error

token, err: = lanzar token.Nuevo (u);

token de devolución, nulo

}
Un código como ese es más legible y menos innecesario (en mi humilde opinión). Y el
El error podría y debería manejarse en la función donde esta función es
usado.

-
Recibes esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/golang/go/issues/19991#issuecomment-301802010 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AAAcA9sIRXX7RSdDcUOidpe-qLTR7unNks5r6bP3gaJpZM4M-dud
.

También me parece que un tipo de "resultado" es una propuesta demasiado específica; tal vez los tipos sean en realidad solo tipos enumerados de dos variantes. Si hubiera un concepto de enumeraciones, se podría crear un paquete de resultados u opciones a partir del árbol y experimentar con él antes de comprometerse a agregarlo al lenguaje y sin agregar mucha sintaxis o métodos adicionales que realmente no se pueden reutilizar y solo son buenos. para tipos de resultados. No sé si las enumeraciones serían útiles en Go o no, pero si puede argumentar el caso más general, probablemente también hará que su caso sea más sólido para el tipo de resultado más específico (sospecho; tal vez me equivoque).

Como se indica en la propuesta original, un tipo de resultado idealmente se implementaría como un tipo de suma (por ejemplo, enums ala Rust's), y hay una propuesta abierta para agregarlos al lenguaje.

Sin embargo, los tipos de suma por sí solos no son suficientes para implementar una biblioteca de tipos de resultado reutilizable sin soporte de idioma adicional. También requieren genéricos.

Esta propuesta estaba explorando la idea de implementar un tipo de resultado que no depende de genéricos, sino que depende de la ayuda de casos especiales del compilador.

Solo agregaré que ahora que lo publiqué, estaría de acuerdo en que la mejor manera de perseguir esto (si es que lo hace) sería con el soporte de genéricos a nivel de idioma.

@davecheney , De hecho, en este caso casi no hay diferencia, pero ¿qué

PD: No estoy en contra de la forma en que Go1 maneja los errores, solo creo que podría ser mejor.

Como se indica en la propuesta original, un tipo de resultado idealmente se implementaría como un tipo de suma (por ejemplo, enums ala Rust's), y hay una propuesta abierta para agregarlos al lenguaje.

Lo siento, debería haber sido más claro: estaba argumentando que esta declaración:

Creo que un tipo de resultado de Go tampoco necesita, y simplemente puede aprovechar la magia del compilador de casos especiales.

me parece una mala idea.

Sin embargo, los tipos de suma por sí solos no son suficientes para implementar una biblioteca de tipos de resultado reutilizable sin soporte de idioma adicional. También requieren genéricos.

Esta propuesta estaba explorando la idea de implementar un tipo de resultado que no depende de genéricos, sino que depende de la ayuda de casos especiales del compilador.

Solo agregaré que ahora que lo publiqué, estaría de acuerdo en que la mejor manera de perseguir esto (si es que lo hace) sería con el soporte de genéricos a nivel de idioma.

Sí, bastante justo; Entonces estoy de acuerdo con su última declaración. Si tenemos que esperar a Go 2 de todos modos, también podríamos resolver el problema más general primero (asumiendo que en realidad es un problema) :)

Además, Rob Pike escribió un artículo sobre el manejo de errores como se mencionó anteriormente. Mientras que este enfoque parece estar "arreglando" el problema, introduce otro: más código saturado con interfaces.

Creo que es importante no confundir "manejo explícito de errores" con "manejo detallado de errores". Go quiere obligar al usuario a considerar el manejo de errores en cada paso en lugar de delegarlo. Para cada función que llame que pueda arrojar un error, debe decidir de alguna manera qué, si desea o no manejar el error, y cómo. A veces significa que ignora el error, a veces significa que vuelve a intentarlo, a menudo significa que simplemente se lo pasa a la persona que llama para que lo maneje.

El artículo de Rob es genial y realmente debería ser parte de Effective Go 2, pero es una estrategia que solo puede llevarte tan lejos. Especialmente cuando se trata de destinatarios heterogéneos, es necesario gestionar muchos errores.

No creo que sea irrazonable considerar el azúcar sintáctico o alguna otra facilidad para ayudar con el manejo de errores. Creo que es importante que no socave los fundamentos del manejo de errores de Go. Por ejemplo, sería malo establecer un manejador de errores a nivel de función que maneje todos los errores que ocurren; significa que estamos permitiendo que el programador haga lo que normalmente hace el manejo de excepciones: mover la consideración de errores de un problema a nivel de declaración a un tema a nivel de bloque o función. Eso definitivamente va en contra de la filosofía.

@billyh Con respecto al artículo "Patrones de manejo de errores en Go", existen otras soluciones:

@egonelbre
Estas soluciones solo son adecuadas si está realizando el mismo tipo de operación repetidamente. Ese no suele ser el caso. Por lo tanto, esto casi nunca se puede aplicar en la práctica.

@urandom , ¿entonces muestra un ejemplo realista?

Seguro que puedo tomar un ejemplo más complicado :

func (conversion *PageConversion) Convert() (page *kb.Page, errs []error, fatal error)

Entiendo que estos no son aplicables en todas partes, pero sin una lista adecuada de ejemplos que queremos mejorar no hay forma de tener una discusión decente.

@egonelbre

https://github.com/juju/juju/blob/01b24551ecdf20921cf620b844ef6c2948fcc9f8/cloudconfig/providerinit/providerinit.go

Descargo de responsabilidad: no he usado juju, ni he leído el código. Es solo un producto de 'producción' que conozco de la parte superior de mi cabeza. Estoy razonablemente seguro de que ese tipo de manejo de errores (donde los errores se verifican entre operaciones independientes) prevalece en el mundo de Go, y dudo mucho que haya alguien por ahí que no se haya topado con esto.

@urandom estoy de acuerdo. El problema principal de discutir sin código del mundo real es que la gente recuerda la "esencia" del problema, no el problema real, lo que a menudo conduce a un planteamiento del problema demasiado simplificado. _PS: Recordé un buen ejemplo en go ._

Por ejemplo, a partir de estos ejemplos del mundo real, podemos ver que hay varias otras cosas que deben tenerse en cuenta:

  • buenos mensajes de error
  • recuperación / rutas alternativas basadas en el valor de error
  • retrocesos
  • ejecución de mejor esfuerzo con errores
  • registro de casos felices
  • registro de fallas
  • seguimiento de fallas
  • se devuelven varios errores
  • _por supuesto, algunos de estos se usarán juntos_
  • ... probablemente algunos que me perdí ...

No solo el camino "feliz" y "fracaso". No estoy diciendo que estos no se puedan resolver, solo que necesitan ser delineados y discutidos.

@egonelbre, aquí hay otro ejemplo del Golang Weekly de esta semana, en el artículo de Mario Zupan titulado "Escribiendo un generador de blogs estático en Go":

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {
    fmt.Printf("Fetching data from %s into %s...\n", from, to)
    if err := createFolderIfNotExist(to); err != nil {
        return nil, err
    }
    if err := clearFolder(to); err != nil {
        return nil, err
    }
    if err := cloneRepo(to, from); err != nil {
        return nil, err
    }
    dirs, err := getContentFolders(to)
    if err != nil {
        return nil, err
    }
    fmt.Print("Fetching complete.\n")
    return dirs, nil
}

Nota: No estoy insinuando ninguna crítica del código de Mario. De hecho, disfruté bastante de su artículo.
Desafortunadamente, ejemplos como este son demasiado comunes en la fuente Go. Go code gravita hacia este patrón de vía de tren de una línea de interés seguida de tres líneas de calderas idénticas o casi idénticas repetidas una y otra vez. Combinar la asignación y el condicional cuando sea posible, como hace Mario, ayuda un poco.

No estoy seguro de que ningún lenguaje de programación haya sido diseñado con el objetivo principal de minimizar las líneas de código, pero a) la relación entre el código significativo y el texto estándar podría ser una (de muchas) medidas válidas de la calidad de un lenguaje de programación, yb) Debido a que gran parte de la programación implica el manejo de errores, este patrón impregna el código Go y, por lo tanto, hace que este caso particular de exceso de repetición merezca una simplificación.

Si podemos identificar una buena alternativa, creo que se adoptará rápidamente y hará que Go sea aún más agradable de leer, escribir y mantener.

Rebecca Skinner (@cercerilla) compartió una excelente reseña de las deficiencias en el manejo de errores de Go junto con un análisis del uso de mónadas como solución en su plataforma de diapositivas Monadic Error Handling en Go . Me gustaron especialmente sus conclusiones al final.

Gracias a @davecheney por referirse al mazo de Rebecca en su artículo, Simplicity Debt Redux, que me permitió encontrarlo. (Gracias también a Dave por cimentar mi optimismo color de rosa para Go 2 con las realidades más valientes).

Go code gravita hacia este patrón de vía de tren de una línea de interés seguida de tres líneas de calderas idénticas o casi idénticas repetidas una y otra vez.

Cada declaración de control de flujo de control es importante. Las líneas de manejo de errores son de importancia crítica desde el punto de vista de la corrección.

la relación entre código significativo y texto estándar podría ser una (de muchas) medidas válidas de la calidad de un lenguaje de programación

Si alguien considera que las declaraciones de manejo de errores no son significativas, entonces buena suerte con la codificación y espero mantenerme alejado de los resultados.

Para abordar uno de los puntos cubiertos en Simplicity Debt Redux de @davecheney (que cubrí, pero creo que pena repetirlo):

La siguiente pregunta es, ¿se convertiría esta forma monádica en la única forma en que se manejan los errores?

Para que algo como esto se convierta en la forma "única" en que se manejan los errores, tendría que ser un cambio importante realizado en toda la biblioteca estándar y en todos los proyectos compatibles con "Go2". Creo que eso es imprudente: la debacle de Python2 / 3 muestra cómo cismas como ese pueden dañar los ecosistemas del lenguaje.

Como se menciona en esta propuesta, si un tipo de resultado pudiera coaccionar automáticamente a la forma de tupla equivalente, podría tener su pastel y comérselo también en términos de una biblioteca estándar hipotética de Go2 que adopte este enfoque en todos los ámbitos y al mismo tiempo mantenga la compatibilidad con el código existente. . Esto permitiría a aquellos que estén interesados ​​aprovecharlo, pero las bibliotecas que aún deseen trabajar en Go1 funcionarán de inmediato. Los autores de bibliotecas podrían tener su opción: escribir bibliotecas que funcionen tanto en Go1 como en Go2 usando el estilo antiguo, o solo en Go2 usando el estilo monádico.

La "forma antigua" y la "nueva forma" de manejo de errores podrían ser compatibles hasta el punto de que los usuarios del lenguaje ni siquiera tendrían que pensar en ello y podrían continuar haciendo las cosas de la "forma antigua" si quisieran. Si bien esto carece de cierta pureza conceptual, creo que es mucho menos importante que permitir que el código existente continúe funcionando sin modificaciones y también permitir que las personas desarrollen bibliotecas que funcionen con todas las versiones del lenguaje, no solo con la última.

Parece confuso y ofrece una guía poco clara a los recién llegados a Go 2.0 para que sigan admitiendo tanto el modelo de interfaz de error como un nuevo tipo tal vez monádico.

Ellos son los frenos: dejar el lenguaje congelado como está o evolucionar el lenguaje, agregando complejidad incidental y relegando las formas anteriores de hacer las cosas a las verrugas heredadas. Realmente creo que esas son las únicas dos opciones, ya que agregar una nueva característica que reemplace a una anterior, ya sea que la característica anterior esté desaprobada, pero sea compatible o que esté fuera de la puerta en forma de un cambio radical, es algo que creo que los usuarios del idioma tendrá que aprender independientemente.

No creo que sea posible cambiar el lenguaje, pero los recién llegados evitan aprender tanto la "forma antigua" como la "nueva" de hacer las cosas, incluso si, hipotéticamente, Go2 adoptara esto por completo. Aún quedaría con un cisma Go1 y Go2, y los recién llegados se preguntarán cuáles son las diferencias e inevitablemente terminarán teniendo que aprender "Go1" de todos modos.

Creo que la compatibilidad con versiones anteriores es útil tanto para enseñar el lenguaje como para la compatibilidad de código: todos los materiales existentes para la enseñanza de Go seguirán siendo válidos, incluso si la sintaxis está desactualizada. No será necesario revisar cada parte del material didáctico de Go e invalidar la sintaxis anterior: el material didáctico podría, en su tiempo libre, agregar un aviso de que hay una nueva sintaxis.

Entiendo que "Hay más de una manera de hacerlo" generalmente va en contra de la filosofía Go de simplicidad y minimalismo, pero es el precio que se debe pagar por agregar nuevas funciones de lenguaje. Las nuevas características del lenguaje, por su naturaleza, harán que los enfoques más antiguos sean obsoletos.

Ciertamente estoy dispuesto a admitir que podría haber una forma de resolver el mismo problema central de una manera que sea más natural para los Gophers, sin embargo, y no un cambio tan discordante del enfoque existente.

Una cosa más a considerar: si bien Go ha hecho un trabajo ejemplar al mantener el idioma fácil de aprender, ese no es el único obstáculo involucrado en la incorporación de personas a un idioma. Creo que es seguro decir que hay varias personas que miran la verbosidad del manejo de errores de Go y se desaniman, algunas hasta el punto de que se niegan a adoptar el lenguaje.

Creo que vale la pena preguntar si las mejoras en el idioma podrían atraer a las personas que actualmente se sienten desanimadas por él, y cómo esto se equilibra con hacer que el idioma sea más difícil de aprender.

Sin embargo, hacer algo como el manejo monádico de errores va en contra de la filosofía de Go de hacerte pensar en los errores. El manejo de errores monádicos y el manejo de excepciones al estilo Java son bastante cercanos en semántica (aunque difieren en la sintaxis). Go adoptó una filosofía deliberadamente diferente de esperar que el programador manejara explícitamente cada error, en lugar de solo agregar código de manejo de errores cuando lo piense. De hecho, el modismo return nil, err estrictamente hablando no es óptimo porque probablemente pueda agregar contexto útil adicional.

Creo que cualquier intento de abordar el manejo de errores de Go debe tener esto en cuenta y no facilitar el evitar pensar en los errores.

@alercah , tengo que rogar por diferir con todo lo que acabas de decir ...

Hacer algo como el manejo monádico de errores va en contra de la filosofía de Go de hacerte pensar en los errores

Viniendo de Rust, creo que Rust (o más bien, el compilador de Rust) en realidad me hace pensar en errores más que en Go. Rust tiene un atributo # [must_use] en su tipo Result que significa que los resultados no utilizados generan una advertencia del compilador. Esto no es así en Go (Rebecca Skinner aborda esto en su charla): el compilador de Go no advertirá, por ejemplo, de valores error no controlados.

El sistema de tipo Rust hace cumplir cada caso de error que se aborda en su código, y si no, es un error de tipo o, en el mejor de los casos, una advertencia.

El manejo de errores monádicos y el manejo de excepciones al estilo Java son bastante cercanos en semántica (aunque difieren en la sintaxis).

Déjame explicar por qué esto no es cierto:

Estrategia de propagación de errores

  • Ir: valor de retorno, propagado explícitamente
  • Java: salto no local, propagado implícitamente
  • Rust: valor de retorno, propagado explícitamente

Tipos de error

  • Ir: un valor de retorno por función, generalmente 2 tuplas de (success, error)
  • Java: excepciones comprobadas que constan de muchos tipos de excepciones, que representan la unión de conjuntos de todas las excepciones potencialmente lanzadas desde un método. También excepciones desmarcadas que no se declaran y pueden ocurrir en cualquier lugar y en cualquier momento.
  • Rust: un valor de retorno por función, generalmente Result tipo de suma, por ejemplo, Result<Success, Error>

En general, siento que Go está mucho más cerca de Rust que de Java en lo que respecta al manejo de errores: los errores en Go y Rust son solo valores, no son excepciones. Debe optar por la propagación explícitamente. Debe convertir los errores de un tipo diferente al que devuelve una función determinada, por ejemplo, mediante el ajuste. En última instancia, ambos representan un par de valor de éxito / error, solo que utilizan diferentes características del sistema de tipos (tuplas frente a tipos de suma genéricos).

Hay algunas excepciones en las que Rust proporciona algunas abstracciones que se pueden usar de forma electiva caja por caja para realizar el manejo de errores implícitos (o más bien, conversión de errores explícitos, aún debe propagar el error manualmente). Por ejemplo, el rasgo From se puede utilizar para convertir automáticamente los errores de un tipo a otro. Personalmente, creo que poder definir una política que esté completamente dentro del alcance de un paquete en particular que le permita convertir automáticamente los errores de un tipo explícito a otro es una ventaja, y no un inconveniente. El sistema de rasgos de Rust solo le permite definir From para los tipos en su propia caja, evitando cualquier tipo de acción espeluznante a distancia.

Sin embargo, eso está fuera del alcance de esta propuesta e implica varias características del lenguaje que Go no tiene funcionando en conjunto, por lo que no creo que haya ningún tipo de pendiente resbaladiza en la que Go esté "en riesgo" de admitir este tipo de conversiones implícitas. , al menos no hasta que Go agregue genéricos y rasgos / clases de tipos.

Para arrojar mis dos centavos en este asunto. Creo que este tipo de funcionalidad sería muy útil para las empresas (como mi propio empleador) en las que las aplicaciones individuales se comunican con un gran número de fuentes de datos subsidiarias y componen los resultados de forma sencilla.

Aquí hay una muestra de datos representativa de algún flujo de código que tendríamos

func generateUser(userID : string) (User, error) {
      siteProperties, err := clients.GetSiteProperties()
      if err != nil {
           return nil, err
     }
     chatProperties, err := clients.GetChatProperties()
      if err != nil {
           return nil, err
     }

     followersProperties, err := clients.GetFollowersProperties()
      if err != nil {
           return nil, err
     }


// ... (repeat X5)
     return createUser(siteProperties, ChatProperties, followersProperties, ... /*other properties here */), nil
}

Entiendo gran parte del rechazo de Go está diseñado para obligar a un usuario a pensar en los errores en cada punto, pero en las bases de código donde la gran mayoría de las funciones devuelven T, err , esto conduce a una hinchazón sustancial del código del mundo real. y en realidad ha provocado fallas en la producción porque alguien se olvida de agregar código de manejo de errores después de realizar una llamada de función adicional, y el error silenciosamente no se marca. Además, no es raro que algunos de nuestros servicios más conversadores sean ~ 20% + manejo de errores, siendo muy poco interesante.

Además, la gran mayoría de esta lógica de manejo de errores es idéntica y, paradójicamente, la gran cantidad de manejo explícito de errores en nuestras bases de código hace que sea difícil encontrar código donde el caso excepcional es realmente interesante porque hay una especie de 'aguja en el pajar 'fenómenos en juego.

Definitivamente puedo ver por qué esta propuesta en particular puede no ser la solución, pero creo que debe haber _alguna_ forma de reducir esta repetición.

Algunos pensamientos más ociosos:

El final de Rust ? es una buena sintaxis. Sin embargo, para Go, dada la importancia del contexto de error, tal vez sugeriría la siguiente variación:

  • El seguimiento de ? funciona como Rust, modificado para Go. Específicamente: solo se puede usar en una función cuyo último valor de retorno sea el tipo error , y debe aparecer inmediatamente después de una llamada de función cuyo último valor de retorno sea también el tipo error (nota: podríamos permitir cualquier tipo que implemente error también, pero requerir error evita que surja el problema de interfaz nula, lo cual es una buena ventaja). El efecto es que si el valor de error no es nulo, la función en la que aparece ? regresa de la función, estableciendo el último parámetro en el valor de error. Para las funciones que usan valores de retorno con nombre, podría devolver ceros para los otros valores o cualquier valor que esté almacenado actualmente; para las funciones que no lo hacen, los otros valores devueltos son siempre cero.
  • El seguimiento de .?("opening %s", file) funciona como el anterior, excepto que, en lugar de devolver el error sin modificar, se pasa a través de una función que compone los errores; en términos generales, .?(str, vals...) muta el error como fmt.Errorf(str + ": %s", vals..., err)
  • Posiblemente debería haber una versión, ya sea una variante de la sintaxis .? o una diferente, que cubra el caso en el que un paquete desea exportar un tipo de error distinguido.

Relacionado con # 19412 (tipos de suma) y # 21161 (manejo de errores) y # 15292 (genéricos).

Relacionado:

"Borradores de diseños" para nuevas funciones de manejo de errores:
https://go.googlesource.com/proposal/+/master/design/go2draft.md

Comentarios sobre el diseño de errores:
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

Me gusta la sugerencia de @alercah para resolver solo esta característica molesta de go-lang de la que habla @LegoRemix , en lugar de crear un tipo de retorno separado.

Solo sugeriría seguir el RFC de Rust aún más para evitar adivinar valores cero e introducir la expresión catch para permitir que la función especifique explícitamente lo que se devuelve en caso de que el cuerpo principal devuelva un error:

Así que esto:

func generateUser(userID string) (*User, error) {
    siteProperties, err := clients.GetSiteProperties()
    if err != nil {
         return nil, errors.Wrapf(err, "error generating user: %s", userID)
    }

    chatProperties, err := clients.GetChatProperties()
    if err != nil {
         return nil, errors.Wrapf(err, "error generating user: %s", userID)
    }

    followersProperties, err := clients.GetFollowersProperties()
    if err != nil {
         return nil, errors.Wrapf(err, "error generating user: %s", userID)
    }

    return createUser(siteProperties, ChatProperties, followersProperties), nil
}

Se convierte en este código DRY:

func generateUser(userID string) (*User, error) {
    siteProperties := clients.GetSiteProperties()?
    chatProperties := clients.GetChatProperties()?
    followersProperties := clients.GetFollowersProperties()?

    return createUser(siteProperties, ChatProperties, followersProperties), nil
} catch (err error) {
    return nil, errors.Wrapf(err, "error generating user: %s", userID)
}

Y requiere que la función que está usando ? operador también debe definir catch

@bradfitz @peterbourgon @SamWhited ¿ Quizás debería haber otro problema para esto?

@sheerun Su ? y su declaración catch ven muy similares al operador check y la declaración handle en el nuevo diseño de borrador de manejo de errores (https: //go.googlesource.com/proposal/+/master/design/go2draft.md).

Se ve aún mejor, para las personas curiosas, así es como se vería mi código con check y handle :

func generateUser(userID string) (*User, error) {
    handle err { return nil, errors.Wrapf(err, "error generating user: %s", userID) }

    siteProperties := check clients.GetSiteProperties()
    chatProperties := check clients.GetChatProperties()
    followersProperties := check clients.GetFollowersProperties()

    return createUser(siteProperties, chatProperties, followersProperties), nil
}

Lo único que cambiaría es deshacerme de handle implícitos y exigir que se defina si se usa check. Evitará que los desarrolladores utilicen la verificación de forma perezosa y piensen más en cómo manejar o ajustar el error. El retorno implícito debe ser una característica separada y podría usarse como se propuso anteriormente:

func generateUser(userID string) (*User, error) {
    handle err { return _, errors.Wrapf(err, "error generating user: %s", userID) }

    siteProperties := check clients.GetSiteProperties()
    chatProperties := check clients.GetChatProperties()
    followersProperties := check clients.GetFollowersProperties()

    return createUser(siteProperties, chatProperties, followersProperties), nil
}

Como autor de esta propuesta, creo que vale la pena señalar que está efectivamente invalidado por # 15292 y funciona como https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md , ya que esta propuesta fue escrito asumiendo que no hay recursos de programación genéricos disponibles. Como tal, sugiere una nueva sintaxis para permitir el polimorfismo de tipos para el caso especial de result() , y si eso se puede evitar usando, por ejemplo, contratos, creo que esta propuesta ya no tiene sentido.

Dado que parece que es probable que al menos uno de ellos termine en Go 2, me pregunto si esta propuesta en particular debería cerrarse y si las personas todavía están interesadas en un tipo de resultado como alternativa a handle , que se reescriba asumiendo, por ejemplo, que los contratos están disponibles.

(Tenga en cuenta que probablemente no tenga tiempo para hacer ese trabajo, pero si alguien más está interesado en ver esta idea en el futuro, hágalo)

@sheerun el lugar para
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

y / o esta lista completa de _Requisitos a considerar para el manejo de errores de Go 2: _
https://gist.github.com/networkimprov/961c9caa2631ad3b95413f7d44a2c98a

¿Fue útil esta página
0 / 5 - 0 calificaciones