Go: propuesta: Ir 2: eliminar el retorno desnudo

Creado en 3 ago. 2017  ·  52Comentarios  ·  Fuente: golang/go

(No pude encontrar un problema existente para esto, así que ciérrelo si se trata de un duplicado y me lo perdí).

Propongo deshacerme de las ganancias desnudas. Los valores de retorno con nombre son geniales, consérvelos. Los retornos desnudos son más problemáticos de lo que valen, elimínelos.

Go2 LanguageChange NeedsDecision Proposal

Comentario más útil

return nils por todas partes es un poco detallado, ¿no crees?

La simplicidad vence a la verbosidad. Un grupo de return nil es mucho más fácil de entender que un grupo de return donde uno tiene que perseguir el último valor de la variable y asegurarse de que no haya sido sombreado (intencionalmente o por error.)

Todos 52 comentarios

Yo estaría en contra de este cambio, por algunas razones.

1) Es un cambio sintáctico relativamente significativo. Esto podría causar confusión con los desarrolladores y contribuir a llevar a Go al mismo lío por el que pasó Python.

2) No creo que sean inútiles, ¿por qué son más problemas de lo que valen? return nil s todo el lugar es un poco detallado, ¿no crees?

@awnumar para 2 esto combinaría muy bien con # 21182

return nils por todas partes es un poco detallado, ¿no crees?

La simplicidad vence a la verbosidad. Un grupo de return nil es mucho más fácil de entender que un grupo de return donde uno tiene que perseguir el último valor de la variable y asegurarse de que no haya sido sombreado (intencionalmente o por error.)

Tenga en cuenta que las devoluciones desnudas le permiten hacer una cosa que de otro modo no podría lograr: cambiar un valor de devolución en un aplazamiento. Citando la wiki de comentarios de revisión de código :

Finalmente, en algunos casos es necesario nombrar un parámetro de resultado para cambiarlo en un cierre diferido. Eso siempre está bien.

Una propuesta completa para eliminar las declaraciones desnudas debe explicar cómo se deben escribir dichos usos y por qué la forma alternativa es preferible (o al menos aceptable). Las reescrituras obvias tienden a involucrar muchas repeticiones y repetición, y aunque generalmente involucran el manejo de errores, el mecanismo es general.

No creo que esto

func oops() (string, *int, string, error) {
    ...
    return "", nil, "", &SomeError{err}
}

es más legible que

func oops() (rs1 string, ri *int, rs2 string, e error) {
    ...
    e = &SomeError{err}
    return
}

,perdón.

OTOH: sí, la sombra apesta. No tendría sentido prohibirlo por completo (me viene a la mente el código generado), pero con mucho gusto votaría por al menos la prohibición del sombreado de nombres de valores de retorno. Ah, y hacer que el compilador genere algún diagnóstico en todos los demás casos también sería bueno :)

Editar: aparentemente hay # 377 que habla de eso

@josharian Estaba asumiendo que las funciones diferidas aún podrían modificar las variables de retorno. Esa semántica parece más o menos ortogonal a la sintaxis de la declaración de retorno.

@josharian Necesita parámetros de retorno con nombre para poder modificarlos en una función diferida. No es necesario utilizar un retorno desnudo para eso. return con valores explícitos copiará los valores en los valores devueltos nombrados antes de que se ejecuten las funciones diferidas.

@bcmills @dominikh cierto, tonta de mí. Lapso mental; Gracias por corregirme.

Una función sin valores de retorno debería poder "desnudar" return :

func noReturn() {
    if !someCondition() {
        return // bail out
    }
    // Happy path
}

Pasé 1,5 minutos mirando el siguiente código de Go en la biblioteca estándar de Go (de hace 4 años , / cc @bradfitz), sin poder creerlo, pensando que podría haber un error grave:

https://github.com/golang/go/blob/2d69e9e259ec0f5d5fbeb3498fbd9fed135fe869/src/net/http/server.go#L3158 -L3166

Específicamente, esta parte:

tc, err := ln.AcceptTCP()
if err != nil {
    return
}

