Go: proposta: Go 2: remova o retorno básico

Criado em 3 ago. 2017  ·  52Comentários  ·  Fonte: golang/go

(Não consegui encontrar um problema existente para isso, então feche se for uma duplicata e eu não entendi.)

Eu proponho me livrar dos retornos vazios. Os valores de retorno nomeados são ótimos, mantenha-os. Retornos em bruto são mais problemas do que valem, elimine-os.

Go2 LanguageChange NeedsDecision Proposal

Comentários muito úteis

return nils em todos os lugares é um pouco prolixo, não acha?

Simplicidade vence verbosidade. Um monte de return nil são muito mais fáceis de entender do que um monte de return onde é preciso perseguir o último valor da variável e ter certeza de que não foi obscurecido (intencionalmente ou por erro.)

Todos 52 comentários

Eu seria contra essa mudança, por alguns motivos.

1) É uma mudança sintática relativamente significativa. Isso poderia causar confusão com os desenvolvedores e contribuir para levar Go à mesma confusão pela qual o Python passou.

2) Não acho que sejam inúteis, por que dão mais trabalho do que valem? return nil s em todo lugar é um pouco prolixo, não acha?

@awnumar para 2, isso combinaria bem com # 21182

return nils em todos os lugares é um pouco prolixo, não acha?

Simplicidade vence verbosidade. Um monte de return nil são muito mais fáceis de entender do que um monte de return onde é preciso perseguir o último valor da variável e ter certeza de que não foi obscurecido (intencionalmente ou por erro.)

Observe que os retornos brutos permitem que você faça uma coisa que não poderia realizar de outra forma: alterar um valor de retorno em um adiamento. Citando o wiki de comentários de revisão de código :

Finalmente, em alguns casos, você precisa nomear um parâmetro de resultado para alterá-lo em um encerramento adiado. Isso está sempre bem.

Uma proposta completa para eliminar as devoluções a descoberto deve explicar como tais usos devem ser redigidos e por que a forma alternativa é preferível (ou pelo menos aceitável). As reescritas óbvias tendem a envolver muitos clichês e repetição e, embora geralmente envolvam tratamento de erros, o mecanismo é geral.

Eu não acho isso

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

é mais legível do que

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

,desculpa.

OTOH - sim, sombreamento é uma merda. Não faria sentido proibi-lo completamente (o código gerado vem à mente), mas eu votaria com prazer em pelo menos a proibição de sombreamento de nomes de valores de retorno. Ah, e fazer o compilador gerar algum diagnóstico em todos os outros casos também seria bom :)

Edit: aparentemente há # 377 que fala sobre isso

@josharian eu estava assumindo que funções adiadas ainda poderiam modificar as variáveis ​​de retorno. Essa semântica parece mais ou menos ortogonal à sintaxe da instrução return.

@josharian Você precisa de parâmetros de retorno nomeados para poder modificá-los em uma função adiada. Você não precisa usar um retorno nu para isso. return com valores explícitos irá copiar os valores para os valores de retorno nomeados antes da execução das funções adiadas.

@bcmills @dominikh certo, boba eu. Lapso mental; obrigado por me esclarecer.

Uma função sem valores de retorno ainda deve ser capaz de "limpar" return :

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

Passei 1,5 minutos olhando para o seguinte código Go na biblioteca padrão Go (de 4 anos atrás , / cc @bradfitz), sem acreditar, pensando que pode haver um bug ruim:

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

Especificamente, esta parte:

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

À primeira vista, pareceu-me que na primeira linha ali, novas variáveis tc e err estavam sendo declaradas usando a sintaxe de declaração de variável curta, distinta do retorno err error variável. Então, pareceu-me que o retorno simples estava efetivamente retornando nil, nil em vez de nil, err como deveria.

Após cerca de 90 segundos pensando muito sobre isso, percebi que na verdade é o código correto. Uma vez que o valor de retorno err error nomeado está no mesmo bloco que o corpo da função, a declaração de variável curta tc, err := ... apenas declara tc como uma nova variável, mas não declara um novo err variável, portanto, a variável de retorno err error nomeada está sendo definida pela chamada para ln.AcceptTCP() , portanto, o return nua realmente retorna um erro não nulo. deve.

(Se estivesse em um novo bloco, seria na verdade um erro de compilação "err é sombreado durante o retorno", veja aqui .)

Eu acho que isso teria sido um código muito mais claro e legível:

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