A primera vista, me pareció que en la primera línea allí, las nuevas variables tc y err se estaban declarando utilizando una sintaxis de declaración de variable corta, distinta de la llamada err error return variable. Entonces, me pareció que el retorno simple devolvía efectivamente nil, nil lugar de nil, err como debería haber sido.

Después de unos 90 segundos de pensarlo mucho, me di cuenta de que en realidad es el código correcto. Dado que el valor de retorno nombrado err error está en el mismo bloque que el cuerpo de la función, la declaración de variable corta tc, err := ... solo declara tc como una nueva variable pero no declara un nuevo err variable, por lo que la llamada err error return variable se establece mediante la llamada a ln.AcceptTCP() , por lo que el return realidad devuelve un error no nulo, ya que deberían.

(Si hubiera estado en un nuevo bloque, en realidad sería un error de compilación "err se sombrea durante el retorno", ver aquí ).

Creo que este habría sido un código mucho más claro y legible:

tc, err := ln.AcceptTCP()
if err != nil {
    return nil, err
}

Quería compartir esta historia porque creo que es un buen ejemplo de ganancias simples que hacen perder el tiempo al programador. En mi opinión, los retornos simples son marginalmente más fáciles de escribir (solo se ahorran algunas pulsaciones de teclas), pero a menudo conducen a un código que es más difícil de leer en comparación con el código equivalente que usa retornos completos (no desnudos). Go generalmente hace lo correcto al optimizar la lectura, ya que eso se hace con mucha más frecuencia (por más personas) que escribir. Me parece que la función de devoluciones simples tiende a reducir la legibilidad, por lo que podría darse el caso de que eliminarla en Go 2 mejoraría el lenguaje.

Descargo de responsabilidad: en el código Go que escribo y leo con mayor frecuencia, tiendo a evitar tener devoluciones simples (porque creo que son menos legibles). Pero eso significa que tengo menos experiencia en la lectura / comprensión de los retornos simples, lo que podría influir negativamente en mi capacidad para leerlos / analizarlos. Es un poco de trampa 22.

@shurcooL , ejemplo interesante que también me tc y c . Pensé que cometiste un error al copiar y pegar el código y en lugar de c debería haber tc . Me tomó casi el mismo tiempo darme cuenta de lo que hace ese código.

Del artículo:

Tenga en cuenta que si no le gusta o prefiere el retorno desnudo que ofrece Golang, puede usar return oi mientras obtiene el mismo beneficio, así:

¡Los valores de retorno con nombre son geniales! El problema es el retorno puro de valores devueltos con nombre. :-)

@awnumar , eso no es relevante. Vea mi comentario sobre Hacker News: https://news.ycombinator.com/item?id=14668595

Creo que eliminarlos seguiría el enfoque de manejo explícito de errores. Yo no los uso.

El código que he revisado en golang-nuts con retornos simples no es difícil de entender, pero analizar el alcance variable es un esfuerzo adicional innecesario para los lectores.

Realmente amo el retorno desnudo, especialmente cuando una función tiene múltiples retornos. Solo hace que el código sea mucho más limpio. La razón principal por la que elegí trabajar con go.

Go tool vet shadow puede detectar posibles vars sombreadas. Si una función tiene menos complejidad ciclomática, además de algunos casos de prueba. No pude ver que tendría algún problema. Podría estar equivocado, pero deseo ver algunos ejemplos para demostrar lo malo que podría ser el retorno al desnudo.

Deseo ver algunos ejemplos para demostrar lo malo que podría ser el retorno al descubierto.

@kelwang ¿ ln.AcceptTCP() de los 7 comentarios anteriores?

Hola, @shurcooL , gracias

Sí, creo que hiciste un gran punto.
Pero como dijiste, han pasado 4 años. Tengo la sensación de que quizás ya te hayas acostumbrado.

Creo que no es realmente un problema para el retorno al descubierto. Pero una confusión en la inicialización de varias vars.

Para mí, la herramienta shadow vet suele funcionar muy bien. Realmente nunca me preocupo por eso.
Tal vez deberíamos llenar un boleto en Go linter para evitar ese tipo de confusión. ya sea que cambie el nombre del error o declare tc en la parte superior primero. Siento que una sugerencia de linter debería ser lo suficientemente buena.

@kelwang En mi opinión, si una función tiene múltiples retornos hasta el punto en que las declaraciones de retorno se están poniendo feas, una estructura / puntero a una estructura debería devolverse en su lugar sobre un retorno simple.

Dado que se ha mencionado el # 377, yo diría que la fuente de confusión en el ejemplo ln.AcceptTCP tiene más que ver con la magia detrás de := que con el retorno en sí mismo.

Creo que el caso ln.AcceptTCP no sería tan malo con una forma más explícita de declaración corta ( propuesta en el número de referencia):

func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
    :tc, err = ln.AcceptTCP()
    if err != nil {
        return
    }
    // ...
}

Con solo mirar una variable := no puede saber qué variables se están declarando: debe tener en cuenta toda la parte anterior del bloque para saberlo.
Un descuido sobre dónde está el límite del bloque y puede terminar con un error difícil de encontrar.
Además, no puede arreglar una variable := para que declare exactamente lo que desea; se ve obligado a renunciar a la declaración corta o reorganizar el código.

He visto muchas propuestas que intentan abordar las consecuencias específicas de esto, pero creo que la raíz del problema es solo la falta de claridad de := (también diría que, siempre que el sombreado se considere un error, es solo culpa de varias variables := ).

No estoy diciendo que los retornos simples valgan necesariamente la pena, solo digo que lo que estamos viendo es un problema compuesto.

Independientemente de los argumentos de legibilidad, creo que el resultado simple es menos coherente y elegante desde el punto de vista lógico. De alguna manera es un punto de vista inestable. Aunque, obviamente, no soy el único que es subjetivo aquí.

@tandr Creo que devolver un objeto de estructura pequeño es mucho más claro que tener más de tres valores de retorno.

Alternativa a un retorno puramente simple, aunque debatido y abandonado en el # 21182 a favor del # 19642:

func f() (e error) {
   return ... // ellipsis trigram
}

Si bien estoy a favor de eliminarlo en Go2, hice # 28160 para (esencialmente) eliminarlo en Go1 a través de gofmt . ¡Los comentarios de la gente serán apreciados! 😄

He descubierto que los retornos desnudos son sorprendentemente útiles en la capa DB.

Este es un código de producción redactado real -

func (p *PostgresStore) GetSt() (st St, err error) {
    var tx *sql.Tx
    var rows *sql.Rows
    tx, err = p.handle.Begin()
    if err != nil {
        return
    }
    defer func() {
        if err != nil {
            tx.Rollback()
        } else {
            tx.Commit()
        }
        rows.Close()
    }()

    rows, err = tx.Query(`...`)
    if err != nil {
        return
    }
    // handle rows
    for rows.Next() {
        var all Call
        err = rows.Scan(&all.Email, &all.Count)
        if err != nil {
            return
        }
        st.Today = append(st.Today, &all)
    }

    rows.Close()
    rows, err = tx.Query(`...`)
    if err != nil {
        return
    }
    // handle rows
    for rows.Next() {
        var all Call
        err = rows.Scan(&all.Email, &all.Count)
        if err != nil {
            return
        }
        st.Books = append(st.Books, &all)
    }
    return
}

Aquí hay 6 declaraciones de devolución. (En realidad, hay 2 más, acabo de acortar el código por brevedad). Pero mi punto es que cuando ocurre un error, la persona que llama solo inspeccionará la variable err . Si tuviera que escribir return err , todavía estaría bien, pero para que coincida con la firma de devolución, tengo que escribir return st, err una y otra vez. Y esto crece linealmente si tiene más variables para devolver; su declaración de devolución sigue creciendo más y más.

Mientras que, si hay parámetros de retorno con nombre, simplemente llamo a return, sabiendo que también devolverá cualquier otra variable en cualquier estado en el que estuvieran. Esto me hace feliz y lo considero una gran característica.