Eu queria compartilhar essa história porque acho que é um bom exemplo de retornos simples que desperdiçam o tempo do programador. Em minha opinião, os retornos simples são marginalmente mais fáceis de escrever (apenas economizando alguns pressionamentos de tecla), mas geralmente levam a um código que é mais difícil de ler em comparação com o código equivalente que usa retornos completos (não básicos). Go geralmente faz a coisa certa ao otimizar a leitura, já que isso é feito com muito mais frequência (por mais pessoas) do que escrever. Parece-me que o recurso de retornos simples tende a diminuir a legibilidade, então pode ser que removê-lo no Go 2 tornaria a linguagem melhor.

Isenção de responsabilidade: no código Go, eu escrevo e leio com mais frequência, tendo a evitar retornos vazios (porque acho que eles são menos legíveis). Mas isso significa que tenho menos experiência em ler / compreender retornos simples, o que pode influenciar negativamente minha capacidade de lê-los / analisá-los. É um pouco complicado.

@shurcooL , exemplo interessante que também me tc e c . Achei que você cometeu um erro ao copiar e colar o código e, em vez de c , deveria haver tc . Levei quase o mesmo tempo para perceber o que aquele código faz.

Do artigo:

Observe que se você não gosta ou prefere o retorno puro que a Golang oferece, você pode usar return oi e ainda obter o mesmo benefício, assim:

Os valores de retorno nomeados são ótimos! O retorno puro de valores de retorno nomeados é o problema. :-)

@awnumar , isso não é relevante. Veja meu comentário no Hacker News: https://news.ycombinator.com/item?id=14668595

Acho que removê-los seguiria a abordagem de tratamento de erros explícito. Eu não os uso.

O código que analisei em golang-nuts com retornos simples não é difícil de entender, mas analisar o escopo da variável é um esforço adicional desnecessário para os leitores.

Eu realmente amo o retorno à vista, especialmente quando uma função tem vários retornos. Só está tornando o código muito mais limpo. O maior motivo, escolhi trabalhar com go.

A sombra do veterinário da ferramenta Go pode detectar possíveis vars sombreados. Se uma função tiver menos complexidade ciclomática, mais alguns casos de teste. Não pude ver que causaria problemas. Posso estar errado, mas gostaria de ver alguns exemplos para demonstrar o quão ruim o retorno à vista pode ser.

Desejo ver alguns exemplos para demonstrar como o retorno à vista pode ser ruim.

@kelwang Você viu meu exemplo sobre ln.AcceptTCP() dos 7 comentários acima?

Olá, @shurcooL , thx

Sim, acho que você fez um ótimo ponto.
Mas, como você disse, já se passaram 4 anos. Tenho a sensação de que talvez você já tenha se acostumado com isso.

Eu acho que não é realmente um problema para o retorno descoberto. Mas uma confusão na inicialização de vários vars.

Para mim, a ferramenta shadow vet geralmente funciona muito bem. Eu nunca realmente me preocupo com isso.
Talvez devêssemos preencher um tíquete no linter para evitar esse tipo de confusão. renomeie o erro ou declare tc no topo primeiro. Acho que uma sugestão de linter deve ser boa o suficiente.

@kelwang Em minha opinião, se uma função tem múltiplos retornos ao ponto onde as declarações de retorno estão ficando feias, um struct / ponteiro para um struct deve ser retornado em vez de um retorno simples.

Visto que # 377 foi mencionado, eu diria que a fonte de confusão no exemplo ln.AcceptTCP é mais sobre a mágica por trás de := do que o retorno em si.

Acho que o caso ln.AcceptTCP não seria tão ruim com uma forma mais explícita de declaração curta ( proposta no problema mencionado):

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

Olhando apenas para uma multivariável := você não pode dizer quais variáveis ​​estão sendo declaradas: você precisa levar em consideração toda a parte anterior do bloco para saber isso.
Um descuido sobre onde está o limite do bloco e você pode acabar com um bug difícil de encontrar.
Além disso, você não pode corrigir uma multivariável := para fazê-la declarar exatamente o que você deseja; você é forçado a desistir da declaração curta ou reorganizar o código.

Já vi muitas propostas tentando abordar consequências específicas disso, mas acho que a raiz do problema é apenas a falta de clareza de := (eu também diria que, sempre que o sombreamento é considerado uma armadilha, é realmente apenas uma falha multivariável de := ).

Não estou dizendo que os retornos vazios necessariamente valem a pena, estou apenas dizendo que o que estamos vendo é um problema composto.