@agnivade no está seguro si esto se debe a que está redactado, pero parece que su código de producción entraría en pánico en el aplazamiento si las filas son nulas, lo que ocurriría si la consulta falla.

@agnivade cinco de sus seis devoluciones deberían evaporarse bajo el próximo esquema de manejo de errores de Go2 :-)

Más información en la wiki de comentarios .

@nhooyr -

@networkimprov - Eso es cierto. No había pensado en el nuevo manejo de errores. Personalmente, estaría bien con esto _cuando_ aterrice el nuevo manejo de errores. Pero incluso entonces, creo que este es un cambio bastante importante que seguramente romperá muchas bases de código. Podría decirse que podemos solucionar este problema con herramientas automatizadas. Pero me pregunto cuántos de estos cambios importantes deberían realizarse para Go 2.

Go2 tiene licencia para no ser compatible con versiones anteriores de Go1, ¿no es así?

Estoy de acuerdo en que se podrían crear herramientas para actualizar el código Go1 existente para que sea compatible con este cambio en Go2.

Go2 tiene licencia para no ser compatible con versiones anteriores de Go1, ¿no es así?

Es casi seguro que no usaremos dicha licencia. Romper la compatibilidad con versiones anteriores es increíblemente difícil y arriesgado desde el punto de vista del ecosistema.

Si los cambios importantes no están planeados actualmente, estoy de acuerdo en que esta propuesta no vale la pena causar un descanso. Espero que consideremos la propuesta si / cuando se planean cambios importantes. Actualizar go fix para acomodar esta propuesta debería ser trivial.

Hay nuevas palabras clave sobre la mesa para el manejo de errores de Go2, y eso rompería algún código.

Para Go 2 podemos romper el código cuando vale la pena, pero conlleva un alto costo. El costo de una nueva palabra clave es menor que el código de eliminar una característica del idioma, ya que es sencillo de diagnosticar y corregir. Es posible imaginar que el beneficio de la nueva palabra clave excedería el costo de tener que reescribir ligeramente el código que usa un identificador que es el mismo que la palabra clave.

Este caso es más difícil de reescribir que una nueva palabra clave, pero es factible. Creo que la pregunta más importante es: ¿la gente malinterpreta lo que significa cuando escribe una declaración desnuda? ¿Hay errores reales en los programas debido al uso de un retorno simple? Incluso si no hay errores reales, cuando las personas escriben código, ¿cometen errores al usar un retorno desnudo incorrectamente?

¿Hay errores reales en los programas debido al uso de un retorno simple?

No estoy seguro de si causa errores de producción, pero para mí, cuando veo un retorno desnudo, generalmente me hace detenerme para tratar de averiguar qué está sucediendo. Me vuelve paranoico acerca de los err sombreados que trabajan de una manera inesperada porque no estoy 100% seguro de mi comprensión de cómo interactúan. Hace que la lectura del código sea mucho más lenta.

Dicho esto, creo que # 28160 es probablemente una mejor solución, ya que es compatible con versiones anteriores y gofmt ya ha tenido varios cambios menores.

Me vuelvo paranoico acerca de que el error en la sombra funciona de una manera inesperada

Recientemente descubrí que en realidad no necesitas preocuparte por el sombreado tanto como pensaba , al menos no en términos de que cause errores. En realidad, es un error en tiempo de compilación usar un retorno simple si alguna de las variables de retorno está sombreada en el punto en que ocurre el return .

Y antes de que alguien diga algo, sí, sé que hay muchos otros problemas con ese ejemplo. Solo estaba lanzando algo para demostrarlo.

28160 se ha cerrado, lo que me parece que cierra la puerta para solucionar este problema en Go1.

Bueno, este tipo de cambio nunca iba a suceder en Go 1 de todos modos, sin importar cómo uno lo abordara.

Quizás, quizás, el problema esté en := , y deberíamos tener una propuesta para eliminarlo.

No se trata solo de sombrear. Se trata de hacer que el lector lleve más contexto del necesario.

Usando el ejemplo de código aquí: https://github.com/golang/go/issues/21291#issuecomment -320120637