Independentemente dos argumentos de legibilidade, acho que o retorno básico é menos logicamente consistente e elegante. É de alguma forma um ponto de vista instável. Embora, obviamente, eu não seja o único sendo subjetivo aqui.

@tandr Acho que retornar um pequeno objeto de estrutura é muito mais claro do que ter mais de três valores de retorno.

Alternativa para um retorno puramente simples, embora debatido e abandonado em # 21182 em favor de # 19642:

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

Embora eu seja a favor de removê-lo do Go2, fiz # 28160 para (essencialmente) removê-lo do Go1 por meio de gofmt . Comentários de pessoal lá seriam apreciados! 😄

Eu descobri que os retornos nus são surpreendentemente úteis na camada DB.

Este é um código de produção real editado -

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
}

Existem 6 declarações de retorno aqui. (Na verdade, existem mais 2, apenas abreviei o código para abreviar). Mas o que quero dizer é que, quando ocorre um erro, o chamador apenas inspeciona a variável err . Se eu apenas tivesse que escrever return err , ainda estaria bem, mas para coincidir com a assinatura de retorno, tenho que escrever return st, err repetidamente. E isso cresce linearmente se você tiver mais variáveis ​​para retornar; sua declaração de retorno está crescendo cada vez mais.

Ao passo que, se houver parâmetros de retorno nomeados, apenas chamo return, sabendo que ele também retornará quaisquer outras variáveis ​​em qualquer estado em que estavam. Isso me deixa feliz e considero um ótimo recurso.

@agnivade não tenho certeza se isso é porque foi editado, mas parece que seu código de produção entraria em pânico no adiamento se as linhas fossem nulas, o que ocorreria se a consulta falhasse.

@agnivade cinco de seus seis retornos devem evaporar sob o próximo esquema de tratamento de erros Go2 :-)

Mais no wiki de feedback .

@nhooyr - Nossa .. parece que estamos dando um empurrãozinho hoje ..: man_facepalming:: sweat_smile:

@networkimprov - Isso é verdade. Eu não tinha pensado no novo tratamento de erros. Pessoalmente, eu ficaria bem com isso _quando_ o novo tratamento de erros ocorrer. Mas, mesmo assim, acho que essa é uma mudança muito importante que está fadada a quebrar muitas bases de código. Sem dúvida, podemos consertar isso com ferramentas automatizadas. Mas eu me pergunto quantas dessas mudanças importantes devem ser feitas para o Go 2.

Go2 recebeu licença para não ser compatível com Go1, não é?

Concordo que o conjunto de ferramentas pode ser feito para atualizar o código Go1 existente para ser compatível com essa mudança no Go2.

Go2 recebeu licença para não ser compatível com Go1, não é?

É quase certo que não usaremos essa licença. Romper a compatibilidade com versões anteriores é incrivelmente difícil e arriscado no que diz respeito ao ecossistema.

Se as principais alterações não estiverem planejadas no momento, concordo que não vale a pena interromper esta proposta. Espero que consideremos a proposta se / quando mudanças importantes forem planejadas. Atualizar go fix para acomodar esta proposta deve ser trivial.

Novas palavras-chave estão disponíveis para tratamento de erros do Go2 e isso quebraria alguns códigos.

Para Go 2, podemos quebrar o código quando vale a pena, mas acarreta um alto custo. O custo de uma nova palavra-chave é menor do que o código de remoção de um recurso de idioma, pois é fácil de diagnosticar e corrigir. É possível imaginar que o benefício da nova palavra-chave excederia o custo de ter que reescrever um pouco o código que usa um identificador igual ao da palavra-chave.

Este caso é mais difícil de reescrever do que uma nova palavra-chave, mas é viável. Acho que a questão mais importante é: as pessoas entendem mal o que significa quando escrevem uma declaração nua? Existem erros reais nos programas devido ao uso de um retorno simples? Mesmo se não houver erros reais, quando as pessoas estão escrevendo código, elas cometem erros ao usar um retorno simples incorretamente?

Existem erros reais nos programas devido ao uso de um retorno simples?

Não tenho certeza se isso causa bugs de produção, mas para mim, quando vejo o nu retornar, geralmente me faz parar para tentar descobrir o que está acontecendo. Fico paranóico com a ideia de que err sombreados estão trabalhando de maneira inesperada, porque não estou 100% confiante em meu entendimento de como eles interagem. Isso torna a leitura do código muito mais lenta.