En el primer ejemplo, el lector sabe exactamente lo que se devuelve con solo tener que rastrear el valor de err .

En el segundo ejemplo, el lector necesitaría rastrear los valores de tres variables adicionales.

Dicho de otra manera

if err != nil {
    return err
}
...
return err

vs

if err != nil {
    return err
}
...
return nil

(devolviendo una variable con un valor nil vs devolviendo nil)

¿Cual prefieres?

Prefiero este último porque es explícito y más fácil de entender a primera vista. Eliminar los retornos simples alentaría valores de retorno explícitos en lugar de alentar valores de retorno que deben deducirse.

Otro ejemplo de la carga cognitiva es que no se puede distinguir una función nula ( func(arg T) ) de una función no nula ( func(arg T) ret R ) cuando observa un retorno simple. Puede ser que la función no tenga valores de retorno o podría ser que tenga valores con nombre que se estén devolviendo. Debe recordar la firma de la función para saber qué significa realmente la devolución. No es un gran problema, sin duda, pero es una cosa más para tener en cuenta.

Las declaraciones de declaraciones simples pueden ser confusas en algunos casos. Pero también hay casos concretos en los que resultan útiles. Y, por supuesto, funcionan hoy, lo que es un fuerte argumento para mantenerlos.

Es probable que la mejor solución aquí sea mantener los retornos en el idioma, pero considere agregar una verificación de pelusa que advierte sobre su uso en casos complejos en los que se produce el sombreado.

Este es mi pensamiento exacto cuando empiezo a leer un código base de Go.
La versión corta debería ser buena para los piratas informáticos que quieren codificar rápido, hacer trucos con la sintaxis aquí y allá y envenenar la computadora víctima.
Pero en términos de aplicación empresarial, el ahorro de pocas pulsaciones de teclas puede costarle millones para detectar

Editar: Quiero aclarar que mi sugerencia es deshabilitar el retorno simple para las funciones que devuelven valor y dejar el retorno desnudo para la función nula tal como está.

Así que encontré un código de muestra sin retorno de 2017. Con go 1.12.5 (windows / amd64), esto está marcado en go build como un retorno faltante. ¿Es este el mismo problema? ¿Se permitían retornos implícitos en el pasado y ahora son inválidos? No pude arreglarlo con una simple devolución. El tipo era uintptr, así que devolví 0 (nill no lint).

@datatribe , no puedo pensar en ningún cambio que cambie eso. Ha sido un error perder una devolución. ¿Presentar un nuevo error con los detalles?

@datatribe , no puedo pensar en ningún cambio que cambie eso. Ha sido un error perder una devolución. ¿Presentar un nuevo error con los detalles?

Es muy posible que el código de ejemplo no fuera bueno. Gracias @bradfitz

No estoy seguro de si causa errores de producción, pero para mí, cuando veo un retorno desnudo, generalmente me hace detenerme para tratar de averiguar qué está sucediendo. Me vuelve paranoico acerca de los err sombreados que trabajan de una manera inesperada porque no estoy 100% seguro de mi comprensión de cómo interactúan. Hace que la lectura del código sea mucho más lenta.

Estoy de acuerdo contigo en eso. Me hace detenerme para tratar de averiguar qué valores se asignan a las variables de parámetro nombradas. Mi incomodidad con las funciones de lectura (especialmente largas) con retornos desnudos es que no puedo seguir fácilmente lo que realmente se devuelve. Cada devolución desnuda en realidad significa return p1, p2, ..., pn para parámetros con nombre, pero a veces los valores de algunos parámetros son obviamente literales como return nil, nil , return nil, err , return "", err y return p, nil lugar de return p, err . Si escribo return nil, err , err aún podría ser nil valor, pero está claro que el primer valor de retorno es nil . Si no se permite el retorno desnudo, es más probable que el escritor escriba valores literales en la declaración de retorno si eso es posible. En realidad, todos usaron valores literales, cuando les pedí que evitaran devoluciones desnudas durante las revisiones del código.

Cuando los escritores escriben con declaraciones desnudas, tienden a no pensar en lo que devuelven. Cuando les pedí que evitaran las devoluciones desnudas, ahora piensan en el valor real que quieren devolver y notan que devuelven valores incorrectos, como valores parcialmente construidos. ¿No es mejor ser explícito que implícito?

Sé que se recomienda sangrar los flujos de error, pero no todos siguen este patrón en la realidad, especialmente las personas a las que les gusta escribir funciones largas con muchos retornos desnudos. A veces, es difícil saber si se trata de un flujo de error o no al mirar muchas declaraciones desnudas. Leí el código cuidadosamente, lo decodifiqué y convertí los retornos desnudos en no desnudos con algunos valores literales, entonces es mucho más fácil de leer. (a veces no es posible averiguar los valores literales si el código es complejo)

Sé que el bloque de comentarios de documentación menciona lo que devuelve para algunas condiciones significativas, pero no todos lo hacen. Con la declaración de retorno con algunos valores literales, puedo seguirlo fácilmente mirando el código.

Sé que podemos usar parámetros con nombre y retornos no desnudos juntos, pero no todos lo saben. A menudo veo algo como lo siguiente, y creo que return io.EOF es mejor.

err = io.EOF
return

Dicho esto, creo que # 28160 es probablemente una mejor solución, ya que es compatible con versiones anteriores y gofmt ya ha tenido varios cambios menores.

No estoy de acuerdo con la otra propuesta de que gofmt debería devolverlos, incluso si ese es el trabajo de gofmt . Eso es porque simplemente poner un montón de return p1, ..., pn mecánicamente no hará que el código sea más comprensible.

Es posible que las herramientas de pelusa puedan verificarlo ( nakedret hace eso), pero en mi opinión, la devolución al desnudo tiene más daños que beneficios. También estoy de acuerdo en que el beneficio de eliminar el retorno desnudo puede ser menor que el daño que causa la ruptura de la compatibilidad. Pero supongo que es fácil de arreglar con go fix incluso si los go fix mecánicos no hacen que el código sea legible.

Go es un lenguaje muy claro y legible en mi opinión. Y el retorno desnudo es una pequeña parte del lenguaje que me dificulta entender el código.

El cambio https://golang.org/cl/189778 menciona este problema: cmd/go/internal/get: propagate parse errors in parseMetaGoImports

Puede que sea una minoría, pero en realidad me gustaría proponer lo contrario. Las funciones con devoluciones con nombre siempre deben usar devoluciones simples. De lo contrario, parece que el código está mintiendo. Usted asigna valores y luego potencialmente los sobrescribe silenciosamente al regresar. Aquí hay un fragmento de código incorrecto para ilustrar lo que quiero decir,

func hello() (n, m int) {
    n = 2
    m = 3
    return m, n
}

Si solo usa un retorno simple allí, el código tiene sentido nuevamente.

@millerlogic Creo que sería demasiado agresivo. Por ejemplo, no hay nada de malo en un código como:

func documentedReturns() (foo, bar string, _ error) {
    [...]
    if err != nil {
        return "", "", fmt.Errorf(...)
    }
}

También creo que su punto podría encajar mejor como una contrapropuesta separada, ya que está sugiriendo lo contrario de lo que presenta la propuesta actual.

@mvdan Buen punto, creo que hay un término medio que no implica demonizarlos. Creo que leí una idea similar a: no devolver explícitamente un valor si previamente asignó la variable de retorno. Incluso podría implementarse en algún lugar, pero nunca lo acerté. Entonces eso significaría que mi ejemplo de hola produciría un error sin un retorno desnudo, y su ejemplo tendría éxito porque no se asignaron foo o bar.

Creo que deberían eliminarse de la gira de Go, de esa forma no se anima a los desarrolladores de Go más nuevos a utilizarlos. En la práctica, son solo un impuesto cognitivo. Los pocos que he visto siempre requirieron que mi cerebro dijera "¡¿Espera qué?! ... oh sí, devoluciones desnudas y valores de devolución con nombre". Entiendo que quizás dejarlos en el recorrido para que todos sepan, pero hay una página completa dedicada a ellos. .

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