Dito isso, acho que # 28160 é provavelmente uma solução melhor, pois é compatível com versões anteriores e o gofmt já teve várias pequenas alterações.

Eu fico paranóico sobre o erro sombrio trabalhando de uma forma inesperada

Eu descobri recentemente que você realmente não precisa se preocupar com o sombreamento tanto quanto eu pensava , pelo menos não em termos de causar bugs. Na verdade, é um erro em tempo de compilação usar um retorno simples se qualquer uma das variáveis ​​de retorno estiver sombreada no ponto em que return acontece.

E antes que alguém diga qualquer coisa, sim, eu sei que há muitos outros problemas com esse exemplo. Eu estava apenas jogando algo para demonstrar.

28160 foi fechado, o que me parece fechar a porta para consertar isso no Go1.

Bem, esse tipo de mudança nunca aconteceria no Go 1 de qualquer maneira, não importa como alguém o abordasse.

Talvez, talvez, o problema esteja em := , e devemos ter uma proposta para removê-lo.

Não se trata apenas de sombreamento. É fazer com que o leitor carregue mais contexto do que o necessário.

Usando o exemplo de código aqui: https://github.com/golang/go/issues/21291#issuecomment -320120637

No primeiro exemplo, o leitor sabe exatamente o que é retornado, bastando rastrear o valor de err .

No segundo exemplo, o leitor precisaria rastrear os valores de três variáveis ​​adicionais.

Dito de outra forma

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

vs

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

(retornando uma variável com um valor nulo vs retornando nulo)

Qual você prefere?

Prefiro o último porque é explícito e mais fácil de entender à primeira vista. Remover retornos vazios encorajaria valores de retorno explícitos em vez de encorajar valores de retorno que devem ser deduzidos.

Outro exemplo da carga cognitiva é que você não pode diferenciar uma função nula ( func(arg T) ) de uma função não nula ( func(arg T) ret R ) quando você olha para um retorno simples. Pode ser que a função não tenha valores de retorno ou pode ser que ela tenha valores nomeados que estão sendo retornados. Você tem que lembrar a assinatura da função para saber o que o retorno realmente significa. Não é um grande problema, com certeza, mas é mais uma coisa para manter em sua cabeça.

As instruções de retorno simples podem ser confusas em alguns casos. Mas também existem casos específicos em que são úteis. E, é claro, eles funcionam hoje, o que é um forte argumento para mantê-los.

A melhor solução aqui é provavelmente manter os retornos vazios na linguagem, mas considere adicionar uma verificação de lint que avisa sobre o uso em casos complexos nos quais ocorre sombreamento.

Este é o meu pensamento exato ao começar a ler uma base de código Go.
A versão curta deve ser boa para hackers que desejam codificar rapidamente, fazer truques com a sintaxe aqui e ali e envenenar o computador vítima.
Mas, em termos de aplicação empresarial, a economia de poucos toques no teclado pode custar milhões para detectar

Edit: Quero esclarecer que minha sugestão é desativar o retorno básico para funções que retornam valor e deixar o retorno básico para a função nula como está.

Portanto, encontrei alguns códigos de amostra sem retorno de 2017. Com o go 1.12.5 (windows / amd64), isso é sinalizado no go build como um retorno ausente. Este é o mesmo problema? As devoluções implícitas eram permitidas no passado e agora são inválidas? Eu não poderia consertar com um simples retorno. O tipo era uintptr, então retornei 0 (nill não lint).

@datatribe , não consigo pensar em nenhuma mudança que mudaria isso. Tem sido um erro perder um retorno. Arquivar um novo bug com os detalhes?

@datatribe , não consigo pensar em nenhuma mudança que mudaria isso. Tem sido um erro perder um retorno. Arquivar um novo bug com os detalhes?

É perfeitamente possível que o código de exemplo não seja bom. Obrigado @bradfitz

Não tenho certeza se isso causa bugs de produção, mas para mim, quando vejo o nu retornar, geralmente me faz parar para tentar descobrir o que está acontecendo. Fico paranóico com a ideia de que err sombreados estão trabalhando de maneira inesperada, porque não estou 100% confiante em meu entendimento de como eles interagem. Isso torna a leitura do código muito mais lenta.

Eu concordo com você nisso. Isso me faz parar para tentar descobrir quais valores são atribuídos às variáveis ​​de parâmetro nomeadas. Minha leitura desconfortável (especialmente longa) funciona com retornos nus é que eu não consigo acompanhar facilmente o que realmente é retornado. Cada retorno simples na verdade significa return p1, p2, ..., pn para parâmetros nomeados, mas às vezes os valores de alguns parâmetros são obviamente literais como return nil, nil , return nil, err , return "", err e return p, nil vez de return p, err . Se eu escrever return nil, err , err ainda pode ser nil value, mas está claro que o primeiro valor de retorno é nil . Se o retorno simples não for permitido, o escritor provavelmente escreverá valores literais na instrução return, se isso for possível. Na verdade, todos eles usaram valores literais, quando pedi que evitassem devoluções simples durante as revisões de código.

Quando os escritores escrevem em retornos nus, eles tendem a não pensar sobre o que é retornado. Quando pedi que evitassem retornos brutos, agora eles pensam sobre o valor real que desejam retornar e percebem que estavam retornando valores errados, como valores parcialmente construídos. Não é melhor ser explícito do que implícito?

Eu sei que é recomendado recuar fluxos de erro, mas nem todo mundo segue esse padrão na realidade, especialmente pessoas que gostam de escrever funções longas com muitos retornos nus. Às vezes, é difícil saber se está em um fluxo de erro ou não olhando muitos retornos nus. Eu li o código cuidadosamente, decodifiquei-o e converti os retornos nus em não nus com alguns valores literais, então é muito mais fácil de ler. (às vezes não é possível descobrir os valores literais se o código for complexo)

Eu sei que o bloco de comentários da documentação menciona o que ele retorna para algumas condições significativas, mas nem todo mundo faz isso. Com a instrução return com alguns valores literais, posso acompanhar isso facilmente olhando para o código.

Eu sei que podemos usar parâmetros nomeados e retornos não nus juntos, mas nem todo mundo sabe disso. Costumo ver algo como o seguinte e acho que return io.EOF é melhor.

err = io.EOF
return

Dito isso, acho que # 28160 é provavelmente uma solução melhor, pois é compatível com versões anteriores e o gofmt já teve várias pequenas alterações.

Eu discordo da outra proposta de que gofmt deveria colocá-los de volta, mesmo que seja gofmt . Isso porque apenas colocar um monte de return p1, ..., pn mecanicamente não tornará o código mais compreensível.

É possível que as ferramentas do lint possam verificar isso ( nakedret faz isso), mas, IMO, o retorno simples tem mais danos do que benefícios. Também concordo que o benefício de remover o retorno puro pode ser menor do que o dano que quebra a compatibilidade. Mas eu acho que é fácil de consertar com go fix mesmo se o go fix mecânico não tornar o código legível.

Go é uma linguagem muito clara e legível na minha opinião. E o retorno puro é uma pequena parte da linguagem que me torna difícil de entender o código.

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

Posso estar em minoria, mas na verdade gostaria de propor o contrário. Funções com retornos nomeados sempre devem usar retorno simples. Caso contrário, parece que o código está mentindo. Você atribui valores e, em seguida, os sobrescreve potencialmente silenciosamente ao retornar. Aqui está um código ruim para ilustrar o que quero dizer,

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

Se você usar apenas um retorno simples, o código fará sentido novamente.

@millerlogic eu acho que seria muito agressivo. Por exemplo, não há nada de errado com códigos como:

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

Também acho que seu ponto pode se encaixar melhor como uma contraproposta separada, já que você está sugerindo o oposto do que a proposta atual apresenta.

@mvdan Bom argumento, acho que existe um meio-termo que não envolve demonizá-los. Acho que li uma ideia semelhante a: não retornar explicitamente um valor se você atribuiu anteriormente à variável de retorno. Pode até ser implementado em algum lugar, mas nunca consegui. Portanto, isso significaria que meu exemplo hello apresentaria um erro sem um retorno simples, e seu exemplo seria bem-sucedido porque foo ou bar não foram atribuídos.

Eu acho que eles devem ser removidos do tour do go, dessa forma os desenvolvedores de Go mais novos não são incentivados a usá-los. Na prática, eles são apenas um imposto cognitivo. Os poucos que vi sempre exigiram que meu cérebro dissesse "espere o quê?! ... oh sim, retornos nus e valores de retorno nomeados" Eu entendo que talvez os tenha deixado no passeio para que todos saibam, mas há uma página inteira dedicada a eles .

Esta página foi útil?
0 / 5 - 0 avaliações