Go: proposta: Go 2: simplificar o tratamento de erros com || sufixo errar

Criado em 25 jul. 2017  ·  519Comentários  ·  Fonte: golang/go

Tem havido muitas propostas de como simplificar o tratamento de erros no Go, todas baseadas na reclamação geral de que muito código Go contém as linhas

if err != nil {
    return err
}

Não tenho certeza se há um problema a ser resolvido aqui, mas como ele continua surgindo, vou colocar essa ideia.

Um dos principais problemas com a maioria das sugestões para simplificar o tratamento de erros é que elas simplificam apenas duas maneiras de tratar erros, mas na verdade existem três:

  1. ignore o erro
  2. retornar o erro não modificado
  3. retornar o erro com informações contextuais adicionais

Já é fácil (talvez fácil demais) ignorar o erro (consulte # 20803). Muitas propostas existentes para tratamento de erros tornam mais fácil retornar o erro não modificado (por exemplo, # 16225, # 18721, # 21146, # 21155). Poucos facilitam o retorno do erro com informações adicionais.

Esta proposta é vagamente baseada nas linguagens Perl e Bourne shell, fontes férteis de ideias de linguagem. Apresentamos um novo tipo de instrução, semelhante a uma instrução de expressão: uma expressão de chamada seguida por || . A gramática é:

PrimaryExpr Arguments "||" Expression

Da mesma forma, introduzimos um novo tipo de declaração de atribuição:

ExpressionList assign_op PrimaryExpr Arguments "||" Expression

Embora a gramática aceite qualquer tipo após || no caso de não atribuição, o único tipo permitido é o tipo pré-declarado error . A expressão após || deve ter um tipo atribuível a error . Pode não ser um tipo booleano, nem mesmo um tipo booleano nomeado atribuível a error . (Esta última restrição é necessária para tornar esta proposta compatível com o idioma existente.)

Esses novos tipos de declaração só são permitidos no corpo de uma função que tenha pelo menos um parâmetro de resultado, e o tipo do último parâmetro de resultado deve ser o tipo pré-declarado error . A função que está sendo chamada deve ter, de maneira semelhante, pelo menos um parâmetro de resultado, e o tipo do último parâmetro de resultado deve ser o tipo pré-declarado error .

Ao executar essas instruções, a expressão de chamada é avaliada normalmente. Se for uma instrução de atribuição, os resultados da chamada serão atribuídos aos operandos do lado esquerdo como de costume. Então o resultado da última chamada, que conforme descrito acima deve ser do tipo error , é comparado a nil . Se o resultado da última chamada não for nil , uma instrução de retorno é executada implicitamente. Se a função de chamada tiver vários resultados, o valor zero será retornado para todos os resultados, exceto o último. A expressão após || é retornada como o último resultado. Conforme descrito acima, o último resultado da função de chamada deve ter o tipo error , e a expressão deve ser atribuível ao tipo error .

No caso de não atribuição, a expressão é avaliada em um escopo no qual uma nova variável err é introduzida e definida com o valor do último resultado da chamada de função. Isso permite que a expressão se refira facilmente ao erro retornado pela chamada. No caso de atribuição, a expressão é avaliada no âmbito dos resultados da chamada, podendo assim referir-se diretamente ao erro.

Essa é a proposta completa.

Por exemplo, a função os.Chdir está atualmente

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

Sob esta proposta, poderia ser escrito como

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

Estou escrevendo esta proposta principalmente para incentivar as pessoas que desejam simplificar o tratamento de erros Go a pensar em maneiras de tornar mais fácil envolver os erros no contexto, não apenas para retornar o erro sem modificações.

FrozenDueToAge Go2 LanguageChange NeedsInvestigation Proposal error-handling

Comentários muito úteis

Uma ideia simples, com suporte para decoração de erro, mas exigindo uma mudança de linguagem mais drástica (obviamente não para go1.10) é a introdução de uma nova palavra-chave check .

Ele teria duas formas: check A e check A, B .

Tanto A quanto B precisam ser error . A segunda forma só seria usada na decoração de erros; pessoas que não precisam ou não desejam decorar seus erros usarão a forma mais simples.

1o formulário (marque A)

check A avalia A . Se nil , não faz nada. Se não nil , check age como um return {<zero>}*, A .

Exemplos

  • Se uma função apenas retorna um erro, ela pode ser usada inline com check , então
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

torna-se

check UpdateDB()
  • Para uma função com vários valores de retorno, você precisará atribuir, como fazemos agora.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

torna-se

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

// use a and b

2º formulário (marque A, B)

check A, B avalia A . Se nil , não faz nada. Se não nil , check age como um return {<zero>}*, B .

Isso é para necessidades de decoração de erro. Ainda verificamos A , mas B é usado no return implícito.

Exemplo

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

torna-se

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

Notas

É um erro de compilação para

  • use a instrução check em coisas que não são avaliadas como error
  • use check em uma função com valores de retorno que não estejam na forma { type }*, error

A forma two-expr check A, B está em curto-circuito. B não é avaliado se A for nil .

Notas sobre praticidade

Há suporte para erros de decoração, mas você paga pela sintaxe mais pesada check A, B apenas quando realmente precisa decorar erros.

Para o boilerplate if err != nil { return nil, nil, err } (que é muito comum), check err é tão breve quanto poderia ser, sem sacrificar a clareza (consulte a nota sobre a sintaxe abaixo).

Notas sobre sintaxe

Eu diria que este tipo de sintaxe ( check .. , no início da linha, semelhante a return ) é uma boa maneira de eliminar o clichê de verificação de erros sem esconder a interrupção do fluxo de controle que os retornos implícitos são introduzidos.

Uma desvantagem de ideias como <do-stuff> || <handle-err> e <do-stuff> catch <handle-err> acima, ou a, b = foo()? proposta em outro thread, é que eles ocultam a modificação do fluxo de controle de uma forma que torna o fluxo mais difícil seguir; o primeiro com || <handle-err> maquinário anexado ao final de uma linha de aparência simples, o último com um pequeno símbolo que pode aparecer em qualquer lugar, incluindo no meio e no final de uma linha de código de aparência simples, possivelmente várias vezes.

Uma instrução check sempre será de nível superior no bloco atual, tendo a mesma proeminência de outras instruções que modificam o fluxo de controle (por exemplo, um return ).

Todos 519 comentários

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

De onde e vem daí? Erro de digitação?

Ou você quis dizer:

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

(Ou seja, a verificação implícita err! = Nil primeiro atribui erro ao parâmetro de resultado, que pode ser nomeado para modificá-lo novamente antes do retorno implícito?)

Suspiro, baguncei meu próprio exemplo. Agora corrigido: o e deve ser err . A proposta coloca err no escopo para conter o valor de erro da chamada de função quando não estiver em uma instrução de atribuição.

Embora eu não tenha certeza se concordo com a ideia ou a sintaxe, devo dar-lhe crédito por dar atenção à adição de contexto aos erros antes de devolvê-los.

Isso pode ser do interesse de @davecheney , que escreveu https://github.com/pkg/errors.

O que acontece neste código:

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

(Minhas desculpas se isso nem mesmo for possível sem a parte || &PathError{"chdir", dir, e} ; estou tentando expressar que isso parece uma substituição confusa do comportamento existente, e os retornos implícitos são ... sorrateiros?)

@ object88 Eu ficaria bem em não permitir este novo caso em um SimpleStmt como usado nas instruções if e for e switch . Isso provavelmente seria melhor, embora complicasse um pouco a gramática.

Mas se não fizermos isso, então o que acontece é que se thing.Nope() retornar um erro não nulo, a função de chamada retorna com &PathError{"chdir", dir, err} (onde err é o variável definida pela chamada para thing.Nope() ). Se thing.Nope() retorna um erro nil , então sabemos com certeza que err == nil é verdadeiro na condição da instrução if , e assim o corpo do se a instrução for executada. A variável ignoreError nunca é lida. Não há ambigüidade ou anulação do comportamento existente aqui; o tratamento de || introduzido aqui só é aceito quando a expressão após || não for um valor booleano, o que significa que não seria compilado atualmente.

Eu concordo que os retornos implícitos são furtivos.

Sim, meu exemplo é muito pobre. Mas não permitir a operação dentro de um if , for ou switch resolveria uma grande confusão potencial.

Como a barra a ser considerada é geralmente algo difícil de fazer no idioma como está, decidi ver como essa variante era difícil de codificar no idioma. Não muito mais difícil do que os outros: https://play.golang.org/p/9B3Sr7kj39

Eu realmente não gosto de todas essas propostas para tornar um tipo de valor e uma posição nos argumentos de retorno especiais. De certa forma, este é realmente pior porque também torna err um nome especial neste contexto específico.

Embora eu certamente concorde que as pessoas (incluindo eu!) Deveriam estar mais cansadas de retornar erros sem contexto extra.

Quando houver outros valores de retorno, como

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

definitivamente pode ser cansativo de ler. Gostei um pouco da sugestão de @nigeltao para return ..., err em https://github.com/golang/go/issues/19642#issuecomment -288559297

Se bem entendi, para construir a árvore de sintaxe, o analisador precisaria saber os tipos de variáveis ​​para distinguir entre

boolean := BoolFunc() || BoolExpr

e

err := FuncReturningError() || Expr

Não parece bom.

menos é mais...

Quando o ExpressionList de retorno contém dois ou mais elementos, como funciona?

BTW, eu quero entrar em pânico em vez disso.

err := doSomeThing()
panicIf(err)

err = doAnotherThing()
panicIf(err)

@ianlancetaylor No exemplo da sua proposta err ainda não foi declarado explicitamente e apresentado como 'mágico' (linguagem predefinida), certo?

Ou será algo como

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

?

Por outro lado (uma vez que já está marcado como uma "mudança de idioma" ...)
Introduza um novo operador (!! ou ??) que faça o atalho em caso de erro! = Nulo (ou qualquer anulável?)

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

Desculpe se isso está muito longe :)

Eu concordo que o tratamento de erros no Go pode ser repetitivo. Não me importo com a repetição, mas muitos deles afetam a legibilidade. Há uma razão pela qual a "Complexidade Ciclomática" (quer você acredite nela ou não) usa os fluxos de controle como uma medida de complexidade. A instrução "if" adiciona ruído extra.

No entanto, a sintaxe proposta "||" não é muito intuitivo de ler, especialmente porque o símbolo é comumente conhecido como um operador OR. Além disso, como você lida com funções que retornam vários valores e erros?

Estou apenas lançando algumas idéias aqui. Que tal, em vez de usar o erro como saída, usarmos o erro como entrada? Exemplo: https://play.golang.org/p/rtfoCIMGAb

Obrigado por todos os comentários.

@opennota Bom ponto. Ainda pode funcionar, mas concordo que esse aspecto é estranho.

@mattn Não acho que haja uma ExpressionList de retorno, então não tenho certeza do que você está perguntando. Se a função de chamada tiver vários resultados, todos, exceto o último, serão retornados como o valor zero do tipo.

@mattn panicif não aborda nenhum dos elementos-chave desta proposta, que é uma maneira fácil de retornar um erro com contexto adicional. E, é claro, pode-se escrever panicif hoje com bastante facilidade.

@tandr Sim, err é definido magicamente, o que é horrível. Outra possibilidade seria permitir que a expressão de erro usasse error para se referir ao erro, o que é terrível de uma maneira diferente.

@tandr Poderíamos usar um operador diferente, mas não vejo nenhuma grande vantagem. Não parece tornar o resultado mais legível.

@henryas Acho que a proposta explica como ela lida com vários resultados.

@henryas Obrigado pelo exemplo. O que não gosto nesse tipo de abordagem é que torna o tratamento de erros o aspecto mais proeminente do código. Quero que o tratamento de erros esteja presente e visível, mas não quero que seja a primeira coisa na linha. Isso é verdade hoje, com o idioma if err != nil e a indentação do código de tratamento de erros, e deve permanecer verdadeiro se quaisquer novos recursos forem adicionados para tratamento de erros.

Obrigado novamente.

@ianlancetaylor Não sei se você deu uma olhada no link do meu playground, mas havia um "panicIf" que permite adicionar contexto extra.

Vou reproduzir uma versão um tanto simplificada aqui:

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

Coincidentemente, acabei de dar uma palestra relâmpago na GopherCon, onde usei (mas não

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

onde =: é o novo bit de sintaxe, um espelho de := que atribui na outra direção. Obviamente, também precisaríamos de algo por = , o que é reconhecidamente problemático. Mas a ideia geral é facilitar ao leitor a compreensão do caminho da felicidade, sem perder informações.

Por outro lado, a forma atual de tratamento de erros tem alguns méritos, pois serve como um lembrete gritante de que você pode estar fazendo muitas coisas em uma única função e que algumas refatorações podem estar atrasadas.

Eu gosto muito da sintaxe proposta por @billyh aqui

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

ou um exemplo mais complexo usando https://github.com/pkg/errors

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

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

    return &p, nil
}

Uma ideia simples, com suporte para decoração de erro, mas exigindo uma mudança de linguagem mais drástica (obviamente não para go1.10) é a introdução de uma nova palavra-chave check .

Ele teria duas formas: check A e check A, B .

Tanto A quanto B precisam ser error . A segunda forma só seria usada na decoração de erros; pessoas que não precisam ou não desejam decorar seus erros usarão a forma mais simples.

1o formulário (marque A)

check A avalia A . Se nil , não faz nada. Se não nil , check age como um return {<zero>}*, A .

Exemplos

  • Se uma função apenas retorna um erro, ela pode ser usada inline com check , então
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

torna-se

check UpdateDB()
  • Para uma função com vários valores de retorno, você precisará atribuir, como fazemos agora.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

torna-se

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

// use a and b

2º formulário (marque A, B)

check A, B avalia A . Se nil , não faz nada. Se não nil , check age como um return {<zero>}*, B .

Isso é para necessidades de decoração de erro. Ainda verificamos A , mas B é usado no return implícito.

Exemplo

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

torna-se

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

Notas

É um erro de compilação para

  • use a instrução check em coisas que não são avaliadas como error
  • use check em uma função com valores de retorno que não estejam na forma { type }*, error

A forma two-expr check A, B está em curto-circuito. B não é avaliado se A for nil .

Notas sobre praticidade

Há suporte para erros de decoração, mas você paga pela sintaxe mais pesada check A, B apenas quando realmente precisa decorar erros.

Para o boilerplate if err != nil { return nil, nil, err } (que é muito comum), check err é tão breve quanto poderia ser, sem sacrificar a clareza (consulte a nota sobre a sintaxe abaixo).

Notas sobre sintaxe

Eu diria que este tipo de sintaxe ( check .. , no início da linha, semelhante a return ) é uma boa maneira de eliminar o clichê de verificação de erros sem esconder a interrupção do fluxo de controle que os retornos implícitos são introduzidos.

Uma desvantagem de ideias como <do-stuff> || <handle-err> e <do-stuff> catch <handle-err> acima, ou a, b = foo()? proposta em outro thread, é que eles ocultam a modificação do fluxo de controle de uma forma que torna o fluxo mais difícil seguir; o primeiro com || <handle-err> maquinário anexado ao final de uma linha de aparência simples, o último com um pequeno símbolo que pode aparecer em qualquer lugar, incluindo no meio e no final de uma linha de código de aparência simples, possivelmente várias vezes.

Uma instrução check sempre será de nível superior no bloco atual, tendo a mesma proeminência de outras instruções que modificam o fluxo de controle (por exemplo, um return ).

@ALTree , não entendi como seu exemplo:

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

Atinge o retorno de três valores do original:

return "", "", err

Ele está apenas retornando valores zero para todos os retornos declarados, exceto o erro final? E quanto aos casos em que você gostaria de retornar um valor válido junto com um erro, por exemplo, número de bytes gravados quando um Write () falha?

Qualquer solução que escolhermos deve restringir minimamente a generalidade do tratamento de erros.

Em relação ao valor de ter check no início da linha, minha preferência pessoal é ver o fluxo de controle primário no início de cada linha e fazer com que o tratamento de erros interfira tão pouco na legibilidade desse fluxo de controle primário que possível. Além disso, se o tratamento de erros for separado por uma palavra reservada como check ou catch , qualquer editor moderno irá destacar a sintaxe da palavra reservada de alguma forma e torná-la perceptível até se estiver do lado direito.

@billyh isso é explicado acima, na linha que diz:

Se não for nulo, o cheque atua como return {<zero>}*, A

check retornará o valor zero de qualquer valor de retorno, exceto o erro (na última posição).

E os casos em que você gostaria de retornar um valor válido junto com um erro

Então você usará o if err != nil { idiom.

Existem muitos casos em que você precisará de um procedimento de recuperação de erros mais sofisticado. Por exemplo, você pode precisar, depois de detectar um erro, reverter algo ou gravar algo em um arquivo de log. Em todos esses casos, você ainda terá o usual if err idiom em sua caixa de ferramentas e pode usá-lo para iniciar um novo bloco, onde qualquer tipo de operação relacionada ao tratamento de erros, não importa quão articulada, pode ser realizado.

Qualquer solução que escolhermos deve restringir minimamente a generalidade do tratamento de erros.

Veja minha resposta acima. Você ainda terá if e qualquer outra coisa que o idioma oferecer agora.

praticamente qualquer editor moderno irá destacar a palavra reservada

Pode ser. Mas introduzir sintaxe opaca, que requer destaque de sintaxe para ser legível, não é ideal.

este bug em particular pode ser corrigido introduzindo um recurso de duplo retorno para a linguagem.
neste caso, a função a () retorna 123:

func a () int {
b ()
retorno 456
}
função b () {
return return int (123)
}

Este recurso pode ser usado para simplificar o tratamento de erros da seguinte maneira:

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

função client_code () (* client_object, error) {
var obj, err = handle (algo_que_can_fail ())
// isso só é alcançado se algo não falhou
// caso contrário, a função client_code propagaria o erro para cima na pilha
afirmar (errar == nulo)
}

Isso permite que as pessoas escrevam funções de tratamento de erros que podem propagar os erros pela pilha
tais funções de tratamento de erros podem ser separadas do código principal

Desculpe se entendi errado, mas quero esclarecer um ponto, a função abaixo irá produzir um erro, vet aviso ou será aceito?

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

@rodcorsi Segundo esta proposta, seu exemplo seria aceito sem aviso do veterinário. Seria equivalente a

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

Que tal expandir o uso de Context para lidar com erros? Por exemplo, dada a seguinte definição:
type ErrorContext interface { HasError() bool SetError(msg string) Error() string }
Agora, na função sujeita a erros ...
func MyFunction(number int, ctx ErrorContext) int { if ctx.HasError() { return 0 } return number + 1 }
Na função intermediária ...
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) return number }
E na função de nível superior
func main() { ctx := context.New() no := MyIntermediateFunction(ctx) if ctx.HasError() { log.Fatalf("Error: %s", ctx.Error()) return } fmt.Printf("%d\n", no) }
Existem vários benefícios com essa abordagem. Primeiro, não distrai o leitor do caminho de execução principal. Há um mínimo de instruções "if" para indicar o desvio do caminho de execução principal.

Em segundo lugar, não esconde o erro. Fica claro a partir da assinatura do método que, se ele aceitar ErrorContext, a função pode ter erros. Dentro da função, ele usa as instruções de ramificação normais (por exemplo, "if") que mostra como o erro é tratado usando o código Go normal.

Terceiro, o erro é automaticamente transmitido à parte interessada, que, neste caso, é o proprietário do contexto. Caso haja um processamento adicional de erro, ele será mostrado claramente. Por exemplo, vamos fazer algumas alterações na função intermediária para encerrar qualquer erro existente:
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) if ctx.HasError() { ctx.SetError(fmt.Sprintf("wrap msg: %s", ctx.Error()) return } number *= 20 number = MyFunction(number, ctx) return number }
Basicamente, você apenas escreve o código de tratamento de erros conforme necessário. Você não precisa fazer bolhas manualmente.

Por último, você, como redator da função, tem uma palavra a dizer se o erro deve ser tratado. Usando a abordagem Go atual, é fácil fazer isso ...
`` ``
// dada a seguinte definição
função MyFunction (number int) error

// então faça isso
MyFunction (8) // sem verificar o erro
With the ErrorContext, you as the function owner can make the error checking optional with this:
função MyFunction (ctx ErrorContext) {
if ctx! = nil && ctx.HasError () {
Retorna
}
// ...
}
Or make it compulsory with this:
função MyFunction (ctx ErrorContext) {
if ctx.HasError () {// entrará em pânico se ctx for nulo
Retorna
}
// ...
}
If you make error handling compulsory and yet the user insists on ignoring error, they can still do that. However, they have to be very explicit about it (to prevent accidental ignore). For instance:
função UpperFunction (ctx ErrorContext) {
ignorado: = contexto.Novo ()
MyFunction (ignorado) // este é ignorado

 MyFunction(ctx) //this one is handled

}
`` ``
Essa abordagem não muda nada para a linguagem existente.

@ALTree Alberto, que tal misturar seu check e o que @ianlancetaylor propôs?

assim

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

torna-se

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

Além disso, podemos limitar check para lidar apenas com tipos de erro, portanto, se você precisar de vários valores de retorno, eles precisam ser nomeados e atribuídos, de modo que atribui "no local" de alguma forma e se comporta como um simples "retorno"

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

Se return se tornasse aceitável na expressão um dia, então check não é necessário, ou se torna uma função padrão

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

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

a última solução parece Perl embora 😄

Não me lembro quem o propôs originalmente, mas aqui está outra ideia de sintaxe (a bicicleta favorita de todos :-). Não estou dizendo que é uma boa, mas se estamos jogando ideias na panela ...

x, y := try foo()

seria equivalente a:

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

e

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

seria equivalente a:

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

A forma try certamente foi proposta antes, várias vezes, por diferenças de sintaxe modulo superficiais. A forma try ... catch é proposta com menos frequência, mas é claramente semelhante à construção check A, B @ALTree e à @tandr . Uma diferença é que esta é uma expressão, não uma afirmação, para que você possa dizer:

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

Você pode ter vários try / catch em uma única instrução:

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

embora eu não queira encorajar isso. Você também precisa ter cuidado aqui com a ordem de avaliação. Por exemplo, se h() é avaliado para efeitos colaterais se e() retorna um erro não nulo.

Obviamente, novas palavras-chave como try e catch quebrariam a compatibilidade do Go 1.x.

Eu sugiro que devemos apertar o alvo desta proporsal. Qual problema será corrigido por esta proposta? Reduzir as três linhas seguintes em duas ou uma? Isso pode ser uma mudança de idioma de retorno / se.

if err != nil {
    return err
}

Ou reduza o número de vezes para verificar errar? Pode ser uma solução try / catch para isso.

Eu gostaria de sugerir que qualquer sintaxe de atalho razoável para tratamento de erros tem três propriedades:

  1. Ele não deve aparecer antes do código que está verificando, para que o caminho sem erro seja proeminente.
  2. Não deve introduzir variáveis ​​implícitas no escopo, para que os leitores não se confundam quando há uma variável explícita com o mesmo nome.
  3. Não deve tornar uma ação de recuperação (por exemplo, return err ) mais fácil do que outra. Às vezes, uma ação totalmente diferente pode ser preferível (como chamar t.Fatal ). Também não queremos desencorajar as pessoas de adicionar contexto adicional.

Dadas essas restrições, parece que uma sintaxe quase mínima seria algo como

STMT SEPARATOR_TOKEN VAR BLOCK

Por exemplo,

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

que é equivalente a

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

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

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

Eu me pergunto como isso se comporta tanto para valor diferente de zero quanto para erro retornado. Por exemplo, bufio.Peek possivelmente retorna um valor diferente de zero e ErrBufferFull ambos ao mesmo tempo.

@mattn você ainda pode usar a sintaxe antiga.

@nigeltao Sim, eu entendo. Suspeito que esse comportamento possivelmente faça um bug no código do usuário, já que bufio.Peek também retorna diferente de zero e nulo. O código não deve implicar valores e erros em ambos. Portanto, o valor e o erro devem ser retornados ao chamador (neste caso).

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

@jba O que você está descrevendo se parece um pouco com um operador de composição de função transposta:

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

Mas o fato de estarmos escrevendo um código imperativo exige que não usemos uma função na segunda posição, porque parte do problema é poder retornar mais cedo.

Portanto, agora estou pensando em três observações que estão todas relacionadas:

  1. O tratamento de erros é como a composição de funções, mas a maneira como fazemos as coisas em Go é meio o oposto da mônada de erro de Haskell: porque estamos escrevendo principalmente código imperativo em vez de código sequencial, queremos transformar o erro (para adicionar contexto) em vez de o valor sem erro (que preferimos apenas vincular a uma variável).

  2. Funções Go que retornam (x, y, error) geralmente significam algo mais como uma união (# 19412) de (x, y) | error .

  3. Em linguagens que descompactam ou combinam uniões de padrões, os casos são escopos separados, e muitos dos problemas que temos com erros em Go são devido ao sombreamento inesperado de variáveis ​​declaradas novamente que podem ser melhoradas separando esses escopos (# 21114).

Então, talvez o que queremos realmente seja como o operador =: , mas com uma espécie de condicional de união de união:

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

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

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

Exceto para o escopo, isso não é muito diferente de apenas mudar gofmt para ser mais receptivo a empresas de uma linha:

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

`` `vá
n, errar: = io.WriteString (w, s); if err! = nil {return err}

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

Mas o escopo é um grande negócio! Os problemas de escopo são onde esse tipo de código cruza a linha de "um pouco feio" para "bugs sutis".

@ianlancetaylor
Embora eu seja um fã da ideia geral, não sou um grande defensor da sintaxe enigmática do perl para ela. Talvez uma sintaxe mais prolixo seja menos confusa, como:

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

syscall.Chdir(dir) or dump

Além disso, não entendi se o último argumento é exibido em caso de atribuição, Ex:

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

Não vamos esquecer que os erros são valores em go e não algum tipo especial.
Não há nada que possamos fazer com outras estruturas que não possamos fazer com os erros e vice-versa. Isso significa que se você entende structs em geral, entende os erros e como eles são tratados (mesmo se você achar que é prolixo)

Essa sintaxe exigiria que desenvolvedores novos e antigos aprendam um novo bit de informação antes de começar a entender o código que a usa.

Isso por si só torna esta proposta não vale a pena IMHO.

Pessoalmente, eu prefiro esta sintaxe

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

sobre

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

É uma linha a mais, mas separa a ação pretendida do tratamento de erros. Este formulário é o mais legível para mim.

@bcmills :

Exceto para o escopo, isso não é muito diferente de apenas mudar o gofmt para ser mais receptivo a frases simples

Não apenas o escopo; há também a borda esquerda. Acho que isso realmente afeta a legibilidade. eu acho que

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

é muito mais claro do que

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

especialmente quando ocorre em várias linhas consecutivas, porque seu olho pode escanear a borda esquerda para ignorar o tratamento de erros.

Misturando a ideia @bcmills , podemos introduzir o operador de encaminhamento de tubulação condicional.

A função F2 será executada se o último valor não for

func F1() (foo, bar){}

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

Um caso especial de encaminhamento de pipe com declaração de retorno

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

Exemplo real trazido por @urandom em outra edição
Para mim, muito mais legível com foco no fluxo primário

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

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

Concordo que o tratamento de erros não é ergonômico. Ou seja, quando você lê o código abaixo, você deve vocalizá-lo para if error not nil then - o que se traduz em if there is an error then .

if err != nil {
    // handle error
}

Eu gostaria de ter a habilidade de expressar o código acima de tal forma - que na minha opinião é mais legível.

if err {
    // handle error
}

Apenas minha humilde sugestão :)

Parece perl, tem até a variável mágica
Para referência, em perl você faria

open (FILE, $ file) ou die ("não é possível abrir $ file: $!");

IMHO, não vale a pena, um ponto que gosto em go é que o tratamento de erros
é explícito e 'na sua cara'

Se continuarmos com ele, eu gostaria de não ter variáveis ​​mágicas, deveríamos ser
capaz de nomear a variável de erro

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

E também podemos usar um símbolo diferente de || específico para esta tarefa,
Acho que símbolos de texto como 'ou' não são possíveis devido ao contrário
compatibilidade

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

Na terça-feira, 1º de agosto de 2017 às 14h47, Rodrigo [email protected] escreveu:

Misturando a ideia @bcmills https://github.com/bcmills podemos apresentar
operador de encaminhamento de tubo condicional.

A função F2 será executada se o último valor não for

função F1 () (foo, bar) {}
primeiro: = F1 ()?> último: F2 (primeiro, último)

Um caso especial de encaminhamento de pipe com declaração de retorno

função Chdir (dir string) error {
syscall.Chdir (dir)?> err: return & PathError {"chdir", dir, err}
retornar nulo
}

Exemplo real
https://github.com/juju/juju/blob/01b24551ecdf20921cf620b844ef6c2948fcc9f8/cloudconfig/providerinit/providerinit.go
trazido por @urandom https://github.com/urandom em outra edição
Para mim, muito mais legível com foco no fluxo primário

func configureCloudinit (icfg * instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig, error) {
// Ao inicializar, queremos apenas apt-get update / upgrade
// e configure as chaves SSH. O resto deixamos para cloudinit / sshinit.
udata: = cloudconfig.NewUserdataConfig (icfg, cloudcfg)?> errar: retornar nil, errar
if icfg.Bootstrap! = nil {
udata.ConfigureBasic ()?> err: return nil, err
return udata, nil
}
udata.Configure ()?> err: return nil, err
return udata, nil
}
função ComposeUserData (icfg * instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig, renderer renderers.ProviderRenderer) ([] byte, erro) {
if cloudcfg == nil {
cloudcfg = cloudinit.New (icfg.Series)?> err: return nil, errors.Trace (err)
}
configureCloudinit (icfg, cloudcfg)?> err: return nil, errors.Trace (err)
operatingSystem: = series.GetOSFromSeries (icfg.Series)?> err: return nil, errors.Trace (err)
udata: = renderer.Render (cloudcfg, operatingSystem)?> err: return nil, errors.Trace (err)
logger.Tracef ("init nuvem gerada: \ n% s", string (udata))
return udata, nil
}

-
Você está recebendo isto porque está inscrito neste tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/21161#issuecomment-319359614 ou mudo
o segmento
https://github.com/notifications/unsubscribe-auth/AbwRO_J0h2dQHqfysf2roA866vFN4_1Jks5sTx5hgaJpZM4Oi1c-
.

Sou o único que pensa que todas essas mudanças propostas seriam mais complicadas do que a forma atual.

Acho que simplicidade e brevidade não são iguais ou intercambiáveis. Sim, todas essas mudanças teriam uma ou mais linhas mais curtas, mas introduziriam operadores ou palavras-chave que um usuário do idioma teria que aprender.

@rodcorsi Eu sei que parece menor, mas acho que é importante para a segunda parte ser um Bloco : as instruções if e for e select e switch ambos usam sintaxe delimitada por chaves, então parece estranho omitir as chaves para esta operação de fluxo de controle em particular.

Também é muito mais fácil garantir que a árvore de análise não seja ambígua se você não precisar se preocupar com expressões arbitrárias após os novos símbolos.

A sintaxe e a semântica que eu tinha em mente para o meu esboço são:


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

Um NonZeroGuardStmt executa Block se o último valor de um Expression não for igual ao valor zero de seu tipo. Se um identifier estiver presente, ele está vinculado a esse valor dentro de Block . Um ZeroGuardStmt executa Block se o último valor de um Expression for igual ao valor zero de seu tipo.

Para a forma := , os outros valores (principais) de Expression são vinculados a IdentifierList como em ShortVarDecl . Os identificadores são declarados no escopo contido, o que implica que eles também são visíveis dentro de Block .

Para a forma assign_op , cada operando do lado esquerdo deve ser endereçável, uma expressão de índice de mapa ou (apenas para = atribuições) o identificador em branco. Operandos podem estar entre parênteses. Os outros valores (principais) do lado direito Expression são avaliados como em Assignment . A atribuição ocorre antes da execução de Block e independentemente de Block ser ou não executado.


Acredito que a gramática proposta aqui é compatível com Go 1: ? não é um identificador válido e não há operadores Go existentes usando esse caractere e, embora ! seja um operador válido, não há produção existente em que pode ser seguido por { .

@bcmills LGTM, com alterações concomitantes para gofmt.

Eu pensei que você tornaria =? e =! cada um um token em seu próprio direito, o que tornaria a gramática trivialmente compatível.

Eu teria pensado que você faria =? e =! cada um um token em seu próprio direito, o que tornaria a gramática trivialmente compatível.

Podemos fazer isso na gramática, mas não no lexer: a sequência "=!" pode aparecer em código Go 1 válido (https://play.golang.org/p/pMTtUWgBN9).

A chave é o que torna a análise inequívoca em minha proposta: =! atualmente só pode aparecer em uma declaração ou atribuição a uma variável booleana, e as declarações e atribuições não podem aparecer imediatamente antes de uma chave (https : //play.golang.org/p/ncJyg-GMuL) a menos que separado por um ponto e vírgula implícito (https://play.golang.org/p/lhcqBhr7Te).

@romainmenke Não. Você não é o único. Não consigo ver o valor do tratamento de erros de uma linha. Você pode salvar uma linha, mas adicionar muito mais complexidade. O problema é que em muitas dessas propostas, a parte de tratamento de erros fica oculta. A ideia não é torná-los menos perceptíveis porque o tratamento de erros é importante, mas tornar o código mais fácil de ler. Brevidade não significa legibilidade fácil. Se você tiver que fazer alterações no sistema de tratamento de erros existente, acho que o convencional try-catch-finally é muito mais atraente do que muitos propósitos de ideias aqui.

Gosto da proposta check porque você também pode estendê-la para lidar com

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

Outras propostas não parecem se misturar com defer também. check também é muito legível e simples para o Google, se você não o conhece. Não acho que seja necessário limitar-se ao tipo error . Qualquer coisa que seja um parâmetro de retorno na última posição pode usá-lo. Portanto, um iterador pode ter um check para um Next() bool .

Uma vez escrevi um Scanner que parece

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

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

Em vez disso, esse último bit poderia ser check s.read(&rt) .

@carlmjohnson

Outras propostas não parecem se misturar com defer também.

Se você está assumindo que expandiremos defer para permitir o retorno da função externa usando a nova sintaxe, você pode aplicar essa premissa igualmente bem a outras propostas.

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

Como a proposta de check @ALTree apresenta uma instrução separada, não vejo como você poderia misturá-la com um defer que faz outra coisa senão simplesmente retornar o erro.

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

Contraste:

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

A justificativa para muitas dessas propostas é a melhor "ergonomia", mas não vejo como qualquer uma delas seja melhor do que torná-la um pouco menos para digitar. Como isso aumenta a capacidade de manutenção do código? A composibilidade? A legibilidade? A facilidade de compreensão do fluxo de controle?

@jimmyfrasche

Como isso aumenta a capacidade de manutenção do código? A composibilidade? A legibilidade? A facilidade de compreensão do fluxo de controle?

Como observei anteriormente, a principal vantagem de qualquer uma dessas propostas provavelmente precisaria vir de um escopo mais claro de atribuições e err variáveis: consulte # 19727, # 20148, # 5634, # 21114 e provavelmente outros para vários maneiras que as pessoas têm encontrado problemas de escopo em relação ao tratamento de erros.

@bcmills, obrigado por fornecer uma motivação e desculpe não ter percebido em seu post anterior.

Dada essa premissa, no entanto, não seria melhor fornecer uma facilidade mais geral para um "escopo mais claro de atribuições" que pudesse ser usado por todas as variáveis? Eu involuntariamente obscureci minha cota de variáveis ​​sem erro também, certamente.

Lembro-me de quando o comportamento atual de := foi introduzido - muito daquele tópico em enlouquecer † clamava por uma maneira de anotar explicitamente quais nomes deveriam ser reutilizados em vez da "reutilização implícita apenas se essa variável existir em exatamente o escopo atual "que é onde todos os problemas sutis difíceis de ver se manifestam, na minha experiência.

† Não consigo encontrar esse tópico, alguém tem um link?

Há muitas coisas que eu acho que poderiam ser melhoradas em Go, mas o comportamento de := sempre me pareceu o único erro sério. Talvez revisitar o comportamento de := seja a maneira de resolver o problema raiz ou pelo menos reduzir a necessidade de outras mudanças mais extremas?

@jimmyfrasche

Dada essa premissa, no entanto, não seria melhor fornecer uma facilidade mais geral para um "escopo mais claro de atribuições" que pudesse ser usado por todas as variáveis?

sim. Essa é uma das coisas que eu gosto no operador =? ou :: que @jba e eu propomos: ele também se estende bem a (um subconjunto reconhecidamente limitado de) não erros.

Pessoalmente, acho que ficaria mais feliz no longo prazo com um recurso de tipo de dados tagged-union / varint / algébrico mais explícito (consulte também # 19412), mas essa é uma mudança muito maior na linguagem: é difícil ver como faríamos o retrofit isso em APIs existentes em um ambiente Go 1 / Go 2 misto.

A facilidade de compreensão do fluxo de controle?

Nas propostas minhas e de @bcmills , seu olho pode percorrer o lado esquerdo e facilmente

@bcmills Acho que sou responsável por pelo menos metade das palavras no # 19412, então você não precisa me convencer de tipos de soma;)

Quando se trata de devolver coisas com erro, existem quatro casos

  1. apenas um erro (não precisa fazer nada, apenas retornar um erro)
  2. coisas E um erro (você lidaria com isso exatamente como faria agora)
  3. uma coisa OU um erro (você pode usar tipos de soma!: tada:)
  4. duas ou mais coisas OU um erro

Se você acertar 4, é aí que as coisas ficam complicadas. Sem introduzir tipos de tupla (tipos de produtos não rotulados para ir com os tipos de produtos rotulados de struct), você teria que reduzir o problema para o caso 3 agrupando tudo em um struct se quiser usar tipos de soma para modelar "isto ou um erro".

A introdução de tipos de tupla causaria todos os tipos de problemas e questões de compatibilidade e sobreposições estranhas ( func() (int, string, error) uma tupla definida implicitamente ou vários valores de retorno são um conceito separado? Se for uma tupla definida implicitamente, isso significa func() (n int, msg string, err error) é uma estrutura definida implicitamente !? Se for uma estrutura, como faço para acessar os campos se não estiver no mesmo pacote!)

Ainda acho que os tipos de soma oferecem muitos benefícios, mas não fazem nada para corrigir os problemas de escopo, é claro. No mínimo, eles poderiam piorar, porque você poderia sombrear a soma inteira do 'resultado ou erro' em vez de apenas sombrear o caso de erro quando havia algo no caso de resultado.

@jba Não vejo como isso é uma propriedade desejável. Além de uma falta geral de facilidade com o conceito de fazer o fluxo de controle bidimensional, por assim dizer, eu também não consigo imaginar por que não é. Você pode explicar o benefício para mim?

Sem introduzir tipos de tupla [...] você teria que [empacotar] tudo em uma estrutura se quiser usar tipos de soma para modelar "isto ou um erro".

Estou bem com isso: acho que teríamos sites de chamada muito mais legíveis dessa maneira (sem ligações posicionais transpostas acidentalmente!), E o # 12854 mitigaria muito da sobrecarga atualmente associada aos retornos de estrutura.

O grande problema é a migração: como iríamos passar do modelo de "valores e erro" do Go 1 para um modelo de "valores ou erro" potencial no Go 2, especialmente dadas APIs como io.Writer que realmente retornam "valores e erro "?

Ainda acho que os tipos de soma oferecem muitos benefícios, mas não fazem nada para corrigir os problemas de escopo, é claro.

Isso depende de como você os desempacota, o que, suponho, nos traz de volta ao ponto em que estamos hoje. Se você preferir uniões, talvez possa imaginar uma versão de =? como uma API de "correspondência de padrões assimétricos":

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

Onde match seria a operação tradicional de correspondência de padrão no estilo ML, mas no caso de uma correspondência não exaustiva retornaria o valor (como interface{} se a união tiver mais de uma alternativa não correspondida) em vez de entrar em pânico com uma falha não exaustiva de partida.

Acabei de verificar um pacote em https://github.com/mpvl/errd que aborda os problemas discutidos aqui programaticamente (sem alterações de idioma). O aspecto mais importante deste pacote é que ele não apenas reduz o tratamento de erros, mas também torna mais fácil fazê-lo corretamente. Dou exemplos nos documentos sobre como o tratamento de erros idiomáticos tradicionais é mais complicado do que parece, especialmente na interação com o adiamento.

Eu considero este um pacote "queimador", no entanto; o objetivo é obter uma boa experiência e insights sobre a melhor forma de estender o idioma. Ele interage muito bem com os genéricos, aliás, se isso se tornasse uma coisa.

Ainda trabalhando em mais alguns exemplos, mas este pacote está pronto para ser experimentado.

@bcmills um milhão: +1: para # 12854

Como você notou, existem "retornar X e erro" e "retornar X ou erro", então você não poderia realmente contornar isso sem alguma macro que traduz o modo antigo para o novo sob demanda (e é claro que haveria bugs ou pelo menos o tempo de execução entra em pânico quando era inevitavelmente usado para uma função "X e erro").

Eu realmente não gosto da ideia de introduzir macros especiais na linguagem, especialmente se for apenas para tratamento de erros, que é o meu maior problema com muitas dessas propostas.

Go não é grande em açúcar ou mágica e isso é uma vantagem.

Há muita inércia e muito pouca informação codificada na prática atual para lidar com um salto em massa para um paradigma de tratamento de erros mais funcional.

Se Go 2 obtiver tipos de soma - o que francamente me chocaria (no bom sentido!) - seria, no mínimo, um processo gradual muito lento para mudar para o "novo estilo" e, nesse ínterim, haveria até mais fragmentação e confusão em como lidar com erros, então não vejo isso sendo um resultado positivo. (No entanto, eu começaria imediatamente a usá-lo para coisas como chan union { Msg1 T; Msg2 S; Err error } vez de três canais).

Se isso fosse pré-Go1 e a equipe de Go pudesse dizer "vamos passar os próximos seis meses mudando tudo e quando quebrar as coisas, continue", isso seria uma coisa, mas agora estamos basicamente presos mesmo se obtivermos tipos de soma.

Como você observou, existem "retornar X e erro" e "retornar X ou erro", então você não poderia realmente contornar isso sem alguma macro que traduz a maneira antiga para a nova sob demanda

Como tentei dizer acima, não acho que seja necessário que a nova forma, seja ela qual for, cubra "retornar X e erro". Se a grande maioria dos casos for "retornar X ou erro", e a nova forma melhorar apenas isso, então isso é ótimo, e você ainda pode usar a antiga forma compatível com Go 1 para a forma mais rara "retornar X e erro".

@nigeltao Verdadeiro, mas ainda precisamos de alguma forma de diferenciá-los durante a transição, a menos que você esteja propondo que mantenhamos toda a biblioteca padrão no estilo existente.

@jimmyfrasche Não acho que posso construir um argumento para isso. Você pode assistir minha palestra ou ver o exemplo no README do repositório . Mas se a evidência visual não for convincente para você, então não há nada que eu possa dizer.

@jba assistiu à palestra e leu o README. Agora eu entendo de onde você está vindo com a coisa entre parênteses / nota de rodapé / nota de fim / nota secundária (e eu sou um fã de notas secundárias (e parênteses)).

Se o objetivo é colocar, por falta de termo melhor, o caminho infeliz para o lado, então um plugin $ EDITOR funcionaria sem mudança de idioma e funcionaria com todo o código existente, independentemente das preferências do autor do código.

Uma mudança de idioma torna a sintaxe um pouco mais compacta. @bcmills menciona que isso melhora o escopo, mas não vejo como poderia, a menos que tenha regras de escopo diferentes de := mas parece que causaria mais confusão.

@bcmills Não entendo seu comentário. Você pode obviamente diferenciá-los. A maneira antiga é assim:

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

A nova forma parece

check foo()

ou

foo() || &FooError{err}

ou qualquer que seja a cor da malha. Estou supondo que a maior parte da biblioteca padrão pode fazer a transição, mas nem tudo precisa.

Para adicionar aos requisitos de

Considere, por exemplo, gravar em um arquivo do Google Cloud Storage, onde queremos abortar a gravação do arquivo em qualquer erro:

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

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

As sutilezas deste código incluem:

  • O erro de Copiar é transmitido sorrateiramente por meio do argumento de retorno nomeado para a função defer.
  • Para ficar totalmente seguro, pegamos pânico de r e garantimos que abortamos a escrita antes de retomar o pânico.
  • Ignorar o erro do primeiro Close é intencional, mas parece um artefato de programador preguiçoso.

Usando o pacote errd, este código se parece com:

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

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

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

errd.Discard é um gerenciador de erros. Manipuladores de erros também podem ser usados ​​para agrupar, registrar, quaisquer erros.

e.Must é o equivalente a foo() || wrapError

e.Defer é extra e trata da passagem de erros para defers.

Usando genéricos, esse trecho de código poderia ser semelhante a:

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

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

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

Se padronizarmos os métodos a serem usados ​​para adiar, isso poderia até ser parecido com:

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

Onde DeferClose escolhe Close ou CloseWithError. Não dizer isso é melhor, mas apenas mostrar as possibilidades.

De qualquer forma, fiz uma apresentação em um encontro em Amsterdã na semana passada sobre esse assunto e parecia que a capacidade de facilitar o tratamento correto de erros é considerada mais útil do que torná-la mais curta.

Uma solução que melhora os erros deve se concentrar tanto em tornar mais fácil fazer as coisas certas do que em torná-las mais curtas.

@ALTree errd trata da "recuperação sofisticada de erros" fora da caixa.

@jimmyfrasche : errd faz mais ou menos o que seu exemplo de playground faz, mas também tece erros e pânico para defers.

@jimmyfrasche : Concordo que a maioria das propostas não adiciona muito ao que já pode ser alcançado no código.

@romainmenke : concorda que há muito foco na brevidade. Para tornar mais fácil fazer as coisas corretamente deve ter um foco maior.

@jba : a abordagem errd torna bastante fácil fazer a varredura do fluxo de erro versus não erro, apenas olhando para o lado esquerdo (qualquer coisa que comece com e. é erro ou adiamento do tratamento). Também torna muito fácil verificar quais dos valores de retorno são tratados para erro ou adiamento e quais não são.

@bcmills : embora errd não conserte os problemas de escopo por si só, ele elimina a necessidade de passar erros de downstream para variáveis ​​de erro declaradas anteriormente e tudo, mitigando assim o problema consideravelmente para tratamento de erros, AFAICT.

errd parece confiar inteiramente no pânico e na recuperação. parece que vem com uma penalidade de desempenho significativa. Não tenho certeza se é uma solução geral por causa disso.

@urandom : nos bastidores, é implementado como um adiamento mais caro, mas único.
Se o código original:

  • não usa adiar: a penalidade de usar errd é grande, cerca de 100 ns *.
  • usa adiamento idiomático: o tempo de execução ou errd é da mesma ordem, embora um pouco mais lento
  • usa o tratamento de erros adequado para adiar: o tempo de execução é quase igual; errd pode ser mais rápido se o número de adiamentos for> 1

Outras despesas gerais:

  • Passar fechamentos (w.Close) para Defer atualmente também adiciona cerca de 25 ns * de sobrecarga, em comparação com o uso da API DeferClose ou DeferFunc (consulte o release v0.1.0). Depois de discutir com @rsc , removi-o para manter a API simples e me preocupar com a otimização posterior.
  • O empacotamento de strings de erro embutidas como manipuladores ( e.Must(err, msg("oh noes!") ) custa cerca de 30 ns com o Go 1.8. Com a dica (1.9), entretanto, embora ainda seja uma alocação, registrei o custo em 2ns. Obviamente, para mensagens de erro pré-declaradas, o custo ainda é insignificante.

(*) todos os números em execução no meu MacBook Pro 2016.

Em suma, o custo parece aceitável se seu código original usar adiar. Caso contrário, Austin está trabalhando para reduzir significativamente o custo do adiamento, de modo que o custo pode até cair com o tempo.

De qualquer forma, o objetivo deste pacote é obter experiência sobre como usar o tratamento de erros alternativo seria útil agora, para que possamos construir a melhor adição de linguagem no Go 2. O caso em questão é a discussão atual, ela se concentra demais na redução de um poucas linhas para casos simples, enquanto há muito mais a ganhar e, sem dúvida, outros pontos são mais importantes.

@jimmyfrasche :

então um plugin $ EDITOR funcionaria sem mudança de idioma

Sim, é exatamente isso que argumento na palestra. Aqui, estou argumentando que, se fizermos uma mudança de linguagem, ela deve estar de acordo com o conceito de "nota lateral".

@nigeltao

Você pode obviamente diferenciá-los. A maneira antiga é assim:

Estou falando sobre o ponto de declaração, não o ponto de uso.

Algumas das propostas discutidas aqui não fazem distinção entre as duas no site da chamada, mas algumas o fazem. Se escolhermos uma das opções que assume "valor ou erro" - como || , try … catch ou match - então deve ser um erro em tempo de compilação para usar essa sintaxe com uma função de "valor e erro", e deve caber ao implementador da função definir qual é.

No ponto de declaração, atualmente não há como distinguir entre "valor e erro" e "valor ou erro":

func Atoi(string) (int, error)

e

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

têm os mesmos tipos de retorno, mas semânticas de erro diferentes.

@mpvl Estou olhando os documentos e src para errd. Acho que estou começando a entender como funciona, mas parece que tem um monte de API que atrapalha o entendimento que parece que poderia ser implementado em um pacote separado. Tenho certeza de que tudo o torna mais útil na prática, mas, como ilustração, adiciona muito ruído.

Se ignorarmos ajudantes comuns como funções de nível superior para operar no resultado de WithDefault (), e assumirmos, por uma questão de simplicidade que sempre usamos contexto, e ignorarmos quaisquer decisões tomadas para desempenho, a API barebone mínima absoluta reduziria para o abaixo das operações?

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

Olhando para o código, vejo alguns bons motivos para que ele não esteja definido como acima, mas estou tentando chegar à semântica principal para entender melhor o conceito. Por exemplo, não tenho certeza se IsSentinel está no núcleo ou não.

@jimmyfrasche

@bcmills menciona que isso melhora o escopo, mas não vejo como poderia

A principal melhoria é manter a variável err fora do escopo. Isso evitaria bugs como os vinculados a https://github.com/golang/go/issues/19727. Para ilustrar com um trecho de um deles:

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

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

O bug ocorre na última instrução if: o erro de Decode é descartado, mas não é óbvio porque um err de uma verificação anterior ainda estava no escopo. Em contraste, usando o operador :: ou =? , isso seria escrito:

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

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

Aqui, há duas melhorias de escopo que ajudam:

  1. O primeiro err (da chamada anterior Get ) está apenas no escopo do bloco return , portanto, não pode ser usado acidentalmente em verificações subsequentes.
  2. Como err de Decode é declarado na mesma instrução em que é verificado quanto a zero, não pode haver distorção entre a declaração e a verificação.

(1) por si só teria sido suficiente para revelar o erro em tempo de compilação, mas (2) torna mais fácil evitar ao usar a instrução guard da maneira óbvia.

@bcmills obrigado pelo esclarecimento

Assim, em res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err } a macro =? expande para

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

Se estiver correto, é o que eu quis dizer quando disse que teria que ter uma semântica diferente de := .

Parece que isso causaria suas próprias confusões, como:

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

A menos que eu tenha entendido mal alguma coisa.

Sim, essa é a expansão correta. Você está certo ao dizer que é diferente de := , e isso é intencional.

Parece que causaria suas próprias confusões

Isso é verdade. Não é óbvio para mim se isso seria confuso na prática. Se for, poderíamos fornecer variantes ":" da instrução de guarda para declaração (e ter apenas as variantes "=" atribuídas).

(E agora isso me faz pensar que os operadores deveriam ser escritos ? e ! vez de =? e =! .)

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

mas

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

@mpvl Minha principal preocupação com errd é com a interface Handler: ela parece encorajar pipelines de estilo funcional de retornos de chamada, mas minha experiência com código de estilo de retorno de chamada / continuação (tanto em linguagens imperativas como Go e C ++ quanto em funcional linguagens como ML e Haskell) é que muitas vezes é muito mais difícil de seguir do que o estilo sequencial / imperativo equivalente, que também se alinha com o resto dos idiomas Go.

Você imagina cadeias no estilo Handler como parte da API ou seu Handler um substituto para alguma outra sintaxe que você está considerando (como algo operando em Block s?)

@bcmills Ainda não estou de acordo com os recursos mágicos que introduzem dezenas de conceitos na linguagem em uma única linha e só funcionam com uma coisa, mas finalmente entendi por que eles são mais do que apenas uma maneira um pouco mais curta de escrever x, err := f(); if err != nil { return err } . Obrigado por me ajudar a entender e desculpe a demora.

@bcmills Eu reescrevi o exemplo motivador de @mpvl , que tem algum tratamento de erros =? mais recente que nem sempre declara uma nova variável err:

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

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

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

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

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

A maior parte do tratamento de erros permanece inalterada. Eu só pude usar =? em dois lugares. Em primeiro lugar, não trouxe nenhum benefício que eu pudesse ver. No segundo, tornou o código mais longo e obscureceu o fato de que io.Copy retorna duas coisas, então provavelmente teria sido melhor simplesmente não usá-lo ali.

@jimmyfrasche Esse código é a exceção, não a regra. Não devemos projetar recursos para torná-lo mais fácil de escrever.

Além disso, eu questiono se recover deveria mesmo estar lá. Se w.Write ou r.Read (ou io.Copy !) Estiver em pânico, provavelmente é melhor encerrar.

Sem o recover , não há necessidade real do defer , e a parte inferior da função pode se tornar

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

@jimmyfrasche

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

Observe que meu texto específico (em https://github.com/golang/go/issues/21161#issuecomment-319434101) é sobre valores zero, não erros especificamente.

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

Não é, embora eu pudesse ter sido mais explícito sobre isso.

Eu particularmente não gosto do uso de recover por @mpvl naquele exemplo: ele incentiva o uso do pânico sobre o fluxo de controle idiomático, enquanto eu acho que devemos eliminar chamadas recover estranhas ( como os de fmt ) da biblioteca padrão do Go 2.

Com essa abordagem, eu escreveria esse código como:

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

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

Por outro lado, você está correto ao dizer que, com a recuperação unidiomática, há pouca oportunidade de aplicar recursos destinados a oferecer suporte ao tratamento de erros idiomáticos. No entanto, separar a recuperação da operação Close leva a um código IMO um tanto mais limpo.

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

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

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

@jba, o manipulador entrar em pânico: ele está lá para tentar notificar o processo no outro computador para que não cometa acidentalmente uma transação incorreta (assumindo que isso ainda seja possível em um estado de erro potencial). Se isso não for bastante comum, provavelmente deveria ser. Concordo que Ler / Gravar / Copiar não deve entrar em pânico, mas se houvesse outro código que pudesse entrar em pânico, por qualquer motivo, estaríamos de volta ao ponto de partida.

@bcmills, essa última revisão parece melhor (mesmo que você tenha retirado =? , na verdade)

@jba :

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

isso ainda não cobre o caso de pânico no leitor. É certo que este é um caso raro, mas bastante importante: ligar para Close aqui em caso de pânico é muito ruim.

Esse código é a exceção, não a regra. Não devemos projetar recursos para torná-lo mais fácil de escrever.

@jba : Discordo totalmente neste caso. É importante obter o tratamento correto de erros. Permitir que o caso simples seja mais fácil encorajará ainda menos as pessoas a pensar sobre o tratamento adequado de erros. Eu gostaria de ver alguma abordagem que, como errd , torna o tratamento de erros conservador trivialmente fácil, enquanto requer algum esforço para relaxar as regras, não qualquer coisa que se mova, mesmo que ligeiramente na outra direção.

@jimmyfrasche : em relação à sua simplificação: você está quase certo.

  • IsSentinel não é essencial, apenas prático e comum. Eu deixei cair, pelo menos por agora.
  • Err no estado é diferente de err, então sua API o descarta. Não é fundamental para a compreensão, no entanto.
  • Os manipuladores podem ser funções, mas são interfaces principalmente por motivos de desempenho. Só sei que muitas pessoas não usarão o pacote se ele não for otimizado. (veja alguns dos primeiros comentários sobre errd nesta edição)
  • O contexto é lamentável. O AppEngine precisa disso, mas não muito mais que eu reconheço. Eu ficaria bem removendo suporte para ele até que as pessoas se recusem.

@mpvl Eu estava apenas tentando reduzir isso a algumas coisas para que fosse mais fácil entender como funcionava, como usá-lo e imaginar como se encaixaria no código que escrevi.

@jimmyfrasche : entendido, embora seja bom se uma API não exigir que você faça isso. :)

@bcmills : Os manipuladores servem para alguns propósitos, por exemplo, em ordem de importância:

  • embrulhe um erro
  • definir para ignorar erros (para tornar isso explícito. Veja o exemplo)
  • erros de registro
  • métricas de erro

Novamente em ordem de importância, eles precisam ter o escopo definido por:

  • quadra
  • linha
  • pacote

Os erros padrão existem apenas para tornar mais fácil garantir que um erro seja tratado em algum lugar,
mas eu poderia viver apenas no nível do bloco. Originalmente, eu tinha uma API com opções em vez de manipuladores. Isso resultou em uma API maior e mais desajeitada, além de ser mais lenta.

Não vejo o problema de retorno de chamada sendo tão ruim aqui. Os usuários definem um Runner passando um Handler que é chamado se houver um erro. O runner específico é explicitamente especificado no bloco onde os erros são tratados. Em muitos casos, um manipulador será apenas um literal de string encapsulado passado inline. Vou brincar um pouco para ver o que é útil e o que não é.

BTW, se não devemos encorajar erros de registro em manipuladores, então o suporte de contexto provavelmente pode ser abandonado.

@jba :

Além disso, questiono se a recuperação deveria mesmo estar lá. Se w.Write ou r.Read (ou io.Copy!) Estiver em pânico, provavelmente é melhor encerrar.

writeToGS ainda termina se houver um pânico, como deveria (!!!), ele meramente garante que chamará CloseWithError com um erro não nulo. Se o pânico não for controlado, o adiamento ainda será chamado, mas com err == nil, resultando na materialização de um arquivo potencialmente corrompido no Cloud Storage. A coisa certa a fazer aqui é chamar CloseWithError com algum erro temporário e continuar o pânico.

Encontrei vários exemplos como este no código Go. Lidar com io.Pipes também costuma resultar em um código um pouco sutil demais. O tratamento de erros geralmente não é tão simples quanto parece, como você mesmo viu agora.

@bcmills

Eu particularmente não gosto do uso de recuperar do @mpvl naquele exemplo: ele incentiva o uso de pânico sobre o fluxo de controle idiomático,

Não estou tentando encorajar o uso do pânico. Observe que o pânico é ressurgido logo após CloseWithError e, portanto, não altera o fluxo de controle. O pânico continua sendo o pânico.
No entanto, não usar recovery aqui é errado, pois um pânico fará com que o adiamento seja chamado com um erro nulo, sinalizando que o que foi escrito até agora pode ser confirmado.

O único argumento válido para não usar a recuperação aqui é que é muito improvável que ocorra um pânico, mesmo para um Leitor arbitrário (o Leitor é de tipo desconhecido neste exemplo por uma razão :)).
No entanto, para código de produção, esta é uma postura inaceitável. Especialmente ao programar em uma escala grande o suficiente, isso pode acontecer em algum momento (pânico pode ser causado por outras coisas além de bugs no código).

BTW, observe que o pacote errd elimina a necessidade de o usuário pensar sobre isso. Qualquer outro mecanismo que sinalize um erro em caso de pânico para adiar está bem, no entanto. Não chamar adiadores em pânico também funcionaria, mas isso vem com seus próprios problemas.

No ponto de declaração, atualmente não há como distinguir entre "valor e erro" e "valor ou erro":

@bcmills Oh, entendo. Para abrir outra lata de bicicletas, suponho que você poderia dizer

func Atoi(string) ?int

em vez de

func Atoi(string) (int, error)

mas WriteString permaneceria inalterado:

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

Eu gosto da proposta =? / =! / :=? / :=! de @bcmills / @jba mais do que propostas semelhantes. Tem algumas propriedades interessantes:

  • composable (você pode usar =? dentro de um bloco =? )
  • geral (só se preocupa com o valor zero, não é específico para o tipo de erro)
  • escopo aprimorado
  • poderia funcionar com adiar (em uma variação acima)

Ele também tem algumas propriedades que não acho tão agradáveis.

Ninhos de composição. O uso repetido continuará a recuar cada vez mais para a direita. Isso não é necessariamente uma coisa ruim em si, mas eu imagino que em situações com tratamento de erros muito complicado que requer lidar com erros que causam erros, o código para lidar com eles ficaria rapidamente muito menos claro do que o status quo atual. Em tal situação, pode-se usar =? para o erro externo e if err != nil nos erros internos, mas isso realmente melhorou o tratamento de erros em geral ou apenas no caso comum? Talvez melhorar o caso comum seja tudo o que é necessário, mas não acho isso convincente, pessoalmente.

Introduz falsidade na linguagem para ganhar sua generalidade. Falsidade sendo definida como "é (não) o valor zero" é perfeitamente razoável, mas if err != nil { é melhor do que if err { uma vez que é explícito, na minha opinião. Eu esperaria ver contorções na natureza tentando usar =? / etc. sobre um fluxo de controle mais natural para tentar obter acesso à sua falsidade. Isso certamente seria unidiomático e desaprovado, mas aconteceria. Embora o abuso potencial de um recurso não seja em si um argumento contra um recurso, é algo a se considerar.

O escopo aprimorado (para as variantes que declaram seus parâmetros) é bom em alguns casos, mas se o escopo precisar ser corrigido, corrija o escopo em geral.

A semântica do "único resultado mais à direita" faz sentido, mas parece um pouco estranha para mim. Isso é mais um sentimento do que uma discussão.

Esta proposta adiciona brevidade à linguagem, mas nenhum poder adicional. Ele pode ser implementado inteiramente como um pré-processador que faz expansão de macro. Isso seria obviamente indesejável: complicaria as compilações e o desenvolvimento de fragmentos e qualquer pré-processador seria extremamente complicado, uma vez que tem que ser compatível com o tipo e higiênico. Não estou tentando ignorar dizendo "apenas faça um pré-processador". Toco isto apenas para salientar que esta proposta é inteiramente açucarada. Ele não permite que você faça nada que não pudesse fazer no Go agora; apenas permite que você escreva de forma mais compacta. Não sou dogmaticamente contra o açúcar. Há poder em uma abstração linguística cuidadosamente escolhida, mas o fato de ser açúcar significa que deve ser considerado 👎 até que se prove sua inocência, por assim dizer.

Os lhs dos operadores são uma declaração, mas um subconjunto muito limitado de declarações. Quais elementos incluir nesse subconjunto são bastante evidentes, mas, se nada mais, seria necessário refatorar a gramática na especificação do idioma para acomodar a mudança.

Algo como

func F() (S, T, error)

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

ser permitido?

Se

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

é permitido que deve ser (de alguma forma) equivalente a

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

o que parece profundamente problemático e provavelmente pode causar situações muito confusas, mesmo ignorando que de uma maneira nada Go-like esconde as implicações de desempenho de alocar implicitamente um fechamento. A alternativa é ter return retornando do fechamento implícito e, em seguida, ter uma mensagem de erro informando que você não pode retornar um valor do tipo error de um func() que é um pouco obtuso.

Porém, na verdade, além de uma correção de escopo ligeiramente melhorada, isso não corrige nenhum dos problemas que enfrento ao lidar com erros no Go. No máximo, digitar if err != nil { return err } é um incômodo, modulo as leves preocupações de legibilidade que expressei em # 21182. Os dois maiores problemas são

  1. pensando em como lidar com o erro - e não há nada que uma linguagem possa fazer sobre isso
  2. fazer a introspecção de um erro para determinar o que fazer em algumas situações - alguma convenção adicional com suporte do pacote errors ajudaria muito aqui, embora eles não possam resolver todos os problemas.

Sei que esses não são os únicos problemas e que muitos acham outros aspectos mais imediatamente preocupantes, mas são com eles que passo mais tempo e que considero mais irritantes e incômodos do que qualquer outra coisa.

Uma melhor análise estática para detectar quando eu baguncei algo sempre seria apreciada, é claro (e em geral, não apenas neste cenário). Também seriam de interesse as alterações e convenções de idioma que facilitam a análise da fonte, de forma que sejam mais úteis.

Acabei de escrever muito (MUITO! Desculpe!) Sobre isso, mas não estou descartando a proposta. Eu realmente acho que tem mérito, mas não estou convencido de que limpe a barra ou puxa seu peso.

@jimmyfrasche

Lembro-me de quando o comportamento atual de: = foi introduzido — muito daquele tópico enlouquecido † clamava por uma maneira de anotar explicitamente quais nomes deveriam ser reutilizados em vez da "reutilização implícita apenas se essa variável existir exatamente no escopo atual "que é onde todos os problemas sutis difíceis de ver se manifestam, na minha experiência.

† Não consigo encontrar esse tópico, alguém tem um link?

Acho que você deve estar se lembrando de um tópico diferente, a menos que esteja envolvido com Go quando ele foi lançado. A especificação de 2009/11/9, pouco antes de ser lançada, tem:

Ao contrário das declarações de variáveis ​​regulares, uma declaração de variável curta pode redeclarar variáveis ​​desde que tenham sido declaradas originalmente no mesmo bloco com o mesmo tipo, e pelo menos uma das variáveis ​​não em branco seja nova.

Lembro-me de ter visto isso ao ler a especificação pela primeira vez e pensar que era uma ótima regra, já que eu já havia usado uma linguagem com: = mas sem aquela regra de reutilização, e pensar em novos nomes para a mesma coisa era entediante.

@mpvl
Eu acho que a complexidade do seu exemplo original é mais resultado do
a API que você está usando lá do que o próprio tratamento de erros do Go.

É um exemplo interessante, principalmente pelo fato de
você não deseja fechar o arquivo normalmente se entrar em pânico, então o
o idioma normal "adiar w.Close ()" não funciona.

Se você não precisava evitar chamar Close quando há um
pânico, então você poderia fazer:

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

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

assumindo que a semântica foi alterada de forma que chamar Close
depois de chamar CloseWithError é um ambiente autônomo.

Não acho mais que isso pareça tão ruim.

Mesmo com a exigência de que o arquivo não seja gravado sem erros quando há um pânico, não deve ser muito difícil de acomodar; por exemplo, adicionando uma função Finalizar que deve ser chamada explicitamente antes de Fechar.

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

Isso não pode anexar a mensagem de erro de pânico, mas um registro decente pode deixar isso mais claro.
(o método Close pode até ter uma chamada de recuperação dentro dele, embora eu não tenha certeza se isso
na verdade, uma ideia muito ruim ...)

No entanto, eu acho que o aspecto de recuperação de pânico deste exemplo é um pouco uma pista falsa neste contexto, já que 99% dos casos de tratamento de erros não fazem a recuperação de pânico.

@rogpeppe :

Isso não pode anexar a mensagem de erro de pânico, mas um registro decente pode deixar isso mais claro.

Não acho que isso seja um problema.

No entanto, sua proposta de alteração de API atenua, mas ainda não resolve totalmente o problema. A semântica necessária requer que outro código também se comporte corretamente. Considere o exemplo abaixo:

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

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

Por si só, este código mostra que o tratamento de erros pode ser complicado ou pelo menos confuso (e eu ficaria curioso em saber como isso poderia ser melhorado com as outras propostas): ele passa furtivamente erros de downstream por meio de uma variável e tem um pouco demais desajeitada instrução if para garantir que o erro correto seja passado. Ambos distraem muito da "lógica de negócios". O tratamento de erros domina o código. E este exemplo ainda nem lida com o pânico.

Para completar, em errd isto _seria_ os pânicos corretamente e se pareceria com:

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

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

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

Se o leitor acima (sem usar errd ) for passado como leitor para writeToGS e o io.Reader retornado por newReader entrar em pânico, isso ainda resultaria em semântica com falha com sua correção de API proposta (pode correr para fechar com sucesso o arquivo GS após o tubo ser fechado em pânico com um erro nulo.)

Isso também prova o ponto. Não é trivial raciocinar sobre o tratamento adequado de erros no Go. Quando observei como ficaria o código reescrevendo-o com errd , encontrei um monte de código com erros. No entanto, eu realmente só aprendi como é difícil e sutil escrever o tratamento idiomático adequado de erros Go, ao escrever os testes de unidade para o pacote errd . :)

Uma alternativa para sua mudança de API proposta seria não lidar com nenhum adiamento ou pânico. Isso tem seus próprios problemas e não resolveria totalmente o problema e provavelmente pode ser desfeito, mas teria algumas qualidades interessantes.

De qualquer maneira, o melhor seria alguma mudança de linguagem que mitigue as sutilezas do tratamento de erros, em vez de uma que se concentre na brevidade.

@mpvl
Costumo descobrir com o código de tratamento de erros em Go que a criação de outra função pode limpar as coisas. Eu escreveria seu código acima mais ou menos assim:

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

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

Estou assumindo que r.Close não retorna um erro útil - se você leu todo o caminho em um leitor e apenas encontrou apenas io.EOF, então quase certamente não importa se ele retorna um erro quando fechado.

Não estou muito interessado na API errd - ela é muito sensível aos goroutines sendo iniciados. Por exemplo: https://play.golang.org/p/iT441gO5us Se doSomething inicia ou não uma goroutine para executar o
A função in não deve afetar a exatidão do programa, mas ao usar errd, afeta. Você está esperando que o pânico atravesse com segurança os limites da abstração, e isso não acontece no Go.

@mpvl

adiar w.CloseWithError (err)

BTW, esta linha sempre chama CloseWithError com um valor de erro nulo. Eu acho que você quis
escrever:

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

@mpvl

Observe que o erro retornado pelo método Close em um io.Reader quase nunca é útil (consulte a lista em https://github.com/golang/go/issues/20803#issuecomment-312318808 )

Isso sugere que devemos escrever seu exemplo hoje como:

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

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

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

... o que me parece perfeitamente normal, além de ser um pouco prolixo.

É verdade que ele passa um erro nulo para w.CloseWithError em caso de pânico, mas todo o programa termina nesse ponto de qualquer maneira. Se for importante nunca fechar com um erro nulo, é uma simples renomeação mais uma linha extra:

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

@rogpeppe : de fato, obrigado. :)

Sim, estou ciente do problema da goroutine. É desagradável, mas provavelmente algo que não é difícil de detectar com um exame veterinário. De qualquer forma, não vejo errd como uma solução final, mas mais como uma forma de obter experiência sobre a melhor forma de lidar com erros. O ideal seria que houvesse uma mudança de idioma que resolvesse os mesmos problemas, mas com as devidas restrições impostas.

Você está esperando que o pânico atravesse com segurança os limites da abstração, e isso não acontece no Go.

Não é isso que estou esperando. Nesse caso, espero que as APIs não relatem sucesso quando não havia. Seu último trecho de código o trata corretamente, porque não usa adiar para o redator. Mas isso é muito sutil. Muitos usuários usariam adiar neste caso porque é considerado idiomático.

Talvez um conjunto de exames veterinários pudesse detectar usos problemáticos de defers. Ainda assim, tanto no código "idiomático" original quanto em sua última parte refatorada, há muitos ajustes para contornar as sutilezas do tratamento de erros para algo que, de outra forma, é um pedaço de código bem simples. O código de solução alternativa não é para descobrir como lidar com certos casos de erro, é puramente um desperdício de ciclos cerebrais que poderiam ser usados ​​para uso produtivo.

Especificamente, o que estou tentando aprender com errd é se torna o tratamento de erros mais simples quando usado de maneira direta. Pelo que posso ver, muitas complicações e sutilezas desaparecem. Seria bom ver se podemos codificar aspectos de sua semântica em novos recursos de linguagem.

@jimmyfrasche

Introduz falsidade na linguagem para ganhar sua generalidade.

Esse é um ponto muito bom. Os problemas usuais com falsidade vêm de esquecer de invocar uma função booleana ou de desreferenciar um ponteiro para nil.

Poderíamos resolver o último definindo o operador para trabalhar apenas com tipos nillable (e provavelmente eliminando =! como resultado, uma vez que seria praticamente inútil).

Poderíamos abordar o primeiro restringindo-o ainda mais para não funcionar com tipos de função, ou para trabalhar apenas com tipos de ponteiro ou interface: então ficaria claro que a variável não é um booleano, e as tentativas de usá-lo para comparações booleanas seriam mais obviamente errado.

Algo como [ MustF ] seria permitido?

sim.

Se [ defer f.Close() :=? err { ] for permitido, deve ser (de alguma forma) equivalente a
[ defer func() { … }() ].

Não necessariamente, não. Ela poderia ter sua própria semântica (mais parecida com call/cc que uma função anônima). Não propus uma alteração nas especificações para usar =? em defer (isso exigiria pelo menos uma mudança na gramática), então não tenho certeza de quão complicada seria tal definição .

Os dois maiores problemas são [...] 2. introspecção de um erro para determinar o que fazer em algumas situações

Concordo que esse é um problema maior na prática, mas parece mais ou menos ortogonal a esse problema (que é mais sobre como reduzir o clichê e o potencial associado para erros).

( @rogpeppe , @davecheney , @dsnet , @crawshaw , I e alguns outros, com certeza estou esquecendo, tivemos uma boa discussão no GopherCon sobre APIs para inspeção de erros e espero que veremos algumas boas propostas nessa frente também , mas eu realmente acho que isso é outro problema.)

@bcmills : este código tem dois problemas 1) igual ao @rogpeppe mencionado: erro passado para CloseWithError é sempre nulo e 2) ele ainda não lida com pânicos, o que significa que a API relatará sucesso explicitamente quando houver um pânico (o retornado r pode emitir um io.EOF mesmo quando nem todos os bytes foram gravados), mesmo se 1 for fixo.

Caso contrário, concordo que o erro retornado por Close geralmente pode ser ignorado. Mas nem sempre (veja o primeiro exemplo).

Acho um tanto surpreendente que tenha havido cerca de 4 ou 5 sugestões erradas em meus exemplos bastante simples (incluindo um meu) e ainda sinto que devo argumentar que o tratamento de erros no Go não é trivial. :)

@bcmills :

É verdade que ele passa um erro nulo para w.CloseWithError em caso de pânico, mas todo o programa termina nesse ponto de qualquer maneira.

É mesmo? Os adiadores daquela goroutine ainda são chamados. Pelo que eu entendo, eles serão executados até a conclusão. Neste caso, o Fechar sinalizará um io.EOF.

Veja, por exemplo, https://play.golang.org/p/5CFbsAe8zF. Depois que a goroutine entra em pânico, felizmente, ele passa "foo" para a outra goroutine, que ainda consegue escrever para Stdout.

Da mesma forma, outro código pode receber um io.EOF incorreto de uma goroutina em pânico (como a de seu exemplo), concluir o sucesso e enviar um arquivo para o GS antes que a goroutina em pânico retome seu pânico.

Seu próximo argumento pode ser: bem, não escreva código com erros, mas:

  • em seguida, torne mais fácil evitar esses bugs e
  • o pânico pode ser causado por fatores externos, como OOMs.

Se for importante nunca fechar com um erro nulo, é uma simples renomeação mais uma linha extra:

Ele ainda deve fechar com nil para sinalizar io.EOF quando for concluído, portanto, não funcionará.

Se for importante nunca fechar com um erro nulo, é uma simples renomeação mais uma linha extra:

Ele ainda deve fechar com nil para sinalizar io.EOF quando for concluído, portanto, não funcionará.

Por que não? O return err no final definirá rerr para nil .

@bcmills : ah, entendo o que você quer dizer agora. Sim, isso deve funcionar. Não estou preocupado com o número de linhas, mas sim com a sutileza do código.

Acho que isso está na mesma categoria de problemas que o sombreamento variável, apenas menos provável de ocorrer (possivelmente tornando-o pior). A maioria dos bugs de sombreamento variável que você pode argumentar ocorrerem com bons testes de unidade. Os pânicos arbitrários são mais difíceis de testar.

Ao operar em escala, é praticamente garantido que você verá bugs como este se manifestando. Posso ser paranóico, mas vi cenários muito menos prováveis ​​de levar à perda e corrupção de dados. Normalmente, isso é bom, mas não para o processamento de transações (como gravar arquivos GS).

Espero que você não se importe que eu sequestre sua proposta com uma sintaxe alternativa - como as pessoas se sentem sobre algo assim:

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

@SirCmpwn Isso enterra o lede. A coisa mais fácil de ler em uma função deve ser o fluxo normal de controle, não o tratamento de erros.

Isso é justo, mas sua proposta também me deixa desconfortável - ela apresenta uma sintaxe opaca (||) que se comporta de maneira diferente de como os usuários foram treinados para esperar || comportar-se. Não tenho certeza de qual é a solução certa, vou meditar sobre isso um pouco mais.

@SirCmpwn Sim, como eu disse no post original "Estou escrevendo esta proposta principalmente para encorajar as pessoas que desejam simplificar o tratamento de erros do Go a pensar em maneiras de tornar mais fácil envolver o contexto em torno dos erros, não apenas para retornar o erro inalterado . " Escrevi minha proposta o melhor que pude, mas não espero que seja adotada.

Entendido.

Isso é um pouco mais radical, mas talvez uma abordagem orientada a macro funcione melhor.

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

try! comeria o último valor na tupla e o retornaria se não fosse nulo e, caso contrário, retornaria o resto da tupla.

Eu gostaria de sugerir que nossa declaração de problema é,

O tratamento de erros no Go é prolixo e repetitivo. O formato idiomático do tratamento de erros do Go torna mais difícil ver o fluxo de controle sem erros e a verbosidade é desagradável, especialmente para os recém-chegados. Até o momento, as soluções propostas para este problema geralmente requerem funções artesanais de tratamento de erros pontuais, reduzem a localização do tratamento de erros e aumentam a complexidade. Como um dos objetivos de Go é forçar o redator a considerar o tratamento e a recuperação de erros, qualquer melhoria no tratamento de erros também deve se basear nesse objetivo.

Para resolver esta declaração de problema, proponho estes objetivos para melhorias no tratamento de erros no Go 2.x:

  1. Reduz o clichê de manipulação de erros repetitivos e maximiza o foco na intenção primária do caminho do código.
  2. Encoraja o tratamento adequado de erros, incluindo o agrupamento de erros ao propagá-los adiante.
  3. Obedece aos princípios de design Go de clareza e simplicidade.
  4. É aplicável na mais ampla gama possível de situações de tratamento de erros.

Avaliando esta proposta:

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

de acordo com esses objetivos, eu concluiria que ele teve um bom desempenho no objetivo nº 1. Não tenho certeza de como isso ajuda com o nº 2, mas também não torna a adição de contexto menos provável (minha própria proposta compartilhava dessa fraqueza no nº 2). Ele realmente não tem sucesso em # 3 e # 4, embora:
1) Como já foi dito, a verificação e atribuição do valor de erro é opaca e incomum; e
2) A sintaxe =? também é incomum. É especialmente confuso se combinado com a sintaxe =! semelhante, mas diferente. Vai demorar um pouco para as pessoas se acostumarem com seus significados; e
3) Retornar um valor válido junto com o erro é comum o suficiente para que qualquer nova solução também trate desse caso.

Pode ser uma boa ideia transformar o tratamento de erro em um bloco, embora, como outros sugeriram, ele seja combinado com alterações em gofmt . Em relação à minha proposta, melhora a generalidade, o que deve ajudar com a meta nº 4 e familiaridade que ajuda a meta nº 3 ao custo de um sacrifício em brevidade para o caso comum de simplesmente retornar o erro com contexto adicionado.

Se você tivesse me perguntado no abstrato, eu poderia ter concordado que uma solução mais geral seria preferível a uma solução específica de tratamento de erros, desde que atendesse aos objetivos de melhoria de tratamento de erros acima. Agora, porém, depois de ler esta discussão e pensar mais a respeito, estou inclinado a acreditar que uma solução específica de tratamento de erros resultará em maior clareza e simplicidade. Embora os erros no Go sejam apenas valores, o tratamento de erros constitui uma parte tão significativa de qualquer programação que parece apropriado ter alguma sintaxe específica para tornar o código de tratamento de erros claro e conciso. Receio que tornaremos um problema já difícil (apresentar uma solução limpa para tratamento de erros) ainda mais difícil e complicado se o confundirmos com outros objetivos, como escopo e capacidade de composição.

Apesar disso, porém, como @rsc aponta em seu artigo, Toward Go 2 , nem a declaração do problema, os objetivos ou qualquer proposta de sintaxe provavelmente avançará sem relatos de experiência que demonstrem que o problema é significativo. Talvez, em vez de debater várias propostas de sintaxe, devêssemos começar a cavar em busca de dados de apoio.

Independentemente disso, porém, como @rsc aponta em seu artigo, Toward Go 2, nem a declaração do problema, os objetivos ou qualquer proposta de sintaxe têm probabilidade de avançar sem relatos de experiência que demonstrem que o problema é significativo. Talvez, em vez de debater várias propostas de sintaxe, devêssemos começar a cavar em busca de dados de apoio.

Acho que isso é evidente se assumirmos que a ergonomia é importante. Abra qualquer base de código Go e procure por lugares onde haja oportunidades para secar as coisas e / ou melhorar a ergonomia que a linguagem pode resolver - no momento, o tratamento de erros é um claro outlier. Acho que a abordagem Toward Go 2 pode advogar erroneamente para desconsiderar problemas que têm soluções alternativas - neste caso, as pessoas apenas sorriem e suportam.

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

Quando há mais clichês do que código, o problema é evidente por si só.

@billyh

Acho que o formato: f.Close() =? err { return fmt.Errorf(…, err) } é excessivamente prolixo e confuso. Eu pessoalmente não acho que a parte do erro deva estar bloqueada. Inevitavelmente, isso o levaria a ser espalhado em 3 linhas em vez de 1. Além disso, na mudança off que você precisa fazer mais do que apenas modificar um erro antes de retorná-lo, pode-se usar o if err != nil { ... } atual

O operador =? também é um pouco confuso. Não é imediatamente óbvio o que está acontecendo lá.

Com algo assim:
file := os.Open("/some/file") or raise(err) errors.Wrap(err, "extra context")
ou a abreviatura:
file := os.Open("/some/file") or raise
e o diferido:
defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2)
é um pouco mais prolixo e a escolha da palavra pode reduzir a confusão inicial (ou seja, as pessoas podem associar imediatamente raise a uma palavra-chave semelhante de outras linguagens como python, ou apenas deduzir que o aumento levanta o erro / last-non- valor padrão na pilha para o chamador).

Também é uma boa solução imperativa, que não tenta resolver todos os possíveis erros obscuros de manipulação sob o sol. De longe, a maior parte do tratamento de erros na natureza é da natureza mencionada acima. Para o posterior, a sintaxe atual também está lá para ajudar.

Editar:
Se quisermos reduzir um pouco a "mágica", os exemplos anteriores também podem ser parecidos com:
file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")
file, err := os.Open("/some/file") or raise err
defer err2 := f.Close() or errors.ReplaceIfNil(err, err2)
Pessoalmente, acho que os exemplos anteriores são melhores, pois movem todo o tratamento de erros para a direita, em vez de dividi-lo como é o caso aqui. Porém, isso pode ser mais claro.

Eu gostaria de sugerir que nossa declaração de problema é, ...

Não concordo com a declaração do problema. Eu gostaria de sugerir uma alternativa:


O tratamento de erros não existe do ponto de vista da linguagem. A única coisa que Go oferece é um tipo de erro pré-declarado e mesmo isso é apenas para conveniência, pois não permite nada realmente novo. Erros são apenas valores . O tratamento de erros é apenas um código de usuário normal. Não há nada de especial sobre isso no ponto de vista da linguagem e não deve haver nada de especial sobre ele. O único problema com o tratamento de erros é que algumas pessoas acreditam que essa valiosa e bela simplicidade deve ser eliminada a qualquer custo.

Seguindo as linhas do que diz Cznic, seria bom ter uma solução que fosse útil para mais do que apenas tratamento de erros.

Uma maneira de tornar o tratamento de erros mais geral é pensar nisso em termos de tipos de união / tipos de soma e desempacotamento. Swift e Rust têm soluções com? ! sintaxe, embora eu ache que Rust tem sido um pouco instável.

Se não quisermos tornar os sum-types um conceito de alto nível, poderíamos torná-los apenas parte de retornos múltiplos, da maneira que tuplas não fazem realmente parte do Go, mas você ainda pode fazer retornos múltiplos.

Uma tentativa de sintaxe inspirada em Swift:

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

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

Você também pode usar isso para outras coisas, como:

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

A solução =? proposta por @bcmills e @jba não é apenas para erros, o conceito é diferente de zero. este exemplo funcionará normalmente.

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

A ideia central dessa proposta são as notas laterais, separar o objetivo principal do código e deixar as secundárias de lado, para facilitar a leitura.
Para mim a leitura de um código Go, em alguns casos, não é contínua, muitas vezes você tem a ideia de parar a ideia com if err!= nil {return err} , então a ideia de notas laterais me parece interessante, como em um livro que lemos a ideia principal continuamente e, em seguida, leia as notas laterais. ( @jba talk )
Em situações muito raras, o erro é o objetivo principal de uma função, talvez em uma recuperação. Normalmente quando temos um erro, adicionamos algum contexto, registramos e retornamos, nestes casos, notas laterais podem tornar seu código mais legível.
Não sei se é a melhor sintaxe, principalmente não gosto do bloco da segunda parte, uma nota lateral precisa ser pequena, uma linha deve bastar

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

@billyh

  1. Como já foi dito, a verificação e atribuição do valor de erro é opaca e incomum; e

Por favor, seja mais concreto: "opaco e incomum" são terrivelmente subjetivos. Você pode dar alguns exemplos de código em que acha que a proposta seria confusa?

  1. O =? a sintaxe também é incomum. […]

IMO isso é um recurso. Se alguém vir um operador incomum, suspeito que ele está mais inclinado a pesquisar o que ele faz em vez de apenas presumir algo que pode ou não ser preciso.

  1. Retornar um valor válido junto com o erro é comum o suficiente para que qualquer nova solução também trate desse caso.

É verdade?

Leia a proposta com atenção: =? realiza atribuições antes de avaliar Block , portanto, também pode ser usado para esse caso:

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

E como @nigeltao observou, você sempre pode usar o padrão existente 'n, err: = r.Read (buf) `. Adicionar um recurso para ajudar com escopo e clichê para o caso comum não significa que devemos usá-lo para casos incomuns também.

Talvez, em vez de debater várias propostas de sintaxe, devêssemos começar a cavar em busca de dados de apoio.

Veja os inúmeros problemas (e seus exemplos) que Ian vinculou na postagem original.
Consulte também https://github.com/golang/go/wiki/ExperienceReports#error -handling.

Se você teve uma visão específica desses relatórios, compartilhe-a.

@urandom

Eu pessoalmente não acho que a parte do erro deva estar bloqueada. Inevitavelmente, isso o levaria a se espalhar em 3 linhas em vez de 1.

O objetivo do bloqueio é duplo:

  1. para fornecer uma quebra visual e gramatical clara entre a expressão que produz erros e seu manipulador, e
  2. para permitir uma gama mais ampla de tratamento de erros (de acordo com o objetivo declarado de

3 linhas vs. 1 não é nem mesmo uma mudança de idioma: se o número de linhas for sua maior preocupação, poderíamos resolver isso com uma simples mudança para gofmt .

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

Já temos return e panic ; adicionar raise em cima desses parece adicionar muitas maneiras de sair de uma função com pouco ganho.

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

errors.ReplaceIfNil(err, err2) exigiria algumas semânticas de passagem por referência muito incomuns.
Em vez disso, você poderia passar err por ponteiro, suponho:

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

mas ainda parece muito estranho para mim. O token or constrói uma expressão, uma declaração ou algo mais? (Uma proposta mais concreta ajudaria.)

@carlmjohnson

Qual seria a sintaxe e semântica concretas de sua instrução guard … else ? Para mim, parece muito com =? ou :: com os tokens e as posições variáveis ​​trocadas. (Novamente, uma proposta mais concreta ajudaria: quais são a sintaxe e semântica reais que você tem em mente?)

@bcmills
O hipotético ReplaceIfNil seria simples:

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

Nada de incomum nisso. Talvez o nome ...

or seria um operador binário, onde o operando esquerdo seria um IdentifierList ou um PrimaryExpr. No caso do primeiro, é reduzido ao identificador mais à direita. Em seguida, permite que o operando da direita seja executado se o da esquerda não for um valor padrão.

É por isso que precisei de outro token depois, para fazer a mágica de retornar os valores padrão, para todos, exceto o último parâmetro na função Result, que levaria o valor da expressão posteriormente.
IIRC, houve outra proposta não muito tempo atrás que teria a linguagem adicionando um '...' ou algo, que tomaria o lugar da tediosa inicialização do valor padrão. Nesse caso, a coisa toda pode ser assim:

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

Quanto ao bloco, entendo que permite um manuseio mais amplo. Pessoalmente, não tenho certeza se o escopo desta proposta deve ser tentar atender a todos os cenários possíveis, em vez de cobrir hipotéticos 80%. E eu pessoalmente acredito que importa quantas linhas um resultado levaria (embora eu nunca tenha dito que essa era minha maior preocupação, que na verdade é a legibilidade, ou a falta dela, ao usar tokens obscuros como =?). Se esta nova proposta abrange várias linhas no caso geral, pessoalmente não vejo seus benefícios em algo como:

if f, err := os.Open("/some/file"); err != nil {
     return errors.Wrap(err, "more context")
}
  • se as variáveis ​​definidas acima fossem disponibilizadas fora do escopo if .
    E isso ainda tornaria uma função com apenas algumas dessas instruções mais difícil de ler, devido ao ruído visual desses blocos de tratamento de erros. E essa é uma das reclamações que as pessoas têm ao discutir o tratamento de erros em go.

@urandom

or seria um operador binário, onde o operando esquerdo seria um IdentifierList ou um PrimaryExpr. […] Ele então permite que o operando da direita seja executado se o da esquerda não for um valor padrão.

Os operadores binários de Go são expressões, não declarações, portanto, tornar or um operador binário levantaria muitas questões. (Qual é a semântica de or como parte de uma expressão maior e como isso corresponde aos exemplos que você postou com := ?)

Supondo que seja realmente uma instrução, qual é o operando à direita? Se for uma expressão, qual é o seu tipo, e raise ser usado como uma expressão em outros contextos? Se for uma declaração, qual é sua semântica senão raise ? Ou você está propondo que or raise seja essencialmente uma única declaração (por exemplo, or raise como uma sintaxe alternativa a :: ou =? )?

Posso escrever

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

?

Posso escrever

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

?

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

Não, isso seria inválido por causa do segundo raise . Se isso não existisse, toda a cadeia de transformação deveria passar e o resultado final deveria ser devolvido ao chamador. Embora essa semântica como um todo provavelmente não seja necessária, já que você pode simplesmente escrever:

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


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

Se assumirmos meu comentário original - onde ou tomaria o último valor do lado esquerdo, de modo que se fosse um valor padrão, a expressão final seria avaliada para o resto da lista de resultados; então sim, isso deve ser válido. Nesse caso, se r.Read retornar um erro, esse erro será retornado ao chamador. Caso contrário, n seria passado para f

Editar:

A menos que eu esteja ficando confuso com os termos, penso em or como um operador binário, cujos operandos devem ser do mesmo tipo (mas um pouco mágico, se o operando esquerdo for uma lista de coisas, e nesse caso, leva o último elemento da referida lista de coisas). raise seria um operador unário que pega seu operando e retorna da função, usando o valor desse operando como o valor do último argumento de retorno, com os anteriores tendo valores padrão. Você poderia então usar tecnicamente raise em uma instrução autônoma, para fins de retorno de uma função, também conhecida return ..., err

Este será o caso ideal, mas também estou bem com or raise sendo apenas uma alternativa de sintaxe para =? , contanto que também aceite uma instrução simples em vez de um bloco, para cobrir a maioria dos casos de uso de maneira menos detalhada. Ou podemos ir com uma gramática do tipo adiar também, onde aceita uma expressão. Isso cobriria a maioria dos casos como:

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

e casos complexos:

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

Pensando um pouco mais na minha proposta, estou deixando de lado os tipos união / soma. A sintaxe que estou propondo é

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

No caso de uma expressão, a expressão é avaliada e se o resultado não for igual a true para expressões booleanas ou o valor em branco para outras expressões, BLOCK é executado. Em uma atribuição, o último valor atribuído é avaliado para != true / != nil . Seguindo uma instrução de guarda, quaisquer atribuições feitas estarão no escopo (isso não cria um novo escopo de bloco [exceto talvez para a última variável?]).

Em Swift, o BLOCK para guard declarações deve conter um de return , break , continue , ou throw . Ainda não decidi se gosto disso ou não. Parece agregar algum valor porque um leitor sabe pela palavra guard que virá a seguir.

Alguém segue Swift bem o suficiente para dizer se guard é bem visto por aquela comunidade?

Exemplos:

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

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

var obj interface{}

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

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

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

Imho todo mundo está discutindo uma gama muito ampla de problemas aqui ao mesmo tempo, mas o padrão mais comum na realidade é "retornar o erro como está". Então, por que não abordar o maior problema com smth como:

code, err ?= fn()

o que significa que a função deve retornar em err! = nil?

para: = operador que podemos apresentar?: =

code, err ?:= fn()

situação com?: = parece ser pior devido ao sombreamento, já que o compilador terá que passar a variável "err" para o mesmo valor de retorno err.

Na verdade, estou muito animado que algumas pessoas estão se concentrando em tornar mais fácil escrever o código correto, em vez de apenas encurtar o código incorreto.

Algumas notas:

Um "relato de experiência" interessante de um dos designers de Midori da Microsoft sobre os modelos de erro.

Acho que algumas ideias deste documento e do Swift podem se aplicar perfeitamente ao Go2.

Apresentando uma nova palavra-chave throws reseved, as funções podem ser definidas como:

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

  return []byte{...}
}

Tentar chamar essa função a partir de outra função que não seja de lançamento resultará em erro de compilação, devido a erro de lançamento não tratado.
Em vez disso, devemos ser capazes de propagar o erro, que todos concordam ser um caso comum, ou lidar com ele.

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

  // ...
}

Para casos em que sabemos que um método não falhará, ou em testes, podemos introduzir try! semelhante ao swift.

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

  // ...
}

Não tenho certeza sobre isso (semelhante ao swift):

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

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

ps1. múltiplos valores de retorno func ReadRune() (ch Rune, size int) throws { ... }
ps2. podemos retornar com return try Get() ou return try! Get()
ps3. agora podemos fazer chamadas em cadeia como buffer.NewBuffer(try Get()) ou buffer.NewBuffer(try! Get())
ps4. Não tenho certeza sobre as anotações (maneira fácil de escrever errors.Wrap(err, "context") )
ps5. na verdade, são exceções
ps6. a maior vitória são os erros de tempo de compilação para exceções ignoradas

As sugestões que você escreve são exatamente descritas no link Midori com todas as coisas ruins
lados disso ... E uma consequência óbvia de "arremessos" será "pessoas
odeio ". Por que alguém deveria escrever" joga "todas as vezes para a maioria dos
funções?

BTW, sua intenção de forçar a verificação dos erros e não ignorá-los pode ser
aplicado a tipos sem erros também e é melhor ter mais
forma generalizada (por exemplo, gcc __attribute __ ((warn_unused_result))).

Quanto à forma do operador, sugiro a forma abreviada ou
forma de palavra-chave como esta:

? = fn () OU verificar fn () - propaga o erro para o chamador
! = fn () OU nofail fn () - pânico em caso de erro

No sábado, 26 de agosto de 2017 às 12h15, nvartolomei [email protected]
escreveu:

Algumas notas:

Um interessante relato de experiência
http://joeduffyblog.com/2016/02/07/the-error-model/ de um dos
designers de Midori na Microsoft sobre os modelos de erro.

Acho que algumas ideias deste documento e Swift
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html
pode se aplicar perfeitamente ao Go2.

Apresentando uma nova palavra-chave throws reseved, as funções podem ser definidas como:

função Get () [] byte throws {
E se (...) {
gerar erros.Novo ("ops")
}

return [] byte {...}
}

Tentar chamar esta função a partir de outra função de não arremesso irá
resultar em erro de compilação, devido a erro descartável não tratado.
Em vez disso, devemos ser capazes de propagar o erro, que todos concordam que é um
caso comum, ou lidar com isso.

função ScrapeDate () time.Time throws {
body: = Get () // erro de compilação, descartável não tratado
body: = try Get () // fomos explícitos sobre o potencial jogável

// ...
}

Para casos em que sabemos que um método não falhará, ou em testes, podemos
apresentar tentar! semelhante ao swift.

função GetWillNotFail () time.Time {
body: = Get () // erro de compilação, descartável não manipulado
body: = try Get () // erro de compilação, throwable não pode ser propagado, porque GetWillNotFail não é anotado com throws
corpo: = tente! Get () // funciona, mas entrará em pânico com lances! = Nulo

// ...
}

Não tenho certeza sobre isso (semelhante ao swift):

func main () {
// 1:
Faz {
fmt.Printf ("% v", tente ScrapeDate ())
} catch err {// err contém capturado que pode ser jogado
// ...
}

// 2:
Faz {
fmt.Printf ("% v", tente ScrapeDate ())
} catch err. (type) {// semelhante a uma instrução switch
erro de caso:
// ...
case io.EOF
// ...
}
}

ps1. múltiplos valores de retorno func ReadRune () (ch Rune, size int) throws {
...}
ps2. podemos retornar com return try Get () ou return try! Pegue()
ps3. agora podemos encadear chamadas como buffer.NewBuffer (tente Get ()) ou buffer.NewBuffer (tente!
Pegue())
ps4. Não tenho certeza sobre as anotações

-
Você está recebendo isto porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/21161#issuecomment-325106225 ou mudo
o segmento
https://github.com/notifications/unsubscribe-auth/AICzv9CLN77RmPceCqvjXVE_UZ6o7JGvks5sb-IYgaJpZM4Oi1c-
.

Acho que o operador proposto por @jba e @bcmills é uma ideia muito boa, embora seja melhor escrito como "??" em vez de "=?" IMO.

Olhando para este exemplo:

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

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

    return y, nil
}

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

Acho que doStuff2 é consideravelmente mais fácil e rápido de ler porque:

  1. desperdiça menos espaço vertical
  2. é fácil de ler rapidamente o caminho feliz no lado esquerdo
  3. é fácil de ler rapidamente as condições de erro no lado direito
  4. não tem variável err poluindo o namespace local da função

Para mim, esta proposta sozinha parece incompleta e tem muita magia. Como o operador ?? seria definido? “Captura o último valor de retorno se não for nulo”? “Captura o último valor de erro se corresponder ao tipo de método?”

Adicionar novos operadores para manipular valores de retorno com base em sua posição e tipo parece um hack.

Em 29 de agosto de 2017, 13:03 +0300, Mikael Gustavsson [email protected] , escreveu:

Acho que o operador proposto por @jba e @bcmills é uma ideia muito boa, embora seja melhor escrito como "??" em vez de "=?" IMO.
Olhando para este exemplo:
função doStuff () (int, error) {
x, errar: = f ()
se errar! = nulo {
retornar 0, wrapError ("f falhou", err)
}

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

   return y, nil

}

função doStuff2 () (int, error) {
x: = f () ?? (erro de erro) {return 0, wrapError ("f falhou", err)}
y: = g (x) ?? (erro de erro) {return 0, wrapError ("g falhou", err)}
return y, nil
}
Acho que doStuff2 é consideravelmente mais fácil e rápido de ler porque:

  1. desperdiça menos espaço vertical
  2. é fácil de ler rapidamente o caminho feliz no lado esquerdo
  3. é fácil de ler rapidamente as condições de erro no lado direito
  4. não tem variável err poluindo o namespace local da função

-
Você está recebendo isto porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub ou ignore a conversa.

@nvartolomei

Como o operador ?? seria definido?

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

Já que @bcmills recomendou ressuscitar um thread quiescente, se formos considerar o uso de outras linguagens, parece que os modificadores de instrução ofereceriam uma solução razoável para tudo isso. Para pegar o exemplo de @slvmnd ,

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

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

        return y, nil
}

Não tão conciso quanto ter a instrução e a verificação de erro em uma única linha, mas a leitura é razoavelmente boa. (Eu sugeriria não permitir a forma: = de atribuição na expressão if, caso contrário, os problemas de escopo provavelmente confundiriam as pessoas, mesmo se eles fossem claros na gramática) Permitir "a menos" como versão negada de "se" é um pouco de açúcar sintático, mas funciona bem para ler e vale a pena considerar.

Eu não recomendaria copiar do Perl aqui, no entanto. (Basic Plus 2 está bem) Dessa forma, estão os modificadores de instrução em loop que, embora às vezes úteis, trazem outro conjunto de questões bastante complexas.

uma versão mais curta:
retornar se errar! = nulo
também deve ser apoiado então.

com tal sintaxe surge a questão - se as declarações de não retorno também
suportado com tais declarações "if", como este:
função (args) se condição

talvez em vez de inventar pós-ação - se vale a pena apresentar
linha se é?

se errar! = retorno nulo
if err! = nil return 0, wrapError ("falhou", err)
se errar! = nil do_smth ()

parece muito mais natural do que formas especiais de sintaxe, não? Embora eu ache
isso introduz muita dor na análise: /

Mas ... são apenas pequenos ajustes e não um suporte de linguagem especial para erros
manipulação / propagação.

Na segunda-feira, 18 de setembro de 2017 às 4:14 PM, dsugalski [email protected] escreveu:

Uma vez que @bcmills https://github.com/bcmills recomendou ressuscitar um
fio quiescente, se vamos considerar o uso de outras línguas,
parece que os modificadores de declaração ofereceriam uma solução razoável para todos
isto. Para usar o exemplo de https://github.com/slvmnd, refaça com
modificadores de declaração:

função doStuff () (int, err) {
x, errar: = f ()
return 0, wrapError ("f falhou", err) if err! = nil

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

    return y, nil

}

Não tão conciso quanto ter a instrução e a verificação de erro em um único
linha, mas lê razoavelmente bem. (Eu sugeriria não permitir a forma: = de
atribuição na expressão if, caso contrário, os problemas de escopo provavelmente
confundir as pessoas, mesmo que sejam claras na gramática) Permitindo "a menos que" como
versão negada de "se" é um pouco de açúcar sintático, mas funciona bem para
ler e valeria a pena considerar.

Eu não recomendaria copiar do Perl aqui, no entanto. (Basic Plus 2 é
bem) Dessa forma, encontram-se os modificadores de instrução em loop que, embora às vezes
útil, traga outro conjunto de questões bastante complexas.

-
Você está recebendo isto porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/21161#issuecomment-330215402 ou mudo
o segmento
https://github.com/notifications/unsubscribe-auth/AICzv1rfnXeGVRRwaigCyyVK_STj-i83ks5sjmylgaJpZM4Oi1c-
.

Pensando um pouco mais na sugestão de @dsugalski , ele não possui a propriedade que @jba e outros solicitaram, ou seja, que o código sem erro seja visivelmente distinto do código de erro. Ainda pode ser uma ideia interessante se tiver benefícios significativos para caminhos de código sem erro também, mas quanto mais eu penso sobre isso, menos atraente parece em comparação com as alternativas propostas.

Não tenho certeza de quanta distinção visual é razoável esperar de texto puro. Em algum ponto, parece mais apropriado direcionar isso para o IDE ou para a camada de coloração de código do seu editor de texto.

Mas para distinção visível baseada em texto, o padrão de formatação que tínhamos quando comecei a usar isso embaraçosamente há muito tempo era que os modificadores de instrução IF / UNLESS tinham que ser justificados à direita, o que os fazia se destacarem bem o suficiente. (Embora tenha concedido um padrão que era mais fácil de aplicar e talvez mais visualmente distinto em um terminal VT-220 do que em editores com tamanhos de janela mais flexíveis)

Para mim, pelo menos, acho que o caso do modificador de instrução é facilmente distinto e lê melhor do que o esquema atual do bloco if. Esse pode não ser o caso de outras pessoas, é claro - eu leio o código-fonte da mesma maneira que leio um texto em inglês, então ele mapeia em um padrão confortável existente, e nem todo mundo faz isso.

return 0, wrapError("f failed", err) if err != nil pode ser escrito em if err != nil { return 0, wrapError("f failed", err) }

if err != nil return 0, wrapError("f failed", err) pode ser escrito da mesma forma.

Talvez tudo o que seja necessário aqui seja gofmt deixar if escritos em uma única linha em uma única linha em vez de expandi-los para três linhas?

Há outra possibilidade que me ocorre. Grande parte do atrito que experimento ao tentar escrever código Go descartável rapidamente é porque tenho que verificar os erros em cada chamada, então não consigo aninhar chamadas de maneira adequada.

Por exemplo, não posso chamar http.Client.Do em um novo objeto de solicitação sem primeiro atribuir o resultado http.NewRequest a uma variável temporária e, em seguida, chamar Do nisso.

Eu me pergunto se poderíamos permitir:

f(y())

para funcionar mesmo se y retornar (T, error) tupla. Quando y retorna um erro, o compilador pode abortar a avaliação da expressão e fazer com que esse erro seja retornado de f. Se f não retornar um erro, ele pode receber um.

Então eu poderia fazer:

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

e o resultado do erro não seria nulo se NewRequest ou Do falharem.

No entanto, isso tem um problema significativo - a expressão acima já é válida se f aceitar dois argumentos, ou argumentos variáveis. Além disso, as regras exatas para fazer isso provavelmente serão bastante complicadas.

Então, em geral, acho que não gosto (também não estou interessado em nenhuma das outras propostas neste tópico), mas pensei em jogar a ideia para consideração de qualquer maneira.

@rogpeppe ou você pode apenas usar json.NewEncoder

@gbbr Ha sim, mau exemplo.

Um exemplo melhor pode ser http.Request. Eu mudei o comentário para usar isso.

Uau. Muitas ideias estão tornando a legibilidade do código ainda pior.
Estou bem com abordagem

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

Só uma coisa é realmente irritante é o escopo das variáveis ​​retornadas.
Neste caso, você deve usar val mas está no escopo de if .
Então você tem que usar else mas o linter será contra ele (e eu também), e a única maneira é

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

Seria bom ter acesso às variáveis ​​do bloco if :

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

@dmbreaker É essencialmente para isso que serve a cláusula de guarda do Swift. Ele atribui uma variável dentro do escopo atual se passar em alguma condição. Veja meu comentário anterior .

Eu sou a favor de simplificar o tratamento de erros no Go (embora eu pessoalmente não me importe muito), mas acho que isso adiciona um pouco de magia a uma linguagem simples e extremamente fácil de ler.

@gbbr
A que 'isso' você está se referindo aqui? Existem algumas sugestões diferentes sobre como fazer as coisas.

Talvez uma solução de duas partes?

Defina try como "descole o valor mais à direita na tupla de retorno; se não for o valor zero para seu tipo, retorne-o como o valor mais à direita desta função com os outros definidos como zero". Isso torna o caso comum

 a := try ErrorableFunction(b)

e permite o encadeamento

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

(Opcionalmente, torne-o não nulo em vez de nulo, para eficiência.) Se as funções com erro retornarem um valor não nulo / diferente de zero, a função "aborta com um valor". O valor mais à direita da função try 'ed deve ser atribuível ao valor mais à direita da função de chamada ou é um erro de verificação de tipo em tempo de compilação. (Portanto, isso não está codificado para manipular apenas error , embora talvez a comunidade deva desencorajar seu uso para qualquer outro código "inteligente".)

Em seguida, permita que retornos de tentativa sejam capturados com uma palavra-chave do tipo adiar:

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

ou, talvez mais detalhadamente, mas mais alinhado com a forma como o Go já funciona:

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

No caso catch , o parâmetro da função deve corresponder exatamente ao valor que está sendo retornado. Se várias funções forem fornecidas, o valor passará por todas elas na ordem reversa. É claro que você pode colocar um valor em que resolva uma função do tipo correto. No caso do exemplo baseado em defer , se uma defer func chamar set_catch a próxima função adiar obterá isso como seu valor de catch() . (Se você for tolo o suficiente para defini-lo de volta para nulo no processo, você obterá um valor de retorno confuso. Não faça isso.) O valor passado para set_catch deve ser atribuível ao tipo retornado. Em ambos os casos, espero que isso funcione como defer no sentido de que é uma instrução, não uma declaração, e só se aplicará ao código depois que a instrução for executada.

Eu tendo a preferir a solução baseada em adiamento de uma perspectiva de simplicidade (basicamente nenhum conceito novo introduzido lá, é um segundo tipo de recover() vez de uma coisa nova), mas reconheço que pode ter alguns problemas de desempenho. Ter uma palavra-chave catch separada pode permitir mais eficiência, sendo mais fácil pular totalmente quando ocorre um retorno normal, e se alguém deseja ir para o máximo de eficiência, talvez vincule-os a escopos para que apenas um tenha permissão para estar ativo por escopo ou função , o que seria, eu acho, quase custo zero. (Possivelmente, o nome do arquivo de código-fonte e o número da linha também devem ser retornados da função catch? É barato na hora de compilar fazer isso e evitaria alguns dos motivos pelos quais as pessoas pedem um rastreamento de pilha completo agora.)

Qualquer um dos dois também permitiria que o tratamento de erros repetitivos fosse efetivamente tratado em um lugar dentro de uma função, e permitiria que o tratamento de erros fosse oferecido como uma função de biblioteca facilmente, o que é IMHO um dos piores aspectos do caso atual, de acordo com os comentários do rsc acima; a laboriosidade do tratamento de erros tende a encorajar o "erro de retorno" ao invés do tratamento correto. Eu sei que eu mesma luto muito com isso.

@thejerf Parte do ponto de Ian com esta proposta é explorar maneiras de abordar o erro padrão sem desencorajar as funções de adicionar contexto ou de outra forma manipular os erros que eles retornam.

Separar o tratamento de erros em try e catch parece que funcionaria contra esse objetivo, embora eu suponha que isso dependa de que tipo de detalhes os programas normalmente irão querer adicionar.

No mínimo, gostaria de ver como funciona com exemplos mais realistas.

Todo o objetivo da minha proposta é permitir adicionar contexto ou manipular os erros, de uma forma que considero mais programaticamente correta do que a maioria das propostas aqui que envolvem repetir esse contexto indefinidamente, o que por si só inibe o desejo de colocar o contexto adicional em .

Para reescrever o exemplo original,

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

sai como

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

    try syscall.Chdir(dir)
    return nil
}

exceto que este exemplo é muito trivial para, bem, qualquer uma dessas propostas, na verdade, e eu diria que, neste caso, deixaríamos apenas a função original.

Pessoalmente, não considero a função Chdir original um problema em primeiro lugar. Estou ajustando isso especificamente para abordar o caso em que uma função é barulhenta por um longo tratamento de erros repetidos, não para uma função de um erro. Eu também diria que, se você tem uma função na qual está literalmente fazendo algo diferente para cada caso de uso possível, a resposta certa provavelmente é continuar escrevendo o que já temos. No entanto, suspeito que seja de longe o caso raro para a maioria das pessoas, com o fundamento de que, se esse fosse o caso comum, não haveria uma reclamação em primeiro lugar. O ruído de verificar o erro só é significativo precisamente porque as pessoas desejam fazer "quase sempre a mesma coisa" em uma função.

Eu também suspeito que muito do que as pessoas querem seria encontrado

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

     x := try Something()

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

     return whatever, nil
}

Se eliminarmos o problema de tentar fazer uma única instrução if parecer boa com o fundamento de que é muito pequena para se preocupar, e eliminarmos o problema de uma função que está realmente fazendo algo único para cada retorno de erro com base em que A : isso é realmente um caso bastante raro e B: nesse caso, a sobrecarga padrão não é realmente tão significativa em comparação com a complexidade do código de tratamento exclusivo, talvez o problema possa ser reduzido a algo que tenha uma solução.

Eu também quero muito ver

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

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

     ...
 }

ou algum equivalente seja possível, para quando funcionar.

E, de todas as propostas da página ... ainda não parece Go? Parece mais Go do que o Go atual.

Para ser honesto, a maior parte da minha experiência profissional em engenharia foi com PHP (eu sei), mas a principal atração do Go sempre foi a legibilidade. Embora eu goste de alguns aspectos do PHP, a parte que mais desprezo é o absurdo "final" "abstrato" "estático" e a aplicação de conceitos complicados a um pedaço de código que faz uma coisa.

Ver essa proposta me deu um flashback imediato da sensação de olhar para uma parte e ter que olhar duas vezes e realmente "pensar" sobre o que aquela parte do código está dizendo / fazendo. Não acho que este código seja legível e realmente não adiciona nada à linguagem. Meu primeiro instinto é olhar para a esquerda e acho que isso sempre retorna nil . No entanto, com essa mudança, eu agora teria que olhar para a esquerda e para a direita para determinar o comportamento do código, o que significa mais tempo de leitura e mais modelo mental.

No entanto, isso não significa que não haja espaço para melhorias no tratamento de erros no Go.

Lamento não ter (ainda) lido todo este tópico (é muito longo), mas vejo pessoas lançando fora uma sintaxe alternativa, então gostaria de compartilhar minha ideia:

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

Espero não ter perdido algo acima que anule isso. Eu prometo que vou passar por todos os comentários algum dia :)

O operador teria que ser permitido apenas no tipo error , acredito, para evitar a confusão semântica de conversão de tipo.

Interessante, @buchanae , mas isso nos

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

Eu vejo que isso permitiria que a escapasse, enquanto que no estado atual, ele tem como escopo os blocos then e else.

@ object88 Você está certo, a mudança é sutil, estética e subjetiva. Pessoalmente, tudo que eu quero do Go 2 neste tópico é uma mudança sutil de legibilidade.

Pessoalmente, acho mais legível porque a linha não começa com if e não requer !=nil . As variáveis ​​estão na borda esquerda, onde estão nas (na maioria?) Outras linhas.

Ótimo ponto no escopo de a , eu não havia considerado isso.

Considerando as outras possibilidades desta gramática, parece que isso é possível.

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

e provavelmente

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

que é talvez onde ele se desfaça.

Talvez retornar um erro deva fazer parte de todas as chamadas de função em Go, então você pode imaginar:
`` `
a: = helloWorld (); errar? {
return fmt.Errorf ("helloWorld falhou:% s", err)
}

Que tal ter um tratamento de exceção real? Quer dizer, tente pegar, finalmente, como muitas línguas modernas?

Não, torna o código implícito e confuso (embora seja um pouco mais curto)

Na quinta-feira, 23 de novembro de 2017 às 07:27, Kamyar Miremadi [email protected]
escreveu:

Que tal ter um tratamento de exceção real? Quer dizer, tente, pegue, finalmente
em vez de muitas línguas modernas?

-
Você está recebendo isto porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/21161#issuecomment-346529787 ou mudo
o segmento
https://github.com/notifications/unsubscribe-auth/AICzvyy_kGAlcs6RmL8AKKS5deNRU4_5ks5s5PQVgaJpZM4Oi1c-
.

Voltando a @mpvl 's WriteToGCS exemplo se-thread , eu gostaria de sugerir (mais uma vez) que o cometem / padrão de reversão não é suficiente comum para justificar uma mudança importante na manipulação de erro de Go. Não é difícil capturar o padrão em uma função ( link do playground ):

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

Então podemos escrever o exemplo como

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

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

Eu sugeriria uma solução mais simples:

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

Para vários retornos de funções múltiplas:

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

E para vários argumentos de retorno:

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

^ sinal significa retorno se o parâmetro não for nulo.
Basicamente, "mova o erro para cima na pilha se isso acontecer"

Alguma desvantagem desse método?

@gladkikhartem
Como modificar o erro antes que ele seja retornado?

@urandom
Erros de empacotamento é uma ação importante que, em minha opinião, deve ser feita explicitamente.
O código Go é sobre legibilidade, não mágica.
Eu gostaria de manter o empacotamento de erros mais claro

Mas, ao mesmo tempo, gostaria de me livrar do código que não carrega muitas informações e apenas ocupa o espaço.

if err != nil {
    return err
}

É como o clichê de Go - você não quer ler, apenas pular.

O que vi até agora nesta discussão é uma combinação de:

  1. reduzindo o detalhamento da sintaxe
  2. melhorando o erro adicionando contexto

Isso está de acordo com a descrição do problema original por @ianlancetaylor que menciona ambos os aspectos, no entanto, na minha opinião, os dois devem ser discutidos / definidos / experimentados separadamente e, possivelmente, em diferentes iterações para limitar o escopo das mudanças e apenas por razões de eficácia (a uma mudança maior no idioma é mais difícil de fazer do que incremental).

1. Redução de verbosidade da sintaxe

Gosto da ideia de @gladkikhartem , mesmo em sua forma original que relato aqui desde que foi editado / estendido:

 result, ^ := someAction()

No contexto de uma função:

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

Esta sintaxe curta - ou na forma proposta por @gladkikhartem com err^ - trataria da verbosidade da sintaxe parte do problema (1).

2. Contexto do erro

Para a 2ª parte, adicionando mais contexto, poderíamos até esquecê-lo completamente por enquanto e mais tarde propor a adição automática de um stacktrace a cada erro se um tipo contextError for usado. Esse novo tipo de erro nativo pode exibir rastreamentos de pilha completos ou curtos (imagine GO_CONTEXT_ERROR=full ) e ser compatível com a interface error , ao mesmo tempo que oferece a possibilidade de extrair pelo menos a função e o nome do arquivo da pilha de chamadas superior entrada.

Ao usar um contextError , de alguma forma, Go deve anexar o rastreamento de pilha de chamadas exatamente no ponto onde o erro é criado.

Novamente com um exemplo de função:

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

Apenas o tipo mudou de error para contextError , que pode ser definido como:

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

(observe como este Stack() é diferente de https://golang.org/pkg/runtime/debug/#Stack, já que esperamos ter uma versão sem bytes da pilha de chamadas goroutine aqui)

O método Cause() retornaria nulo ou o contextError como resultado do aninhamento.

Estou muito bem ciente das implicações potenciais para a memória de carregar pilhas como essa, portanto, sugeri a possibilidade de ter uma pilha curta padrão que conteria apenas 1 ou mais algumas entradas. Um desenvolvedor normalmente habilitaria stracktraces completos em versões de desenvolvimento / depuração e deixaria o padrão (short stacktraces) de outra forma.

Técnica anterior:

Apenas alimento para o pensamento.

@gladkikhartem @ gdm85

Acho que você não entendeu o ponto principal desta proposta. De acordo com a postagem original de Ian:

Já é fácil (talvez fácil demais) ignorar o erro (consulte # 20803). Muitas propostas existentes para tratamento de erros tornam mais fácil retornar o erro não modificado (por exemplo, # 16225, # 18721, # 21146, # 21155). Poucos facilitam o retorno do erro com informações adicionais.

Retornar erros não modificados geralmente é errado e, geralmente, no mínimo, inútil. Queremos encorajar o tratamento cuidadoso de erros: abordar apenas o caso de uso “devolver sem modificações” desviaria os incentivos na direção errada.

@bcmills se o contexto (na forma de um rastreamento de pilha) estiver sendo adicionado, o erro será retornado com informações adicionais. Anexar uma mensagem legível, por exemplo, "erro ao inserir o registro", seria considerado "tratamento cuidadoso de erros"? Como decidir em que ponto da pilha de chamadas essas mensagens devem ser adicionadas (em cada função, parte superior / inferior, etc.)? Todas essas são perguntas comuns ao codificar melhorias no tratamento de erros.

O "retorno não modificado" poderia ser neutralizado conforme explicado acima com "retorno não modificado com rastreamento de pilha" por padrão e (em um estilo reativo) adicionar uma mensagem legível por humanos conforme necessário. Não especifiquei como essa mensagem legível por humanos poderia ser adicionada, mas pode-se ver como funciona o empacotamento em pkg/errors para algumas idéias.

"Retornar erros não modificados geralmente é errado": portanto, proponho um caminho de atualização para o caso de uso preguiçoso, que é o mesmo caso de uso atualmente apontado como prejudicial.

@bcmills
Concordo 100% com # 20803 que os erros devem ser sempre tratados ou explicitamente ignorados (e não tenho ideia de por que isso não foi feito antes ...)
sim, não abordei o ponto da proposta e não tenho de o fazer. Preocupo-me com a solução real proposta, não com as intenções por trás dela, porque as intenções não correspondem aos resultados. E quando eu ver || tal || coisas sendo propostas - isso me deixa muito triste.

Se incorporar informações, como códigos de erro e mensagens de erro, for fácil e transparente - você não precisará encorajar o tratamento cuidadoso de erros - as pessoas farão isso sozinhas.
Por exemplo, apenas transforme o erro em um alias. Poderíamos devolver qualquer tipo de material e usá-lo fora da função sem lançar. Tornaria a vida muito mais fácil.

Adoro que Go me lembre de lidar com erros, mas odeio quando o design me incentiva a fazer algo que é questionável.

@ gdm85
Adicionar rastreio de pilha a um erro automaticamente é uma ideia terrível, basta olhar os rastreamentos de pilha Java.
Quando você mesmo resolve os erros, é muito mais fácil navegar e entender o que está errado. Esse é o ponto principal de embrulhar.

@gladkikhartem Eu discordo que uma forma de "empacotamento automático" seria muito pior de navegar e ajudar a entender o que está errado. Eu também não obtenho exatamente o que você se refere em rastreamentos de pilha Java (suponho que haja exceções? Esteticamente feio? Que problema específico?), Mas para discutir em uma direção construtiva: o que poderia ser uma boa definição de "erro cuidadosamente tratado"?

Peço que aumente minha compreensão das melhores práticas de Go (as mais ou menos canônicas que possam ser) e porque acho que essa definição pode ser a chave para fazer alguma proposta de melhoria em relação à situação atual.

@gladkikhartem Eu sei que essa proposta já está em todo lugar, mas vamos fazer o que pudermos para mantê-la focada nos objetivos que estabeleci inicialmente. Como eu disse ao postar este problema, já existem várias propostas diferentes que tratam de simplificar if err != nil { return err } , e essas são o lugar para discutir a sintaxe que só melhora aquele caso específico. Obrigado.

@ianlancetaylor
Desculpe se tirei a discussão do caminho.

Se você quiser adicionar informações de contexto a um erro, sugiro usar esta sintaxe:
(e obrigar as pessoas a usarem apenas um tipo de erro para uma função para fácil extração de contexto)

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

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


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

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

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

   // another variant with different symbol and return statement

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

   // using original approach

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

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

O símbolo ^ é usado para indicar o parâmetro de erro, bem como diferenciar a definição da função do tratamento de erros para "someAction () {}"
{} pode ser omitido se o erro for retornado sem modificações

Adicionando mais alguns recursos para responder ao meu próprio convite para definir melhor "tratamento cuidadoso de erros":

Por mais tediosa que seja a abordagem atual, acho que é menos confusa do que as alternativas, embora as instruções if de uma linha possam funcionar. Pode ser?

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

... ou mesmo ...

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

Alguém pode já ter mencionado isso, mas existem muitos, muitos posts e eu li muitos deles, mas não todos. Dito isso, eu pessoalmente acho que seria melhor ser explícito do que implícito.

Eu sou um fã de programação orientada para ferrovias, a ideia veio da declaração with do Elixir.
else bloco e == nil entrar em curto-circuito.

Aqui está minha proposta com pseudo código à frente:

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

@ardhitama Não é então como Try catch, exceto que "Com" é como a declaração "Try" e "Else" é como "Catch"?
Por que não implementar o tratamento de exceções como Java ou C #?
agora em go, se um programador não quiser tratar a exceção nessa função, ele a retornará como resultado dessa função. Ainda assim, não há como forçar um programador a lidar com uma exceção se ele não quiser e muitas vezes você realmente não precisa, mas o que obtemos aqui são muitas instruções if err! = Nil que tornam o código feio e não legível (muito ruído). Não é esse o motivo pelo qual a instrução Try Catch Finalmente foi inventada em primeiro lugar em outra linguagem de programação?

Então, eu acho que é melhor se Go Authors "Try" para não ser teimoso !! e apenas apresentar a instrução "Try Catch Finalmente" nas próximas versões. Obrigada.

@KamyarM
Você não pode introduzir o tratamento de exceções em go, porque não há exceções em Go.
Apresentar try {} catch {} em Go é como apresentar try {} catch {} em C - é totalmente errado .

@ianlancetaylor
Que tal não mudar o tratamento de erros Go, mas sim mudar a ferramenta gofmt como esta para tratamento de erros de linha única?

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

É compatível com versões anteriores e você pode aplicá-lo aos seus projetos atuais

As exceções são instruções goto decoradas, elas transformam sua pilha de chamadas em um gráfico de chamadas e há uma boa razão para a maioria dos projetos não acadêmicos sérios barrarem ou limitarem seu uso. Um objeto com estado chama um método que transfere o controle arbitrariamente para cima na pilha e então retoma a execução das instruções ... parece uma má ideia porque é.

@KamyarM Em essência é, mas na prática não é. Na minha opinião, porque estamos sendo explícitos aqui e não quebrando nenhum idioma do Go.

Por quê?

  1. Expressões dentro da instrução with não podem declarar novo var, portanto, afirma explicitamente que a intenção é avaliar fora do bloco vars.
  2. As declarações dentro de with se comportarão como dentro do bloco try e catch . Na verdade, será mais lento a cada instrução seguinte, para avaliar as condições de with no pior caso.
  3. Pelo projeto, a intenção é remover excessiva if s e não para criar manipulador de exceção como o manipulador vai (o sempre locais with 's expressão e else bloco).
  4. Não há necessidade de desenrolar da pilha por causa de throw

ps. Por favor me corrija se eu estiver errado.

@ardhitama
KamyarM está certo no sentido de que com instrução parece tão feio quanto try catch e também introduz um nível de indentação para o fluxo normal de código.
Sem falar na ideia da proposta original de modificar cada erro individualmente. Simplesmente não funcionará elegantemente com try catch , com ou qualquer outro método que agrupe as instruções.

@gladkikhartem
Sim, portanto, proponho adotar "programação orientada para ferrovias" e não tento remover a explicitação. É apenas outro ângulo para atacar o problema, as outras soluções querem resolvê-lo não permitindo que o compilador escreva automaticamente if err != nil para você.

with também não apenas para tratamento de erros, mas pode ser útil para qualquer outro fluxo de controle.

@gladkikhartem
Deixe-me deixar claro que acho o bloco Try Catch Finally lindo. If err!=nil ... é na verdade o código feio.

Go é apenas uma linguagem de programação. Existem tantas outras línguas. Eu descobri que muitos na comunidade Go olham para isso como se fossem sua religião e não estão abertos para mudar ou admitir os erros. Isto está errado.

@gladkikhartem

Tudo bem se os autores do Go chamem de Go ++ ou Go # ou GoJava e apresentem o Try Catch Finally lá;)

@KamyarM

Evitar mudanças desnecessárias é necessário - crítico - para qualquer empreendimento de engenharia. Quando as pessoas dizem mudança neste contexto, elas realmente querem dizer _mudar para melhor_, o que elas transmitem de forma eficaz com _argumentos_ orientando uma premissa para a conclusão pretendida.

O apelo _apenas abra sua mente, cara! _ Não é convincente. Ironicamente, ele tenta dizer que algo que a maioria dos programadores considera antigo e desajeitado é _novo e melhorado_.

Existem também muitas propostas e discussões em que a comunidade Go discute erros anteriores. Mas concordo com você quando diz que Go é apenas uma linguagem de programação. Isso é dito no site Go e em outros lugares, e falei com algumas pessoas que também confirmaram.

Eu descobri que muitos na Comunidade Go vêem isso como sua religião e não estão abertos para mudar ou admitir os erros.

Go é baseado em pesquisas acadêmicas; opiniões pessoais não importam.

Mesmo os principais desenvolvedores do compilador C # da Microsoft reconheceram publicamente que _exceptions_ são uma maneira ruim de gerenciar erros, enquanto consideravam o modelo Go / Rust uma alternativa melhor: http://joeduffyblog.com/2016/02/07/the-error-model/

Certamente, há espaço para melhorar o modelo de erro de Go, mas não pela adoção de soluções do tipo exceções, uma vez que elas apenas adicionam uma complexidade enorme em troca de alguns benefícios questionáveis.

@Dr-Terrível Obrigado pelo artigo.

Mas não encontrei nenhum lugar que mencionasse GoLang como uma linguagem acadêmica.

A propósito, para deixar meu ponto claro, neste exemplo

func Execute() error {
    err := Operation1()
    if err!=nil{
        return err
    }

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

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

    err = Operation4()
    return err
}

É semelhante a implementar o tratamento de exceções em C # assim:

         public void Execute()
        {

            try
            {
                Operation1();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation2();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation3();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation4();
            }
            catch (Exception)
            {
                throw;
            }
        }

Não é uma maneira terrível de tratamento de exceções em C #? Minha resposta é sim, não sei sobre a sua! Em Go, não tenho outra escolha. É aquela escolha ou rodovia terrível. É assim no GO e não tenho escolha.

A propósito, como também mencionado no artigo que você compartilhou, qualquer linguagem pode implementar tratamento de erros como Go sem qualquer necessidade de qualquer sintaxe extra, então Go realmente não implementou nenhuma forma revolucionária de tratamento de erros. Ele simplesmente não possui nenhuma forma de tratamento de erros e, portanto, você está limitado a usar a instrução If para tratamento de erros.

A propósito, eu sei que o GO tem uma Panic, Recover , Defer não recomendada que é semelhante a Try Catch Finally mas na minha opinião pessoal a sintaxe de Try Catch Finally é muito mais limpa e organizada de lidar com exceções.

@ Dr-Terrible

Além disso, verifique isto:
https://github.com/manucorporat/try

@KamyarM , ele não disse que Go é uma linguagem acadêmica, ele disse que é baseada em pesquisas acadêmicas. O artigo também não era sobre Go, mas investiga o paradigma de tratamento de erros empregado por Go.

Se você achar que manucorporat/try funciona para você, use-o em seu código. Mas os custos (desempenho, complexidade da linguagem, etc.) de adicionar try/catch à própria linguagem não valem a pena.

@KamyarM
Seu exemplo não é preciso. Alternativa para

    err := Operation1()
    if err!=nil {
        return err
    }
    err = Operation2()
    if err!=nil{
        return err
    }
    err = Operation3()
    if err!=nil{
        return err
    }
    return Operation4()

vai ser

            Operation1();
            Operation2();
            Operation3();
            Operation4();

o tratamento de exceções parece uma opção muito melhor neste exemplo. Em teoria deveria ser bom, mas na prática
você deve responder com uma mensagem de erro precisa para cada erro ocorrido em seu endpoint.
Todo o aplicativo em Go geralmente tem um tratamento de erros de 50%.

         err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

E se as pessoas tiverem uma ferramenta tão poderosa como try catch, estou 100% certo de que elas vão abusar dela em favor de um tratamento cuidadoso de erros.

É interessante que a academia seja mencionada, mas Go é uma coleção de lições aprendidas com a experiência prática. Se o objetivo é escrever uma API inválida que retorna mensagens de erro incorretas, o tratamento de exceções é o caminho a percorrer.

No entanto, não quero um erro invalid HTTP header quando minha solicitação contém um JSON request body malformado, o tratamento de exceções é o botão mágico de disparar e esquecer que consegue isso nas APIs C ++ e C # que usam eles.

Para uma grande cobertura de API, é impossível fornecer contexto de erro suficiente para obter um tratamento de erros significativo. Isso porque qualquer bom aplicativo tem 50% de tratamento de erros em Go e 90% em um idioma que requer uma transferência de controle não local para tratar os erros.

@gladkikhartem

A maneira alternativa que você mencionou é a maneira certa de escrever o código em C #. São apenas 4 linhas de código e mostram o caminho de execução feliz. Ele não tem aqueles if err!=nil ruídos. Se ocorrer uma exceção, a função que se preocupa com essas exceções pode tratá-la usando Try Catch Finally (pode ser a mesma função ou o chamador ou o chamador do chamador ou o chamador do chamador do chamador do chamador ... ou apenas um manipulador de eventos que processa todos os erros não tratados em um aplicativo. O programador tem opções diferentes.)

err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

Parece simples, mas é complicado. Eu acho que você poderia juntar um tipo de erro personalizado que carrega o erro do sistema, o erro do usuário (sem vazar o estado interno para o usuário que pode não ter a melhor das intenções) e o código HTTP.

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

Mas dá-lhe uma chance

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



md5-9bcd2745464e8d9597cba6d80c3dcf40



```go
func Chdir(dir string) error {
    n , _ := syscall.Chdir(dir):
               // something to do
               fmt.Println(n)
}

Todos eles contêm algum tipo de magia nada óbvia, o que não simplifica as coisas para o leitor. Nos dois primeiros exemplos, err torna-se algum tipo de pseudo-palavra-chave ou variável de ocorrência espontânea. Nos dois últimos exemplos, não está nada claro o que aquele operador : deve estar fazendo - um erro será retornado automaticamente? O RHS do operador é uma única instrução ou um bloco?

FWIW, eu escreveria uma função de wrapper para que você pudesse fazer return newPathErr("chdir", dir, syscall.Chdir(dir)) e retornaria automaticamente um erro nulo se o terceiro parâmetro fosse nulo. :-)

IMO, a melhor proposta que vi para atingir os objetivos de "simplificar o tratamento de erros no Go" e "retornar o erro com informações contextuais adicionais" é de @mrkaspa em # 21732:

a, b, err? := f1()

se expande para este:

if err != nil {
   return nil, errors.Wrap(err, "failed")
}

e posso forçá-lo a entrar em pânico com isto:

a, b, err! := f1()

se expande para este:

if err != nil {
   panic(errors.Wrap(err, "failed"))
}

Isso manterá a compatibilidade com versões anteriores e corrigirá todos os pontos problemáticos do tratamento de erros em go

Isso não lida com casos como funções bufio que retornam valores diferentes de zero, bem como erros, mas acho que é normal fazer o tratamento de erros explícito nos casos em que você se preocupa com os outros valores de retorno. E, claro, os valores de retorno sem erro precisariam ser o valor nulo apropriado para esse tipo.

O ? o modificador reduzirá o erro padrão de entrega de funções e o! o modificador fará o mesmo para lugares onde assert seria usado em outras linguagens, como em algumas funções principais.

Esta solução tem a vantagem de ser muito simples e não tentar fazer muito, mas acho que atende aos requisitos estabelecidos nesta declaração de proposta.

No caso de você ter ...

func foo() (int, int, error) {
    a, b, err? := f1()
    return a, b, nil
}
func bar() (int, error) {
    a, b, err? := foo()
    return a+b, nil
}

Se algo der errado em foo , então no site de chamada de bar , o erro será duplamente envolvido com o mesmo texto, sem adicionar qualquer significado. No mínimo, eu objetaria à parte errors.Wrap da sugestão.

Mas expandindo ainda mais, qual é o resultado esperado disso?

func baz() (a, b int, err error) {
  a = 1
  b = 2
  a, b, err? = f1()
  return

a e b reatribuídos a valores nulos? Nesse caso, isso é mágica, que acho que devemos evitar. Eles cumprem os valores previamente atribuídos? (Eu não ligo para valores de retorno nomeados, mas eles ainda devem ser considerados para os fins desta proposta.)

@ dup2X Sim, vamos remover o idioma, deve ser mais assim

@ object88 é natural esperar que, em caso de erro, todo o resto seja anulado. Isso é simples de entender e não tem mágica, praticamente já é uma convenção para o código Go, requer pouco para lembrar e não tem casos especiais. Se permitirmos que os valores sejam retidos, isso complica as coisas. Se você se esquecer de verificar se há erros, os valores retornados podem ser usados ​​acidentalmente. Nesse caso, tudo pode acontecer. Como chamar métodos em uma estrutura parcialmente alocada em vez de entrar em pânico com nada. Os programadores podem até começar a esperar que certos valores sejam retornados em caso de erro. Na minha opinião, seria uma bagunça e nada de bom seria ganho com isso.

Quanto ao empacotamento, não acho que a mensagem padrão forneça algo útil. Seria bom apenas encadear os erros. Como quando as exceções têm exceções internas. Muito útil para depurar erros em uma biblioteca.

Respeitosamente, eu discordo, @creker. Temos exemplos desse cenário no Go stdlib de valores de retorno não nulos, mesmo no caso de um erro não nulo, e de fato são funcionais, como várias funções na estrutura bufio.Reader . Nós, os programadores Go, somos ativamente encorajados a verificar / lidar com todos os erros; parece mais notório ignorar erros do que obter valores de retorno não nulos e um erro. No caso de você citar, se você retornar um nils e não verificar o erro, você ainda pode operar com um valor inválido.

Mas deixando isso de lado, vamos examinar um pouco mais adiante. Qual seria a semântica do operador ? ? Só pode ser aplicado a tipos que implementam a interface error ? Pode ser aplicado a outros tipos ou argumentos de retorno? Se ele pode ser aplicado a tipos que não implementam erro, ele é disparado por qualquer valor / ponteiro diferente de nulo? O operador ? ser aplicado a mais de um valor de retorno ou isso é um erro do compilador?

@erwbgy
Se você deseja apenas retornar o erro sem nada de útil anexado a ele - seria muito mais simples apenas dizer ao compilador para tratar todos os erros não manipulados como "if err! = Nil return ...", por exemplo:

func doStuff() error {
    doAnotherStuff() // returns error
}

func doStuff() error {
    res := doAnotherStuff() // returns string, error
}

E não há necessidade de loucura adicional? símbolo neste caso.

@ object88
Tentei aplicar a maioria das propostas de empacotamento de erro mostradas aqui em código real e enfrentei um grande problema - o código se torna muito denso e ilegível.
O que ele faz é apenas sacrificar a largura do código em favor da altura do código.
Encapsular erros com if err! = Nil usual, na verdade, permite espalhar o código para melhor legibilidade, então não acho que deveríamos nem mesmo mudar nada para o encapsulamento de erros.

@ object88

No caso do seu site, se você retornar um nils e não verificar o erro, ainda poderá operar com um valor inválido.

Mas isso produzirá erros óbvios e fáceis de detectar, como pânico no zero. Se você precisar retornar valores significativos em caso de erro, deverá fazê-lo explicitamente e documentar exatamente qual valor pode ser usado em cada caso. Apenas retornar coisas aleatórias que por acaso estavam nas variáveis ​​após o erro é perigoso e levará a erros sutis. Novamente, nada se ganha com isso.

@gladkikhartem o problema com if err! = nil é que a lógica real está completamente perdida nele e você tem que pesquisá-lo ativamente se quiser entender o que o código faz em seu caminho de sucesso e não se importar com todo aquele tratamento de erros . É como ler muito código C onde você tem várias linhas de código real e todo o resto é apenas verificação de erro. As pessoas até recorrem a macroses que envolvem tudo isso e vão para o final da função.

Não vejo como a lógica pode se tornar muito densa em um código escrito corretamente. É lógico. Cada linha do seu código contém o código real do seu interesse, é isso que você deseja. O que você não quer é passar por linhas e mais linhas de clichê. Use comentários e divida seu código em blocos, se isso ajudar. Mas isso soa mais como um problema com o código real e não com a linguagem.

Isso funciona no playground, se você não reformatá-lo:

a, b, err := Frob("one string"); if err != nil { return a, b, fmt.Errorf("couldn't frob: %v", err) }
// continue doing stuff with a and b

Portanto, parece-me que a proposta original, e muitas das outras mencionadas acima, estão tentando criar uma sintaxe abreviada clara para esses 100 caracteres e impedir o gofmt de insistir em adicionar quebras de linha e reformatar o bloco em 3 linhas.

Então, vamos imaginar que mudamos gofmt para parar de insistir em blocos de várias linhas, começar com a linha acima e tentar encontrar maneiras de torná-la mais curta e clara.

Não acho que a parte antes do ponto-e-vírgula (a atribuição) deva ser alterada, de modo que sobram 69 caracteres que podemos cortar. Destes, 49 são a instrução de retorno, os valores a serem retornados e o agrupamento de erros, e não vejo muito valor em alterar a sintaxe disso (digamos, tornando as instruções de retorno opcionais, o que confunde os usuários).

Assim, resta encontrar um atalho para ; if err != nil { _ } onde o sublinhado representa um pedaço de código. Acho que qualquer atalho deve incluir explicitamente err para maior clareza, mesmo que torne a comparação nula um pouco invisível, então ficamos com um atalho para ; if _ != nil { _ } .

Imagine por um momento que usamos um único personagem. Vou escolher § como um espaço reservado para qualquer que seja esse caractere. A linha de código seria:

a, b, err := Frob("one string") § err return a, b, fmt.Errorf("couldn't frob: %v", err)

Não vejo como você poderia fazer muito melhor do que sem alterar a sintaxe de atribuição existente ou a sintaxe de retorno, ou sem ter que acontecer uma mágica invisível. (Ainda há um pouco de mágica, pois o fato de estarmos comparando err com zero não é imediatamente aparente.)

São 88 caracteres, salvando um total de 12 caracteres em uma linha de 100 caracteres.

Portanto, minha pergunta é: Vale a pena fazer isso?

Edit: Acho que meu ponto é, quando as pessoas olham para os blocos de if err != nil Go e dizem "Eu gostaria que pudéssemos nos livrar dessa porcaria", 80-90% do que eles estão falando são _coisas que você tem inerentemente a fazer para lidar com os erros_. A sobrecarga real causada pela sintaxe do Go é mínima.

@lpar , você está seguindo basicamente a mesma lógica que apliquei acima , então, naturalmente, concordo com seu raciocínio. Mas acho que você desconsidera o apelo visual de colocar todas as coisas de erro à direita:

a, b := Frob("one string")  § err { return ... }

é mais legível por um fator que excede a mera redução de caracteres.

@lpar, você pode salvar ainda mais caracteres se remover o praticamente inútil fmt.Errorf , alterar o retorno para alguma sintaxe especial e introduzir a pilha de chamadas para os erros para que tenham contexto real e não sejam meras strings glorificadas. Isso te deixaria com algo assim

a, b, err? := Frob("one string")

O problema com os erros do Go para mim sempre foi a falta de contexto. Retornar e agrupar strings não é útil para determinar onde o erro realmente aconteceu. É por isso que github.com/pkg/errors por exemplo, se tornou uma obrigação para mim. Com erros como esse, obtenho os benefícios da simplicidade de tratamento de erros do Go e os benefícios das exceções que capturam o contexto perfeitamente e permitem que você encontre o local exato da falha.

E, mesmo se tomarmos seu exemplo como ele é, o fato de que o tratamento de erros está à direita é uma atualização significativa de legibilidade. Você não precisa mais pular várias linhas do boilerplate para chegar ao significado real do código. Você pode dizer o que quiser sobre a importância do tratamento de erros, mas quando leio o código para entendê-lo, não me importo com os erros. Tudo que preciso é um caminho de sucesso. E quando eu precisar de erros, irei procurá-los especificamente. Os erros, por sua natureza, são casos excepcionais e devem ocupar o mínimo de espaço possível.

Acho que a questão de se fmt.Errorf é "inútil" em comparação com errors.Wrap é ortogonal a esse problema, já que ambos são igualmente prolixos. (Em aplicativos reais que eu também não uso, uso outra coisa que também registra o erro e a linha de código em que ele ocorreu.)

Então eu acho que algumas pessoas realmente gostam que o tratamento de erros esteja certo. Só não estou tão convencido, mesmo vindo de uma experiência em Perl e Ruby.

@lpar Eu uso errors.Wrap porque ele captura automaticamente a pilha de chamadas - eu realmente não preciso de todas essas mensagens de erro. Preocupo-me mais com o local onde aconteceu e, talvez, quais argumentos foram passados ​​para a função que produziu o erro. Você até mesmo dizendo que está fazendo algo semelhante - logar uma linha de código para fornecer algum contexto para suas mensagens de erro. Considerando que podemos pensar em maneiras de reduzir o clichê e, ao mesmo tempo, dar mais contexto aos erros (essa é a proposta aqui).

Quanto aos erros, estar à direita. Para mim, não se trata apenas do lugar, mas da redução da carga cognitiva necessária para ler um código repleto de manipulação de erros. Não aceito o argumento de que os erros são tão importantes que você deseja que ocupem tanto espaço quanto ocupam. Na verdade, eu preferia que eles fossem embora o máximo possível. Eles não são a história principal.

@creker

É mais provável que isso descreva um erro trivial do desenvolvedor do que um erro em um sistema de produção que gera um erro devido a uma entrada incorreta do usuário. Se tudo o que você precisa para determinar o erro é o número da linha e o caminho do arquivo, provavelmente você acabou de escrever o código e já sabe o que está errado.

@ como é semelhante às exceções. Na maioria dos casos, a pilha de chamadas e a mensagem de exceção são suficientes para definir o local e causar o erro. Em casos mais complexos, você pelo menos sabe o lugar onde o erro aconteceu. As exceções oferecem esse benefício por padrão. Com Go, você precisa encadear os erros, basicamente emulando a pilha de chamadas, ou incluir a pilha de chamadas real.

Em um código escrito corretamente, na maioria das vezes, você saberia, a partir do número da linha e do caminho do arquivo, a causa exata, porque o erro seria esperado. Na verdade, você escreveu um código em preparação para que isso pudesse acontecer. Se algo não esperado acontecesse, sim, a pilha de chamadas não forneceria a causa, mas reduziria significativamente o espaço de pesquisa.

@Como

Na minha experiência, os erros de entrada do usuário são tratados quase imediatamente. Os verdadeiros erros de produção problemáticos acontecem profundamente no código (por exemplo, um serviço está inativo, fazendo com que outro serviço lance erros) e é bastante útil obter um rastreamento de pilha adequado. Pelo que vale a pena, os rastreamentos de pilha java são extremamente úteis ao depurar problemas de produção, não as mensagens.

@creker
Os erros são apenas valores e fazem parte das entradas e saídas da função. Eles não podiam ser "inesperados".
Se você quiser descobrir por que a função gerou um erro - use o teste, o registro e etc ...

@gladkikhartem no mundo real não é tão simples. Sim, você espera erros no sentido de que a assinatura da função inclui um erro como seu valor de retorno. Mas o que penso em esperar é saber exatamente por que aconteceu e o que o causou, para que você realmente saiba o que fazer para consertar ou não consertar. A entrada incorreta do usuário geralmente é muito simples de corrigir, basta olhar para a mensagem de erro. Se você usar buffers de profocol e algum campo obrigatório não estiver definido, isso é esperado e é realmente simples de consertar se você validar corretamente tudo o que recebe na transmissão.

Neste ponto, não entendo mais sobre o que estamos discutindo. O rastreamento de pilha ou a cadeia de mensagens de erro são bastante semelhantes, se implementados de maneira adequada. Eles reduzem o espaço de pesquisa e fornecem um contexto útil para reproduzir e corrigir um erro. O que precisamos é pensar em maneiras de simplificar o tratamento de erros e, ao mesmo tempo, fornecer contexto suficiente. Não estou de forma alguma defendendo que a simplicidade é mais importante do que o contexto adequado.

Esse é o argumento Java - mova todo o código de erro para outro lugar para que você não precise examiná-lo. Acho que está errado; não apenas porque o mecanismo do Java para fazer isso falhou amplamente, mas porque quando estou olhando para o código, como ele se comportará quando houver um erro é tão importante para mim quanto como se comportará quando tudo funcionar.

Ninguém está fazendo esse argumento. Não vamos confundir o que está sendo discutido aqui com o tratamento de exceções, onde todo o tratamento de erros está em um só lugar. Chamar isso de "falhou em grande parte" é apenas uma opinião, mas eu não acho que Go jamais retornará a isso em qualquer caso. O tratamento de erros Go é apenas diferente e pode ser melhorado.

@creker Eu tentei o mesmo ponto e pedi para esclarecer o que é considerado uma mensagem de erro significativa / útil.

A verdade é que eu daria a qualquer dia um texto de mensagem de erro de qualidade variável (que tem o viés do desenvolvedor escrevê-lo naquele momento e com esse conhecimento) em troca da pilha de chamadas e dos argumentos da função. Com um texto de mensagem de erro ( fmt.Errorf ou errors.New ), você acaba pesquisando o texto no código-fonte, enquanto lê as pilhas de chamadas / backtraces (que são aparentemente odiadas e espero que não por razões estéticas) corresponde a pesquisar diretamente pelo número do arquivo / linha ( errors.Wrap e similares).

Dois estilos diferentes, mas o objetivo é o mesmo: tentar reproduzir em sua mente o que aconteceu em tempo de execução nessas condições.

Nesse tópico, a edição nº 19991 talvez esteja fazendo um resumo válido para uma abordagem do segundo estilo de definição de erros significativos.

mova todo o código de erro para outro lugar para que você não precise olhar para ele

@lpar , se você está respondendo ao meu ponto sobre mover o tratamento de erros para a direita: há uma grande diferença entre notas de rodapé / notas de fim (Java) e notas laterais (minha proposta). As notas laterais requerem apenas uma pequena mudança de visão, sem perda de contexto.

@ gdm85

você acaba pesquisando o texto no código-fonte

Exatamente o que eu quis dizer com rastreamentos de pilha e mensagens de erro encadeadas são semelhantes. Ambos registram o caminho percorrido até o erro. Apenas no caso de mensagens, você pode acabar com mensagens completamente inúteis que podem vir de qualquer lugar do programa se você não for cuidadoso o suficiente ao escrevê-las. O único benefício dos erros encadeados é a capacidade de registrar valores de variáveis. E mesmo isso poderia ser automatizado no caso de argumentos de função ou mesmo variáveis ​​em geral e, pelo menos para mim, cobriria quase tudo que preciso desde erros. Eles ainda seriam valores, você ainda pode digitar switch-los se precisar. Mas em algum ponto você provavelmente os registraria e ser capaz de ver o rastreamento da pilha é extremamente útil.

Basta ver o que Go faz com o pânico. Você obtém rastreamento de pilha completo de cada goroutine. Não me lembro quantas vezes me ajudou a descobrir a causa do erro e corrigi-lo em nenhum momento. Muitas vezes me surpreendeu como é fácil. Ele flui perfeitamente com toda a linguagem sendo muito previsível que você nem precisa do depurador.

Parece haver estigma em torno de tudo relacionado a Java e as pessoas geralmente não trazem nenhum argumento. É ruim porque sim. Não sou fã de Java, mas esse tipo de raciocínio não está ajudando ninguém.

Novamente, os erros não são para o desenvolvedor corrigir os bugs. Esse é um benefício do tratamento de erros. O método Java ensinou aos desenvolvedores que isso é tratamento de erros, não. Erros podem existir na camada de aplicativo e, além disso, em uma camada de fluxo. Erros no Go são rotineiramente usados ​​para controlar a estratégia de recuperação de um sistema - em tempo de execução, não em tempo de compilação.

Isso pode ser incompreensível quando as linguagens prejudicam seu controle de fluxo como resultado de um erro ao desvendar a pilha e perder a memória de tudo o que fizeram antes de ocorrer o erro. Os erros são realmente úteis em tempo de execução no Go; Não vejo por que eles deveriam carregar coisas como números de linha - o código em execução dificilmente se preocupa com isso.

@Como

Novamente, os erros não são para o desenvolvedor corrigir bugs

Isso está completamente errado. Os erros são exatamente por esse motivo. Eles não estão limitados a isso, mas é um dos principais usos. Os erros indicam que há algo errado com o sistema e você deve fazer algo a respeito. Para erros esperados e fáceis, você pode tentar recuperar como, por exemplo, tempo limite de TCP. Para algo mais sério, você despeja nos logs e depois depura o problema.

Esse é um benefício do tratamento de erros. O método Java ensinou aos desenvolvedores que isso é tratamento de erros, não.

Eu não sei o que Java te ensinou, mas eu uso exceções pelo mesmo motivo - para controlar a estratégia de recuperação que o sistema leva em tempo de execução. Go não tem nada de especial em termos de tratamento de erros.

Isso pode ser incompreensível quando as linguagens prejudicam seu controle de fluxo como resultado de um erro ao desvendar a pilha e perder a memória de tudo o que fizeram antes de ocorrer o erro

Pode ser para alguém, não para mim.

Os erros são realmente úteis em tempo de execução no Go; Não vejo por que eles deveriam carregar coisas como números de linha - o código em execução dificilmente se preocupa com isso.

Se você se preocupa em consertar bugs em seu código, então os números de linha são a maneira de fazê-lo. Não foi o Java que nos ensinou isso, C tem __LINE__ e __FUNCTION__ exatamente por esse motivo. Você deseja registrar seus erros e registrar o local exato onde isso aconteceu. E quando algo dá errado, você pelo menos tem algo para começar. Não é uma mensagem de erro aleatória causada por um erro irrecuperável. Se você não precisa desse tipo de informação, ignore-o. Não te machuca. Mas pelo menos está lá e pode ser usado quando necessário.

Não entendo por que as pessoas aqui continuam mudando a conversa para exceções e valores de erro. Ninguém estava fazendo essa comparação. A única coisa que foi discutida é que os rastreamentos de pilha são muito úteis e carregam muitas informações de contexto. Se isso for incompreensível, provavelmente você vive em um universo completamente diferente, onde o rastreamento não existe.

Isso está completamente errado.

Mas o sistema de produção ao qual estou me referindo ainda está em execução e usa erros para controle de fluxo, foi escrito em Go e substituiu uma implementação mais lenta em uma linguagem que usava rastreamentos de pilha para propagação de erros.

Se isso for incompreensível, provavelmente você vive em um universo completamente diferente, onde o rastreamento não existe.

Para encadear as informações da pilha de chamadas para cada função que retorna um tipo de erro, faça isso a seu critério. Os rastreamentos de pilha são mais lentos e inadequados para uso fora de projetos de brinquedos por motivos de segurança. É uma falta técnica torná-los cidadãos de Go de primeira classe apenas para auxiliar em estratégias impensadas de propagação de erros.

se você não precisa desse tipo de informação, ignore-a. Não te machuca.

O excesso de software é o motivo pelo qual os servidores são reescritos no Go. O que você não vê ainda pode degradar a taxa de transferência do seu pipeline.

Eu preferiria exemplos de software real que se beneficia de ter esse recurso, em vez de uma lição ligeiramente irrelevante sobre como lidar com tempos limite de TCP e despejo de log.

Os rastreamentos de pilha são mais lentos

Dado que os rastreamentos de pilha são gerados no caminho do erro, ninguém se importa com o quão lentos eles são. O funcionamento normal do software já foi interrompido.

e inadequada para uso fora de projetos de brinquedos por razões de segurança

Até agora, eu ainda não vi um único sistema de produção desligar os rastreamentos de pilha por "motivos de segurança", ou mesmo nenhum. Por outro lado, ser capaz de identificar rapidamente o caminho que o código percorreu para produzir um erro tem sido extremamente útil. E isso é para grandes projetos, com muitas equipes diferentes trabalhando na base do código e ninguém tendo conhecimento total de todo o sistema.

O que você não vê ainda pode degradar a taxa de transferência do seu pipeline.

Não, realmente não importa. Como eu disse antes, os rastreamentos de pilha são gerados em erros. A menos que seu software os encontre constantemente, o rendimento não será afetado nem um pouco.

Dado que os rastreamentos de pilha são gerados no caminho do erro, ninguém se importa com o quão lentos eles são. O funcionamento normal do software já foi interrompido.

Falso.

  • Erros podem ocorrer como parte da operação normal.
  • Os erros podem ser recuperados e o programa pode continuar, portanto, o desempenho ainda está em questão.
  • Retardar uma rotina suga recursos de outras rotinas que _estam_ operando no caminho da felicidade.

@ object88 imagine um código de produção real. Quantos erros você espera que ele gere? Eu não pensaria muito. Pelo menos em um aplicativo devidamente escrito. Se uma goroutine está em um loop ocupado e constantemente lança erros em cada iteração, há algo errado com o código. Mas mesmo se for esse o caso, considerando que a maioria dos aplicativos Go são limitados por IO, mesmo isso não seria um problema sério.

@Como

Mas o sistema de produção ao qual estou me referindo ainda está em execução e usa erros para controle de fluxo, foi escrito em Go e substituiu uma implementação mais lenta em uma linguagem que usava rastreamentos de pilha para propagação de erros.

Sinto muito, mas esta é uma frase sem sentido que não tem nada a ver com o que eu disse. Não vou atender.

Os rastreamentos de pilha são mais lentos

Mais lento, mas quanto? Isso importa? Acho que não. Os aplicativos Go são limitados por IO em geral. Perseguir os ciclos da CPU é uma tolice neste caso. Você tem problemas muito maiores no tempo de execução do Go, que consome CPU. Não é um argumento para jogar fora um recurso útil que ajuda a consertar bugs.

impróprio para uso fora de projetos de brinquedos por razões de segurança.

Não vou me preocupar em cobrir "razões de segurança" inexistentes. Mas gostaria de lembrar a você que geralmente os rastreamentos de aplicativos são armazenados internamente e apenas os desenvolvedores têm acesso a eles. E tentar ocultar os nomes das funções é perda de tempo de qualquer maneira. Não é segurança. Espero não precisar entrar em detalhes sobre isso.

Se você insiste em questões de segurança, gostaria que você pensasse em macOS / iOS, por exemplo. Eles não têm problemas em lançar pânicos e despejos de memória que contêm pilhas de todos os threads e valores de todos os registros da CPU. Não os veja sendo afetados por esses "motivos de segurança".

É uma falta técnica torná-los cidadãos de Go de primeira classe apenas para auxiliar em estratégias impensadas de propagação de erros.

Você poderia ser mais subjetivo? "estratégias de propagação de erro impensadas", onde você viu isso?

O excesso de software é o motivo pelo qual os servidores são reescritos no Go. O que você não vê ainda pode degradar a taxa de transferência do seu pipeline.

De novo, por quanto?

Eu preferiria exemplos de software real que se beneficia de ter esse recurso, em vez de uma lição ligeiramente irrelevante sobre como lidar com tempos limite de TCP e despejo de log.

Neste ponto, parece que estou falando com qualquer um, menos com um programador. O rastreamento beneficia todo e qualquer software. É uma técnica comum em todas as linguagens e todos os tipos de software que ajuda a corrigir bugs. Você pode ler a Wikipedia se desejar obter mais informações sobre isso.

Ter tantas discussões improdutivas sem nenhum consenso significa que não há uma maneira elegante de resolver esse problema.

@ object88
Os rastreamentos de pilha podem ser lentos se você quiser rastrear todos os goroutines, porque Go deve esperar que outros goroutines sejam desbloqueados.
Se você apenas rastrear a goroutine que está executando - não é tão lento.

@creker
O rastreamento beneficia todos os softwares, mas depende do que você está rastreando. Na maioria dos projetos Go em que estive envolvido, rastrear pilhas não foi uma ótima ideia, porque a simultaneidade está envolvida. Os dados estão se movendo para frente e para trás, muitas coisas estão se comunicando entre si e algumas goroutines são apenas algumas linhas de código. Ter rastreamento de pilha nesse caso não ajuda em nada.
É por isso que uso erros agrupados com informações de contexto gravadas em log para recriar o mesmo rastreamento de pilha, mas que não está vinculado à pilha de goroutine real, mas à própria lógica do aplicativo.
Para que eu pudesse apenas fazer cat * .log | grep "orderID = xxx" e obtenha o rastreamento da pilha da seqüência real de ações que levaram a um erro.
Devido à natureza simultânea do Go, os erros ricos em contexto são mais valiosos do que os rastreamentos de pilha.

@gladkikhartem obrigado por

Eu entendo seu argumento e concordo parcialmente com ele. Ainda assim, tenho que lidar com pilhas de pelo menos 5 funções de profundidade. Isso já é grande o suficiente para ser capaz de entender o que está acontecendo e onde você deve começar a procurar. Mas, em um aplicativo altamente simultâneo com muitos rastros de pilha de goroutines muito pequenos, os benefícios são perdidos. Concordo com isso.

@creker

imagine um código de produção real. Quantos erros você espera que ele gere? [...], dado que a maioria das aplicações Go são limitadas por IO, mesmo isso não seria um problema sério.

Que bom que você mencionou operações vinculadas a IO. O método io.Reader Read retorna um erro de

@urandom

Os rastreamentos de pilha involuntariamente expõem informações valiosas para a criação de perfil de um sistema.

  • Nomes de usuário
  • Caminhos do sistema de arquivos
  • Tipo / versão do banco de dados de back-end
  • Fluxo de transação
  • Estrutura do objeto
  • Algoritmos de criptografia

Não sei se o aplicativo médio notaria a sobrecarga de coleta de frames de pilha em tipos de erro, mas posso dizer que, para aplicativos críticos de desempenho, muitas pequenas funções Go são embutidas manualmente por causa da sobrecarga da chamada de função atual. O rastreamento tornará tudo pior.

Acredito que o objetivo do Go é ter um software simples e rápido, e rastrear seria um passo atrás. Devemos ser capazes de escrever pequenas funções e retornar erros dessas funções sem degradação de desempenho que incentiva tipos de erros não convencionais e alinhamento manual.

@creker

Evitarei dar exemplos que causem mais dissonância. Sinto muito por ter frustrado você.

Eu proporia o uso de uma nova palavra-chave "returnif", cujo nome revela instantaneamente sua função. Além disso, é flexível o suficiente para ser usado em mais casos de uso do que no tratamento de erros.

Exemplo 1 (usando retorno nomeado):

a, errar = algo (b)
se errar! = nulo {
Retorna
}

Se tornaria:

a, errar = algo (b)
returnif err! = nulo

Exemplo 2 (não usando retorno nomeado):

a, err: = algo (b)
se errar! = nulo {
return a, err
}

Se tornaria:

a, err: = algo (b)
returnif err! = nil {a, err}

Em relação ao seu exemplo de retorno nomeado, você quer dizer ...

a, err = something(b)
returnif err != nil

@ambernardino
Por que não apenas atualizar a ferramenta fmt e você não precisa atualizar a sintaxe de uma linguagem e adicionar novas palavras-chave inúteis

a, err := something(b)
if err != nil { return a, err }

ou

a, err := something(b)
    if err != nil { return a, err }

@gladkikhartem a ideia não é digitar isso toda vez que quiser propagar o erro, prefiro isso e deveria funcionar da mesma forma

a, err? := something(b)

@mrkaspa
A ideia é tornar o código mais legível . Digitar código não é problema, mas ler.

@gladkikhartem rust usa essa abordagem e não acho que isso a torne menos legível

@gladkikhartem Não acho que ? torne menos legível. Eu diria que elimina completamente o ruído. O problema para mim é que, com o ruído, ele também elimina a possibilidade de fornecer um contexto útil. Simplesmente não vejo onde você poderia inserir a mensagem de erro usual ou erros de empacotamento. A pilha de chamadas é uma solução óbvia, mas, como já foi mencionado, não funciona para todos.

@mrkaspa
E eu acho que isso torna menos legível, o que vem a seguir? Estamos tentando encontrar a melhor solução ou apenas compartilhando opiniões?

@creker
'?' personagem adiciona carga cognitiva ao leitor, porque não é tão óbvio o que será retornado e é claro que a pessoa deve saber o que isso está fazendo. E claro ? sinal levanta questões na mente do leitor.

Como eu disse anteriormente, se você quiser se livrar do err! = Nil, o compilador pode detectar parâmetros de erro não utilizados e encaminhá-los ele mesmo.
E

a, err? := doStuff(a,b)
err? := doAnotherStuff(b,z,d,g)
a, b, err? := doVeryComplexStuff(b)

se tornará mais legível

a := doStuff(a,b)
doAnotherStuff(b,z,d,g)
a, b := doVeryComplexStuff(b)

mesma magia, apenas menos coisas para digitar e menos coisas para pensar

@gladkikhartem bem, não acho que haja uma solução que não exija que os leitores aprendam algo novo. Essa é a consequência de mudar o idioma. Temos que fazer uma troca - ou vivemos com verbose em sua sintaxe de rosto que mostra exatamente o que está sendo feito em termos primitivos ou introduzimos uma nova sintaxe que poderia esconder a verbosidade, adicionar algum açúcar de sintaxe etc. Não há outra maneira. Simplesmente rejeitar qualquer coisa que acrescente algo para o leitor aprender é contraproducente. Podemos muito bem fechar todos os problemas do Go2 e encerrar o dia.

Quanto ao seu exemplo, ele introduz ainda mais coisas mágicas e esconde qualquer ponto de injeção para introduzir sintaxe que permitiria ao desenvolvedor fornecer contexto aos erros. E, o mais importante, ele oculta completamente qualquer informação sobre qual chamada de função pode gerar um erro. Isso cheira cada vez mais a exceções. E se levarmos a sério apenas relançar os erros, os rastreamentos de pilha se tornarão obrigatórios, porque essa é a única maneira de reter o contexto nesse caso.

Na verdade, a proposta original já cobre tudo isso muito bem. É detalhado o suficiente e fornece um bom local para encerrar erros e fornecer um contexto útil. Mas um dos principais problemas é esse 'erro' mágico. Eu acho que é feio porque não é mágico o suficiente e não é prolixo o suficiente. É uma espécie de meio. O que pode torná-lo melhor é introduzir mais magia.

E se || produzisse um novo erro que envolve automagicamente o erro original. Então, o exemplo fica assim

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

err simplesmente desaparece e toda a embalagem é tratada implicitamente. Agora precisamos acessar esses erros internos de alguma forma. Adicionar outro método como Inner() error a error interface não funcionará, eu acho. Uma maneira é introduzir funções integradas como unwrap(error) []error . O que ele faz é retornar uma fatia de todos os erros internos na ordem em que foram agrupados. Dessa forma, você pode acessar qualquer erro interno ou intervalo sobre eles.

A implementação disso é questionável, visto que error é apenas uma interface e você precisa de um lugar onde colocar esses erros agrupados para qualquer tipo error .

Para mim, isso preenche todas as caixas, mas pode ser um tanto mágico. Mas, considerando que a interface de erro é muito especial por definição, trazê-la para mais perto de um cidadão de primeira classe pode não ser uma má ideia. O tratamento de erros é detalhado porque é apenas um código Go normal, não há nada de especial nisso. Isso pode ser bom no papel e para fazer manchetes chamativas, mas os erros são muito especiais para justificar esse tratamento. Eles precisam de um invólucro especial.

A proposta original é sobre a redução do número de verificações de erro ou a duração de cada verificação individual?

Se for o último, então é trivial refutar a necessidade da proposta com base no fato de que há apenas uma declaração condicional e uma declaração de repetição. Algumas pessoas não gostam de loops for; devemos fornecer uma construção de loop implícita também?

As alterações de sintaxe propostas até agora serviram como um experimento de pensamento interessante, mas nenhuma delas é tão clara, pronunciável ou simples quanto o original. Go não é bash e nada deve ser "mágico" sobre os erros.

Cada vez mais leio essas propostas, cada vez mais vejo pessoas cujos argumentos nada mais são do que “acrescenta algo novo, então é ruim, ilegível, deixa tudo como está”.

@ à

tão claro, pronunciável ou simples quanto o original

Qualquer proposta irá introduzir uma nova sintaxe e ser novo para algumas pessoas soa o mesmo que "ilegível, complicado etc etc". Só porque é novo não o torna menos claro, pronunciável ou simples. "||" e "?" os exemplos são tão claros e simples quanto a sintaxe existente, uma vez que você saiba o que ela faz. Ou devemos começar a reclamar que "->" e "<-" são mágicos demais e o leitor precisa saber o que eles significam? Vamos substituí-los por chamadas de método.

Go não é bash e nada deve ser "mágico" sobre os erros.

Isso é totalmente infundado e não conta como argumento para nada. O que isso tem a ver com Bash está além de mim.

@creker
Sim, concordo totalmente com você que introduziu um evento mais mágico. Meu exemplo é apenas uma continuação de? ideia do operador de digitar menos coisas.

Concordo que precisamos sacrificar algo e introduzir algumas mudanças e, claro, alguma magia. É apenas um equilíbrio entre os prós da usabilidade e os contras dessa mágica.

Original || proposta é muito boa e praticamente testada, mas a formatação é feia na minha opinião, eu sugiro mudar a formatação para

syscal.Chdir(dir)
    || return &PathError{"chdir", dir}

PS o que você acha dessa variante da sintaxe mágica?

syscal.Chdir(dir) {
    return &PathError{"chdir", dir}
}

@gladkikhartem parecem muito bons do ponto de vista da legibilidade, mas tenho um mau pressentimento do último. Ele apresenta esse estranho escopo de bloco sobre o qual não tenho tanta certeza.

Eu o encorajo a não olhar para a sintaxe isoladamente, mas sim no contexto de uma função. Este método tem alguns blocos de tratamento de erros diferentes.

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath, err := p.preparePath()
        if err != nil {
                return nil, err
        }
    fis, err := l.context.ReadDir(absPath)
    if err != nil {
        return nil, err
    } else if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

Como as mudanças propostas limpam esta função?

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath, err? := p.preparePath()
    fis, err? := l.context.ReadDir(absPath)
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
         if _, ok := err.(*build.NoGoError); ok {
             // There isn't any Go code here.
             return nil, nil
         }
         return nil, err
    }

    return buildPkg, nil
}

A proposta @bcmills se encaixa melhor.

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath := p.preparePath()        =? err { return nil, err }
    fis := l.context.ReadDir(absPath) =? err { return nil, err }
    if len(fis) == 0 {
        return nil, nil
    }
    buildPkg := l.context.Import(".", absPath, 0) =? err {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }
    return buildPkg, nil
}

@ object88

func (l *Loader) processDirectory(p *Package) (p *build.Package, err error) {
        absPath, err := p.preparePath() {
        return nil, fmt.Errorf("prepare path: %v", err)
    }
    fis, err := l.context.ReadDir(absPath) {
        return nil, fmt.Errorf("read dir: %v", err)
    }
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0) {
        err, ok := err.(*build.NoGoError)
                if !ok {
            return nil, fmt.Errorf("buildpkg: %v",err)
        }
        return nil, nil
    }
    return buildPkg, nil
}

Continuando a cutucar isso. Talvez possamos apresentar exemplos mais completos de uso.

@erwbgy , este parece o melhor para mim, mas não estou convencido de que o pagamento seja tão bom.

  • Quais são os valores de retorno sem erro? Eles são sempre o valor zero? Se _existia_ um valor atribuído anteriormente para um retorno nomeado, ele é sobrescrito? Se o valor nomeado for usado para armazenar o resultado de uma função com erro, isso será retornado?
  • O operador ? ser aplicado a um valor sem erro? Posso fazer algo como (!ok)? ou ok!? (o que é um pouco estranho, porque você está agrupando atribuição e operação)? Ou esta sintaxe é boa apenas para error ?

@rodcorsi , este me incomoda porque e se a função não fosse ReadDir mas ReadBuildTargetDirectoryForFileInfo ou algo bobo longo como isso. Ou talvez você tenha muitos argumentos. O tratamento de erros para preparePath também seria empurrado para fora da tela. Em um dispositivo com tamanho de tela horizontal limitado (ou janela de visualização que não seja tão larga, como o Github), você provavelmente perderá a parte =? . Somos muito bons em rolagem vertical; não tanto na horizontal.

@gladkikhartem , parece que está vinculado a algum (apenas o último?) argumento que implementa a interface error . Parece muito com uma declaração de função, e isso é ... _sente_ estranho. Existe alguma maneira de ele ser vinculado a um valor de retorno ok ? No geral, você está comprando apenas 1 linha.

@ object88
a quebra de palavras resolve problemas de código realmente amplos. não é amplamente utilizado?

@ object88 em relação à chamada de função muito longa. Vamos lidar com a questão principal aqui. O problema não é o tratamento de erros empurrado para fora da tela. O problema é um nome de função longo e / ou uma grande lista de argumentos. Isso precisa ser corrigido antes que qualquer argumento possa ser feito sobre o tratamento de erros fora da tela.

Ainda estou para ver um IDE ou editor de texto amigável que foi configurado para quebra de linha por padrão. E eu não encontrei uma maneira de fazer isso com o Github além de hackear manualmente o CSS depois que a página for carregada.

E eu acho que a largura do código é um fator importante - ele fala sobre a _readability_, que é o ímpeto para esta proposta. A alegação é que há "código demais" em torno dos erros. Não que a funcionalidade não esteja lá, ou que os erros precisem ser implementados de alguma outra forma, mas o código não lê bem.

@ object88
sim, este código funcionará para qualquer função que retorne interface de erro como último parâmetro.

Em relação à economia de linha, você simplesmente não pode colocar mais informações em menos número de linhas. O código deve ser distribuído uniformemente, não muito denso e sem espaço após cada instrução.
Eu concordo que parece uma declaração de função, mas ao mesmo tempo é muito semelhante a existente se ...; err! = nil {declaração, então as pessoas não ficarão muito confusas.

A largura do código é um fator importante. E se eu tiver um editor de 80 linhas e 80 linhas de código for uma chamada de função e depois disso eu tenho || declaração de

Apenas para completar, lançarei um exemplo com a sintaxe || , meu empacotamento de erro automagic e zeramento automático de valores de retorno sem erro

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath := p.preparePath() || errors.New("prepare path")
    fis := l.context.ReadDir(absPath) || errors.New("ReadDir")
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

Em relação à sua dúvida sobre outros valores de retorno. Em caso de erro, terão valor zero em todos os casos. Já falei porque acredito que isso é importante.

O problema é que o seu exemplo não é tão complicado para começar. Mas ainda mostra o que essa proposta, pelo menos para mim, representa. O que eu quero resolver é o idioma mais comum e amplamente usado

err := func()
if err != nil {
    return err
}

Podemos todos concordar que esse tipo de código é uma grande parte (se não a maior) do tratamento de erros em geral. Portanto, é lógico resolver esse caso. E se você quiser fazer algo mais relacionado ao erro, aplique alguma lógica - vá em frente. É onde a verbosidade deve estar, onde há uma lógica real para o programador ler e entender. O que não precisamos é perder espaço e tempo lendo clichês estúpidos. É uma parte irracional, mas ainda essencial do código Go.

Em relação à conversa anterior sobre o retorno implícito de valores zero. Se você precisar retornar um valor significativo em caso de erro, vá em frente. Novamente, o detalhamento é bom aqui e ajuda a entender o código. Nada de errado em descartar o açúcar sintático se você precisar fazer algo mais complicado. E || é flexível o suficiente para resolver ambos os casos. Você pode omitir os valores sem erro e eles serão zerados implicitamente. Ou você pode especificá-los explicitamente, se necessário. Lembro que há até uma proposta separada para isso que também envolve casos em que você deseja retornar o erro e zerar todo o resto.

@ object88

A alegação é que há "código demais" em torno dos erros.

Não é qualquer código. O principal problema é que há muitos clichês sem sentido em torno dos erros e casos muito comuns de tratamento de erros. O detalhamento é importante quando há algo de valor para ler. Não há nada de valor em if err == nil then return err exceto que você deseja relançar o erro. Para uma lógica tão primitiva, é preciso muito espaço. E quanto mais você tiver lógica, chamadas de biblioteca, invólucros, etc, que podem muito bem retornar um erro, mais esse clichê começa a dominar coisas importantes - a lógica real de seu código. E essa lógica pode realmente conter alguma lógica importante de tratamento de erros. Mas ele se perde na natureza repetitiva da maior parte do clichê ao seu redor. E isso pode ser resolvido e outras linguagens modernas com as quais Go compete tentam resolver isso. Como o tratamento de erros é tão importante, não é apenas um código normal.

@creker
Eu concordo que se err != nil return err for muito clichê, tememos que, se criarmos uma maneira fácil de apenas encaminhar o erro para a pilha - estatisticamente os programadores, especialmente os juniores, usarão o método mais fácil, em vez de fazer o que é apropriado em uma determinada situação.
É a mesma ideia com o tratamento de erros de Go - força você a fazer uma coisa decente.
Portanto, nesta proposta, queremos encorajar outras pessoas a lidar e encerrar os erros de maneira cuidadosa.

Eu diria que devemos fazer o tratamento de erros simples parecer feio e demorado de implementar, mas o tratamento de erros elegante com empacotamento ou rastreamentos de pilha parece bom e fácil de fazer.

@gladkikhartem Eu sempre achei essa discussão sobre editores antigos tola. Quem se importa com isso e por que a linguagem deve sofrer com isso? É 2018, quase todo mundo tem uma tela grande e um editor decente. A minoria muito pequena não deve influenciar todos os outros. Deveria ser o contrário - a minoria deveria lidar com isso sozinha. Role, quebra de linha, qualquer coisa.

@gladkikhartem Go já tem esse problema e acho que não podemos fazer nada a respeito. Os desenvolvedores sempre serão preguiçosos até que você os force com uma compilação falha ou pânico em tempo de execução, o que, a propósito, Go não está fazendo.

O que Go realmente faz não é forçar nada. A noção de que Go o força a lidar com os erros é enganosa e sempre foi. Os autores de Go forçam você a fazer isso em seus posts e palestras em conferências. A linguagem real permite que você faça o que quiser. E o principal problema aqui é o que Go escolhe por padrão - por padrão, o erro é ignorado silenciosamente. Existe até uma proposta para mudar isso. Se Go pretendia forçar você a fazer uma coisa decente, deveria fazer o seguinte. Retorne um erro em tempo de compilação ou entre em pânico em tempo de execução se o erro retornado não for tratado corretamente. Pelo que entendi, Rust faz isso - os erros se apavoram por padrão. Isso é o que chamo de forçar a fazer a coisa certa.

O que realmente me forçou a lidar com os erros em Go foi minha consciência de desenvolvedor, nada mais. Mas é sempre tentador ceder. No momento, se eu não começar a ler explicitamente a assinatura da função, ninguém vai me dizer nada que ela retornou um erro. Existe um exemplo real. Por muito tempo eu não tinha ideia de que fmt.Println retornava um erro. Não tenho uso para o seu valor de retorno, só quero imprimir algumas coisas. Portanto, não há incentivo para eu olhar para o que ele retorna. Esse é o mesmo problema que C tem. Erros são valores e você pode ignorá-los o quanto quiser até que seu código seja interrompido em tempo de execução e você não saberá nada sobre isso porque não há travamento com pânico útil como, por exemplo, com exceções não tratadas.

@gladkikhartem, pelo que entendi sobre esta proposta, não é para encorajar os desenvolvedores a embrulhar os erros de maneira cuidadosa. Trata-se de encorajar aqueles que vierem com propostas a não se esquecerem de cobrir isso. Porque muitas vezes as pessoas vêm com soluções que apenas relançam o erro e esquecem que você realmente deseja dar mais contexto e só então relançá-lo.

Estou escrevendo esta proposta principalmente para incentivar as pessoas que desejam simplificar o tratamento de erros Go a pensar em maneiras de tornar mais fácil envolver os erros no contexto, não apenas para retornar o erro sem modificações.

@creker
Meu editor tem 100 caracteres de largura, porque tenho explorador de arquivos, console git e etc ...., em nossa equipe ninguém escreve um código com mais de 100 caracteres, é simplesmente bobo (com algumas exceções)

Go não está forçando o tratamento de erros, mas os linters sim. (Talvez devêssemos apenas escrever um linter para isso?)

Ok, se não podemos encontrar uma solução e todos entendem a proposta à sua maneira - por que não especificar alguns requisitos para o que precisamos? tipo de concordar com os requisitos primeiro e, em seguida, desenvolver a solução seria uma tarefa muito mais fácil.

Por exemplo:

  1. A sintaxe da nova proposta deve ter a instrução return no texto, caso contrário, não é óbvio para o leitor o que está acontecendo. ( concordar discordar )
  2. A nova proposta deve oferecer suporte a funções que retornam vários valores (concordar / discordar)
  3. A nova proposta deve ocupar menos espaço (1 linha, 2 linhas, discordo)
  4. A nova proposta deve ser capaz de lidar com expressões muito longas (concordo / discordo)
  5. A nova proposta deve permitir várias declarações em caso de erro (concordo / discordo)
  6. .....

@creker , cerca de 75% do meu desenvolvimento é feito em um laptop de 15 "em VSCode. Eu maximizo meu espaço horizontal, mas ainda há um limite, especialmente se estou editando lado a lado. Aposto isso entre os alunos , há muito mais laptops do que desktops. Eu não gostaria de limitar a acessibilidade da linguagem porque prevemos que todos terão monitores de formato grande.

E, infelizmente, não importa o tamanho da tela que você tenha, o github ainda limita a janela de visualização.

@gladkikhartem

Preguiça de principiante é aplicável aqui, mas o uso liberal de errors.New em alguns desses exemplos também demonstra uma falta de compreensão da linguagem. Os erros não devem ser alocados em valores de retorno, a menos que sejam dinâmicos, e se esses erros fossem colocados em uma variável de escopo de pacote comparável, a sintaxe seria mais curta na página e realmente aceitável no código de produção também. Aqueles que sofrem mais com o "boilerplate" de manipulação de erros do Go pegam a maioria dos atalhos e não têm experiência suficiente para lidar com os erros de maneira adequada.

Não é óbvio o que constitui simplifying error handling , mas o precedente é que less runes != simple . Acho que existem alguns qualificadores para simplicidade que podem medir uma construção de uma forma quantificável:

  • O número de maneiras que a construção é expressa
  • A semelhança desse construto com outro construto e a coesão entre esses construtos
  • O número de operações lógicas resumidas pela construção
  • A semelhança do construto com a linguagem natural (ou seja, ausência de negação, etc)

Por exemplo, a proposta original aumenta o número de maneiras de propagar erros de 2 para 3. É semelhante ao OR lógico, mas tem semântica diferente. Ele resume um retorno condicional de baixa complexidade (em comparação com copy ou append ou >> ). O novo método é menos natural do que o antigo e, se falado em voz alta, provavelmente seria abs, err := path(foo) || return err -> if theres an error, it's returning err , caso em que seria um mistério porque é possível usar as barras verticais se você pode escrever da mesma forma que é dito em voz alta em uma revisão de código.

@Como
Concordo totalmente que less runes != simple .
Por simples quero dizer legível e compreensível.
Para que quem não esteja familiarizado com go, leia e entenda o que ele faz.
Deve ser como uma piada - você não precisa explicar.

O tratamento de erros atual é realmente compreensível, mas não completamente legível se você tiver muito if err != nil return.

@ object88 tudo bem. Eu disse mais em geral porque esse argumento surge com bastante frequência. Tipo, vamos imaginar uma tela de terminal antiga ridícula que pudesse ser usada para escrever Go. Que tipo de argumento é esse? Onde está o limite de seu ridículo? Se levarmos isso a sério, devemos observar os fatos concretos - qual é o tamanho e a resolução de tela mais populares. E só disso podemos desenhar algo. Mas o argumento geralmente apenas imagina algum tamanho de tela que ninguém usa, mas há uma pequena possibilidade de que alguém pudesse.

@gladkikhartem não,

Concordo, devemos formular melhor o que queremos porque a proposta não cobre todos os aspectos.

@Como

O novo método é menos natural que o antigo e, se falado em voz alta, provavelmente seria abs, err: = path (foo) || return err -> se houver um erro, ele está retornando err, caso em que seria um mistério porque é possível usar as barras verticais se você pode escrever da mesma forma que é dito em voz alta em uma revisão de código.

O novo método é menos natural apenas por um motivo - não faz parte da linguagem agora. Não há outro motivo. Imagine Go já com essa sintaxe - seria natural porque você está familiarizado com ela. Assim como você está familiarizado com -> , select , go e outras coisas que não estão presentes em outros idiomas. Por que é possível usar barras verticais em vez de retorno? Eu respondo com uma pergunta. Por que existe uma maneira de anexar fatias em uma chamada quando você pode fazer a mesma coisa com o loop? Por que há uma maneira de copiar coisas da interface do leitor para o gravador em uma chamada quando você pode fazer o mesmo com o loop? etc etc etc Porque você deseja que seu código seja mais compacto e mais legível. Você está apresentando esses argumentos quando Go já os contradiz com vários exemplos. Mais uma vez, vamos ser mais abertos e não derrubar nada só porque é novo e ainda não está na linguagem. Não vamos conseguir nada com isso. Há um problema, muitas pessoas estão pedindo uma solução, vamos lidar com isso. Go não é uma linguagem sagrada ideal que será profanada por qualquer coisa adicionada a ela.

Por que existe uma maneira de anexar fatias em uma chamada quando você pode fazer a mesma coisa com o loop?

Escrever uma verificação de erro de instrução if é trivial, eu estaria interessado em ver sua implementação de append .

Por que há uma maneira de copiar coisas da interface do leitor para o gravador em uma chamada quando você pode fazer o mesmo com o loop?

Um leitor e um escritor abstraem as origens e destinos de uma operação de cópia, a estratégia de armazenamento em buffer e, às vezes, até os valores sentinela no loop. Você não pode expressar essa abstração com um loop e uma fatia.

Você está apresentando esses argumentos quando Go já os contradiz com vários exemplos.

Não acredito que seja esse o caso, pelo menos não com esses exemplos.

Mais uma vez, vamos ser mais abertos e não derrubar nada só porque é novo e ainda não está na linguagem.

Dado que Go tem uma garantia de compatibilidade, você deve examinar os novos recursos ao máximo, pois terá que lidar com eles para sempre se forem terríveis. O que ninguém fez aqui até agora foi criar uma prova de conceito real e usá-la com uma pequena equipe de desenvolvimento.

Se você olhar o histórico de algumas propostas (por exemplo, genéricos), verá que depois de fazer apenas isso a constatação é frequente: "uau, essa não é uma boa solução, não vamos fazer nenhuma alteração ainda". A alternativa é uma linguagem cheia de sugestões e nenhuma maneira fácil de despejá-los retroativamente.

Sobre a questão da tela larga versus fina, outra coisa a se considerar é a multitarefa .

Você pode ter várias janelas lado a lado para, ocasionalmente, manter o controle de outra coisa enquanto junta um pouco de código, em vez de apenas olhar para o editor, mudando completamente os contextos para outra janela para pesquisar uma função, talvez StackOverflow, e pule de volta para o editor.

@Como
Concordo totalmente que a maioria dos recursos propostos é impraticável e estou começando a pensar que || e ? coisas poderiam ser o caso.

@creker
copy () e append () não são tarefas triviais de implementar

Eu tenho linters em CI / CD e eles literalmente me forçam a lidar com todos os erros. Eles não fazem parte da linguagem, mas não importa - eu só preciso de resultados.
(e por falar nisso, eu tenho uma opinião forte - se alguém não está usando linters no Go - ele apenas ........)

Sobre o tamanho da tela - não é nem engraçado, sério. Por favor, pare com essa discussão irrelevante. Sua tela pode ser tão larga quanto você quiser - você sempre terá a probabilidade de que || return &PathError{Err:err} parte do código não estará visível. Apenas google a palavra "ide" e veja que tipo de espaço está disponível para o código.

E por favor, leia o texto alheio com atenção, eu não disse que Go te força a lidar com todos os erros

É a mesma ideia com o tratamento de erros de Go - força você a fazer uma coisa decente.

@gladkikhartem Go não força nada em termos de tratamento de erros, esse é o problema. Coisa decente ou não, não importa, isso é apenas picuinhas. Mesmo que para mim isso signifique lidar com todos os erros em todos os casos, exceto talvez coisas como fmt.Println .

se alguém não está usando linters no Go - ele está apenas

Talvez seja. Mas se algo não for realmente forçado, não vai voar. Alguns o usarão, outros não.

Sobre o tamanho da tela - não é nem engraçado, sério. Por favor, pare com essa discussão irrelevante.

Não fui eu que comecei a jogar números aleatórios que deveriam de alguma forma afetar a tomada de decisão. Afirmo claramente que entendo o problema, mas deve ser objetivo. Não "Eu tenho 80 símbolos de largura de IDE, Go deve levar isso em consideração e ignorar todos os outros".

Se estamos falando sobre o tamanho da minha tela. o código do Visual Studio me dá 270 símbolos de espaço horizontal. Não vou defender que é normal ocupar tanto espaço. Mas meu código pode facilmente exceder 120 símbolos quando você leva em conta structs com comentários e tipos de campos nomeados particularmente longos. Se eu fosse usar a sintaxe || , ela caberia facilmente em 100-120 no caso de uma chamada de função de 3-5 argumentos e erro agrupado com mensagem personalizada.

Maneira do Ether, se algo como || fosse implementado, então gofmt provavelmente não deveria forçá-lo a escrevê-lo em uma linha. Em alguns casos, pode muito bem ocupar muito espaço.

@erwbgy , este parece o melhor para mim, mas não estou convencido de que o pagamento seja tão bom.

@ object88 A

val, err := func()
if err != nil {
    return nil, errors.WithStack(err)
}

mais simples:

val, err? := func()

Nada impede que o tratamento de erros mais complexo seja feito da maneira atual.

Quais são os valores de retorno sem erro? Eles são sempre o valor zero? Se houver um valor atribuído anteriormente para um retorno nomeado, ele será substituído? Se o valor nomeado for usado para armazenar o resultado de uma função com erro, isso será retornado?

Todos os outros parâmetros de retorno são valores nulos apropriados. Para parâmetros nomeados, eu esperaria que eles mantivessem qualquer valor atribuído anteriormente, pois já teriam algum valor atribuído a eles.

Pode o ? operador ser aplicado a um valor sem erro? Posso fazer algo como (! Ok)? ou ok !? (o que é um pouco estranho, porque você está agrupando atribuição e operação)? Ou esta sintaxe é boa apenas para erros?

Não, não acho que faça sentido usar essa sintaxe para outra coisa senão valores de erro.

Acho que as funções "obrigatórias" vão proliferar devido ao desespero por um código mais legível.

sqlx

db.MustExec(schema)

template html

var t = template.Must(template.New("name").Parse("html"))

Proponho a operadora de pânico (não tenho certeza se devo chamá-la de 'operadora')

a,  😱 := someFunc(b)

mesmo que, mas talvez mais imediato do que

a, err := someFunc(b)
if err != nil {
  panic(err)
}

😱 provavelmente é muito difícil de digitar, poderíamos usar algo como!, Ou !!, ou

a,  !! := someFunc(b)
!! = maybeReturnsError()

Pode ser !! pânico e! retorna

Está na hora dos meus 2 centavos. Por que não podemos simplesmente usar debug.PrintStack() da biblioteca padrão para rastreamentos de pilha? A ideia é imprimir o rastreamento da pilha apenas no nível mais profundo, onde ocorreu o erro.

Por que não podemos simplesmente usar debug.PrintStack() da biblioteca padrão para rastreamentos de pilha?

Os erros podem transitar por muitas pilhas. Eles podem ser enviados através de canais, armazenados em variáveis, etc. Geralmente é mais útil conhecer esses pontos de transição do que saber o fragmento onde o erro foi gerado pela primeira vez.

Além disso, o rastreamento de pilha em si geralmente inclui funções auxiliares internas (não exportadas). Isso é útil quando você está tentando depurar uma falha inesperada, mas não é útil para erros que ocorrem durante a operação normal.

Qual é a abordagem mais amigável para iniciantes em programação?

Sou achado de versão mais simples. Precisa de apenas um if !err
Nada de especial, intuitivo, sem pontuação extra, código muito menor

`` `vá
absPath, err: = p.preparePath ()
retorna nulo, errar se errar

errar: = doSomethingWith (absPath) if! err
doSomethingElse () if! err

doSomethingRegardlessOfErr ()

// Manipule o erro em um só lugar; se necessário; tipo pega sem recuo
if err {
retornar "erro sem poluição do código", err
}
`` `

err := doSomethingWith(absPath) if !err
doSomethingElse() if !err

Bem-vindo de volta, boas e velhas condições de postagem do MUMPS ;-)

Obrigado, mas não, obrigado.

@dmajkic Isso não faz nada para ajudar com "retornar o erro com informações contextuais adicionais".

@erwbgy, o título deste problema é _proposal: Go 2: simplifique o tratamento de erros com || err sufixo_ meu comentário foi nesse contexto. Desculpe se eu pisei na discussão anterior.

@cznic Yup. As pós-condições não são boas, mas as pré-condições também parecem poluídas:

if !err; err := doSomethingWith(absPath)
if !err; doSomethingElse()

@dmajkic A proposta é mais do que apenas o título - ianlancetaylor descreve três maneiras de lidar com erros e aponta especificamente que poucas propostas tornam mais fácil retornar o erro com informações adicionais.

@erwbgy Eu passei por todos os problemas especificados por @ianlancetaylor Todos eles try() ) ou usando caracteres especiais não alfanuméricos. Pessoalmente - não gosto disso, pois o código está sobrecarregado com! "# $% & Tende a parecer ofensivo, como palavrões.

Eu concordo, e sinto, o que as primeiras linhas deste problema afirmam: muito código Go continua no tratamento de erros. A sugestão que fiz vai ao encontro desse sentimento, com uma sugestão muito próxima do que Go sente agora, sem a necessidade de palavras-chave ou caracteres-chave extras.

Que tal um adiamento condicional

func something() (int, error) {
    var error err
    var oth err

    defer err != nil {
        return 0, mycustomerror("More Info", err)
    }
    defer oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }

    _, err = a()
    _, err = b()
    _, err = c()
    _, oth = d()
    _, err = e()

    return 2, nil
}


func something() (int, error) {
    var error err
    var oth err

    _, err = a()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = b()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = c()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, oth = d()
    if oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }
    _, err = e()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }

    return 2, nil
}

Isso mudaria significativamente o significado de defer - é apenas algo que é executado no final do escopo, não algo que faz com que um escopo saia mais cedo.

Se eles introduzirem o Try Catch nesta linguagem, todos esses problemas serão resolvidos de uma forma muito fácil.

Eles deveriam apresentar algo assim. Se o valor de erro for definido como algo diferente de nil, ele pode interromper o fluxo de trabalho atual e acionar automaticamente a seção catch e, em seguida, a seção finally e as bibliotecas atuais também podem funcionar sem alterações. Problema resolvido!

try (var err error){
     i, err:=DoSomething1()
     i, err=DoSomething2()
     i, err=DoSomething3()
} catch (err error){
   HandleError(err)
   // return err  // similar to throw err
} finally{
  // Do something
}

image

@sbinet Isso é melhor do que nada, mas se eles simplesmente usarem o mesmo paradigma try-catch com o qual todos estão familiarizados, é muito melhor.

@KamyarM Você parece estar sugerindo adicionar um mecanismo para lançar uma exceção sempre que uma variável for definida com um valor diferente de zero. Esse não é o "paradigma com o qual todos estão familiarizados". Não conheço nenhuma linguagem que funcione assim.

É semelhante ao Swift, que também possui "exceções" que não funcionam exatamente como exceções.

Diferentes linguagens têm mostrado que try catch é realmente uma solução de segunda classe, embora eu acho que Go não será capaz de resolver isso como com uma mônada Maybe e assim por diante.

@ianlancetaylor Acabei de me referir ao Try-Catch em outras linguagens de programação, como C ++, Java, C #, ... e não a solução que tive aqui. Seria melhor se GoLang tivesse o Try-Catch desde o dia 1, então não precisamos lidar com essa forma de tratamento de erros (que não era realmente nova. Você pode escrever o mesmo tratamento de erros GoLang com qualquer outra linguagem de programação se desejar código assim), mas o que eu sugiro é uma maneira de ter compatibilidade com as bibliotecas atuais que podem retornar objeto de erro.

As exceções do Java são um desastre, então tenho que discordar firmemente de você aqui @KamyarM. Só porque algo é familiar, não significa que seja uma boa escolha.

O que eu quero dizer.

@KamyarM Obrigado pelo esclarecimento. Consideramos e rejeitamos explicitamente as exceções. Os erros não são excepcionais; eles acontecem por todos os tipos de razões completamente normais. https://blog.golang.org/errors-are-values

Excepcionais ou não, mas eles resolvem o problema do inchaço do código devido ao clichê de manipulação de erros. O mesmo problema prejudicou o Objective-C, que funciona exatamente como o Go. Os erros são apenas valores do tipo NSError, nada de especial sobre eles. E tem o mesmo problema com muitos ifs e empacotamento de erros. É por isso que Swift mudou as coisas. Eles acabaram com uma mistura de dois - funciona como exceções, o que significa que termina a execução e você deve capturar a exceção. Mas não desfaz a pilha e funciona como um retorno regular. Portanto, o argumento técnico contra o uso de exceções para o fluxo de controle não se aplica aqui - essas "exceções" são tão rápidas quanto o retorno normal. É mais um açúcar sintático. Mas Swift tem um problema único com eles. Muitas das APIs Cocoa são assíncronas (callbacks e GCD) e simplesmente não são compatíveis com esse tipo de tratamento de erros - as exceções são inúteis sem algo como await. Mas quase todo o código Go é síncrono e essas "exceções" podem funcionar.

@urandom
As exceções em Java não são ruins. O problema são os programadores ruins que não sabem como usá-lo.

Se o seu idioma tiver recursos terríveis, alguém eventualmente usará esse recurso. Se o seu idioma não tiver esse recurso, há 0% de chance. É matemática simples.

Como não concordo com você que try-catch seja um recurso terrível. É um recurso muito útil e torna nossa vida muito mais fácil e é por isso que estamos comentando aqui, então pode ser que a equipe do Google GoLang adicione uma funcionalidade semelhante. Eu pessoalmente odeio aqueles códigos if-elses de tratamento de erros em GoLang e não gosto muito desse conceito defer-panic-recover (é semelhante ao try-catch, mas não tão organizado quanto é com os blocos Try-Catch-Finalmente) . Ele adiciona tanto ruído ao código que o torna ilegível em muitos casos.

A funcionalidade para lidar com erros sem clichê já existe na linguagem. Adicionar mais recursos para saciar os iniciantes vindos de linguagens baseadas em exceções não parece uma boa ideia.

E quem está vindo de C / C ++, Objective-C, onde temos exatamente o mesmo problema com boilerplate? E é frustrante ver uma linguagem moderna como Go sofrer exatamente dos mesmos problemas. É por isso que todo esse exagero em torno dos erros como valores parece tão falso e tolo - isso já é feito há anos, dezenas de anos. Parece que Go não aprendeu nada com essa experiência. Especialmente olhando para Swift / Rust, que está realmente tentando encontrar uma maneira melhor. Vá resolvido com a solução existente como Java / C # resolvido com exceções, mas pelo menos essas são linguagens muito mais antigas.

@KamyarM Você já usou programação orientada para ferrovias? A viga?

Você não elogiaria tanto as exceções, se as usasse, imho.

@ShalokShalom Não muito. Mas isso não é apenas uma máquina de estado? Em caso de falha fazer isso e em caso de sucesso fazer aquilo? Bem, eu acho que nem todos os tipos de erros devem ser tratados como exceções. Quando apenas uma validação de entrada do usuário é necessária, pode-se simplesmente retornar um valor booleano com os detalhes do (s) erro (s) de validação. As exceções devem ser limitadas ao acesso IO ou à rede ou entradas de função incorretas e, no caso de um erro ser realmente crítico e você desejar interromper o caminho de execução feliz a todo custo.

Uma das razões pelas quais algumas pessoas dizem que o Try-Catch não é bom é por causa de seu desempenho. Provavelmente isso é causado pelo uso de uma tabela de mapa do manipulador para cada lugar em que uma exceção pode ocorrer. Eu li em algum lugar que mesmo as exceções são mais rápidas (Zero-Cost quando nenhuma exceção ocorre, mas tem muito mais custo quando elas realmente acontecem) comparando-o com a verificação If Error (é sempre verificado independentemente de haver erro ou não). Fora isso, não acho que haja qualquer problema com a sintaxe Try-Catch. É apenas a maneira como ele é implementado pelo compilador que o torna diferente, não sua sintaxe.

Pessoas que vêm de C / C ++ elogiam exatamente Go por NÃO ter exceções e
por fazer uma escolha sábia, resistindo àqueles que afirmam que é "moderno" e
agradecendo a Deus sobre o fluxo de trabalho legível (especialmente após C ++).

Na terça, 17 de abril de 2018 às 03:46, Antonenko Artem [email protected]
escreveu:

E quem está vindo de C / C ++, Objective-C, onde temos o mesmo
problema exato com o clichê? E frustrante ver uma linguagem moderna
como Go sofrem exatamente dos mesmos problemas. É por isso que todo esse hype
em torno de erros, pois os valores parecem tão falsos e tolos - já foi feito
por anos, dezenas de anos. Parece que Go não aprendeu nada com isso
experiência. Especialmente olhando para Swift / Rust, que está realmente tentando encontrar
uma maneira melhor. Vá resolvido com a solução existente como Java / C # resolvido com
exceções, mas pelo menos essas são linguagens muito mais antigas.

-
Você está recebendo isto porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/golang/go/issues/21161#issuecomment-381793840 ou mudo
o segmento
https://github.com/notifications/unsubscribe-auth/AICzv9w608ea2fwPq_wNpTDBnKMAdAKTks5tpTtsgaJpZM4Oi1c-
.

@kirillx Eu nunca disse que quero exceções como em C ++. Por favor, leia meu comentário novamente. E o que isso tem a ver com C, onde o tratamento de erros é ainda mais horrível? Não só você tem toneladas de boilerplate, mas também falta defer e múltiplos valores de retorno, o que o força a retornar valores usando argumentos de ponteiro e usar goto para organizar sua lógica de limpeza. Go usa o mesmo conceito de erros, mas resolve alguns dos problemas com adiar e vários valores de retorno. Mas o clichê ainda está lá. Outras linguagens modernas também não querem exceções, mas também não querem se contentar com o estilo C por causa de sua verbosidade. É por isso que temos esta proposta e tanto interesse neste problema.

Pessoas que defendem exceções devem ler este artigo: https://ckwop.me.uk/Why-Exceptions-Suck.html

A razão pela qual as exceções de estilo Java / C ++ são inerentemente ruins não tem nada a ver com o desempenho de implementações específicas. As exceções são ruins porque são do BASIC "on error goto", com os gotos invisíveis no contexto onde podem ter efeito. As exceções ocultam o tratamento de erros onde você pode facilmente esquecê-lo. As exceções verificadas do Java deveriam resolver esse problema, mas na prática não o fizeram porque as pessoas simplesmente pegaram e comeram as exceções ou despejaram rastreamentos de pilha em todos os lugares.

Escrevo Java na maioria das semanas e, enfaticamente, não quero ver exceções no estilo Java no Go, não importa o quão alto seja o desempenho.

@lpar não é todos os loops for, while, if elses, troca de casos, quebra e continua meio que coisas GoTo. O que resta de uma linguagem de programação então?

Enquanto, for e if / else não envolvem o fluxo de execução pulando invisivelmente para algum outro lugar, sem nenhum marcador para indicar que isso acontecerá.

O que é diferente se alguém simplesmente passa o erro para o chamador anterior no GoLang e esse chamador apenas retorna para o chamador anterior e assim por diante (exceto muito ruído de código)? Quantos códigos precisamos examinar e analisar para ver quem vai lidar com o erro? O mesmo acontece com o try-catch.

O que pode parar o programador? Às vezes, a função realmente não precisa lidar com um erro. queremos apenas passar o erro para a IU para que um usuário ou administrador do sistema possa resolvê-lo ou encontrar uma solução alternativa.

Se uma função não deseja tratar uma exceção, ela simplesmente não usa o bloco try-catch para que o chamador anterior possa tratá-la. Não acho que a sintaxe tenha problemas. Também é muito mais limpo. Porém, o desempenho e a maneira como ele é implementado em uma linguagem são diferentes.

Como você pode ver abaixo, precisamos adicionar 4 linhas de código apenas para não tratar um erro:

func myFunc1() error{
  // ...
  if (err){
      return err
  }
  return nil
}

Se você quiser passar os erros de volta para o responsável pela chamada, tudo bem. A questão é que é visível que você está fazendo isso, no ponto em que o erro foi retornado para você.

Considerar:

x, err := lib.SomeFunc(100, 4)
if err != nil {
  // A
}
// B

Observando o código, você sabe que pode ocorrer um erro ao chamar a função. Você sabe que se o erro ocorrer, o fluxo de código terminará no ponto A. Você sabe que o único outro lugar onde o fluxo de código terminará é o ponto B. Também há um contrato implícito de que se err for nulo, x é algum valor válido, zero ou não.

Compare com Java:

x = SomeFunc(100, 4)

Olhando para o código, você não tem ideia se um erro pode ocorrer quando a função é chamada. Se um erro ocorrer e for expresso como uma exceção, então um goto acontece e você pode acabar em algum lugar no final do código ao redor ... ou se a exceção não for detectada, você pode acabar em algum lugar no final de uma parte completamente diferente do seu código. Ou você pode acabar no código de outra pessoa. Na verdade, como o manipulador de exceções padrão pode ser substituído, você pode acabar literalmente em qualquer lugar, com base em algo feito pelo código de outra pessoa.

Além disso, não há contrato implícito de que x é válido - é comum que as funções retornem nulo para indicar erros ou valores ausentes.

Com o Java, esses problemas podem ocorrer com cada chamada - não apenas com código ruim, é algo com que você deve se preocupar com _todo_ o código Java. É por isso que os ambientes de desenvolvimento Java têm ajuda pop-up para mostrar se a função para a qual você está apontando pode causar uma exceção ou não e quais exceções ela pode causar. É por isso que o Java adicionou exceções verificadas, de modo que, para erros comuns, você tinha que ter pelo menos algum aviso de que a chamada de função poderia gerar uma exceção e desviar o fluxo do programa. Enquanto isso, os nulos retornados e a natureza não verificada de NullPointerException são um problema tão grande que eles adicionaram a classe Optional ao Java 8 para tentar melhorá-lo, mesmo que o custo seja explicitamente embrulhar o valor de retorno em cada função que retorna um objeto.

Minha experiência é que NullPointerException de um valor nulo inesperado que recebi é a forma mais comum pela qual meu código Java acaba travando, e geralmente acabo com um grande backtrace que é quase totalmente inútil e um mensagem de erro que não indica a causa porque foi gerada longe do código com falha. Em Go, honestamente não achei que o pânico de desreferência nula fosse um problema significativo, embora eu tenha muito menos experiência com Go. Isso, para mim, indica que Java deve aprender com Go, e não o contrário.

Não acho que a sintaxe tenha problemas.

Não acho que alguém esteja dizendo que a sintaxe é o problema com as exceções do estilo Java.

@lpar , Por que nenhum pânico de desreferência em Go é melhor que NullPointerException em Java? Qual é a diferença de "Panic" e "Throw"? Qual é a diferença em sua semântica?

Os pânicos são apenas recuperáveis ​​e os arremessos são capturáveis? Direito?

Acabei de me lembrar de uma diferença, com o pânico você pode entrar em pânico um objeto de erro ou um objeto de string ou pode ser qualquer outro tipo de objeto (corrija-me se eu estiver errado), mas com o lançamento você pode lançar um objeto do tipo Exception ou uma subclasse de exceção apenas.

Por que nenhum pânico de desreferência em Go é melhor do que NullPointerException em Java?

Porque as primeiras quase nunca acontecem na minha experiência, enquanto as últimas acontecem o tempo todo, pelos motivos que expliquei.

@lpar Bem, não tenho programado com Java recentemente e acho que isso é uma coisa nova (últimos 5 anos), mas C # tem um operador de navegação seguro para evitar referências nulas para criar exceções, mas o que Go tem? Não tenho certeza, mas acho que não tem nada para lidar com essas situações. Portanto, se você deseja evitar o pânico, ainda precisa adicionar aquelas instruções if-not-nil-else feias e aninhadas ao código.

Geralmente, você não precisa verificar os valores de retorno para ver se eles são nulos em Go, contanto que verifique o valor de retorno do erro. Portanto, não há instruções if aninhadas feias.

A anulação da referência nula foi um mau exemplo. Se você não detectá-lo, Go e Java funcionam exatamente da mesma forma - você obterá um travamento com rastreamento de pilha. Como o rastreamento de pilha pode ser inútil, não sei agora. Você sabe o lugar exato onde aconteceu. Tanto em C # quanto em Go, para mim, geralmente é trivial consertar esse tipo de travamento, porque a desreferenciação nula em minha experiência se deve a um simples erro do programador. Neste caso particular, não há nada a aprender com ninguém.

@lpar

Porque as primeiras quase nunca acontecem na minha experiência, enquanto as últimas acontecem o tempo todo, pelos motivos que expliquei.

Isso é acidental e não vi nenhuma razão em seu comentário de que o Java de alguma forma é pior em nil / nulo do que Go. Observei várias falhas de desreferência nula no código Go. Eles são exatamente iguais à desreferência nula em C # / Java. Você pode estar usando mais tipos de valor no Go, o que ajuda (C # também os tem), mas não muda nada.

Quanto às exceções, vejamos o Swift. Você tem uma palavra-chave throws para funções que podem gerar um erro. Função sem ele não pode jogar. Em termos de implementação, funciona como o retorno - provavelmente algum registro é reservado para retornar o erro e toda vez que você lançar a função retorna normalmente, mas carrega consigo um valor de erro. Portanto, o problema de erros inesperados foi resolvido. Você sabe exatamente qual função pode ser acionada, sabe o lugar exato onde isso pode acontecer. Os erros são valores e não exigem o desenrolamento da pilha. Eles apenas retornam até que você os pegue.

Ou algo semelhante a Rust, onde você tem um tipo de Resultado especial que carrega um resultado e um erro. Os erros podem ser propagados sem nenhuma declaração condicional explícita. Além de uma tonelada de bondade de combinação de padrões, mas isso provavelmente não é para Go.

Ambas as linguagens pegam as duas soluções (C e Java) e as combinam em algo melhor. Propagação de erro de exceções + valores de erro e fluxo de código óbvio de C + nenhum código clichê feio que não faz nada útil. Portanto, acho que é sábio olhar para essas implementações em particular e não rejeitá-las completamente apenas porque se assemelham a exceções de alguma forma. Há um motivo pelo qual as exceções são usadas em tantos idiomas porque elas têm um lado positivo. Caso contrário, as línguas os ignorariam. Especialmente depois de C ++.

Como o rastreamento de pilha pode ser inútil, não sei agora.

Eu disse "quase totalmente inútil". Tipo, eu só preciso de uma linha de informação, mas tem dezenas de linhas.

Isso é acidental e não vi nenhuma razão em seu comentário de que o Java de alguma forma é pior em nil / nulo do que Go.

Então você não está ouvindo. Volte e leia a parte sobre contratos implícitos.

Os erros podem ser propagados sem nenhuma declaração condicional explícita.

E esse é exatamente o problema - erros sendo propagados e o fluxo de controle mudando sem nada explícito para marcar que isso vai acontecer. Aparentemente, você não acha que isso seja um problema, mas os outros discordam.

Se as exceções implementadas por Rust ou Swift sofrem dos mesmos problemas que Java eu ​​não sei, vou deixar isso para alguém com experiência nas linguagens em questão.

@KamyarM Basicamente, você torna o nil supérfluo e obtém total segurança de tipo para isso:

https://fsharpforfunandprofit.com/posts/the-option-type/

E esse é exatamente o problema - erros sendo propagados e o fluxo de controle mudando sem nada explícito para marcar que isso vai acontecer.

Isso soa verdadeiro para mim. Se eu desenvolver algum pacote que consome outro pacote e esse pacote gerar uma exceção, agora _Eu_ também tenho que estar ciente disso, independentemente de eu querer usar esse recurso. Esta é uma faceta incomum entre os recursos de linguagem propostos; a maioria são coisas que um programador pode optar ou simplesmente não usar a seu critério. As exceções, por sua própria intenção, cruzam todos os tipos de limites, esperados ou não.

Eu disse "quase totalmente inútil". Tipo, eu só preciso de uma linha de informação, mas tem dezenas de linhas.

E enormes traços de Go com centenas de goroutines são, de alguma forma, mais úteis? Eu não entendo onde você quer chegar com isso. Java e Go são exatamente iguais aqui. E, ocasionalmente, você acha útil observar a pilha completa para entender como seu código acabou onde travou. Os rastreamentos de C # e Go me ajudaram várias vezes com isso.

Então você não está ouvindo. Volte e leia a parte sobre contratos implícitos.

Eu li, nada mudou. Na minha experiência, isso não é um problema. É para isso que serve a documentação em ambas as línguas ( net.ParseIP por exemplo). Se você esquecer de verificar se seu valor é nulo / nulo ou não, você terá exatamente o mesmo problema em ambos os idiomas. Na maioria dos casos, Go retornará um erro e C # lançará uma exceção, então você nem precisa se preocupar com nada. Uma boa API não retorna apenas null sem lançar uma exceção ou algo para dizer o que está errado. Em outros casos, você verifica explicitamente. Os tipos mais comuns de erros com nulo na minha experiência são quando você tem buffers de protocolo onde cada campo é um ponteiro / objeto ou você tem lógica interna onde os campos de classe / estrutura podem ser nulos dependendo do estado interno e você se esquece de verificar antes Acesso. Esse é o padrão mais comum para mim e nada no Go alivia significativamente esse problema. Posso citar duas coisas que ajudam um pouco - valores vazios úteis e tipos de valor. Mas é mais sobre facilidade de programação porque você não precisa construir todas as variáveis ​​antes de usar.

E esse é exatamente o problema - erros sendo propagados e o fluxo de controle mudando sem nada explícito para marcar que isso vai acontecer. Aparentemente, você não acha que isso seja um problema, mas os outros discordam.

Isso é um problema, eu nunca disse o contrário, mas as pessoas aqui estão tão fixadas nas exceções Java / C # / C ++ que ignoram qualquer coisa que se pareça um pouco com elas. Exatamente por que o Swift exige que você marque as funções com throws para que possa ver exatamente o que você deve esperar de uma função e onde o fluxo de controle pode ser interrompido e em Rust que você usa? para propagar explicitamente um erro com vários métodos auxiliares para dar a ele mais contexto. Ambos usam o mesmo conceito de erros como valores, mas envolvem-no em açúcar sintático para reduzir o clichê.

E enormes traços de Go com centenas de goroutines são, de alguma forma, mais úteis?

Com Go, você lida com erros registrando-os junto com o local no ponto em que são detectados. Não há retrocesso, a menos que você escolha adicionar um. Eu só precisei fazer isso uma vez.

Na minha experiência, isso não é um problema.

Bem, minha experiência é diferente, e acho que a experiência da maioria das pessoas é diferente, e como evidência disso, cito o fato de que o Java 8 adicionou tipos opcionais.

Este tópico discutiu muitos dos pontos fortes e fracos do Go e seu sistema de tratamento de erros, incluindo uma discussão sobre exceções ou não, eu recomendo a leitura:

https://elixirforum.com/t/discussing-go-split-thread/13006/2

Meus 2 centavos para o tratamento de erros (desculpe se essa ideia foi mencionada acima).

Queremos relançar os erros na maioria dos casos. Isso leva a esses trechos:

a, err := fn()
if err != nil {
    return err
}
use(a)
return nil

Vamos relançar o erro não nulo automaticamente se ele não foi atribuído a uma variável (sem nenhuma sintaxe extra). O código acima se tornará:

a := fn()
use(a)

// or just

use(fn())

O compilador salvará err na variável implícita (invisível), verifique se há nil e prossiga (se errar == nulo) ou retornará (se errar! = Nulo) e retornará nulo no final de função se nenhum erro ocorreu durante a execução da função normalmente, mas automaticamente e implicitamente.

Se err deve ser manipulado, ele deve ser atribuído a uma variável explícita e usado:

a, err := fn()
if err != nil {
    doSomething(err)
} else {
    use(a)
}
return nil

O erro pode ser suprimido da seguinte maneira:

a, _ := fn()
use(a)

Em casos raros (fantásticos) com mais de um erro retornado, o tratamento explícito de erros será obrigatório (como agora):

err1, err2 := fn2()
if err1 != nil || err2 != nil {
    return err1, err2
}
return nil, nil

Esse é o meu argumento também - queremos relançar os erros na maioria dos casos, geralmente é o caso padrão. E talvez dê algum contexto. Com exceções, o contexto é adicionado automaticamente por rastreamentos de pilha. Com erros como no Go, fazemos isso manualmente, adicionando uma mensagem de erro. Por que não torná-lo mais simples. E isso é exatamente o que outras linguagens estão tentando fazer enquanto equilibram isso com a questão da clareza.

Portanto, concordo com "Vamos relançar o erro não nulo automaticamente se ele não tiver sido atribuído a uma variável (sem nenhuma sintaxe extra)", mas a última parte me incomoda. É aí que está a raiz do problema com exceções e por que, eu acho, as pessoas são tão contra falar sobre qualquer coisa ligeiramente relacionada a elas. Eles mudam o fluxo de controle sem nenhuma sintaxe extra. Isso é uma coisa ruim.

Se você olhar para o Swift, por exemplo, este código não irá compilar

func a() throws {}
func b() throws {
  a()
}

a pode gerar um erro, então você deve escrever try a() para propagar um erro. Se você remover throws de b então ele não irá compilar mesmo com try a() . Você tem que lidar com o erro dentro de b . Essa é uma maneira muito melhor de lidar com erros que resolve tanto o problema de fluxo de controle pouco claro de exceções quanto a verbosidade de erros Objective-C. O último sendo quase exatamente como os erros em Go e o que o Swift pretende substituir. O que eu não gosto é de try, catch coisas que Swift também usa. Eu preferiria deixar os erros como parte de um valor de retorno.

Então, o que eu proporia é realmente ter a sintaxe extra. Para que o site de chamada diga por si mesmo que é um lugar potencial onde o fluxo de controle pode mudar. O que eu também proporia é que não escrever essa sintaxe extra produziria um erro de compilação. Isso, ao contrário de como o Go funciona agora, forçaria você a lidar com o erro. Você poderia adicionar a capacidade de apenas silenciar o erro com algo como _ porque, em alguns casos, seria muito frustrante lidar com cada pequeno erro. Por exemplo, printf . Eu não me importo se ele falhar em registrar algo. Go já tem essas importações irritantes. Mas isso foi resolvido com ferramentas, pelo menos.

Existem duas alternativas para o erro de tempo de compilação, nas quais posso pensar agora. Como Vá agora, deixe o erro ser ignorado silenciosamente. Eu não gosto disso e esse sempre foi o meu problema com o tratamento de erros Go. Não força nada, o comportamento padrão é ignorar silenciosamente o erro. Isso é ruim, não é assim que você escreve programas robustos e fáceis de depurar. Tive muitos casos em Objective-C em que estava preguiçoso ou sem tempo e ignorei o erro apenas para ser atingido por um bug no mesmo código, mas sem nenhuma informação de diagnóstico de por que isso aconteceu. Ao menos registrá-lo me permitiria resolver o problema ali mesmo em muitos casos.

A desvantagem é que as pessoas podem começar a ignorar os erros, coloque try, catch(...) todos os lugares, por assim dizer. Essa é uma possibilidade, mas, ao mesmo tempo, com erros ignorados por padrão, é ainda mais fácil de fazer. Acho que o argumento sobre exceções não se aplica aqui. Com exceções, o que algumas pessoas estão tentando alcançar é a ilusão de que seu programa é mais estável. O problema aqui é o fato de uma exceção não tratada travar o programa.

Outra alternativa seria entrar em pânico. Mas isso é frustrante e traz lembranças de exceções. Isso definitivamente levaria as pessoas a fazer uma codificação "defensiva" para que seu programa não travasse. Para mim, a linguagem moderna deve fazer o máximo possível em tempo de compilação e deixar o mínimo de decisões para o tempo de execução possível. Onde o pânico pode ser apropriado está no topo da pilha de chamadas. Por exemplo, não tratar um erro na função principal produziria automaticamente um pânico. Isso também se aplica a goroutines? Provavelmente não deveria.

Por que considerar compromissos?

@ nick-korsakov a proposta original (este problema) deseja adicionar mais contexto aos erros:

Já é fácil (talvez fácil demais) ignorar o erro (consulte # 20803). Muitas propostas existentes para tratamento de erros tornam mais fácil retornar o erro não modificado (por exemplo, # 16225, # 18721, # 21146, # 21155). Poucos facilitam o retorno do erro com informações adicionais.

Veja também este comentário .

Neste comentário , sugiro que, para progredir nesta discussão (em vez de executar em loops), devemos definir melhor os objetivos, por exemplo, o que é uma mensagem de erro cuidadosamente tratada. A coisa toda é muito interessante de ler, mas parece afetada por um problema de memória de peixinho dourado de três segundos (não muito focado / avançando, repetindo boas mudanças criativas de sintaxe e argumentos sobre exceções / pânicos etc).

Outra bicicleta:

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    try err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    try errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    try errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

try significa "retorno se não o valor vazio". Neste, estou assumindo que errors.Errorf retornará nil quando err for nulo. Acho que isso representa a maior economia que podemos esperar, mantendo o objetivo de embrulhar facilmente.

Os tipos de scanner na biblioteca padrão armazenam o estado de erro dentro de uma estrutura cujos métodos podem verificar com responsabilidade a existência de um erro antes de prosseguir.

type Scanner struct{
    err error
}
func (s *Scanner) Scan() bool{
   if s.err != nil{
       return false
   }
   // scanning logic
}
func (s *Scanner) Err() error{ return s.err }

Ao usar tipos para armazenar o estado de erro, é possível manter o código que usa esse tipo livre de verificações de erro redundantes.

Também não requer alterações de sintaxe criativas e bizarras ou transferências de controle inesperadas no idioma.

Também tenho que sugerir algo como try / catch, onde err é definido dentro de try {}, e se err é definido como valor não nulo - quebra de fluxo de try {} para blocos de manipulador de erro (se houver).

Internamente não há exceções, mas tudo deve ser mais próximo
para a sintaxe que faz if err != nil break verificações após cada linha onde err pode ser atribuído.
Por exemplo:

...
try(err) {
   err = doSomethig()
   err, value := doSomethingElse()
   doSomethingObliviousToErr()
   err = thirdErrorProneThing()
} 
catch(err SomeErrorType) {
   handleSomeSpecificErr(err)
}
catch(err Error) {
  panic(err)
}

Eu sei que se parece com C ++, mas também é bem conhecido e mais limpo do que if err != nil {...} manual após cada linha.

@Como

O tipo de scanner funciona porque está fazendo todo o trabalho, portanto, pode manter o controle de seu próprio erro ao longo do caminho. Não vamos nos enganar pensando que esta é uma solução universal, por favor.

@carlmjohnson

Se quisermos um tratamento de linha para erros simples, poderíamos mudar a sintaxe para permitir que a instrução de retorno seja o início de um bloco de uma linha.
Isso permitiria que as pessoas escrevessem:

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    if err != nil return err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    if err != nil return errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    if err != nil return errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

Acho que a especificação deve ser alterada para algo como (isso pode ser bastante ingênuo :))

Block = "{" StatementList "}" | "return" Expression .

Não acho que o retorno de caixa especial seja realmente melhor do que apenas alterar gofmt para simplificar se err verificar uma linha em vez de três.

@urandom

O erro se aglutina além de um tipo em caixa e suas ações não devem ser encorajadas. Para mim, isso indica uma falta de esforço para agrupar ou adicionar contexto de erro entre erros originados de diferentes ações não relacionadas.

A abordagem do scanner é uma das piores coisas que li no contexto de todo esse mantra "erros são valores":

  1. É inútil em praticamente todos os casos de uso que exigem muitos erros de tratamento de erros. As funções que chamam vários pacotes externos não se beneficiarão disso.
  2. É um conceito complicado e desconhecido. Apresentá-lo apenas confundirá os leitores futuros e tornará seu código mais complicado do que o necessário, apenas para que você possa contornar a deficiência do design da linguagem.
  3. Ele oculta a lógica e tenta ser semelhante às exceções tirando o pior dela (fluxo de controle complexo) sem tirar nenhum benefício.
  4. Em alguns casos, isso desperdiçará recursos de computação. Cada chamada terá que perder tempo com uma verificação de erro inútil que aconteceu há muito tempo.
  5. Ele esconde o local exato onde o erro aconteceu. Imagine um caso em que você analisa ou serializa algum formato de arquivo. Você teria uma cadeia de chamadas de leitura / gravação. Imagine que o primeiro falhe. Como você diria onde exatamente o erro aconteceu? Qual campo ele estava analisando ou serializando? "Erro IO", "tempo limite" - esses erros seriam inúteis neste caso. Você pode fornecer contexto para cada leitura / gravação (nome do campo, por exemplo). Mas, neste ponto, é melhor simplesmente desistir de toda a abordagem, pois ela está trabalhando contra você.

Em alguns casos, isso desperdiçará recursos de computação.

Benchmarks? O que é exatamente um "recurso de computação"?

Ele esconde o local exato onde o erro aconteceu.

Não, não importa, porque erros não nulos não são substituídos

As funções que chamam vários pacotes externos não se beneficiarão disso.
É um conceito complicado e desconhecido
A abordagem do scanner é uma das piores coisas que li no contexto de todo esse "erros são valores"

Minha impressão é que você não entende a abordagem. É logicamente equivalente à verificação de erro regular em um tipo independente, sugiro que você estude o exemplo de perto, então talvez possa ser a pior coisa que você entende, em vez de apenas a pior coisa que você _ler_.

Desculpe, vou adicionar minha própria proposta à pilha. Li a maior parte do que está aqui e, embora goste de algumas das propostas, sinto que estão tentando fazer muito. O problema é o clichê de erro. Minha proposta é simplesmente eliminar esse clichê no nível da sintaxe e deixar as formas como os erros são transmitidos em paz.

Proposta

Reduza o padrão de erro habilitando o uso do token _! como açúcar sintático para causar pânico quando atribuído a um error não nulo

val, err := something.MayError()
if err != nil {
    panic(err)
}

poderia se tornar

val, _! := something.MayError()

e

if err := something.MayError(); err != nil {
    panic(err)
}

poderia se tornar

_! = something.MayError()

Claro, o símbolo específico está em debate. Também considerei _^ , _* , @ e outros. Escolhi _! como a sugestão de fato porque achei que seria a mais familiar à primeira vista.

Sintaticamente, _! (ou o token escolhido) seria um símbolo do tipo error disponível no escopo em que é usado. Ele começa como nil e, sempre que é atribuído, uma verificação nil é realizada. Se for definido com um valor não nulo error , um pânico é iniciado. Como _! (ou, novamente, o token escolhido) não seria um identificador sintaticamente válido em go, a colisão de nomes não seria uma preocupação. Essa variável etérea seria introduzida apenas nos escopos onde é usada, semelhante aos valores de retorno nomeados. Se um identificador sintaticamente válido for necessário, talvez um espaço reservado pudesse ser usado, que seria reescrito para um nome exclusivo em tempo de compilação.

Justificação

Uma das críticas mais comuns que vejo levantadas no início é a verbosidade do tratamento de erros. Erros nos limites da API não são uma coisa ruim. Ter que levar os erros aos limites da API pode ser uma dor, especialmente para algoritmos profundamente recursivos. Para contornar o detalhamento adicionado que a propagação do erro introduz no código recursivo, pânicos podem ser usados. Acho que essa é uma técnica muito usada. Eu o usei em meu próprio código e já o vi ser usado livremente, incluindo no analisador de go. Às vezes, você fez a validação em outro lugar em seu programa e espera que o erro seja nulo. Se um erro não nulo fosse recebido, isso violaria seu invariante. Quando um invariante é violado, é aceitável entrar em pânico. No código de inicialização complexo, às vezes faz sentido transformar erros em pânicos e recuperá-los para serem retornados em algum lugar com mais conhecimento do contexto. Em todos esses cenários, há uma oportunidade de reduzir o clichê de erro.

Sei que é filosofia da Go evitar o pânico tanto quanto possível. Eles não são uma ferramenta para propagação de erros através dos limites da API. No entanto, eles são um recurso da linguagem e têm casos de uso legítimos, como os descritos acima. Os pânicos são uma maneira fantástica de simplificar a propagação de erros em código privado, e uma simplificação da sintaxe ajudaria muito para tornar o código mais limpo e, sem dúvida, mais claro. Acho que é mais fácil reconhecer _! (ou @ , ou `_ ^, etc ...) de relance do que o formulário" if-error-panic ". Um token pode diminuir drasticamente a quantidade de código que deve ser escrito / lido para transmitir / compreender:

  1. pode haver um erro
  2. se houver um erro, não o esperamos
  3. se houver um erro, provavelmente ele está sendo tratado na cadeia

Como acontece com qualquer recurso de sintaxe, existe o potencial para abuso. Nesse caso, a comunidade go já tem um conjunto de práticas recomendadas para lidar com o pânico. Como essa adição de sintaxe é um açúcar sintático para o pânico, esse conjunto de melhores práticas pode ser aplicado para seu uso.

Além da simplificação dos casos de uso aceitáveis ​​para o pânico, isso também torna a prototipagem rápida mais fácil. Se eu tenho uma ideia que quero anotar no código, e só quero que os erros travem o programa enquanto eu brinco, eu poderia usar essa adição de sintaxe em vez da forma "if-error-panic". Se eu puder me expressar em menos linhas nos estágios iniciais de desenvolvimento, posso colocar minhas ideias no código mais rapidamente. Depois de ter uma ideia completa do código, volto e refatoro meu código para retornar erros nos limites apropriados. Eu não deixaria pânico livre no código de produção, mas eles podem ser uma ferramenta de desenvolvimento poderosa.

Os pânicos são apenas exceções com outro nome, e uma coisa que adoro em Go é que as exceções são excepcionais. Não quero encorajar mais exceções, dando-lhes açúcar sintático.

@carlmjohnson Uma de duas coisas tem que ser verdade:

  1. Os pânicos são uma parte da linguagem com casos de uso legítimos, ou
  2. Panics não têm casos de uso legítimos e, portanto, devem ser removidos da linguagem

Suspeito que a resposta seja 1.
Também discordo que "os pânicos são apenas exceções com outro nome". Acho que esse tipo de aceno evita uma discussão real. Existem diferenças importantes entre pânicos e exceções, como visto na maioria das outras línguas.

Eu entendo a reação automática de "pânico é ruim", mas os sentimentos pessoais sobre o uso do pânico não mudam o fato de que o pânico é usado e, de fato, são úteis. O compilador go usa o pânico para escapar de processos profundamente recursivos no analisador e na fase de verificação de tipo (última consulta).
Usá-los para propagar erros por meio de código profundamente recursivo parece não apenas ser um uso aceitável, mas também endossado pelos desenvolvedores de go.

O pânico comunica algo específico:

algo deu errado aqui que aqui não estava preparado para lidar com

Sempre haverá lugares no código em que isso é verdade. Especialmente no início do desenvolvimento. Go foi modificado para melhorar a experiência de refatoração antes: a adição de apelidos de tipo. Ser capaz de propagar erros indesejados com pânico até que você possa descobrir se e como lidar com eles em um nível mais próximo da fonte pode tornar a escrita e a refatoração progressiva do código muito menos prolixa.

Eu sinto que a maioria das propostas aqui estão propondo grandes mudanças no idioma. Esta é a abordagem mais transparente que eu poderia sugerir. Ele permite que todo o modelo cognitivo atual de tratamento de erros em go permaneça intacto, enquanto permite a redução de sintaxe para um caso específico, mas comum. As melhores práticas atualmente ditam que "o código go não deve entrar em pânico além dos limites da API". Se eu tiver métodos públicos em um pacote, eles devem retornar erros se algo der errado, exceto em raras ocasiões em que o erro é irrecuperável (violações invariáveis, por exemplo). Esse acréscimo ao idioma não substitui essa prática recomendada. Esta é simplesmente uma maneira de reduzir o clichê no código interno e tornar mais claras as ideias de esboço. Certamente torna o código mais fácil de ler linearmente.

var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

é muito mais legível do que

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

Realmente não há diferença fundamental entre um pânico em Go e uma exceção em Java ou Python etc. além da sintaxe e falta de uma hierarquia de objetos (o que faz sentido porque Go não tem herança). O modo como funcionam e são usados ​​é o mesmo.

É claro que o pânico tem um lugar legítimo na linguagem. Os pânicos são para lidar com erros que só devem ocorrer devido a erros do programador que, de outra forma, são irrecuperáveis. Por exemplo, se você dividir por zero em um contexto de inteiro, não há valor de retorno possível e a culpa é sua por não verificar o zero primeiro, então entra em pânico. Da mesma forma, se você ler uma fatia fora dos limites, tente usar nil como um valor, etc. Essas coisas são causadas por erro do programador - não por uma condição antecipada, como a rede sendo desligada ou um arquivo com permissões ruins - então eles entram em pânico e explodir a pilha. Go fornece algumas funções auxiliares que causam pânico, como template. Deve-se porque é antecipado que elas serão usadas com strings codificadas onde qualquer erro teria que ser causado por erro do programador. Falta de memória não é uma falha do programador em si, mas também é irrecuperável e pode acontecer em qualquer lugar, portanto, não é um erro, mas sim um pânico.

As pessoas às vezes também usam o pânico como uma forma de contornar a pilha, mas isso geralmente é desaprovado por motivos de legibilidade e desempenho, e não vejo nenhuma chance de Go mudar para encorajar seu uso.

Go panics e as exceções não verificadas do Java são praticamente idênticas e existem pelos mesmos motivos e para lidar com os mesmos casos de uso. Não incentive as pessoas a usar o pânico em outros casos, porque esses casos têm os mesmos problemas que as exceções em outros idiomas.

As pessoas às vezes também usam o pânico como forma de atuar na pilha, mas isso geralmente é desaprovado por motivos de legibilidade e desempenho

Em primeiro lugar, o problema de legibilidade é algo que essa mudança de sintaxe aborda diretamente:

// clearly, linearly shows that these steps must occur in order,
// and any errors returned cause a panic, because this piece of
// code isn't responsible for reporting or handling possible failures:
// - IO Error: either network or disk read/write failed
// - External service error: some unexpected response from the external service
// - etc...
// It's not this code's responsibility to be aware of or handle those scenarios.
// That's perhaps the parent process's job.
var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

vs

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

Deixando a legibilidade de lado por enquanto, a outra razão dada é o desempenho.
Sim, é verdade que usar pânico e declarações defer incorre em uma penalidade de desempenho, mas em muitos casos essa diferença é insignificante para a operação que está sendo executada. A E / S de disco e rede vai, em média, demorar muito mais do que qualquer mágica de pilha potencial para gerenciar defers / panics.

Eu ouço esse ponto papagaio muito quando se discute pânico, e acho que é falso dizer que pânico é uma degradação do desempenho. Certamente PODEM ser, mas não precisam ser. Assim como muitas outras coisas em um idioma. Se você está entrando em pânico dentro de um loop apertado onde o impacto no desempenho realmente importa, você também não deve adiar dentro desse loop. Na verdade, qualquer função que escolha por si mesma para causar pânico geralmente não deve capturar seu próprio pânico. Da mesma forma, uma função go escrita hoje não retornaria um erro e entraria em pânico. Isso não é claro, é bobo e não é a melhor prática. Talvez seja assim que estejamos acostumados a ver exceções usadas em Java, Python, Javascript, etc, mas não é assim que pânicos são geralmente usados ​​em código go, e não acredito que adicionar um operador especificamente para o caso de propagação de um erro Aumentar a pilha de chamadas por meio do pânico mudará a maneira como as pessoas usam o pânico. Eles estão usando o pânico de qualquer maneira. O objetivo dessa extensão de sintaxe é reconhecer o fato de que os desenvolvedores usam o pânico, e ele tem usos perfeitamente legítimos, e reduzir o clichê em torno dele.

Você pode me dar alguns exemplos de código problemático que você acha que esse recurso de sintaxe permitiria, que atualmente não são possíveis / contra as práticas recomendadas? Se alguém está comunicando erros aos usuários de seu código por meio de pânico / recuperação, isso é atualmente desaprovado e, obviamente, continuaria a ser, mesmo se uma sintaxe como essa fosse adicionada. Se você pudesse, responda o seguinte:

  1. Que abusos você imagina que surgiriam de uma extensão de sintaxe como esta?
  2. O que var1, err := trySomeTask1(); if err != nil { panic(err) } transmite que var1, _! := trySomeTask1() não transmite? Por quê?

Parece-me que o ponto crucial do seu argumento é que "os pânicos são maus e não devemos usá-los".
Não posso desempacotar e discutir os motivos por trás disso se eles não forem compartilhados.

Essas coisas são causadas por erro do programador - não por uma condição antecipada, como a rede estar fora do ar ou um arquivo com permissões incorretas - então eles entram em pânico e explodem a pilha.

Eu, como a maioria dos esquilos, gosto da ideia de erros como valores. Acredito que ajuda a comunicar claramente quais partes de uma API garantem um resultado e quais podem falhar, sem ter que olhar a documentação.

Ele permite coisas como coletar erros e aumentar os erros com mais informações. Isso é muito importante nos limites da API, onde seu código se cruza com o código do usuário. No entanto, dentro desses limites da API, muitas vezes não é necessário fazer tudo isso com seus erros. Especialmente se você está esperando o caminho feliz e tem outro código responsável por lidar com o erro se esse caminho falhar.

Às vezes, não é função do seu código lidar com um erro.
Se estou escrevendo uma biblioteca, não me importo se a pilha da rede estiver inativa - isso está fora do meu controle como desenvolvedor de biblioteca. Retornarei esses erros ao código do usuário.

Mesmo em meu próprio código, há momentos em que escrevo um trecho de código cujo único trabalho é devolver os erros a uma função pai.

Por exemplo, digamos que você tenha um http.HandlerFunc lendo um arquivo do disco como a resposta - isso quase sempre funcionará e, se falhar, é provável que o programa não esteja escrito corretamente (erro do programador) ou haja um problema com o sistema de arquivos fora do escopo de responsabilidade do programa. Assim que o http.HandlerFunc entrar em pânico, ele será encerrado e algum gerenciador de base pegará esse pânico e escreverá 500 para o cliente. Se em algum momento no futuro eu quiser tratar esse erro de forma diferente, posso substituir _! por err e fazer o que quiser com o valor do erro. O fato é que, durante a vida do programa, provavelmente não precisarei fazer isso. Se estou tendo problemas como esse, o manipulador não é a parte do código responsável por lidar com esse erro.

Posso, e normalmente faço, escrever if err != nil { panic(err) } ou if err != nil { return ..., err } em meus manipuladores para coisas como falhas de IO, falhas de rede, etc. Quando preciso verificar um erro, ainda posso fazer isso. Na maioria das vezes, porém, estou apenas escrevendo if err != nil { panic(err) } .

Ou, outro exemplo, se estou pesquisando recursivamente em um trie (digamos, em uma implementação de roteador http), declararei uma função func (root *Node) Find(path string) (found Value, err error) . Essa função adiará uma função para recuperar qualquer pânico gerado na descida da árvore. E se o programa estiver criando tentativas malformadas? E se algum IO falhar porque o programa não está sendo executado como um usuário com as permissões corretas? Esses problemas não são o problema do meu algoritmo de busca trie - a menos que eu explicitamente faça isso mais tarde - mas são possíveis erros que posso encontrar. Retorná-los totalmente para cima na pilha leva a muito detalhamento extra, incluindo manter o que seria idealmente vários valores de erro nulos na pilha. Em vez disso, posso optar por colocar um erro em pânico até essa função de API pública e devolvê-lo ao usuário. No momento, isso ainda incorre em uma verbosidade extra, mas não é necessário.

Outras propostas estão discutindo como tratar um valor de retorno como especial. É essencialmente o mesmo pensamento, mas em vez de usar recursos já integrados à linguagem, eles procuram modificar o comportamento da linguagem para certos casos. Em termos de facilidade de implementação, esse tipo de proposta (açúcar sintático para algo já suportado) vai ser a mais fácil.

Edite para adicionar:
Não sou casado com a proposta que fiz conforme está escrita, mas acho que olhar para o problema de tratamento de erros de um novo ângulo é importante. Ninguém está sugerindo nada assim, e quero ver se podemos reformular nossa compreensão do assunto. O problema é que há muitos lugares onde os erros estão sendo explicitamente manipulados quando não precisam ser, e os desenvolvedores gostariam de uma maneira de propagá-los pela pilha sem código clichê extra. Acontece que Go já tem esse recurso, mas não há uma sintaxe legal para ele. Esta é uma discussão sobre como envolver a funcionalidade existente em uma sintaxe menos detalhada para tornar o idioma mais ergonômico sem alterar o comportamento. Não é uma vitória, se conseguirmos?

@mccolljr Obrigado, mas um dos objetivos desta proposta é encorajar as pessoas a desenvolverem novas maneiras de lidar com todos os três casos de tratamento de erros: ignorar o erro, retornar o erro sem modificações, retornar o erro com informações contextuais adicionais. Sua proposta de pânico não aborda o terceiro caso. É importante.

@mccolljr Acho que os limites da API são muito mais comuns do que você parece supor. Não vejo chamadas dentro da API como o caso comum. No mínimo, pode ser o contrário (alguns dados seriam interessantes aqui). Portanto, não tenho certeza se o desenvolvimento de sintaxe especial para chamadas dentro da API é a direção certa. Além disso, usar erros return ed, em vez de erros panic ed, dentro de uma API é normalmente um bom caminho a percorrer (especialmente se chegarmos a um plano para esse problema). panic erros

Não acredito que adicionar um operador especificamente para o caso de propagação de um erro pela pilha de chamadas via pânico vá mudar a maneira como as pessoas usam o pânico.

Acho que você está enganado. As pessoas procurarão seu operador taquigráfico porque é muito conveniente e, então, acabarão usando o pânico muito mais do que antes.

Se os pânicos são úteis às vezes ou raramente, e se eles são úteis dentro ou fora dos limites da API, são uma pista falsa. Existem várias ações que podem ser realizadas em caso de erro. Estamos procurando uma maneira de reduzir o código de tratamento de erros sem privilegiar uma ação sobre as outras.

mas em muitos casos essa diferença é insignificante para a operação que está sendo realizada

Embora seja verdade, acho que é um caminho perigoso a seguir. Parecendo insignificante no início, ele se acumularia e, eventualmente, causaria gargalos no futuro, quando já fosse tarde. Acho que devemos ter o desempenho em mente desde o início e tentar encontrar soluções melhores. O Swift e o Rust já mencionados têm propagação de erro, mas a implementam como, basicamente, retornos simples embrulhados em açúcar sintático. Sim, é fácil reutilizar a solução existente, mas eu preferiria deixar tudo como está do que simplificar e encorajar as pessoas a usar pânico escondido atrás de açúcar sintático desconhecido que tenta esconder o fato de que é, basicamente, exceções.

Parecendo insignificante no início, ele se acumularia e, eventualmente, causaria gargalos no futuro, quando já fosse tarde.

Não, obrigado. Gargalos de desempenho imaginários são gargalos de desempenho geometricamente insignificantes.

Não, obrigado. Gargalos de desempenho imaginários são gargalos de desempenho geometricamente insignificantes.

Por favor, deixe seus sentimentos pessoais fora deste tópico. Obviamente, você tem algum problema comigo e não quer trazer nada de útil, então ignore meus comentários e deixe um voto negativo, como fez com praticamente todos os comentários anteriores. Não há necessidade de continuar postando essas respostas sem sentido.

Não tenho nenhum problema com você, você está apenas fazendo afirmações sobre gargalos de desempenho, sem quaisquer dados para comprovar isso e estou apontando isso com palavras e polegares.

Pessoal, por favor, mantenham a conversa respeitosa e direta. Este problema é sobre como lidar com erros Go.

https://golang.org/conduct

Eu gostaria de revisitar a parte "retornar o erro / com contexto adicional" novamente, uma vez que suponho que ignorar o erro já está coberto pelo _ já existente.

Estou propondo uma palavra-chave de duas palavras que pode ser seguida por uma string (opcionalmente). A razão de ser uma palavra-chave de duas palavras é dupla. Primeiro, ao contrário de um operador que é inerentemente enigmático, é mais fácil entender o que ele faz sem muito conhecimento prévio. Escolhi "ou bolha", porque espero que a palavra or com a ausência de um erro atribuído signifique para o usuário que o erro está sendo tratado aqui, se não for nulo. Alguns usuários já irão associar or com o manuseio de um valor falso de outras linguagens (perl, python), e lendo data := Foo() or ... pode inconscientemente dizer a eles que data é inutilizável se o or parte da declaração foi alcançada. Em segundo lugar, a palavra-chave bubble embora seja relativamente curta, pode significar para o usuário que algo está subindo (a pilha). A palavra up também pode ser adequada, embora eu não tenha certeza se a palavra or up inteira é compreensível o suficiente. Finalmente, a coisa toda é uma palavra-chave, primeiro e principalmente porque é mais legível, e segundo porque esse comportamento não pode ser escrito por uma função em si (você pode ser capaz de chamar o pânico para escapar da função em que está, mas então você pode ' (t pare, outra pessoa terá que se recuperar).

O seguinte é apenas para propagação de erro, portanto, só pode ser usado em funções que retornam um erro e os valores zero de quaisquer outros argumentos de retorno:

Para retornar um erro sem modificá-lo de nenhuma forma:

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble

    return data;
}

Para retornar um erro com uma mensagem adicional:

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble fmt.Sprintf("reading file %s", path)

    modified := modifyData(data) or bubble "modifying the data"

    return data;
}

E, finalmente, apresente um mecanismo de adaptador global para modificação de erro personalizada:

// Default Bubble Processor
errors.BubbleProcessor(func(msg string, err error) error {
    return fmt.Errorf("%s: %v", msg, err)
})

// Some program might register the following:
errors.BubbleProcessor(func(msg string, err error) error {
    return errors.WithMessage(err, msg)
})

Finalmente, para os poucos lugares onde o manuseio realmente complexo é necessário, a forma verbosa já existente é a melhor.

Interessante. Ter um gerenciador de bolhas global dá às pessoas que desejam rastreamentos de pilha um lugar para colocar a chamada para um rastreamento, o que é um grande benefício desse método. OTOH, se tiver a assinatura func(string, error) error , isso significa que o borbulhamento deve ser feito com o tipo de erro embutido e não com qualquer outro tipo, como um tipo concreto implementando error .

Além disso, a existência de or bubble sugere a possibilidade de or die ou or panic . Não tenho certeza se isso é um recurso ou um bug.

ao contrário de um operador que é inerentemente enigmático, é mais fácil entender o que ele faz sem muito conhecimento prévio

Isso pode ser bom quando você o encontra pela primeira vez. Mas ler e escrever repetidas vezes - parece muito prolixo e ocupa muito espaço para transmitir uma coisa muito simples - erro não tratado com bolhas na pilha. Os operadores são enigmáticos no início, mas são concisos e têm um bom contraste com todos os outros códigos. Eles separam claramente a lógica principal do tratamento de erros porque, na verdade, é um separador. Ter tantas palavras em uma linha prejudicará a legibilidade, na minha opinião. Pelo menos mescle-os em orbubble ou elimine um deles. Não vejo sentido em ter duas palavras-chave aí. Transforma o Go em uma língua falada e nós sabemos como é (VB, por exemplo)

Não sou muito fã de um adaptador global. Se meu pacote define um processador personalizado e o seu também define um processador personalizado, quem ganha?

@ object88
Estou pensando que é semelhante ao logger padrão. Você define a saída apenas uma vez (em seu programa), e isso afeta todos os pacotes que você usa.

O tratamento de erros é muito diferente do registro; um descreve a saída informativa do programa, o outro gerencia o fluxo do programa. Se eu configurar o adaptador para fazer algo em meu pacote, que preciso gerenciar adequadamente o fluxo lógico, e outro pacote ou programa alterar isso, você está em um lugar ruim.

Por favor, traga de volta o Try Catch Finalmente e não precisamos mais lutas. Isso deixa todo mundo feliz. Não há nada de errado em emprestar recursos e sintaxes de outras linguagens de programação. Java fez isso e C # fez isso também, e ambos são linguagens de programação realmente bem-sucedidas. Comunidade GO (ou autores), esteja aberto a mudanças quando necessário.

@KamyarM , discordo respeitosamente; try / catch _não_ faz todo mundo feliz. Mesmo se você quiser implementar isso em seu código, uma exceção lançada significa que todos que usam seu código precisam lidar com exceções. Essa não é uma mudança de idioma que pode ser localizada em seu código.

@ object88
Na verdade, parece-me que um processador de bolhas descreve a saída de erro informativa do programa, o que não é muito diferente de um registrador. E imagino que você queira uma única representação de erro em todo o seu aplicativo e que não varie de pacote para pacote.

Embora você possa fornecer um pequeno exemplo, talvez haja algo que esteja faltando.

Muito obrigado por seus polegares para baixo. Esse é exatamente o problema de que estou falando. A comunidade GO não está aberta a mudanças e eu sinto isso e realmente não gosto disso.

Isso provavelmente não está relacionado a este caso, mas eu estava procurando outro dia por Go equivalente ao operador ternário de C ++ e me deparei com esta abordagem alternativa:

v: = map [bool] int {true: first_expression, false: second_expression} [condição]
em vez de simplesmente
v = condição? primeira_expressão: segunda_expressão;

Qual dos 2 formulários vocês preferem? Um código ilegível acima (Go My Way) com provavelmente muitos problemas de desempenho ou a segunda sintaxe simples em C ++ (Highway)? Eu prefiro os caras da estrada. Eu não sei sobre você.

Então, para resumir, traga novas sintaxes, pegue-as emprestadas de outras linguagens de programação. Não há nada de errado com isso.

Cumprimentos,

A comunidade GO não está aberta a mudanças e eu sinto isso e realmente não gosto disso.

Acho que isso descaracteriza a atitude subjacente ao que você está experimentando. Sim, a comunidade rebate muito quando alguém propõe try / catch ou?:. Mas a razão não é que somos resistentes a novas ideias. Quase todos nós temos experiência no uso de idiomas com esses recursos. Estamos bastante familiarizados com eles e alguém de nós os usa diariamente há anos. Nossa resistência se baseia no fato de que essas são _idéias antigas_, não novas. Já adotamos uma mudança: deixar de usar try / catch e deixar de usar?:. O que somos resistentes é mudar _de volta_ para usar essas coisas que já usamos e não gostamos.

Na verdade, parece-me que um processador de bolhas descreve a saída de erro informativa do programa, o que não é muito diferente de um registrador. E imagino que você queira uma única representação de erro em todo o seu aplicativo e que não varie de pacote para pacote.

E se alguém quisesse usar o bubbling para passar rastreamentos de pilha e depois usar isso para tomar uma decisão. Por exemplo, se o erro se originar de uma operação de arquivo, então falhe, mas se for originado da rede, espere e tente novamente. Pude ver a construção de alguma lógica para isso em um manipulador de erros, mas se houver apenas um manipulador de erros por tempo de execução, isso seria uma receita para conflito.

@urandom , talvez este seja um exemplo trivial, mas digamos que meu adaptador retorne outra estrutura que implementa error , que espero consumir em outro lugar em meu código. Se outro adaptador vier e substituir meu adaptador, meu código deixará de funcionar corretamente.

@KamyarM A linguagem e seus idiomas andam juntos. Quando consideramos as mudanças no tratamento de erros, não estamos falando apenas sobre a mudança da sintaxe, mas (potencialmente) também da própria estrutura do código.

Try-catch-finally seria uma mudança muito invasiva: mudaria fundamentalmente a maneira como os programas Go são estruturados. Em contraste, a maioria das outras propostas que você vê aqui são locais para cada função: erros ainda são valores retornados explicitamente, o fluxo de controle evita saltos não locais etc.

Para usar o seu exemplo de operador ternário: sim, você pode falsificar um hoje usando um mapa, mas espero que você não encontre isso no código de produção. Não segue os idiomas. Em vez disso, você geralmente verá algo mais como:

    var v int
    if condition {
        v = first_expression
    } else {
        v = second_expression
    }

Não é que não queiramos emprestar sintaxe, é que temos que considerar como ela se encaixaria no resto da linguagem e no resto do código que já existe hoje.

@KamyarM Eu uso Go e Java e enfaticamente _não_ quero que Go copie o tratamento de exceções do Java. Se você quiser Java, use Java. E, por favor, leve a discussão sobre operadores ternários para um assunto apropriado, por exemplo, # 23248.

@lpar Então, se eu trabalhar para uma empresa e por alguma razão desconhecida eles escolherem GoLang como sua linguagem de programação, eu simplesmente preciso sair do meu emprego e me inscrever em um Java !? Vamos lá, cara!

@bcmills Você pode contar o código que sugeriu lá. Acho que são 6 linhas de código em vez de uma e provavelmente você obtém alguns pontos de complexidade ciclomática de código por isso (vocês usam Linter, certo?).

@carlmjohnson e @bcmills Qualquer sintaxe que seja antiga e madura não significa que seja ruim. Na verdade, acho que a sintaxe if else é muito mais antiga do que a sintaxe do operador ternário.

Que bom que você trouxe essa coisa do idioma GO. Acho que é apenas uma das questões dessa linguagem. Sempre que há um pedido de mudança, alguém diz oh não, isso é contra o idioma Go. Eu vejo isso como apenas uma desculpa para resistir às mudanças e bloquear quaisquer novas ideias.

@KamyarM por favor seja educado. Se você quiser ler mais sobre como manter a linguagem pequena, recomendo https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html.

Além disso, um comentário geral, não relacionado à discussão recente de try / catch.

Tem havido muitas propostas neste tópico. Falando por mim, ainda não sinto que tenho uma forte noção do (s) problema (s) a ser resolvido. Eu adoraria ouvir mais sobre eles.

Eu também ficaria animado se alguém quisesse assumir a tarefa nada invejável, mas importante, de manter uma lista organizada e resumida dos problemas que foram discutidos.

@josharian Eu estava falando francamente lá. Eu queria mostrar os problemas exatos no idioma ou na comunidade. Considere isso como mais crítica. GoLang está aberto a críticas, certo?

@KamyarM Se você trabalhasse para uma empresa que escolheu o Rust para sua linguagem de programação, você iria para o Rust Github e começaria a exigir gerenciamento de memória com coleta de lixo e ponteiros no estilo C ++ para não ter que lidar com o verificador emprestado?

O motivo pelo qual os programadores de Go não desejam exceções no estilo Java não tem nada a ver com falta de familiaridade com elas. Encontrei exceções pela primeira vez em 1988, via Lisp, e tenho certeza de que outras pessoas neste tópico as encontraram ainda antes - a ideia remonta ao início dos anos 1970.

O mesmo é ainda mais verdadeiro para expressões ternárias. Leia sobre a história de Go - Ken Thompson, um dos criadores de Go, implementou o operador ternário na linguagem B (predecessor de C) no Bell Labs em 1969. Acho que é seguro dizer que ele estava ciente de seus benefícios e armadilhas ao considerar se deve ser incluído no Go.

Go está aberto a críticas, mas exigimos que a discussão nos fóruns de Go seja educada. Ser franco não é o mesmo que ser indelicado. Consulte a seção "Valores Gopher" de https://golang.org/conduct. Obrigado.

@lpar Sim, se Rust tiver um fórum, eu faria isso ;-) Sério, eu faria isso. Porque eu quero que minha voz seja ouvida.

@ianlancetaylor Usei palavras ou linguagem vulgar? Usei linguagem discriminatória ou qualquer intimidação de alguém ou quaisquer avanços sexuais indesejáveis? Acho que não.
Vamos cara, estamos falando apenas sobre a linguagem de programação Go aqui. Não se trata de religião ou política ou qualquer coisa assim.
Eu fui franco. Eu queria que minha voz fosse ouvida. Acho que é por isso que existe este fórum. Para que as vozes sejam ouvidas. Você pode não gostar de minha sugestão ou crítica. Isso está ok. Mas acho que você precisa me deixar falar e discutir, caso contrário, todos podemos concluir que tudo está perfeito e não há nenhum problema e, portanto, não há necessidade de mais discussões.

@josharian Obrigado pelo artigo, vou dar uma olhada nisso.

Bem, eu olhei para trás em meus comentários para ver se há algo ruim lá. A única coisa que eu poderia ter insultado (eu ainda chamo isso de crítica btw) é a linguagem de programação GoLang Idioms! Hahah!

Para voltar ao nosso tópico, se você ouvir minha voz, vá. Autores considerem trazer de volta os blocos Try catch. Deixe para o programador decidir se vai usar no lugar certo ou não (você já tem algo semelhante, quero dizer, o pânico adiar se recuperar, então por que não tentar o Catch, que é mais familiar para os programadores?).
Eu sugeri uma solução alternativa para o tratamento atual do Go Error para compatibilidade com versões anteriores. Não estou dizendo que essa seja a melhor opção, mas acho que é viável.

Vou parar de discutir mais sobre este assunto.

Obrigado pela oportunidade.

@KamyarM Você está confundindo nossos pedidos para que seja educado com nossa discordância com seus argumentos. Quando as pessoas discordam de você, você está respondendo em termos pessoais com comentários como "Muito obrigado por seus polegares para baixo. Esse é exatamente o problema de que estou falando. A comunidade GO não está aberta a mudanças e eu sinto isso, e realmente não." assim. "

Mais uma vez: seja educado. Atenha-se a argumentos técnicos. Evite argumentos ad hominem que ataquem mais as pessoas do que as idéias. Se você genuíno não entende o que quero dizer, estou disposto a discutir isso off-line; me mande um e-mail. Obrigado.

Vou jogar meu 2c, e espero que não esteja literalmente repetindo algo nos outros N cem comentários (ou entrando na discussão da proposta do urandom).

Eu gosto da ideia original que foi postada, mas com dois ajustes principais:

  • Bicicleta sintática: Eu acredito fortemente que qualquer coisa que tenha fluxo de controle implícito deve ser um operador por conta própria, ao invés de uma sobrecarga de um operador existente. Vou lançar ?! por aí, mas estou feliz com o que não é facilmente confundido com uma operadora existente em Go.

  • O RHS desse operador deve assumir uma função, em vez de uma expressão com um valor injetado arbitrariamente. Isso deixaria os desenvolvedores escreverem um código de tratamento de erros bastante conciso, ao mesmo tempo em que seriam claros sobre suas intenções e seriam flexíveis com o que podem fazer, por exemplo

func returnErrorf(s string, args ...interface{}) func(error) error {
  return func(err error) error {
    return errors.New(fmt.Sprintf(s, args...) + ": " + err.Error())
  }
}

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) ?! returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() ?! returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() ?! func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

O RHS nunca deve ser avaliado se um erro não acontecer, portanto, este código não alocará quaisquer fechamentos ou qualquer coisa no caminho feliz.

Também é bastante simples "sobrecarregar" esse padrão para funcionar em casos mais interessantes. Tenho três exemplos em mente.

Primeiro, poderíamos ter return condicional se o RHS for func(error) (error, bool) , assim (se permitirmos isso, acho que devemos usar um operador distinto dos retornos incondicionais. use ?? , mas minha declaração "Eu não me importo, contanto que seja distinto" ainda se aplica):

func maybeReturnError(err error) (error, bool) {
  if err == io.EOF {
    return nil, false
  }
  return err, true
}

func id(err error) error { return err }

func ignoreError(err error) (error, bool) { return nil, false }

func foo(n int) error {
  // Does nothing
  id(io.EOF) ?? ignoreError
  // Still does nothing
  id(io.EOF) ?? maybeReturnError
  // Returns the given error
  id(errors.New("oh no")) ?? maybeReturnError
  return nil
}

Como alternativa, poderíamos aceitar funções RHS que têm tipos de retorno correspondentes aos da função externa, assim:

func foo(r io.Reader) ([]int, error) {
  returnError := func(err error) ([]int, error) { return []int{0}, err }
  // returns `[]int{0}, err` on a Read failure
  n := r.Read(make([]byte, 4)) ?! returnError
  return []int{n}, nil
}

E, finalmente, se realmente quisermos, podemos generalizar isso para trabalhar com mais do que apenas erros, alterando o tipo de argumento:

func returnOpFailed(name string) func(bool) error {
  return func(_ bool) error {
    return errors.New(name + " failed")
  }
}

func returnErrOpFailed(name string) func(error) error {
  return func(err error) error {
    return errors.New(name + " failed: " + err.Error())
  }
}

func foo(c chan int, readInt func() (int, error), d map[int]string) (string, error) {
  n := <-c ?! returnOpFailed("receiving from channel")
  m := readInt() ?! returnErrOpFailed("reading an int")
  result := d[n + m] ?! returnOpFailed("looking up the number")
  return result, nil
}

... O que eu pessoalmente consideraria muito útil quando tenho que fazer algo terrível, como decodificar manualmente um map[string]interface{} .

Para ser claro, estou mostrando principalmente as extensões como exemplos. Não tenho certeza de qual deles (se houver) atinge um bom equilíbrio entre simplicidade, clareza e utilidade geral.

Eu gostaria de revisitar a parte "retornar o erro / com contexto adicional" novamente, uma vez que suponho que ignorar o erro já esteja coberto pelo _ já existente.

Estou propondo uma palavra-chave de duas palavras que pode ser seguida por uma string (opcionalmente).

@urandom a primeira parte de sua proposta é aceitável, pode-se sempre começar com isso e deixar BubbleProcessor para uma segunda revisão. As preocupações levantadas por @ object88 são IMO válidas; Recentemente, vi avisos como "você não deve sobrescrever o cliente / transporte padrão de http ", isso se tornaria outro daqueles.

Tem havido muitas propostas neste tópico. Falando por mim, ainda não sinto que tenho uma forte noção do (s) problema (s) a ser resolvido. Eu adoraria ouvir mais sobre eles.

Eu também ficaria animado se alguém quisesse assumir a tarefa nada invejável, mas importante, de manter uma lista organizada e resumida dos problemas que foram discutidos.

Poderia ser você @josharian se @ianlancetaylor o nomear? : blush: Não sei como outras questões estão sendo planejadas / discutidas, mas talvez esta discussão esteja sendo usada apenas como uma "caixa de sugestões"?

@KamyarM

@bcmills Você pode contar o código que sugeriu lá. Acho que são 6 linhas de código em vez de uma e provavelmente você obtém alguns pontos de complexidade ciclomática de código por isso (vocês usam Linter, certo?).

Ocultar a complexidade ciclomática torna mais difícil de ver, mas não a remove (lembre-se de strlen ?). Assim como tornar o tratamento de erros "abreviado" torna a semântica de tratamento de erros mais fácil de ignorar - mas mais difícil de ver.

Quaisquer declarações ou expressões na origem que redirecionem o controle de fluxo devem ser óbvias e concisas, mas se for uma decisão entre óbvio ou conciso, o óbvio deve ser preferido neste caso.

Que bom que você trouxe essa coisa do idioma GO. Acho que é apenas uma das questões dessa linguagem. Sempre que há um pedido de mudança, alguém diz oh não, isso é contra o idioma Go. Eu vejo isso como apenas uma desculpa para resistir às mudanças e bloquear quaisquer novas ideias.

Existe uma diferença entre novo e benéfico. Você acredita que porque você tem uma ideia, a própria existência dela merece aprovação? Como exercício, por favor, olhe para o rastreador de problemas e tente imaginar o Go hoje se todas as ideias fossem aprovadas, independentemente do que a comunidade pensasse.

Talvez você acredite que sua ideia é melhor do que as outras. É aí que entra a discussão. Em vez de degenerar a conversa para falar sobre como todo o sistema está quebrado por causa de expressões idiomáticas, dirija as críticas diretamente, ponto a ponto, ou encontre um meio-termo entre você e seus colegas.

@ gdm85
Eu adicionei o processador para algum tipo de customização por parte do erro retornado. E embora eu acredite que é um pouco como usar o logger padrão, no sentido de que você pode se safar usando-o na maior parte do tempo, eu disse que estou aberto a sugestões. E, para constar, não acredito que o logger padrão e o cliente http padrão estejam nem remotamente na mesma categoria.

Também gosto da proposta de @gburgessiv , embora não seja um grande fã do operador enigmático em si (talvez pelo menos escolha ? como em Rust, embora ainda ache que isso seja enigmático). Isso ficaria mais legível:

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) or returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() or returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() or func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

E esperançosamente, sua proposta também incluirá uma implementação padrão de uma função semelhante ao seu returnErrorf em algum lugar do pacote errors . Talvez errors.Returnf() .

@KamyarM
Você já expressou sua opinião aqui, e não obteve nenhum comentário ou reação simpática à causa da exceção. Não vejo o que vai adiantar repetir a mesma coisa, tbh, além de atrapalhar as outras discussões. E se esse for o seu objetivo, simplesmente não é legal.

@josharian , tentarei resumir a discussão brevemente. Vai ser tendencioso, já que tenho uma proposta no mix, e incompleto, já que não tenho condições de reler o thread inteiro.

O problema que estamos tentando resolver é a confusão visual causada pelo tratamento de erros Go. Aqui está um bom exemplo ( fonte ):

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {
    if err := createFolderIfNotExist(to); err != nil {
        return nil, err
    }
    if err := clearFolder(to); err != nil {
        return nil, err
    }
    if err := cloneRepo(to, from); err != nil {
        return nil, err
    }
    dirs, err := getContentFolders(to)
    if err != nil {
        return nil, err
    }
    return dirs, nil
}

Vários comentaristas neste tópico não acham que isso precise ser consertado; eles estão felizes com o fato de o tratamento de erros ser intrusivo, porque lidar com erros é tão importante quanto lidar com o caso de não erro. Para eles, nenhuma das propostas aqui vale a pena.

As propostas que tentam simplificar o código como este se dividem em alguns grupos.

Alguns propõem uma forma de tratamento de exceções. Dado que Go poderia ter escolhido o tratamento de exceções no início e optado por não fazê-lo, parece improvável que sejam aceitos.

Muitas das propostas aqui escolhem uma ação padrão, como retornar da função (a proposta original) ou entrar em pânico, e sugerem um pouco de sintaxe que torna essa ação fácil de expressar. Em minha opinião, todas essas propostas fracassam, porque privilegiam uma ação em detrimento de outras. Eu uso regularmente devoluções t.Fatal e log.Fatal para lidar com erros, às vezes todos no mesmo dia.

Outras propostas não fornecem nenhuma maneira de aumentar ou embrulhar o erro original, ou torná-lo significativamente mais difícil de embrulhar. Eles também são inadequados, uma vez que o empacotamento é a única maneira de adicionar contexto aos erros e, se tornarmos muito fácil ignorá-los, isso será feito com ainda menos frequência do que agora.

A maioria das propostas restantes adiciona um pouco de açúcar e às vezes um pouco de mágica para simplificar as coisas sem restringir as ações possíveis ou a capacidade de embrulhar. As propostas minhas e de @bcmills adicionam uma quantidade mínima de açúcar e zero de magia para aumentar ligeiramente a legibilidade e também para prevenir um tipo de bug desagradável .

Algumas outras propostas adicionam algum tipo de fluxo de controle não local restrito, como uma seção de tratamento de erros no início ou no final de uma função.

Por último, mas não menos importante, @mpvl reconhece que o tratamento de erros pode ser muito complicado na presença de pânicos. Ele sugere uma mudança mais radical no tratamento de erros Go para melhorar a exatidão e a legibilidade. Ele tem um argumento convincente, mas no final acho que seus casos não requerem mudanças drásticas e podem ser tratados com os mecanismos existentes .

Peço desculpas a todos cujas ideias não estão representadas aqui.

Tenho a sensação de que alguém vai me perguntar sobre a diferença entre açúcar e mágica. (Estou perguntando a mim mesmo.)

Sugar é um pouco de sintaxe que encurta o código sem alterar fundamentalmente as regras da linguagem. O operador de atribuição curta := é açúcar. O mesmo ocorre com o operador ternário de C ?: .

Magia é uma interrupção mais violenta da linguagem, como introduzir uma variável em um escopo sem declará-la ou realizar uma transferência de controle não local.

A linha está definitivamente borrada.

Obrigado por fazer isso, @jba. Muito útil. Apenas para destacar os destaques, os problemas identificados até agora são:

a confusão visual causada pelo tratamento de erros Go

e

tratamento de erros pode ser muito complicado na presença de pânico

Se houver qualquer outro problema fundamentalmente diferente (não soluções) que @jba e eu não contato (qualquer pessoa). FWIW, eu consideraria ergonomia, desordem de código, complexidade ciclomática, gagueira, clichê, etc. como variantes do problema de "desordem visual" (ou conjunto de problemas).

@josharian Você deseja considerar os problemas de escopo (https://github.com/golang/go/issues/21161#issuecomment-319277657) como uma variante do problema de “desordem visual” ou um problema separado?

@bcmills parece diferente para mim, uma vez que se trata de questões sutis de correção, ao contrário de estética / ergonomia (ou no máximo questões de correção envolvendo volume de código). Obrigado! Quer editar meu comentário e adicionar uma sinopse de uma linha dele?

Tenho a sensação de que alguém vai me perguntar sobre a diferença entre açúcar e mágica. (Estou perguntando a mim mesmo.)

Eu uso esta definição de mágica: olhando um pouco do código-fonte, se você conseguir descobrir o que ele deve fazer por alguma variante do seguinte algoritmo:

  1. Procure todos os identificadores, palavras-chave e construções gramaticais presentes na linha ou na função.
  2. Para as construções gramaticais e palavras-chave, consulte a documentação do idioma oficial.
  3. Para identificadores, deve haver um mecanismo claro para localizá-los usando as informações no código que você está olhando, usando os escopos em que o código está localizado atualmente, conforme definido pela linguagem, a partir do qual você pode obter a definição do identificador, que irá ser preciso em tempo de execução.

Se este algoritmo _fiavelmente_ produz entendimentos corretos do que o código vai fazer, não é mágico. Se não, então há alguma quantidade de magia nele. O quão recursivamente você deve aplicá-lo ao tentar seguir as referências da documentação e definições de identificador para outras definições de identificador afeta a _complexidade_, mas não a _magicidade_, das construções / código em questão.

Os exemplos de mágica incluem: Identificadores sem um caminho claro de volta à sua origem porque você os importou sem um namespace (ponto importa no Go, especialmente se você tiver mais de um). Qualquer habilidade que uma linguagem possa ter para definir não localmente o que algum operador resolverá, como em linguagens dinâmicas onde o código pode redefinir completamente uma referência de função não localmente ou redefinir o que a linguagem faz para identificadores inexistentes. Objetos construídos por esquemas carregados de um banco de dados em tempo de execução, portanto, em tempo de código, espera-se mais ou menos cegamente que eles estejam lá.

O bom disso é que quase toda a subjetividade está fora de questão.

Voltando ao assunto em questão, parece que já há uma tonelada de propostas feitas, e as chances de alguém resolver isso com outra proposta que faz todos pensarem "Sim! É isso!" se aproximar de zero.

Parece-me que talvez a conversa deva se mover no sentido de categorizar as várias dimensões das propostas feitas aqui, e ter uma noção da priorização. Eu gostaria especialmente de ver isso com o objetivo de revelar requisitos contraditórios que estão sendo vagamente aplicados pelas pessoas aqui.

Por exemplo, tenho visto algumas reclamações sobre saltos adicionais sendo adicionados no fluxo de controle. Mas para mim, no jargão da proposta muito original, eu valorizo ​​não ter que adicionar || &PathError{"chdir", dir, err} oito vezes dentro de uma função se eles forem comuns. (Eu sei que Go não é tão alérgico a código repetido como algumas outras linguagens, mas ainda assim, código repetido tem um risco muito alto de bugs de divergência.) Mas, basicamente, por definição, se houver um mecanismo para fatorar tal tratamento de erros, o código não pode fluir de cima para baixo, da esquerda para a direita, sem saltos. O que geralmente é considerado mais importante? Suspeito que um exame cuidadoso dos requisitos que as pessoas estão implicitamente colocando no código revelaria outros requisitos mutuamente contraditórios.

Mas, em geral, sinto que se a comunidade pudesse concordar com os requisitos depois de toda essa análise, a solução correta pode muito bem sair deles claramente ou, pelo menos, o conjunto de soluções correto será tão obviamente restrito que o problema se torna tratável.

(Gostaria também de salientar que, por se tratar de uma proposta, o comportamento atual deve, em geral, ser submetido à mesma análise das novas propostas. O objetivo é uma melhoria significativa, não a perfeição; rejeitar duas ou três melhorias significativas porque nenhuma delas são perfeitos é um caminho para a paralisia. Todas as propostas são compatíveis com versões anteriores de qualquer forma, então, nos casos em que a abordagem atual já é a melhor de qualquer maneira (imho, o caso em que cada erro é tratado legitimamente de forma diferente, o que na minha experiência é raro, mas acontece) , a abordagem atual ainda estará disponível.)

Tenho pensado nisso desde a segunda vez que escrevi if err! = Nil em uma função, me parece que uma solução bastante simples seria permitir um retorno condicional que se pareça com a primeira parte do ternário com o entendimento sendo se a condição falha, não voltamos.

Não tenho certeza se isso funcionaria bem em termos de análise / compilação, mas parece que deve ser fácil de interpretar como uma instrução if em que o caractere '?' é visto sem quebrar a compatibilidade onde não é visto, então pensei em jogá-lo lá como uma opção.

Além disso, haveria outros usos para isso além do tratamento de erros.

então você pode fazer algo assim:

func example1() error {
    err := doSomething()
    return err != nil ? err
    //more code
}

func example2() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, err
    //more code
}

Também podemos fazer coisas como esta quando temos algum código de limpeza, assumindo que handleErr retornou um erro:

func example3() error {
    err := doSomething()
    return err !=nil ? handleErr(err)
    //more code
}

func example4() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, handleErr(err)
    //more code
}

Talvez, então, você também seja capaz de reduzir isso a uma linha, se quiser:

func example5() error {
    return err := doSomething(); err !=nil ? handleErr(err)
    //more code
}

func example6() (*Mything, error) {
    return err := doSomething(); err !=nil ? nil, handleErr(err)
    //more code
}

o exemplo de busca anterior de @jba poderia ser parecido com este:

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return err := createFolderIfNotExist(to); err != nil ? nil, err
    return err := clearFolder(to); err != nil ? nil, err
    return err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

Estaria interessado em reações a esta sugestão, talvez não uma grande vitória em salvar clichês, mas mantém bastante explícito e esperançosamente requer apenas uma pequena mudança compatível com versões anteriores (talvez algumas suposições maciçamente imprecisas nessa frente).

Você poderia separar isso com um retorno separado? palavra-chave que pode aumentar a clareza e tornar a vida mais simples em termos de não ter que se preocupar com a compatibilidade com o retorno (pensando em todas as ferramentas), elas poderiam então ser reescritas internamente como instruções if / return, nos dando o seguinte:

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return? err := createFolderIfNotExist(to); err != nil ? nil, err
    return? err := clearFolder(to); err != nil ? nil, err
    return? err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

Não parece haver muita diferença entre

return err != nil ? err

e

 if err != nil { return err }

Além disso, às vezes você pode querer fazer algo diferente de retornar, como chamar panic ou log.Fatal .

Estive pensando nisso desde que apresentei uma proposta na semana passada e cheguei à conclusão de que concordo com @thejerf : estivemos discutindo proposta após proposta sem realmente dar um passo para trás e examinar o que gostamos sobre cada um, não gosto de cada um e quais são as prioridades para uma solução "adequada".

Os requisitos mais comumente declarados são que, no final do dia, Go precisa ser capaz de lidar com 4 casos de tratamento de erros:

  1. Ignorando o erro
  2. Retornando o erro inalterado
  3. Retornando o erro com contexto adicionado
  4. Em pânico (ou matando o programa)

As propostas parecem se enquadrar em uma das três categorias:

  1. Reverta para um estilo try-catch-finally de tratamento de erros.
  2. Adicione uma nova sintaxe / embutidos para lidar com todos os 4 casos listados acima
  3. Afirmar que go lida com alguns casos bem o suficiente e propor sintaxe / builtins para ajudar com os outros casos.

As críticas às propostas dadas parecem dividir-se entre preocupações sobre a legibilidade do código, saltos não óbvios, adição implícita de variáveis ​​aos escopos e concisão. Pessoalmente, acho que tem havido muita opinião pessoal nas críticas às propostas. Não estou dizendo que isso seja ruim, mas me parece que não há realmente um critério objetivo para avaliar as propostas.

Provavelmente não sou a pessoa para tentar criar essa lista de critérios, mas acho que seria muito útil se alguém fizesse isso. Tentei delinear minha compreensão do debate até agora como um ponto de partida para analisar 1. o que vimos, 2. o que há de errado com isso, 3. por que essas coisas estão erradas e 4. o que havíamos gostaria de ver em vez disso. Acho que capturei uma quantidade razoável dos 3 primeiros itens, mas estou tendo problemas para encontrar uma resposta para o item 4 sem recorrer a "o que Go tem atualmente".

@jba tem outro comentário de resumo legal acima, para mais contexto. Ele diz muito do que eu disse aqui, em palavras diferentes.

@ianlancetaylor , ou qualquer outra pessoa mais envolvida com o projeto do que eu, você se sentiria confortável adicionando um conjunto "formal" (tudo em um comentário, organizado e um tanto abrangente, mas de forma alguma vinculativo) de critérios que precisam ser atendidos? Talvez se discutirmos esses critérios e definirmos de 4 a 6 pontos que as propostas precisam atender, possamos reiniciar a discussão com um pouco mais de contexto.

Não acho que posso escrever um conjunto formal abrangente de critérios. O melhor que posso fazer é uma lista incompleta de coisas importantes que só devem ser desconsideradas se houver um benefício significativo em fazê-lo.

  • Bom suporte para 1) ignorar um erro; 2) retornar um erro não modificado; 3) agrupar um erro com contexto adicional.
  • Embora o código de tratamento de erros deva ser claro, ele não deve dominar a função. Deve ser fácil ler o código que não trata de erros.
  • O código Go 1 existente deve continuar a funcionar ou, pelo menos, deve ser possível traduzir mecanicamente o Go 1 para a nova abordagem com total confiabilidade.
  • A nova abordagem deve encorajar os programadores a lidar com os erros corretamente. Idealmente, deveria ser fácil fazer a coisa certa, seja qual for a coisa certa em qualquer situação.
  • Qualquer nova abordagem deve ser mais curta e / ou menos repetitiva do que a abordagem atual, embora permaneça clara.
  • A linguagem funciona hoje e cada mudança tem um custo. O benefício da mudança deve valer claramente o custo. Não deve ser apenas uma lavagem, deve ser claramente melhor.

Espero reunir as notas em algum ponto aqui eu mesmo, mas quero abordar o que é IMHO outro grande obstáculo nesta discussão, a trivialidade do (s) exemplo (s).

Extraí isso de um projeto real meu e limpei para liberação externa. (Eu acho que o stdlib não é a melhor fonte, pois está faltando questões de registro, entre outros ..)

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    if err != nil {
        return nil, err
    }

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    if err != nil {
        return nil, err
    }

    session, err := communicationProtocol.FinalProtocol(conn)
    if err != nil {
        return nil, err
    }
    client.session = session

    return client, nil
}

(Não vamos alterar muito o código. Não posso impedi-lo, é claro, mas lembre-se de que este não é meu código real e foi um pouco mutilado. E não posso impedi-lo de postar seu próprio código de amostra.)

Observações:

  1. Este não é um código perfeito; Eu retorno muito erros simples porque é tão fácil, exatamente o tipo de código com o qual temos problemas. As propostas devem ser pontuadas pela concisão e pela facilidade com que demonstram a correção desse código.
  2. A cláusula if forwardPort == 0 _deliberadamente_ continua com erros e, sim, este é o comportamento real, não algo que eu adicionei neste exemplo.
  3. Este código OU retorna um cliente conectado válido OU retorna um erro e nenhum recurso vaza, portanto, a manipulação de .Close () (somente se a função apresentar erros) é deliberada. Observe também que os erros de Fechar desaparecem, como é típico no Go real.
  4. O número da porta está restrito em outro lugar, então url.Parse não pode falhar (por exame).

Eu não diria que isso demonstra todos os comportamentos de erro possíveis, mas cobre uma grande gama. (Eu geralmente defendo Go on HN e tal, apontando que quando meu código termina de cozinhar, é frequente o caso em meus servidores de rede que eu tenho _todos os tipos_ de comportamento de erro; examinando meu próprio código de produção, a partir de 1/3 a metade dos erros fez algo diferente de simplesmente ser retornado.)

Também irei (re) postar minha própria proposta (atualizada) conforme aplicada a este código (a menos que alguém me convença de que eles têm algo ainda melhor antes disso), mas no interesse de não monopolizar a conversa, vou esperar pelo menos no fim de semana. (Este é menos texto do que parece, porque é um grande pedaço da fonte, mas ainda ...)

Usar try onde try é apenas um atalho para if! = Nil return reduz o código em 6 linhas de 59, o que é cerca de 10%.

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    try err

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    try err

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    try err

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    try err

    session, err := communicationProtocol.FinalProtocol(conn)
    try err

    client.session = session

    return client, nil
}

Notavelmente, em vários lugares eu quis escrever try x() mas não pude, pois precisava errar para ser definido para que os defers funcionassem corretamente.

Mais um. Se tivermos try sendo algo que pode acontecer em linhas de atribuição, ele desce para 47 linhas.

func NewClient(...) (*Client, error) {
    try listener, err := net.Listen("tcp4", listenAddr)

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    try conn, err := ConnectionManager{}.connect(server, tlsConfig)

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    try session, err := communicationProtocol.FinalProtocol(conn)

    client.session = session

    return client, nil
}
import "github.com/pkg/errors"

func Func3() (T1, T2, error) {...}

type PathError {
    err Error
    x   T3
    y   T4
}

type MiscError {
    x   T5
    y   T6
    err Error
}


func Foo() (T1, T2, error) {
    // Old school
    a, b, err := Func(3)
    if err != nil {
        return nil
    }

    // Simplest form.
    // If last unhandled arg's type is same 
    // as last param of func,
    // then use anon variable,
    // check and return
    a, b := Func3()
    /*    
    a, b, err := Func3()
    if err != nil {
         return T1{}, T2{}, err
    }
    */

    // Simple wrapper
    // If wrappers 1st param TypeOf Error - then pass last and only unhandled arg from Func3() there
    a, b, errors.WithStack() := Func3() 
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithStack(err)
    }
    */

    // Bit more complex wrapper
    a, b, errors.WithMessage("unable to get a and b") := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithMessage(err, "unable to get a and b")
    }
    */

    // More complex wrapper
    // If wrappers 1nd param TypeOf is not Error - then pass last and only unhandled arg from Func3() as last
    a, b, fmt.Errorf("at %v Func3() return error %v", time.Now()) := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, fmt.Errorf("at %v Func3() return error %v", time.Now(), err)
    }
    */

    // Wrapping with error types
    a, b, &PathError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &PathError{err, x, y}
    }
    */
    a, b, &MiscError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &MiscError{x, y, err}
    }
    */

    return a, b, nil
}

Ligeiramente mágico (sinta-se à vontade para -1), mas suporta tradução mecânica

Esta é a aparência (um tanto atualizada) da minha proposta:

func NewClient(...) (*Client, error) {
    defer annotateError("client couldn't be created")

    listener := pop net.Listen("tcp4", listenAddr)
    defer closeOnErr(listener)
    conn := pop ConnectionManager{}.connect(server, tlsConfig)
    defer closeOnErr(conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    pop toServer.Send(&client.serverConfig)
    pop toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    session := pop communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

func closeOnErr(c io.Closer) {
    if err := erroring(); err != nil {
        closeErr := c.Close()
        if err != nil {
            seterr(multierror.Append(err, closeErr))
        }
    }
}

func annotateError(annotation string) {
    if err := erroring(); err != nil {
        log.Printf("%s: %v", annotation, err)
        seterr(errwrap.Wrapf(annotation +": {{err}}", err))
    }
}

Definições:

pop é válido para expressões de função onde o valor mais à direita é um erro. É definido como "se o erro não for nulo, retorne da função com os valores zero para todos os outros valores nos resultados e esse erro, caso contrário, produz um conjunto de valores sem o erro". pop não tem interação privilegiada com erroring() ; um retorno normal de um erro ainda será visível para erroring() . Isso também significa que você pode retornar valores diferentes de zero para os outros valores de retorno e ainda usar o tratamento de erro adiado. A metáfora é retirar o elemento mais à direita da "lista" de valores de retorno.

erroring() é definido como subir a pilha para a função adiada que está sendo executada e, em seguida, o elemento da pilha anterior (a função em que o adiamento está sendo executado, NewClient neste caso), para acessar o valor do erro retornado atualmente em andamento. Se a função não tiver esse parâmetro, entre em pânico ou retorne nil (o que fizer mais sentido). Este valor de erro não precisa vir de pop ; é qualquer coisa que retorna um erro da função de destino.

seterr(error) permite que você altere o valor de erro que está sendo retornado. Será então o erro visto por quaisquer chamadas erroring() futuras, que, conforme mostrado aqui, permite o mesmo encadeamento baseado em adiamento que pode ser feito agora.

Estou usando o envoltório hashicorp e o multierror aqui; insira seus próprios pacotes inteligentes conforme desejado.

Mesmo com a função extra definida, a soma é mais curta. Espero amortizar as duas funções em usos adicionais, portanto, eles devem contar apenas parcialmente.

Observe que apenas deixo o tratamento forwardPort sozinho, em vez de tentar inserir um pouco mais de sintaxe em torno dele. Como um caso excepcional, não há problema em ser mais prolixo.

A coisa mais interessante sobre esta proposta IMHO só pode ser visto se você imaginar tentando escrever isso com exceções convencionais. Ele acaba ficando bastante aninhado e lidar com a _coleção_ dos erros que podem ocorrer é muito tedioso com o tratamento de exceções. (Assim como no código Go real, os erros .Close tendem a ser ignorados, os erros que acontecem nos próprios manipuladores de exceção tendem a ser ignorados no código baseado em exceção.)

Isso estende os padrões Go existentes, como defer e o uso de erros como valores para facilitar a correção de padrões de tratamento de erros que, em alguns casos, são difíceis de expressar com Go atual ou exceções, não requer cirurgia radical para o tempo de execução (eu não acho), e também, na verdade, não _requer_ um Go 2.0.

As desvantagens incluem reivindicar erroring , pop e seterr como palavras-chave, incorrendo na sobrecarga de defer para essas funcionalidades, o fato de que o tratamento de erros fatorado salta alguns às funções de manuseio, e que nada faz para "forçar" o manuseio correto. Embora eu não tenha certeza de que a última opção seja possível, já que pelo requisito (correto) de compatibilidade com versões anteriores, você sempre pode fazer a coisa atual.

Discussão muito interessante aqui.

Eu gostaria de manter a variável de erro no lado esquerdo para que nenhuma variável aparecendo magicamente seja introduzida. Como na proposta original, gostaria de lidar com o material de erro na mesma linha. Eu não usaria o operador || porque ele parece "muito booleano" para mim e de alguma forma esconde o "retorno".

Portanto, eu o tornaria mais legível usando a palavra-chave estendida "return?". Em C #, o ponto de interrogação é usado em alguns lugares para fazer atalhos. Por exemplo. em vez de escrever:

if(foo != null)
{ foo.Bar(); }

você pode apenas escrever:
foo?.Bar();

Portanto, para Go 2, gostaria de propor esta solução:

func foobar() error {
    return fmt.Errorf("Some error happened")
}

// Implicitly return err (there must be exactly one error variable on the left side)
err := foobar() return?
// Explicitly return err
err := foobar() return? err
// Return extended error info
err := foobar() return? &PathError{"chdir", dir, err}
// Return tuple
err := foobar() return? -1, err
// Return result of function (e. g. for logging the error)
err := foobar() return? handleError(err)
// This doesn't compile as you ignore the error intentionally
foobar() return?

Apenas um pensamento:

foo, err: = meuFunc ()
err! = nulo? voltar envoltório (errar)

Ou

se errar! = nulo? voltar envoltório (errar)

Se você estiver disposto a colocar chaves em volta disso, não precisamos mudar nada!

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

Você pode ter _todos_ o tratamento personalizado que desejar (ou nenhum), você salvou duas linhas de código do caso típico, é 100% compatível com versões anteriores (porque não há alterações na linguagem), é mais compacto e é fácil. Atinge muitos dos pontos de direção de gofmt ?

Eu escrevi isso antes de ler a sugestão de carlmjohnson, que é semelhante ...

Apenas # antes de um erro.

Mas em um aplicativo do mundo real, você ainda teria que escrever o normal if err != nil { ... } para que possa registrar os erros, isso torna o tratamento de erros minimalista inútil, a menos que você pudesse adicionar um middleware de retorno por meio de anotações chamadas after , que é executado depois que as funções retornam ... (como defer mas com args).

@after(func (data string, err error) {
  if err != nil {
    log.Error("error", data, " - ", err)
  }
})
func alo() (string, error) {
  // this is the equivalent of
  // data, err := db.Find()
  // if err != nil { 
  //   return "", err 
  // }
  str, #err := db.Find()

  // ...

  #err = db.Create()

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  // this is the equivalent of
  // data, data2, err, errNotFound := db.Find()
  // if err != nil { 
  //   return nil, nil, err, nil
  // } else if errNotFound != nil {
  //   return nil, nil, nil, errNotFound
  // }
  data, data2, #err, #errNotFound := db.Find()

  // ...

  return data, data2, nil, nil
}

mais limpo do que:

func alo() (string, error) {
  str, err := db.Find()
  if err != nil {
    log.Error("error on find in database", err)
    return "", err
  }

  // ...

  if err := db.Create(); err != nil {
    log.Error("error on create", err)
    return "", err
  }

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  data, data2, err, errNotFound := db.Find()
  if err != nil { 
    return nil, nil, err, nil
  } else if errNotFound != nil {
    return nil, nil, nil, errNotFound
  }

  // ...

  return data, data2, nil, nil
}

Que tal uma instrução Swift como guard , exceto que em vez de guard...else é guard...return :

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

Eu gosto da clareza do tratamento de erros do Go. O único problema, imho, é quanto espaço leva. Eu sugeriria 2 ajustes:

  1. permite que itens nulos sejam usados ​​em um contexto booleano, onde nil é equivalente a false , não- nil a true
  2. suporta operadores de instrução condicional de linha única, como && e ||

assim

file, err := os.Open("fails.txt")
if err != nil {
    return &FooError{"Couldn't foo fails.txt", err}
}

pode se tornar

file, err := os.Open("fails.txt")
if err {
    return &FooError{"Couldn't foo fails.txt", err}
}

ou ainda mais curto

file, err := os.Open("fails.txt")
err && return &FooError{"Couldn't foo fails.txt", err}

e podemos fazer

i,ok := v.(int)
ok || return fmt.Errorf("not a number")

ou talvez

i,ok := v.(int)
ok && s *= i

Se sobrecarregar && e || criar muita ambigüidade, talvez alguns outros caracteres (não mais que 2) possam ser selecionados, por exemplo, ? e # ou ?? e ## , ou ?? e !! , tanto faz. O objetivo é oferecer suporte a uma declaração condicional de linha única com o mínimo de caracteres "ruidosos" (sem a necessidade de parênteses, colchetes, etc.). Os operadores && e || são bons porque este uso tem precedentes em outras línguas.

Esta não é uma proposta para oferecer suporte a expressões condicionais de uma linha complexas, apenas declarações condicionais de uma linha.

Além disso, esta não é uma proposta para oferecer suporte a uma gama completa de "veracidade", como algumas outras linguagens. Essas condicionais suportariam apenas nil / não nil ou booleanos.

Para esses operadores condicionais, pode até ser adequado restringir a variáveis ​​únicas e não oferecer suporte a expressões. Qualquer coisa mais complexa ou com uma cláusula else seria tratada com construções if ... padrão.

Por que simplesmente não invente uma roda e use a forma try..catch conhecida como

try {
    a := foo() // func foo(string, error)
    b := bar() // func bar(string, error)
} catch (err) {
    // handle error
}

Parece que não há razão para distinguir a fonte do erro detectado, porque se você realmente precisar disso, sempre poderá usar a forma antiga de if err != nil sem try..catch .

Além disso, não tenho certeza sobre isso, mas pode ser adicionada a capacidade de "lançar" um erro se não for resolvido?

func foo() (string, error) {
    f := bar() // similar to if err != nil { return "", err }
}

func baz() string {
    // Compilation error.
    // bar's error must be handled because baz() does not return error.
    return bar()
}

@gobwas do ponto de vista da legibilidade, é muito importante entender completamente o fluxo de controle. Olhando para o seu exemplo, não há como saber qual linha pode causar um salto para o bloco catch. É como uma instrução goto oculta. Não é à toa que as linguagens modernas tentam ser explícitas sobre isso e exigem que o programador marque explicitamente os locais onde o fluxo de controle pode divergir devido a um erro. Muito parecido com return ou goto mas com uma sintaxe muito mais agradável.

@creker sim, concordo totalmente com você. Eu estava pensando em controle de fluxo no exemplo acima, mas não percebi como fazer isso de uma forma simples.

Talvez algo como:

try {
    a ::= foo() // func foo(string, error)
    b ::= bar() // func bar(string, error)
} catch (err) {
    // handle error
}

Ou outras sugestões antes, como try a := foo() ..?

@gobwas

Quando eu aterrissar no bloco catch, como sei qual função no bloco try causou o erro?

@urandom se você precisa saber, provavelmente deseja fazer if err != nil sem try..catch .

@ robert-wallis: Eu mencionei a declaração de guarda do Swift anteriormente no tópico, mas a página é tão grande que o Github não a carrega mais por padrão. : -P Eu ainda acho que é uma boa ideia e, em geral, eu apoio olhar para outras linguagens para exemplos positivos / negativos.

@pdk

permite que itens nulos sejam usados ​​em um contexto booleano, onde nil é equivalente a falso, não nulo a verdadeiro

Eu vejo isso levando a muitos bugs usando o pacote flag onde as pessoas escreverão if myflag { ... } mas pretendem escrever if *myflag { ... } e ele não será detectado pelo compilador.

try / catch é apenas mais curto do que if / else quando você tenta várias coisas consecutivas, que é algo que mais ou menos todos concordam que é ruim por causa dos problemas de fluxo de controle, etc.

FWIW, a tentativa / captura de Swift pelo menos resolve o problema visual de não saber quais declarações podem lançar:

do {
    let dragon = try summonDefaultDragon() 
    try dragon.breathFire()
} catch DragonError.dragonIsMissing {
    // ...
} catch DragonError.halatosis {
    // ...
}

@ robert-wallis, você tem um exemplo:

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

No primeiro uso de guard , parece muito com if err != nil { return &FooError{"Couldn't foo fails.txt", err}} , então não tenho certeza se isso é uma grande vitória.

No segundo uso, não está imediatamente claro de onde vem o err . Quase parece que foi devolvido por os.Open , o que, suponho, não era sua intenção. Isso seria mais preciso?

guard err = os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

Nesse caso, parece ...

if err = os.Remove("fails.txt"); err != nil { return &FooError{"Couldn't remove fails.txt", err}}

Mas ainda tem menos confusão visual. if err = , ; err != nil { - mesmo se for uma linha ainda há muito acontecendo para uma coisa tão simples

Concordou que há menos desordem. Mas consideravelmente menos para justificar a adição à linguagem? Não tenho certeza se concordo nisso.

Acho que a legibilidade dos blocos try-catch em Java / C # / ... é muito boa, pois você pode seguir a sequência do "caminho feliz" sem nenhuma interrupção pelo tratamento de erros. A desvantagem é que você basicamente tem um mecanismo goto oculto.

Em Go, começo a inserir linhas vazias após o manipulador de erros para tornar a continuação da lógica do "caminho feliz" mais visível. Portanto, com base neste exemplo de golang.org (9 linhas)

record := new(Record)
err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}
err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

Costumo fazer isso (11 linhas)

record := new(Record)

err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}

err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

Agora voltando à proposta, como já postei algo assim seria bom (3 linhas)

record := new(Record)
err := datastore.Get(c, key, record) return? &appError{err, "Record not found", 404}
err := viewTemplate.Execute(w, record) return? &appError{err, "Can't display record", 500}

Agora vejo claramente o caminho da felicidade. Meus olhos ainda estão cientes de que no lado direito há um código para tratamento de erros, mas eu só preciso "analisar os olhos" quando realmente necessário.

Uma pergunta para todos: este código deve compilar?

func foobar() error {
    return fmt.Errorf("Some error")
}
func main() {
    foobar()
}

IMHO o usuário deve ser forçado a dizer que ignora intencionalmente o erro com:

func main() {
    _ := foobar()
}

Adicionando um mini relato de experiência relacionado ao ponto 3 da postagem original de retorne o erro com informações contextuais adicionais .

Ao desenvolver uma biblioteca flac para Go, queríamos adicionar informações contextuais aos erros usando o pacote @davecheney pkg / errors (https://github.com/mewkiz/flac/issues/22). Mais especificamente, agrupamos os erros retornados usando

Como o erro é anotado, ele precisa criar um novo tipo subjacente para armazenar essas informações adicionais, no caso de erros.ComStack, o tipo é errors.withStack .

type withStack struct {
    error
    *stack
}

Agora, para recuperar o erro original, a convenção é usar errors.Cause . Isso permite que você compare o erro original com, por exemplo, io.EOF .

Um usuário da biblioteca pode escrever algo parecido com https://github.com/mewkiz/flac/blob/0884ed715ef801ce2ce0c262d1e674fdda6c3d94/cmd/flac2wav/flac2wav.go#L78 usando errors.Cause para verificar o erro original valor:

frame, err := stream.ParseNext()
if err != nil {
    if errors.Cause(err) == io.EOF {
        break
    }
    return errors.WithStack(err)
}

Isso funciona bem em quase todos os casos.

Ao refatorar nosso tratamento de erros para fazer uso consistente de pkg / errors para informações de contexto adicionadas, encontramos um problema bastante sério. Para validar o preenchimento de zero, implementamos um io.Reader que simplesmente verifica se os bytes lidos são zero e, caso contrário, relata um erro. O problema é que, tendo executado uma refatoração automática para adicionar informações contextuais aos nossos erros, de repente nossos casos de teste começaram a falhar .

O problema era que o tipo subjacente do erro retornado por zeros.Read agora é errors.withStack, em vez de io.EOF. Assim, subsequentemente, causamos problemas quando usamos esse leitor em combinação com io.Copy , que verifica io.EOF especificamente e não sabe usar errors.Cause para "desembrulhar" um erro anotado com Informação contextual. Como não podemos atualizar a biblioteca padrão, a solução foi retornar o erro sem informações anotadas (https://github.com/mewkiz/flac/commit/6805a34d854d57b12f72fd74304ac296fd0c07be).

Embora perder as informações anotadas para interfaces que retornam valores concretos seja uma perda, é possível conviver com isso.

O que aprendemos com nossa experiência é que tivemos sorte, pois nossos casos de teste detectaram isso. O compilador não produziu nenhum erro, já que o tipo zeros ainda implementa a interface io.Reader . Também não pensamos que iríamos encontrar um problema, já que a anotação de erro adicionada era uma reescrita gerada por máquina, simplesmente adicionar informações contextuais aos erros não deve afetar o comportamento do programa em um estado normal.

Mas assim foi, e por isso queremos contribuir com nosso relato de experiência para consideração; ao pensar em como integrar a adição de informações contextuais ao tratamento de erros para Go 2, de forma que a comparação de erros (como usada em contratos de interface) ainda se mantenha perfeitamente.

Gentilmente,
Robin

@mewmew , vamos manter este problema sobre os aspectos do fluxo de controle do tratamento de erros. A melhor forma de embrulhar e desembrulhar os erros deve ser discutida em outro lugar, uma vez que é amplamente ortogonal para controlar o fluxo.

Não estou familiarizado com sua base de código e percebo que você disse que era uma refatoração automatizada, mas por que você precisou incluir informações contextuais com EOF? Embora seja tratado como um erro pelo sistema de tipos, EOF é mais um valor de sinal, não um erro real. Em uma implementação io.Reader em particular, é um valor esperado na maioria das vezes. A melhor solução não seria apenas encerrar o erro se não fosse io.EOF ?

Sim, proponho que deixemos as coisas como estão. Fiquei com a impressão de que o sistema de erros de Go foi deliberadamente projetado dessa forma para desencorajar os desenvolvedores de apontar erros na pilha de chamadas. Esses erros devem ser resolvidos onde eles ocorrem e para saber quando é mais apropriado usar o pânico quando você não pode.

Quero dizer, try-catch-throw não é essencialmente o mesmo comportamento de pânico () e recuperação () de qualquer maneira?

Suspiro, se realmente vamos começar a tentar seguir esse caminho. Por que não podemos simplesmente fazer algo como

_, ? := foo()
x?, err? := bar()

ou talvez até algo como

_, err := foo(); return err?
x, err := bar(); return x? || err?
x, y, err := baz(); return (x? && y?) || err?

onde o ? torna-se um apelido abreviado para if var! = nil {return var}.

Podemos até definir outra interface interna especial que é satisfeita pelo método

func ?() bool //looks funky but avoids breakage.

que podemos usar para substituir essencialmente o comportamento padrão do novo e aprimorado operador condicional.

@mortdeus

Eu acho que concordo.
Se o problema é ter uma boa maneira de apresentar o caminho feliz, um plug-in para um IDE poderia dobrar / desdobrar cada instância de if err != nil { return [...] } com um atalho?

Eu sinto que cada parte agora é importante. err != nil é importante. return ... é importante.
É um pouco chato de escrever, mas tem que ser escrito. E isso realmente desacelera as pessoas? O que demora é pensar no erro e no que devolver, não escrever.

Eu estaria muito mais interessado em uma proposta que permitisse limitar o escopo da variável err .

Acho que minha ideia condicional é a maneira mais legal de resolver esse problema. Acabei de pensar em algumas outras coisas que tornariam esse recurso digno o suficiente para ser incluído no Go. Vou escrever minha ideia em uma proposta separada.

Não vejo como isso poderia funcionar:

x, y, err: = baz (); return ( x? && y? ) || err?

onde o ? torna-se um apelido abreviado para if var == nil {return var}.

x, y, err: = baz (); return ( if x == nil{ return x} && if y== nil{ return y} ) || if err == nil{ return err}

x, y, err: = baz (); return (x? && y?) || errar?

torna-se

x, y, err: = baz ();
if ((x! = nulo && y! = nulo) || err! = nulo)) {
retornar x, y, err
}

quando você vê x? && y? || errar? você deve estar pensando "x e y são válidos? E errar?"

caso contrário, a função de retorno não será executada. Acabei de escrever uma nova proposta sobre essa ideia que leva a ideia um pouco mais longe com um novo tipo de interface embutida especial

Sugiro que Go adicione o tratamento de erros padrão na versão 2 do Go.

Se o usuário não manipular o erro, o compilador retorna err se não for nulo, portanto, se o usuário escrever:

func Func() error {
    func1()
    func2()
    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

compile transformá-lo em:

func Func() error {
    err := func1()
    if err != nil {
        return err
    }

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

    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

Se o usuário manipular o erro ou ignorá-lo usando _, o compilador não fará nada:

_ = func1()

ou

err := func1()

para vários valores de retorno, é semelhante:

func Func() (*YYY, error) {
    ch, x := func1()
    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

o compilador se transformará em:

func Func() (*YYY, error) {
    ch, x, err := func1()
    if err != nil {
        return nil, err
    }

    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

Se a assinatura de Func () não retornar erro, mas chamar funções que retornam erro, o compilador relatará Erro: "Por favor, trate de seu erro em Func ()"
Então o usuário pode simplesmente registrar o erro em Func ()

E se o usuário quiser envolver algumas informações para o erro:

func Func() (*YYY, error) {
    ch, x := func1() ? Wrap(err, "xxxxx", ch, "abc", ...)
    return yyy, nil
}

ou

func Func() (*YYY, error) {
    ch, x := func1() ? errors.New("another error")
    return yyy, nil
}

O benefício é,

  1. o programa simplesmente falha no ponto em que ocorre o erro, o usuário não pode ignorar o erro implicitamente.
  2. pode reduzir notavelmente as linhas de código.

não é tão fácil porque Go pode ter vários valores de retorno e a linguagem não deve atribuir o que essencialmente equivale a valores padrão para argumentos de retorno sem que o desenvolvedor esteja explicitamente ciente do que está acontecendo.

Acredito que colocar o tratamento de erros na sintaxe de atribuição não resolve a raiz do problema, que é "o tratamento de erros é repetitivo".

Usar if (err != nil) { return nil } (ou similar) depois de muitas linhas de código (onde fizer sentido) vai contra o princípio DRY (não se repita). Eu acredito que é por isso que não gostamos disso.

Também existem problemas com try ... catch . Você não precisa lidar explicitamente com o erro na mesma função em que ele ocorre. Acredito que essa é uma razão notável pela qual não gostamos de try...catch .

Não acredito que sejam mutuamente exclusivos; podemos ter uma espécie de try...catch sem um throws .

Outra coisa que pessoalmente não gosto em try...catch é a necessidade arbitrária da palavra-chave try . Não há razão para que você não possa catch após qualquer limitador de escopo, no que diz respeito a uma gramática funcional. (alguém me avise se eu estiver errado sobre isso)

Isto é o que proponho:

  • usando ? como espaço reservado para um erro retornado, onde _ seria usado para ignorá-lo
  • em vez de catch como no meu exemplo abaixo, error? poderia ser usado para compatibilidade total com versões anteriores

^ Se minha suposição de que eles são compatíveis com versões anteriores estiver incorreta, indique-o.

func example() {
    {
        // The following line will invoke the catch block
        data, ? := foopkg.buyIt()
        // The next two lines handle an error differently
        otherData, err := foopkg.useIt()
        if err != nil {
            // Here we eliminated deeper indentation
            otherData, ? = foopkg.breakIt()
        }
        if data == "" || otherData == "" {
        }
    } catch (err) {
        return errors.Label("fix it", err)
        // Aside: why not add Label() to the error package?
    }
}

Pensei em um argumento contra isso: se você escrever desta forma, alterar aquele bloco catch pode ter efeitos indesejados no código em escopos mais profundos. Este é o mesmo problema que temos com try...catch .

Acho que se você só pode fazer isso no escopo de uma única função, o risco é administrável - possivelmente o mesmo que o risco atual de esquecer de alterar uma linha de código de tratamento de erros quando você pretende alterar muitos deles. Vejo isso como a mesma diferença entre as consequências da reutilização de código e as consequências de não seguir o DRY (ou seja, sem almoço grátis, como dizem)

Edit: esqueci de especificar um comportamento importante para o meu exemplo. No caso de ? ser usado em um escopo sem catch , acho que isso deve ser um erro do compilador (em vez de causar pânico, que foi reconhecidamente a primeira coisa em que pensei)

Edição 2: Idéia maluca: talvez o bloco catch simplesmente não afetasse o fluxo de controle ... seria literalmente como copiar e colar o código dentro de catch { ... } na linha após o erro ser ? ed (bem, não exatamente - ele ainda teria seu próprio escopo). Parece estranho, já que nenhum de nós está acostumado com isso, então catch definitivamente não deveria ser a palavra-chave se for feito dessa forma, mas caso contrário ... por que não?

@mewmew , vamos manter este problema sobre os aspectos do fluxo de controle do tratamento de erros. A melhor forma de embrulhar e desembrulhar os erros deve ser discutida em outro lugar, uma vez que é amplamente ortogonal para controlar o fluxo.

Ok, vamos manter este tópico para controlar o fluxo. Eu o adicionei simplesmente porque era um problema relacionado ao uso concreto do ponto 3, retornar o erro com informações contextuais adicionais .

@jba Você conhece algum problema especificamente dedicado ao empacotamento / desembrulhamento de informações contextuais para erros?

Não estou familiarizado com sua base de código e percebo que você disse que era uma refatoração automatizada, mas por que você precisou incluir informações contextuais com EOF? Embora seja tratado como um erro pelo sistema de tipos, EOF é mais um valor de sinal, não um erro real. Em uma implementação io.Reader em particular, é um valor esperado na maioria das vezes. A melhor solução não seria apenas encerrar o erro se não fosse io.EOF?

@DeedleFake Eu posso elaborar um pouco, mas para ficar no tópico farei isso na edição mencionada, dedicada a embrulhar / desembrulhar informações contextuais para erros.

Quanto mais leio todas as propostas (incluindo a minha), menos acho que realmente temos problemas com o tratamento de erros em go.

O que eu gostaria é alguma aplicação para não ignorar acidentalmente um valor de retorno de erro, mas impor pelo menos
_ := returnsError()

Eu sei que existem ferramentas para encontrar esses problemas, mas um suporte de primeiro nível da linguagem pode detectar alguns bugs. Não lidar com um erro de forma alguma é como ter uma variável não utilizada para mim - o que já é um erro. Também ajudaria na refatoração, ao introduzir um tipo de retorno de erro em uma função, já que você é forçado a tratá-lo em todos os lugares.

O principal problema que a maioria das pessoas tenta resolver aqui parece ser a "quantidade de digitação" ou o "número de linhas". Eu concordaria com qualquer sintaxe que reduza o número de linhas, mas isso é principalmente um problema gofmt. Basta permitir "escopos de linha única" em linha e estamos bem.

Outra sugestão para salvar um pouco de digitação é implícita nil checando como com booleanos:

err := returnsError()
if err { return err }

ou mesmo

if err := returnsError(); err { return err }

Isso funcionaria com todos os tipos de apontadores de causa.

Minha sensação é que tudo o que reduz a chamada de função + tratamento de erros em uma única linha levará a um código menos legível e uma sintaxe mais complexa.

código menos legível e sintaxe mais complexa.

Já temos um código menos legível por causa do tratamento detalhado de erros. Adicionar o já mencionado truque da API do Scanner, que supostamente esconderia o detalhamento, torna tudo ainda pior. Adicionar uma sintaxe mais complexa pode ajudar na legibilidade, para isso que serve o açúcar sintático no final. Caso contrário, não há sentido nesta discussão. O padrão de borbulhar um erro e retornar valor zero para todo o resto é comum o suficiente para justificar uma mudança de linguagem, em minha opinião.

O padrão de borbulhar um erro e retornar valor zero para tudo

19642 tornaria isso mais fácil.


Além disso, obrigado @mewmew pelo relato de experiência. Ele está definitivamente relacionado a este segmento, na medida em que se relaciona a perigos em tipos específicos de designs de tratamento de erros. Eu adoraria ver mais desses.

Não sinto que expliquei minha ideia muito bem, então criei uma essência (e revisei muitas das deficiências que acabei de notar)

https://gist.github.com/KernelDeimos/384aabd36e1789efe8cbce3c17ffa390

Há mais de uma ideia nesta essência, então espero que elas possam ser discutidas separadamente

Deixando de lado por um momento a ideia de que a proposta aqui deve ser explicitamente sobre tratamento de erros, e se Go introduzisse algo como uma instrução collect ?

Uma instrução collect teria a forma collect [IDENT] [BLOCK STMT] , onde ident deve ser uma variável dentro do escopo de um tipo nil -able. Dentro de uma instrução collect , uma variável especial _! está disponível como um apelido para a variável que está sendo coletada. _! não pode ser usado em qualquer lugar a não ser como uma atribuição, o mesmo que _ . Sempre que _! é atribuído a, uma verificação implícita nil é executada, e se _! não for nulo, o bloco cessa a execução e continua com o resto do código.

Teoricamente, seria algo assim:

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    collect err {
        intermediate1, _! := Step1()
        intermediate2, _! := Step2(intermediate1, "something")
        // assign to result from the outer scope
        result, _! = Step3(intermediate2, 12)
    }
    return result, err
}

que é equivalente a

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    {
        var intermediate1 SomeType
        intermediate1, err = Step1()
        if err != nil { goto collectEnd }

        var intermediate2 SomeOtherType
        intermediate2, err = Step2(intermediate1, "something")
        if err != nil { goto collectEnd }

        result, err = Step3(intermediate2, 12)
        // if err != nil { goto collectEnd }, but since we're at the end already we can omit this
    }

collectEnd:
    return result, err
}

Algumas outras coisas boas que um recurso de sintaxe como este permitiria:

// try several approaches for acquiring a value
func GetSomething() (s *Something) {
    collect s {
        _! = fetchOrNil1()
        _! = fetchOrNil2()
        _! = new(Something)
    }
    return s
}

Novos recursos de sintaxe necessários:

  1. palavra-chave collect
  2. ident especial _! (joguei com isso no analisador, não é difícil fazer essa correspondência como um ident sem quebrar mais nada)

Estou sugerindo algo assim porque o argumento "tratamento de erros é muito repetitivo" pode ser reduzido a "verificações nulas são muito repetitivas". Go já tem muitos recursos de tratamento de erros que funcionam como estão. Você pode ignorar um erro com _ (ou apenas não capturar os valores de retorno), pode retornar um erro não modificado com if err != nil { return err } ou adicionar contexto e retornar com if err != nil { return wrap(err) } . Nenhum desses métodos, por si só, é muito repetitivo. A repetitividade ( obviamente ) vem de ter que repetir essas ou instruções de sintaxe semelhantes em todo o código. Acho que introduzir uma maneira de executar instruções até que um valor diferente de zero seja encontrado é uma boa maneira de manter o mesmo tratamento de erros, mas reduza a quantidade de clichê necessária para fazer isso.

  • Bom suporte para 1) ignorar um erro; 3) agrupar um erro com contexto adicional.

verifique, uma vez que permanece o mesmo (principalmente)

  • Embora o código de tratamento de erros deva ser claro, ele não deve dominar a função.

verifique, já que o código de tratamento de erros agora pode ir para um lugar se necessário, enquanto a parte mais importante da função pode acontecer de forma linearmente legível

  • O código Go 1 existente deve continuar a funcionar ou, pelo menos, deve ser possível traduzir mecanicamente o Go 1 para a nova abordagem com total confiabilidade.

verifique, isso é um acréscimo e não uma alteração

  • A nova abordagem deve encorajar os programadores a lidar com os erros corretamente.

verificar, eu acho, uma vez que os mecanismos para tratamento de erros não são diferentes - teríamos apenas uma sintaxe para "coletar" o primeiro valor não nulo de uma série de execuções e atribuições, que podem ser usadas para limitar o número de lugares que temos que escrever nosso código de tratamento de erros em uma função

  • Qualquer nova abordagem deve ser mais curta e / ou menos repetitiva do que a abordagem atual, embora permaneça clara.

Não tenho certeza se isso se aplica aqui, uma vez que o recurso sugerido se aplica a mais do que apenas o tratamento de erros. Eu realmente acho que ele pode encurtar e esclarecer o código que pode gerar erros, sem confundir em verificações nulas e retornos antecipados

  • A linguagem funciona hoje e cada mudança tem um custo. Não deve ser apenas uma lavagem, deve ser claramente melhor.

Concordo e, portanto, parece que uma mudança cujo escopo se estenda além do tratamento de erros pode ser apropriada. Acredito que o problema subjacente é que nil cheques em go tornam-se repetitivos e prolixos, e acontece que error é um tipo nil -able.

@KernelDeimos Basicamente, acabamos de chegar à mesma coisa. No entanto, eu dei um passo adiante e expliquei por que o método x, ? := doSomething() não funciona tão bem na prática. Embora seja legal ver que não sou a única pessoa que está pensando em adicionar o? operador para o idioma de uma forma interessante.

https://github.com/golang/go/issues/25582

Isso não é basicamente uma armadilha ?

Aqui está uma cuspideira:

func NewClient(...) (*Client, error) {
    trap(err error) {
        return nil, err
    }

    listener, err? := net.Listen("tcp4", listenAddr)
    trap(_ error) {
        listener.Close()
    }

    conn, err? := ConnectionManager{}.connect(server, tlsConfig)
    trap(_ error) {
        conn.Close()
    }

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?

    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?

    session, err? := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

59 linhas → 44

trap significa "executar este código em ordem de pilha se uma variável marcada com ? do tipo especificado não for o valor zero." É como defer mas pode afetar o fluxo de controle.

Eu meio que gosto da ideia trap , mas a sintaxe me incomoda um pouco. E se fosse um tipo de declaração? Por exemplo, trap err error {} declara um trap chamado err do tipo error que, quando atribuído, executa o código fornecido. O código nem mesmo precisa retornar; é apenas permitido fazer isso. Isso também quebra a dependência de nil ser especial.

Edit: Expandindo e dando um exemplo agora que não estou em um telefone.

func Example(r io.Reader) error {
  trap err error {
    if err != nil {
      return err
    }
  }

  n, err? := io.Copy(ioutil.Discard, r)
  fmt.Printf("Read %v bytes.\n", n)
}

Essencialmente, um trap funciona como um var , exceto que sempre que é atribuído com o operador ? anexado a ele, o bloco de código é executado. O operador ? também evita que seja obscurecido quando usado com := . Redeclarar trap no mesmo escopo, ao contrário de var , é permitido, mas deve ser do mesmo tipo que o existente; isso permite que você altere o bloco de código associado. Como o bloco em execução não retorna necessariamente, ele também permite que você tenha caminhos separados para coisas específicas, como verificar se err == io.EOF .

O que eu gosto nessa abordagem é que ela parece semelhante ao exemplo errWriter de Erros são valores , mas em uma configuração um pouco mais genérica que não requer a declaração de um novo tipo.

@carlmjohnson para quem você estava respondendo?
Independentemente disso, esse conceito trap parece ser apenas uma maneira diferente de escrever uma instrução defer , não? O código, conforme escrito, seria essencialmente o mesmo se você panic 'd em um erro não nulo e, em seguida, usasse um encerramento adiado para definir os valores de retorno nomeados e realizar a limpeza. Acho que isso apresenta os mesmos problemas de minha proposta anterior de usar _! para entrar em pânico automaticamente, pois coloca um método de tratamento de erros sobre o outro. FWIW também senti que o código, conforme escrito, era muito mais difícil de raciocinar do que o original. Esse conceito trap poderia ser imitado com go hoje, mesmo que menos claro do que ter uma sintaxe para ele? Eu sinto que poderia e seria if err != nil { panic (err) } e defer capturar e lidar com isso.

Parece semelhante ao conceito do bloco collect sugerido acima, que, pessoalmente, acho que fornece uma maneira mais limpa de expressar a mesma ideia ("se este valor não for nulo, quero capturá-lo e fazer algo com isso"). Go gosta de ser linear e explícito. trap parece uma nova sintaxe para panic / defer mas com um fluxo de controle menos claro.

@mccolljr , parecia ser uma resposta para mim . Deduzo de sua postagem que você não viu minha proposta (agora nos "itens ocultos", embora não tão longe lá em cima), porque na verdade usei uma instrução defer em minha proposta, estendida com um recover - como função para tratamento de erros.

Eu também observei que a reescrita de "trap" eliminou muitas funcionalidades da minha proposta (_muito_ erros diferentes aparecem) e, além disso, não está claro para mim como fatorar o tratamento de erros com as instruções trap. Grande parte dessa redução da minha proposta vem na forma de descartar a correção do tratamento de erros e, acredito, voltar a tornar mais fácil apenas retornar os erros diretamente do que qualquer outra coisa.

A capacidade de continuar o fluxo é permitida pelo exemplo trap modificado que dei acima . Eu editei depois, então não sei se você viu ou não. É muito semelhante a collect , mas acho que dá um pouco mais de controle sobre ele. Dependendo de como as regras de escopo funcionassem, poderia ser um pouco espaguete, mas acho que seria possível encontrar um bom equilíbrio.

@thejerf Ah, isso faz mais sentido. Não sabia que era uma resposta à sua proposta. No entanto, não está claro para mim qual seria a diferença entre erroring() e recover() , além do fato de que recover responde a panic . Parece que estaríamos implicitamente cometendo alguma forma de pânico quando um erro precisa ser retornado. Adiar também é uma operação um tanto cara, então não tenho certeza de como me sinto sobre usá-lo em todas as funções que podem gerar erros.

@DeedleFake O mesmo vale para trap , porque da maneira que eu vejo trap é essencialmente uma macro que insere código quando o operador ? é usado e apresenta seu próprio conjunto de preocupações e considerações, ou é implementado como um goto ... que, e se o usuário não retornar no bloco trap , ou for apenas um defer sintaticamente diferente. Além disso, e se eu declarar vários blocos trap em uma função? Isso é permitido? Em caso afirmativo, qual é executado? Isso adiciona complexidade de implementação. Go gosta de opinar, e gosto disso nisso. Acho que collect ou uma construção linear semelhante está mais alinhada com a ideologia de Go do que trap , que, como foi apontado para mim após minha primeira proposta, parece ser try-catch construir em traje.

e se o usuário não retornar no bloco trap

Se trap não retornar ou modificar de outra forma o fluxo de controle ( goto , continue , break , etc.), o fluxo de controle retorna para onde o código bloco foi 'chamado' de. O bloco em si funcionaria de forma semelhante a chamar um encerramento, com a exceção de que ele tem acesso aos mecanismos de fluxo de controle. Os mecanismos funcionariam no local em que o bloco é declarado, não no local de onde é chamado, portanto

for {
  trap err error {
    break
  }

  err? = errors.New("Example")
}

trabalharia.

Além disso, e se eu declarar vários blocos trap em uma função? Isso é permitido? Em caso afirmativo, qual é executado?

Sim, isso é permitido. Os blocos são nomeados pela armadilha, então é bastante simples descobrir qual deles deve ser chamado. Por exemplo, em

trap err error {
  // Block 1.
}

trap n int {
  // Block 2.
}

n? = 3

o bloco 2 é chamado. A grande questão nesse caso provavelmente seria o que acontece no caso de n?, err? = 3, errors.New("Example") , o que provavelmente exigiria que a ordem das atribuições fosse especificada, como surgiu em # 25609.

Acho que collect ou uma construção linear semelhante está mais alinhada com a ideologia de Go do que trap, que, como foi apontado para mim após minha primeira proposta, parece ser uma construção try-catch em traje.

Acho que collect e trap são essencialmente try-catch s ao contrário. Um try-catch padrão é uma política de falha por padrão que exige que você verifique ou explode. Este é um sistema bem-sucedido por padrão que permite especificar um caminho de falha, essencialmente.

Uma coisa que complica tudo é o fato de que os erros não são inerentemente tratados como uma falha, e alguns erros, como io.EOF , não especificam realmente a falha. Acho que é por isso que os sistemas que não estão vinculados a erros especificamente, como collect ou trap , são o caminho a percorrer.

"Ah, isso faz mais sentido. Não sabia que era uma resposta à sua proposta. No entanto, não está claro para mim qual seria a diferença entre errar () e recuperar (), além do fato de que recuperar responde a pânico."

Não ter muita diferença é o ponto. Estou tentando minimizar o número de novos conceitos criados e, ao mesmo tempo, obter o máximo de poder possível deles. Eu considero construir uma funcionalidade existente como um recurso, não um bug.

Um dos pontos da minha proposta é explorar além de apenas "e se consertarmos este pedaço recorrente de três linhas onde return err e substituí-lo por ? " para "como isso afeta o resto da linguagem? Que novos padrões permite? Que novas 'melhores práticas' cria? Que velhas 'melhores práticas' deixam de ser melhores práticas? " Não estou dizendo que terminei esse trabalho. E mesmo se for julgada, a ideia na verdade tem muito poder para o gosto de Go (já que Go não é uma linguagem de maximização de poder e mesmo com a escolha de design para limitá-la ao tipo error ainda é provavelmente a mais poderosa proposta feita neste tópico, o que quero dizer plenamente tanto no bom quanto no mau sentido de "poderoso"), acho que poderíamos estar explorando as questões do que os novos construtos farão aos programas como um todo, ao invés do que eles fará as funções de exemplo de sete linhas, e é por isso que tentei trazer os exemplos até ~ 50-100 linhas do intervalo de "código real". Tudo parece igual em 5 linhas, o que inclui o tratamento de erros do Go 1.0, o que talvez seja parte do motivo pelo qual todos sabemos, por experiência própria, que há um problema real aqui, mas a conversa simplesmente gira em círculos se falarmos sobre em uma escala muito pequena até que algumas pessoas comecem a se convencer de que talvez não haja um problema, afinal. (Confie em suas experiências reais de codificação, não nas amostras de 5 linhas!)

"Parece que estaríamos implicitamente cometendo alguma forma de pânico quando um erro precisa ser retornado."

Não está implícito. É explícito. Você usa o operador pop quando ele faz o que você deseja. Quando não faz o que você quer, você não o usa. O que ele faz é simples o suficiente para ser capturado em uma única frase simples, embora a especificação provavelmente levasse um parágrafo inteiro, pois é assim que essas coisas funcionam. Não há implícito. Além disso, não é um pânico porque só desdobra um nível da pilha, exatamente como um retorno; é tanto pânico quanto uma volta, o que não é nada.

Eu também não me importo se você soletrar pop como? como queiras. Pessoalmente, acho que uma palavra parece um pouco mais com Go, já que Go não é atualmente uma linguagem rica em símbolos, mas não posso negar que um símbolo tem a vantagem de não entrar em conflito com nenhum código-fonte existente. Estou interessado na semântica e no que podemos construir sobre eles e quais comportamentos a nova semântica oferece para programadores novos e experientes mais do que a ortografia.

"Adiar também é uma operação um tanto cara, então não tenho certeza de como me sinto sobre usá-lo em todas as funções que podem gerar erros."

Eu já reconheci isso. Embora eu sugira que em geral não seja tão caro e não me sinta mal em dizer que, para fins de otimização, se você tiver uma função importante, escreva da maneira atual. Não é explicitamente meu objetivo tentar modificar 100% de todas as funções de tratamento de erros, mas tornar 80% delas muito mais simples e corretas e deixar os casos de 20% (provavelmente mais como 98/2, honestamente) permanecerem como estão são. Grande parte do código Go não é sensível ao uso de adiar, que é, afinal, a razão pela qual defer existe.

Na verdade, você pode modificar trivialmente a proposta para não usar adiar e usar alguma palavra-chave como trap como uma declaração que é executada apenas uma vez, independentemente de onde apareça, em vez de a forma como adiar é na verdade uma instrução que empurra um manipulador para a pilha de funções adiadas. Eu escolhi deliberadamente reutilizar defer para evitar adicionar novos conceitos à linguagem ... até mesmo entendendo as armadilhas que poderiam resultar de defers em loops inesperadamente mordendo pessoas. Mas ainda é apenas um conceito defer para entender.

Apenas para deixar claro que adicionar uma nova palavra-chave ao idioma é uma alteração importante.

package main

import (
    "fmt"
)

func return(i int)int{
    return i
}

func main() {
    return(1)
}

resulta em

prog.go:7:6: syntax error: unexpected select, expecting name or (

O que significa que se tentarmos adicionar try , trap , assert , qualquer palavra-chave na linguagem, corremos o risco de quebrar uma tonelada de código. Código que pode ser mantido por mais tempo.

É por isso que inicialmente propus adicionar um operador ? go especial que pode ser aplicado a variáveis ​​no contexto de instruções. O caractere ? partir de agora é designado como caractere ilegal para nomes de variáveis. O que significa que ele não está em uso em nenhum código Go atual e, portanto, podemos introduzi-lo sem incorrer em alterações significativas.

Agora, a questão de usá-lo no lado esquerdo de uma atribuição é que ele não leva em consideração que Go permite vários argumentos de retorno.

Por exemplo, considere esta função

func getCoord() x int, y int, z int, err error{
    x, err = getX()
    if err != nil{
        return 
    }

    y, err = getY()
    if err != nil{
        return 
    }

    z, err = getZ()
        if err != nil{
        return 
    }
    return
}

se usarmos? ou tente nos lhs de atribuição para se livrar dos blocos if err! = nil, presumimos automaticamente que os erros significam que todos os outros valores agora são lixo? E se nós fizéssemos assim

func GetCoord() (x, y, z int, err error) {
    err = try GetX(&x) // or err? = GetX(&x) 
    err = try GetY(&y) // or err? = GetY(&x) 
    err = try GetZ(&z) // or err? = GetZ(&x) 
}

que suposições fazemos aqui? Que não deve ser prejudicial simplesmente presumir que não há problema em jogar fora o valor? e se o erro for mais um aviso e o valor x estiver correto? E se a única função que gera o erro for a chamada para GetZ () e os valores x, y forem realmente bons? Presumimos devolvê-los. E se não usarmos argumentos de retorno nomeados? E se os argumentos de retorno forem tipos de referência como um mapa ou um canal, devemos presumir que é seguro retornar nil para o chamador?

TLDR; adicionando? ou try para atribuições em um esforço para eliminar

if err != nil{
    return err
}

introduz muita confusão do que vantagens.

E adicionar algo como a sugestão trap introduz a possibilidade de quebra.

É por isso que na minha proposta eu fiz em uma edição separada. Permiti a capacidade de declarar func ?() bool em qualquer tipo para que, quando você ligasse, dissesse

x, err := doSomething; return x, err?    

você pode fazer com que o efeito colateral da armadilha aconteça de uma forma que se aplique a qualquer tipo.

E aplicando o? trabalhar apenas em declarações como mostrei permite a programação das declarações. Em minha proposta, sugiro permitir uma instrução switch especial que permite a alguém alternar casos que são a palavra-chave +?

switch {
    case select?:
    //side effect/trap code specific to select
    case return?:
    //side effect/trap code specific to returns
    case for?: 
    //side effect/trap code specific to for? 

    //etc...
}  

Se estivermos usando? em um tipo que não tem um explícito? função declarada ou um tipo embutido, então o comportamento padrão de verificar se var == nil || valor zero {execute a instrução} é a intenção presumida.

Idk, não sou um especialista em design de linguagem de programação, mas este não é

Por exemplo, a função os.Chdir está atualmente

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

Sob esta proposta, poderia ser escrito como

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

essencialmente a mesma coisa que as funções de seta do javascript ou como Dart define "sintaxe de seta gorda"

por exemplo

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

do tour de dardos .

Para funções que contêm apenas uma expressão, você pode usar uma sintaxe abreviada:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
A sintaxe => expr é uma abreviação para {return expr; } A notação => às vezes é chamada de sintaxe de seta grande.

@mortdeus , o lado esquerdo da seta do dardo é uma assinatura de função, enquanto syscall.Chdir(dir) é uma expressão. Eles parecem mais ou menos não relacionados.

@mortdeus Esqueci de esclarecer antes, mas a ideia que comentei aqui dificilmente é semelhante à proposta que você marcou. Eu gosto da ideia de ? como um espaço reservado, então copiei, mas minha ideia enfatizou a reutilização de um único bloco de código para lidar com os erros, evitando alguns dos problemas conhecidos com try...catch . Tive muito cuidado ao inventar algo sobre o qual não havia falado antes, para que pudesse contribuir com uma nova ideia.

Que tal uma nova instrução condicional return (ou returnIf )?

return(bool expression) ...

ou seja,

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

a, b, err := Foo()    // signature: func Foo() (string, string, error)
return(err != nil) "", "", err

Ou apenas deixe fmt formatar as funções de retorno de uma linha em uma linha em vez de três:

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

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil { return "", "", err }

Ocorre-me que posso obter tudo o que desejo apenas com a adição de algum operador que retorna antes se o erro mais à direita não for nulo, se eu combiná-lo com parâmetros nomeados:

func NewClient(...) (c *Client, err error) {
    defer annotateError(&err, "client couldn't be created")

    listener := net.Listen("tcp4", listenAddr)?
    defer closeOnErr(&err, listener)
    conn := ConnectionManager{}.connect(server, tlsConfig)?
    defer closeOnErr(&err, conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?
    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?
    session := communicationProtocol.FinalProtocol(conn)?
    client.session = session

    return client, nil
}

func closeOnErr(err *error, c io.Closer) {
    if *err != nil {
        closeErr := c.Close()
        if err != nil {
            *err = multierror.Append(*err, closeErr)
        }
    }
}

func annotateError(err *error, annotation string) {
    if *err != nil {
        log.Printf("%s: %v", annotation, *err)
        *err = errwrap.Wrapf(annotation +": {{err}}", err)
    }
}

Atualmente, meu entendimento do consenso de Go é contra os parâmetros nomeados, mas se as possibilidades dos parâmetros de nome mudam, o consenso também pode. Claro, se o consenso for suficientemente forte contra isso, há uma opção de incluir acessores.

Essa abordagem me dá o que estou procurando (tornando mais fácil lidar com erros sistematicamente na parte superior de uma função, capacidade de fatorar esse código, também redução da contagem de linha) com praticamente qualquer uma das outras propostas aqui também, incluindo a original . E mesmo que a comunidade Go decida que não gosta, não tenho que me importar, porque está em meu código em uma base de função por função e não tem incompatibilidade de impedância em nenhuma direção.

Embora eu expresse uma preferência por uma proposta que permite uma função de assinatura func GetInt() (x int, err error) a ser usada no código com OtherFunc(GetInt()?, "...") (ou qualquer que seja o resultado final) para uma que não pode ser composta em uma expressão. Embora seja um aborrecimento menor para a cláusula de manipulação de erros simples repetitiva constante, a quantidade de meu código que desempacota uma função de arity 2 apenas para que possa ter o primeiro resultado ainda é irritantemente substancial e realmente não acrescenta nada à clareza do código resultante.

@thejerf , acho que há muitos comportamentos estranhos aqui. Você está chamando net.Listen , que retorna um erro, mas não é atribuído. E então você adia, passando err . Cada novo defer substitui o último, de modo que annotateError nunca seja invocado? Ou eles se acumulam, de forma que se um erro for retornado de, digamos, toServer.Send , então closeOnErr é chamado duas vezes e, em seguida, annotateError é chamado? closeOnErr chamado apenas se a chamada anterior tiver uma assinatura correspondente? Que tal este caso?

conn := ConnectionManager{}.connect(server, tlsConfig)?
fmt.Printf("Attempted to connect to server %#v", server)
defer closeOnErr(&err, conn)

Ler o código também confunde as coisas, como por que não posso simplesmente dizer

client.session = communicationProtocol.FinalProtocol(conn)?

Presumivelmente, porque FinalProtocol está retornando um erro? Mas isso está escondido do leitor.

Por fim, o que acontece quando desejo relatar um erro e me recuperar em uma função? Parece que seu exemplo impediria esse caso?

_Termo aditivo_

Ok, acho que quando você quiser se recuperar de um erro, pelo seu exemplo, você o atribui, como nesta linha:

env, err := environment.GetRuntimeEnvironment()

Tudo bem porque err é sombreado, mas então se eu mudei ...

forwardPort, err = env.PortToForward()
if err != nil {
    log.Printf("env couldn't provide forward port: %v", err)
}

para somente

forwardPort = env.PortToForward()

Então seu erro adiado manipulado não o pegará, porque você está usando o err criado no escopo. Ou eu estou esquecendo de alguma coisa?

Acredito que uma adição à sintaxe que indica que uma função pode falhar é um bom começo. Proponho algo neste sentido:

func (r Reader) Read(b []byte) (n int) fails {
    if somethingFailed {
        fail errors.New("something failed")
    }

    return 0
}

Se uma função falhar (usando a palavra-chave fail vez de return , ela retornará o valor zero para cada parâmetro de retorno.

func (c EmailClient) SendEmail(to, content string) fails {
    if !c.connected() {
        fail errors.New("could not connect")
    }

    // You can handle it and execution will continue if you don't fail or return
    n := r.Read(b) handle (err) {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    n := r.Read(b)
}

Essa abordagem terá o benefício de permitir que o código atual funcione, enquanto habilita um novo mecanismo para melhor tratamento de erros (pelo menos na sintaxe).

Comentários sobre as palavras-chave:

  • fails pode não ser a melhor opção, mas é a melhor que posso pensar no momento. Pensei em usar err (ou errs ), mas como eles são usados ​​atualmente pode tornar isso uma escolha ruim devido às expectativas atuais ( err é provavelmente um nome de variável, e errs pode ser considerado um slice ou array ou erros).
  • handle pode ser um pouco enganador. Eu queria usar recover , mas é usado para panic s ...

editar: Alteração da invocação de r.Read para corresponder a io.Reader.Read() .

Parte da razão para essa sugestão é porque a abordagem atual em Go não ajuda as ferramentas a entender se um error retornado denota uma falha de função ou está retornando um valor de erro como parte de sua função (por exemplo, github.com/pkg/errors ).

Acredito que habilitar funções para expressar falhas explicitamente é o primeiro passo para melhorar o tratamento de erros.

@ibrasho , como seu exemplo é diferente de ...

func (c EmailClient) SendEmail(to, content string) error {
    // ...

    // You can handle it and execution will continue if you don't fail or return
    _, _, err := r.Read()
        if err != nil {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    _, _, err := r.Read()
}

... se dermos avisos do compilador ou lint para instâncias não tratadas de error ? Nenhuma mudança de idioma necessária.

Duas coisas:

  • Acho que a sintaxe proposta parece melhor. 😁
  • Minha versão requer dar às funções a capacidade de declarar que falham explicitamente. Isso é algo que está faltando no Go atualmente e que pode permitir que as ferramentas façam mais. Sempre podemos tratar uma função que retorna um error como falha, mas isso é uma suposição. E se a função retornar 2 error valores?

Minha sugestão tinha algo que eu removi, que era a propagação automática:

func (c EmailClient) SendEmail(to, content string) fails {
    n := r.Read(b)

    // Would automaticaly propgate the error, so it will be equivlent to this:
    // n := r.Read(b) handle (err) {
    //  fail err
    // }
}

Eu removi isso, pois acho que o tratamento de erros deve ser explícito.

editar: Alteração da invocação de r.Read para corresponder a io.Reader.Read() .

Então, essa seria uma assinatura ou protótipo válido?

func (r *MyFileReader) Read(b []byte) (n int, err error) fails

(Considerando que uma implementação io.Reader atribui io.EOF quando não há mais nada para ler e algum outro erro para condições de falha.)

sim. Mas ninguém deve esperar que err denote uma falha na função que está fazendo seu trabalho. O erro de falha de leitura deve ser passado para o bloco de manuseio. Erros são valores em Go, e aquele retornado por esta função não deve ter nenhum significado especial além de ser algo que esta função retorna (por algum motivo estranho).

Eu estava propondo que uma falha resultaria em valores de retorno sendo retornados como valores zero. O Reader.Read atual já faz algumas promessas que podem não ser possíveis com esta nova abordagem.

Quando Read encontra um erro ou condição de fim de arquivo depois de ler com sucesso n> 0 bytes, ele retorna o número de bytes lidos. Ele pode retornar o erro (não nulo) da mesma chamada ou retornar o erro (en == 0) de uma chamada subsequente. Uma instância desse caso geral é que um Reader retornando um número diferente de zero de bytes no final do fluxo de entrada pode retornar err == EOF ou err == nil. A próxima leitura deve retornar 0, EOF.

Os chamadores devem sempre processar os n> 0 bytes retornados antes de considerar o erro err. Isso trata corretamente os erros de E / S que acontecem após a leitura de alguns bytes e também de ambos os comportamentos EOF permitidos.

As implementações de Read são desencorajadas a retornar uma contagem de zero byte com um erro nulo, exceto quando len (p) == 0. Os chamadores devem tratar um retorno de 0 e nulo como uma indicação de que nada aconteceu; em particular, não indica EOF.

Nem todo esse comportamento é possível na abordagem proposta atualmente. No contrato de interface de leitura atual, vejo algumas deficiências, por exemplo, como lidar com leituras parciais.

Em geral, como uma função deve se comportar quando estiver parcialmente concluída no momento em que falhar? Sinceramente, ainda não pensei nisso.

O caso de io.EOF é simples:

func DoSomething(r io.Reader) fails {
    // I'm using rerr so that I don't shadow the err returned from the function
    n, err := r.Read(b) handle (rerr) {
        if rerr != io.EOF {
            fail err
        }
        // Else do nothing?
    }
}

@thejerf , acho que há muitos comportamentos estranhos aqui. Você está chamando net.Listen, que retorna um erro, mas não é atribuído.

Estou usando o operador ? proposto por várias pessoas para indicar o retorno do erro com valores zero para os outros valores, se o erro não for nulo. Eu prefiro um pouco uma palavra curta a um operador, pois não acho que Go seja uma linguagem com muitos operadores, mas se ? fosse para o idioma eu ainda contaria meus ganhos em vez de pisar em um bufo.

Cada novo adiamento substitui o último, de modo que annotateError nunca seja chamado? Ou eles empilham, de forma que se um erro for retornado de, digamos, toServer.Send, então closeOnErr é chamado duas vezes e, em seguida, annotateError é chamado?

Funciona como o adiamento agora: https://play.golang.org/p/F0xgP4h5Vxf Eu esperava alguns downthumbs para aquele post, para o qual minha resposta planejada seria apontar que _já_ é como o adiamento funciona e você apenas estar diminuindo o comportamento atual do Go, mas não conseguiu. Ai de mim. Como esse trecho também mostra, sombreamento não é um problema, ou pelo menos, não é mais um problema do que já é. (Isso não iria consertá-lo, nem torná-lo particularmente pior.)

Eu acho que um aspecto que pode ser confuso é que já é o caso no Go atual que um parâmetro nomeado vai acabar sendo "o que quer que seja realmente retornado", então você pode fazer o que eu fiz e pegar um ponteiro para ele e passar isso para uma função adiada e manipulá-la, independentemente de você retornar diretamente um valor como return errors.New(...) , que pode intuitivamente parecer uma "nova variável" que não é a variável nomeada, mas na verdade Go vai acabar com ele atribuído à variável nomeada no momento em que o adiamento é executado. É fácil ignorar este detalhe específico do Go atual agora. Afirmo que, embora possa ser confuso agora, se você trabalhou até mesmo em uma base de código que usava esse idioma (ou seja, não estou dizendo que isso ainda tem que se tornar "Go melhor prática", apenas algumas exposições fariam), você ' eu descobriria muito rapidamente. Porque, só para dizer mais uma vez para ficar bem claro, é assim que o Go já funciona, não uma mudança proposta.

Aqui está uma proposta que eu acho que não foi sugerida antes. Usando um exemplo:

 r, !handleError := something()

O significado disso é o mesmo:

 r, _xyzzy := something()
 if ok, R := handleError(_xyzzy); !ok { return R }

(onde _xyzzy é uma variável nova cujo escopo se estende apenas a essas duas linhas de código, e R pode ter vários valores).

As vantagens desta proposta não são específicas para erros, não trata de valores zero de maneira especial e é fácil especificar concisamente como agrupar erros dentro de qualquer bloco de código em particular. As mudanças de sintaxe são pequenas. Dada a tradução direta, é fácil entender como esse recurso funciona.

As desvantagens são que ele introduz um retorno implícito, não se pode escrever um manipulador genérico que apenas retorne o erro (uma vez que seus valores de retorno precisam ser baseados na função de onde é chamado) e que o valor que é passado para o manipulador é não disponível no código de chamada.

Veja como você pode usá-lo:

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

Ou você pode usá-lo em um teste para chamar t.Fatal se o seu código init falhar:

func TestSomething(t *testing.T) {
  must := func(err error) bool { t.Fatalf("init code failed: %s", err); return true }
  !must := setupTest()
  !must := clearDatabase()
  ...
}

Eu sugeriria alterar a assinatura da função para apenas func(error) error . Isso simplifica a maioria dos casos e, se você precisar analisar mais a fundo o erro, basta usar o mecanismo atual.

Pergunta de sintaxe: você pode definir a função embutida?

func Read(filename string) error {
    f, !func(err error) error {
        if err != nil { return true, fmt.Errorf("... %s", err) }
        return false, nil
    } := OpenFile(filename)
    /...

Estou confortável com "não faça isso", mas a sintaxe provavelmente deve permitir reduzir a contagem de casos especiais. Isso também permitiria:

func failed(s string) func(error) error {
    return func(err error) {
       // returns a decorated error with the given string
   }
}

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

O que me parece uma das melhores propostas para esse tipo de coisa, pelo menos em termos de colocar essas coisas de forma concisa. Este é outro caso em que IMHO você está emitindo erros melhores neste ponto do que o código baseado em exceção tende a fazer, que muitas vezes não consegue obter isso detalhado sobre a natureza do erro porque é tão fácil permitir que as exceções se propaguem.

Também sugiro, por motivos de desempenho, que as funções de erro bang sejam definidas como não sendo chamadas com valores zero. Isso mantém o impacto no desempenho mínimo; no caso que acabei de mostrar, se Read for bem-sucedido normalmente, não será mais caro do que uma implementação de leitura atual que já está if ing em cada erro e falha na cláusula if . Se estivermos chamando uma função em nil o tempo todo, isso se tornará muito caro sempre que não puder ser embutido, o que acabará sendo uma quantidade não trivial de tempo. (Se um erro estiver ocorrendo ativamente, provavelmente podemos justificar e permitir uma chamada de função em quase todas as circunstâncias (se você não puder voltar para o método atual), mas realmente não queremos isso para não erros.) também significa que as funções bang podem assumir um valor diferente de zero em sua implementação, o que as simplifica também.

@thejerf caminho bom, mas feliz está fortemente alterado.
muitas mensagens atrás houve sugestões para ter um tipo de Ruby como "ou" sintaxe - f := OpenFile(filename) or failed("couldn't open file") .

Uma preocupação adicional - isso é para qualquer tipo de parâmetro ou apenas para erros? se for apenas para erros - o erro de tipo deve ter um significado especial para o compilador.

@thejerf caminho bom, mas feliz está fortemente alterado.

Eu recomendaria distinguir entre o caminho provavelmente comum da proposta original onde se parece com a sugestão original de paulhankin:

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

talvez até mesmo com herr fatorado em algum lugar, e minhas explorações do que seria necessário para especificá-lo completamente, o que é uma necessidade para esta conversa, e minhas próprias reflexões sobre como eu poderia usá-lo em meu código pessoal, que é meramente uma exploração do que mais é permitido e oferecido por uma sugestão. Eu já disse literalmente que inlining uma função provavelmente é uma má ideia, mas a gramática provavelmente deve permitir que ela mantenha a gramática simples. Já posso escrever uma função Go que tenha três funções e incorporar todas elas diretamente na chamada. Isso não significa que o Go esteja quebrado ou que ele precise fazer algo para evitar isso; significa que não devo fazer isso se valorizo ​​a clareza do código. Gosto do fato de que Go oferece clareza de código, mas ainda há algum grau de responsabilidade irredutível dos desenvolvedores em manter o código claro.

Se você vai me dizer que o "caminho feliz" para

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

está amassado e difícil de ler, mas o caminho feliz é fácil de ler com

func Read(filename string) error {
  f, err := OpenFile(filename)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  b, err := ReadBytes(f)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  err = ProcessBytes(b)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  return nil
}

então, sugiro que a única maneira possível pela qual a segunda seja mais fácil de ler é que você já está acostumado a lê-la e seus olhos estão treinados para pular exatamente o caminho certo para ver o caminho da felicidade. Eu posso adivinhar isso, porque os meus também. Mas, depois de se acostumar com uma sintaxe alternativa, você também será treinado para isso. Você deve analisar a sintaxe com base em como se sentirá quando já estiver acostumado, não como se sente agora.

Eu também observaria que as novas linhas adicionadas no segundo exemplo representam o que acontece com meu código real. Não são apenas "linhas" de código que o tratamento de erros Go atual tende a adicionar ao código, mas também adiciona muitos "parágrafos" ao que, de outra forma, deveria ser uma função muito simples. Quero abrir o arquivo, ler alguns bytes e processá-los. Eu não quero

Abra um arquivo.

E então leia alguns bytes, se estiver tudo bem.

E então processe-os, se estiver tudo bem.

Eu sinto que há muitos votos negativos que chegam a "não é isso que estou acostumado", em vez de uma análise real de como isso vai funcionar no código real, uma vez que você está acostumado com eles e usando-os fluentemente .

Não gosto da ideia de esconder a declaração de retorno, prefiro:

f := OpenFile(filename) or return failed("couldn't open file")
....
func failed(msg string, err error) error { ... } 

Neste caso or é um operador de encaminhamento condicional zero ,
encaminhando o último retorno se for diferente de zero.
Existe uma proposta semelhante em C # usando o operador ?>

f := OpenFile(filename) ?> return failed("couldn't open file")

@thejerf "caminho feliz" no seu caso prefixado por chamada para falha (...), que pode ser muito longa. também soa como yoda: rofl:

Os caracteres

Por favor, não torne este caminho mais complexo do que é agora. Mover realmente os mesmos códigos em uma linha (em vez de 3 ou mais) não é realmente uma solução. Eu pessoalmente não vejo nenhuma dessas propostas tão viável. Pessoal, a matemática é muito simples. Adote a ideia "Try-catch" ou mantenha as coisas como estão agora, o que significa muitos "if then else" se ruídos de código e não é realmente adequado para usar em padrões OO como Interface Fluent.

Muito obrigado por todas as suas mãos para baixo e talvez algumas mãos para cima ;-) (brincadeira)

@KamyarM IMO, "use a alternativa mais conhecida ou não faça nenhuma alteração" não é uma declaração muito produtiva. Ele interrompe a inovação e facilita argumentos circulares.

@KernelDeimos Eu concordo com você, mas vejo muitos comentários neste tópico que estava essencialmente defendendo o antigo com mover 4 5 linhas exatas em uma única linha que eu não vejo como uma solução real e também muitos na comunidade Go rejeitam RELIGIOSAMENTE o uso Try-Catch que fecha as portas a quaisquer outras opiniões. Eu pessoalmente acho que aqueles que inventaram este conceito de try-catch realmente pensaram sobre isso e embora possa ter poucas falhas, essas falhas são apenas causadas por hábitos de programação ruins e não há como forçar os programadores a escrever bons códigos, mesmo se você remover ou limitar todos os recursos bons ou alguns podem dizer ruins que uma linguagem de programação pode ter.
Eu propus algo assim antes e isso não é exatamente java ou C # try-catch e eu acho que pode suportar o tratamento de erros e bibliotecas atuais e eu uso um dos exemplos acima. Então, basicamente, o compilador verifica os erros após cada linha e pula para o bloco catch se o valor de err não for definido como nulo:

try (var err error){ 
    f, err := OpenFile(filename)
    b, err := ReadBytes(f)
    err = ProcessBytes(b)
    return nil
} catch (err error){ //Required
   return err
} finally{ // Optional
    // Do something else like close the file or connection to DB. Not necessary in this example since we  return earlier.
}

@KamyarM
Em seu exemplo, como posso saber (no momento de escrever o código) qual método retornou o erro? Como faço para cumprir a terceira forma de lidar com o erro ("retornar o erro com informações contextuais adicionais")?

@urandom
uma maneira é usar a opção Go e encontrar o tipo de exceção na captura. Vamos dizer que você tem a exceção pathError que você sabe que é causada por OpenFile () dessa forma. Outra maneira que não é muito diferente da atual manipulação de erros if err! = Nil em GoLang é esta:

try (var err error){ 
    f, err := OpenFile(filename)
} catch (err error){ //Required
   return err
}
try (var err error){ 
    b, err := ReadBytes(f)
catch (err error){ //Required
   return err
}
try (var err error){ 
    err = ProcessBytes(b)
catch (err error){ //Required
   return err
}
return nil

Portanto, você tem opções dessa forma, mas não está limitado. Se você realmente deseja saber exatamente qual linha causou o problema, coloque cada linha em um try catch da mesma maneira que agora escreve muitos if-then-elses. Se o erro não for importante para você e você quiser passá-lo para o método do chamador que, nos exemplos discutidos neste tópico, são na verdade sobre isso, acho que meu código proposto apenas dá conta do recado.

@KamyarM Vejo de onde você está vindo agora. Eu diria que se houver tantas pessoas contra try ... catch, vejo isso como uma evidência de que try ... catch não é perfeito e tem falhas. É uma solução fácil para um problema, mas se Go2 puder fazer o tratamento de erros melhor do que o que vimos em outras linguagens, acho que seria muito legal.

Acho que é possível pegar o que há de bom em try ... catch sem pegar o que há de ruim em try ... catch, que propus anteriormente. Concordo que transformar três linhas em 1 ou 2 não resolve nada.

O problema fundamental, a meu ver, é que o código de tratamento de erros em uma função é repetido se parte da lógica for "retornar ao chamador". Se você quiser alterar a lógica em qualquer ponto, terá que alterar todas as instâncias de if err != nil { return nil } .

Dito isso, eu realmente gosto da ideia de try...catch desde que as funções não possam throw nada implicitamente.

Outra coisa que acho que seria útil é se a lógica em catch {} exigisse uma palavra-chave break para interromper o fluxo de controle. Às vezes, você deseja tratar um erro sem interromper o fluxo de controle. (ex: "para cada item desses dados, faça algo, adicione um erro não nulo a uma lista e continue")

@KernelDeimos Concordo totalmente com isso. Eu vi a situação exata assim. Você precisa capturar os erros o máximo que puder antes de quebrar os códigos. Se algo como canais pudesse ser usado nessas situações no GoLang, isso seria bom. Então você poderia enviar todos os erros para aquele canal que o catch está esperando e então o catch poderia tratá-los um por um.

Prefiro misturar "ou retornar" com # 19642, # 21498 do que usar try..catch (defer / panic / recover já existe; jogar dentro da mesma função é como ter várias instruções goto e ficar confuso com a troca de tipo adicional dentro de catch; permite esquecer o tratamento de erros por ter try..catch no alto da pilha (ou complicar significativamente o compilador se o escopo try..catch dentro de uma única função)

@egorse
Parece que a sintaxe try-catch que @KamyarM está sugerindo é um açúcar de sintaxe para lidar com variáveis ​​de retorno de erro, não uma introdução para exceções. Embora eu prefira uma sintaxe de tipo "ou retorno" por vários motivos, parece uma sugestão legítima.

Dito isso, @KamyarM , por que try tem uma parte de definição de variável nele? Você está definindo uma variável err , mas ela está sendo sombreada pelas outras err variáveis ​​dentro do próprio bloco. Qual é seu propósito?

Acho que é para dizer a ele em qual variável ficar de olho, permitindo que seja desacoplado do tipo error . Provavelmente exigiria uma mudança nas regras de sombreamento, a menos que você apenas precisasse que as pessoas tomassem muito cuidado com isso. Não tenho certeza sobre a declaração no bloco catch , no entanto.

@egorse Exatamente o que @DeedleFake mencionou, é o propósito disso. Isso significa que o bloco try está de olho naquele objeto. Também limita seu escopo. É algo semelhante à instrução using em C #. Em C #, o (s) objeto (s) que são definidos usando a palavra-chave são automaticamente descartados assim que o bloco é executado e o escopo desse (s) objeto (s) é limitado ao bloco "Usando".
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement

Usar o catch é necessário porque queremos forçar o programador a decidir como lidar com o erro da maneira certa. Em C # e Java, a captura também é obrigatória. em C #, se você não quiser tratar uma exceção, não use try-catch nessa função. Quando ocorre uma exceção, qualquer método na hierarquia de chamadas pode tratar a exceção ou até mesmo relançá-la (ou envolvê-la em outra exceção) novamente. Não acho que você possa fazer a mesma coisa em Java. Em Java, um método que pode lançar uma exceção precisa declará-la na assinatura da função.

Quero enfatizar que este bloco try-catch não é o exato. Usei essas palavras-chave porque são semelhantes no que se deseja alcançar e também é o que muitos programadores estão familiarizados e são ensinados na maioria dos cursos de conceito de programação.

Pode haver uma atribuição _return on error_, que funciona apenas se houver um _parâmetro de retorno de erro nomeado_, como em:

func process(someInput string) (someOutput string, err error) {
    err ?= otherAction()
    return
}

Se err não for nil então retorne.

Acho que esta discussão sobre adicionar um açúcar try ao Rust seria esclarecedora para os participantes desta discussão.

FWIW, um pensamento antigo sobre como simplificar o tratamento de erros (desculpas se isso for um absurdo):

O identificador de aumento , denotado pelo símbolo circunflexo ^ , pode ser usado como um dos operandos no lado esquerdo de uma atribuição. Para os propósitos da atribuição, o identificador de aumento é um apelido para o último valor de retorno da função contida, quer o valor tenha um nome ou não. Após a conclusão da atribuição, a função testa o último valor de retorno em relação ao valor zero de seu tipo (nil, 0, falso, ""). Se for considerado zero, a função continua em execução, caso contrário, retorna.

O objetivo principal do identificador de aumento é propagar de forma concisa os erros das funções chamadas de volta ao chamador em um determinado contexto, sem ocultar o fato de que isso está ocorrendo.

Como exemplo, considere o seguinte código:

func Alpha() (string, error) {

    b, ^ := beta()
    g, ^ := gamma()
    return b + g, nil
}

Isso é aproximadamente equivalente a:

func Alpha() (ret1 string, ret2 error) {

    b, ret2 := beta()
    if ret2 != nil {
        return
    }

    g, ret2 := gamma()
    if ret2 != nil {
        return
    }

    return b + g, nil
}

O programa está malformado se:

  • o identificador de aumento é usado mais de uma vez em uma atribuição
  • a função não retorna um valor
  • o tipo do último valor de retorno não tem um teste significativo e eficiente para zero

Essa sugestão é semelhante a outras no sentido de que não aborda o problema de fornecer maiores informações contextuais, seja lá o que for.

@gboyle É por isso que o último valor de retorno IMO deve ser nomeado e do tipo error . Isso tem duas implicações importantes:

1 - outros valores de retorno são nomeados também, portanto
2 - eles já têm valores zero significativos.

@ object88 Como nos context , isso requer alguma ação da equipe principal, como definir um tipo error embutido (apenas um Go error normal) com alguns atributos comuns (mensagem? pilha de chamadas? etc etc).

AFAIK, não há muitas construções de linguagem contextual em Go. Além de go e defer não existem outros e mesmo estes dois são muito explícitos e claros (bot na sintaxe - e à vista - e semântica).

Que tal algo assim?

(copiou algum código real em que estou trabalhando):

func (g *Generator) GenerateDevices(w io.Writer) error {
    var err error
    catch err {
        _, err = io.WriteString(w, "package cc\n\nconst (") // if err != nil { goto Caught }
        for _, bd := range g.zwClasses.BasicDevices {
            _, err = w.Write([]byte{'\t'}) // if err != nil { goto Caught }
            _, err = io.WriteString(w, toGoName(bd.Name)) // if err != nil { goto Caught }
            _, err = io.WriteString(w, " BasicDeviceType = ") // if err != nil { goto Caught }
            _, err = io.WriteString(w, bd.Key) // if err != nil { goto Caught }
            _, err = w.Write([]byte{'\n'}) // if err != nil { goto Caught }
        }
        _, err = io.WriteString(w, ")\n\nvar BasicDeviceTypeNames = map[BasicDeviceType]string{\n") // if err != nil { goto Caught }
       // ...snip
    }
    // Caught:
    return err
}

Quando err não é nulo, ele chega ao final da instrução "catch". Você pode usar "catch" para agrupar chamadas semelhantes que normalmente retornam o mesmo tipo de erro. Mesmo se as chamadas não estivessem relacionadas, você poderia verificar os tipos de erro posteriormente e envolvê-los de forma adequada.

@lukescott leu esta postagem do blog por @robpike https://blog.golang.org/errors-are-values

@davecheney A ideia catch (sem tentar) mantém o espírito desse sentimento. Ele trata o erro como um valor. Ele simplesmente é interrompido (dentro da mesma função) quando o valor não é mais nulo. Não bloqueia o programa de forma alguma.

@lukescott você pode usar a técnica de Rob hoje, você não precisa mudar o idioma.

Há uma diferença bastante grande entre exceções e erros:

  • erros são esperados (podemos escrever um teste para eles),
  • exceções não são esperadas (daí a "exceção"),

Muitas línguas tratam ambos como exceções.

Entre os genéricos e o melhor tratamento de erros, eu escolheria o melhor tratamento de erros, uma vez que a maior parte da confusão de código em Go vem do tratamento de erros. Embora possa ser dito que esse tipo de verbosidade é bom e favorece a simplicidade, IMO também obscurece o _caminho feliz_ de um fluxo de trabalho ao nível de ser ambíguo.

Eu gostaria de desenvolver a proposta de @thejerf um pouco.

Primeiro, em vez de ! , um operador or é introduzido, esse deslocamento é o último argumento retornado da chamada de função no lado esquerdo e invoca uma instrução de retorno à direita, cuja expressão é uma função que é chamada, se o argumento deslocado for diferente de zero (não nill para tipos de erro), passando esse argumento. Tudo bem se as pessoas pensarem que deveria ser apenas para tipos de erro também, embora eu sinta que essa construção será útil para funções que retornam um booleano como seu último argumento também (é algo ok / não ok).

O método Read terá a seguinte aparência:

func Read(filename string) error {
  f := OpenFile(filename) or return errors.Contextf("opening file %s", filename)
  b := ReadBytes(f) or return errors.Contextf("reading file %s", filename)
  ProcessBytes(b) or return errors.Context("processing data")
  return nil
}

Estou assumindo que o pacote de erros fornece funções de conveniência como as seguintes:

func Noop() func(error) error {
   return func(err error) {
       return err   
   }
}


func Context(msg string) func(error) error {
    return func(err error) {
        return fmt.Errorf("%s: %v", msg, err)
    }
}
...

Parece perfeitamente legível, ao mesmo tempo que cobre todos os pontos necessários, e também não parece muito estranho, devido à familiaridade da instrução return.

@urandom Nesta declaração f := OpenFile(filename) or return errors.Contextf("opening file %s", filename) como o motivo pode ser conhecido? Por exemplo, é a falta de permissão de leitura ou o arquivo não existe?

@ dc0d
Bem, mesmo no exemplo acima, o erro original está incluído, pois a mensagem fornecida pelo usuário é apenas um contexto adicionado. Conforme declarado, e derivado da proposta original, or return espera uma função que recebe um único parâmetro do tipo deslocado. Isso é fundamental e permite não apenas funções de utilitário que atendam a um grande número de pessoas, mas você pode escrever as suas próprias se precisar de um tratamento realmente personalizado de valores específicos.

@urandom IMO esconde muito.

Meus 2 centavos aqui, gostaria de propor uma regra simples:

"parâmetro de erro de resultado implícito para funções"

Para qualquer função, um parâmetro de erro está implícito no final da lista de parâmetros de resultado
se não for definido explicitamente.

Suponha que temos uma função definida como segue para fins de discussão:

função f () (int) {}
que é idêntico a: func f () (int, error) {}
de acordo com nossa regra de erro de resultado implícito.

para atribuição, você pode aparecer, ignorar ou detectar o erro da seguinte maneira:

1) borbulhar

x: = f ()

se f retornar erro, a função atual retornará imediatamente com o erro
(ou criar uma nova pilha de erros?)
se a função atual for principal, o programa será interrompido.

É equivalente ao seguinte snippet de código:

x, errar: = f ()
se errar! = nulo {
voltar ... errar
}

2) ignorar

x, _: = f ()

um identificador em branco no final da lista de expressão de atribuição para sinalizar explicitamente o descarte do erro.

3) pegar

x, errar: = f ()

err deve ser tratado como de costume.

Acredito que essa mudança de convenção de código idiomática deve exigir apenas mudanças mínimas no compilador
ou um pré-processador deve fazer o trabalho.

@ dc0d Você pode dar um exemplo do que ele esconde e como?

@urandom É o motivo que causou a pergunta "onde está o erro original?", como perguntei em um comentário anterior. Ele passa o erro implicitamente e não está claro (por exemplo) onde está o erro original colocado nesta linha: f := OpenFile(filename) or return errors.Contextf("opening file %s", filename) . O erro original retornado por OpenFile() - que pode ser algo como falta de permissão de leitura ou arquivo ausente e não apenas "há algo errado com o nome do arquivo".

@ dc0d
Discordo. É quase tão claro quanto lidar com http.Handlers, em que, no segundo, você os passa para algum mux e, de repente, recebe uma solicitação e um redator de resposta. E as pessoas estão acostumadas com esse tipo de comportamento. Como as pessoas sabem o que a instrução go faz? Obviamente, não está claro no primeiro encontro, mas é bastante difundido e está na linguagem.

Não acho que devemos ser contra qualquer proposta com o fundamento de que é nova e ninguém tem a menor ideia de como funciona, porque isso é verdade para a maioria deles.

@urandom Agora isso faz mais sentido (incluindo http.Handler exemplo).

E estamos discutindo coisas. Não falo contra ou a favor de nenhuma ideia específica. Mas eu apóio a simplicidade e ser explícito e, ao mesmo tempo, transmito um pouco de sanidade sobre a experiência do desenvolvedor.

@ dc0d

que pode ser algo como falta de permissão de leitura ou arquivo ausente

Nesse caso, você não iria apenas relançar o erro, mas verificar o conteúdo real. Para mim, essa questão é sobre como cobrir o caso mais popular. Ou seja, relançar o erro com contexto adicionado. Apenas em casos mais raros você converte o erro em algum tipo concreto e verifica o que ele realmente diz. E para esse erro atual, a sintaxe de tratamento é perfeitamente adequada e não irá a lugar nenhum, mesmo que uma das propostas aqui seja aceita.

Erros @creker não são exceções (algum comentário anterior meu). Erros são valores, portanto, jogá-los ou re-lançá-los não é possível. Para cenários do tipo try / catch, Go tem pânico / recuperação.

@ dc0d Não estou falando sobre exceções. Por relançar, quero dizer retornar o erro ao chamador. O or return errors.Contextf("opening file %s", filename) proposto basicamente envolve e relança um erro.

@creker Obrigado pela explicação. Ele também adiciona algumas chamadas de função extras que afetam o planejador que, por sua vez, pode não produzir o comportamento desejado em algumas situações.

@ dc0d é um detalhe de implementação e pode mudar no futuro. E isso pode realmente mudar, a preempção não cooperativa está em andamento agora.

@creker
Acho que você pode cobrir ainda mais casos do que apenas retornar um erro modificado:

func retryReadErrHandler(filename string, count int) func(error) error {
     return func(err error) error {
          if os.IsTimeout(err) {
               count++
               return Read(filename, count)
          }
          if os.IsPermission(err) {
               log.Fatal("Permission")
          }

          return fmt.Errorf("opening file %s: %v", filename, err)
      }
}

func Read(filename string, count int) error {
  if count > 3 {
    return errors.New("max retries")
  }

  f := OpenFile(filename) or return retryReadErrHandler(filename, count)

  ...
}

@ dc0d
As chamadas de funções extras provavelmente serão sequenciadas pelo compilador

@urandom que parece muito interessante. Um pouco mágico com argumento implícito, mas este poderia ser geral e conciso o suficiente para cobrir tudo. Apenas em casos muito raros você teria que recorrer ao if err != nil normal

@urandom , estou confuso com seu exemplo. Por que retryReadErrHandler retornando uma função?

@ object88
Essa é a ideia por trás do operador or return . Ele espera uma função que chamará no caso de um último argumento diferente de zero retornado do lado esquerdo. Nesse sentido, ele age exatamente da mesma forma que um http.Handler, deixando a lógica real de como lidar com o argumento e seu retorno (ou a solicitação e sua resposta, no caso de um manipulador) para o retorno de chamada. E para usar seus próprios dados personalizados no retorno de chamada, você cria uma função de wrapper que recebe esses dados como parâmetros e retorna o que é esperado.

Ou em termos mais familiares, é semelhante ao que costumamos fazer com manipuladores:
`` `vá
função nodesHandler (repo Repo) http.Handler {
return http.HandlerFunc (func (w http.ResponseWriter, r * http.Request) {
dados, _: = json.Marshal (repo.GetNodes ())
w.Write (dados)
})
}

@urandom , você pode evitar um pouco de mágica deixando o LHS igual ao de hoje e mudando or ... return para returnif (cond) :

func Read(filename string) error {
   f, err := OpenFile(filename) returnif(err != nil) errors.Contextf(err, "opening file %s", filename)
   b, err := ReadBytes(f) returnif(err != nil) errors.Contextf(err, "reading file %s", filename)
   err = ProcessBytes(b) returnif(err != nil) errors.Context(err, "processing data")
   return nil
}

Isso melhora a generalidade e a transparência dos valores de erro à esquerda e a condição de acionamento à direita.

Quanto mais vejo essas propostas diferentes, mais inclinado a querer uma mudança apenas para gofmt. O idioma já tem o poder, vamos apenas torná-lo mais digitalizável. @billyh , para não returnif(cond) ... é apenas uma forma de reescrever if cond { return ...} . Por que não podemos simplesmente escrever o último? Já sabemos o que isso significa.

x, err := foo()
if err != nil { return fmt.Errorf(..., err) }

ou mesmo

if x, err := foo(); err != nil { return fmt.Errorf(..., err) }

ou

x, err := foo(); if err != nil { return fmt.Errorf(..., err) }

Sem novas palavras-chave mágicas, sintaxe ou operadores.

(Pode ajudar se também corrigirmos # 377 para adicionar alguma flexibilidade ao uso de := .)

@ randall77 Também estou cada vez mais inclinado a isso.

@ randall77 Em que ponto esse bloco terá uma

A solução acima é mais agradável quando comparada às alternativas propostas aqui, mas não estou convencido de que seja melhor do que não agir. Gofmt deve ser o mais determinista possível.

Como não pensei muito bem, mas talvez "se o corpo de uma instrução if contiver uma única instrução return , então a instrução if será formatada como uma única linha. "

Talvez seja necessário haver uma restrição adicional na condição if , como deve ser uma variável booleana ou um operador binário de duas variáveis ​​ou constantes.

@billyh
Não vejo necessidade de torná-lo mais prolixo, pois não vejo nada de confuso com aquele pouco de magia em or . Presumo que, ao contrário de @as , muitas pessoas também não acham nada confuso na maneira como trabalhamos com manipuladores http.

@ randall77
O que você sugere está mais alinhado com uma sugestão de estilo de código, e é para onde ir é altamente opinativo. Pode não funcionar bem na comunidade como um todo, surgindo repentinamente 2 estilos de instruções if de formatação.

Sem mencionar que forros como esses são muito mais difíceis de ler. if != ; { } é muito mesmo em várias linhas, daí esta proposta. O padrão é fixo para quase todos os casos e pode ser transformado em um açúcar sintático fácil de ler e entender.

O problema que tenho com a maioria dessas sugestões é que não está claro o que está acontecendo. Na postagem de abertura, é sugerido reutilizar || para retornar um erro. Não está claro para mim se um retorno está acontecendo lá. Eu acho que se uma nova sintaxe for inventada, ela precisa se alinhar com as expectativas da maioria das pessoas. Quando vejo || não espero um retorno, nem mesmo uma interrupção na execução. É chocante para mim.

Eu gosto do sentimento de Go de "erros são valores", mas também concordo que if err := expression; err != nil { return err } é muito prolixo, principalmente porque quase todas as chamadas devem retornar um erro. Isso significa que você terá muitos deles e é fácil bagunçar tudo, dependendo de onde err for declarado (ou sombreado). Aconteceu com nosso código.

Como Go não usa try / catch e usa panic / defer para circunstâncias "excepcionais", podemos ter a oportunidade de reutilizar as palavras-chave try e / ou catch para encurtar o tratamento de erros sem travar o programa.

Aqui está um pensamento que tive:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
}

A ideia é que você prefixe err no LHS com a palavra-chave try . Se err não for nulo, um retorno ocorrerá imediatamente. Você não precisa usar uma captura aqui, a menos que o retorno não seja totalmente satisfeito. Isso se alinha mais com as expectativas das pessoas de "tentar interromper a execução", mas em vez de travar o programa, ele apenas retorna.

Se o retorno não for completamente satisfeito (verificação do tempo de compilação) ou se quisermos encerrar o erro, poderíamos usar catch como rótulo somente encaminhamento especial como este:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
catch:
    return &CustomError{"some detail", err}
}

Isso também permite que você verifique e ignore certos erros:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, err = io.WriteString(w, "bar")
        if err == io.EOF {
            err = nil
        } else {
            goto catch
        }
    return
catch:
    return &CustomError{"some detail", err}
}

Talvez você possa até mesmo fazer com que try especifique o rótulo:

func WriteFooBar(w io.Writer) (err error) {
    _, try(handle1) err = io.WriteString(w, "foo")
    _, try(handle2) err = w.Write([]byte{','})
    _, try(handle3) err = io.WriteString(w, "bar")
    return
handle1:
    return &CustomError1{"...", err}
handle2:
    return &CustomError2{"...", err}
handle3:
    return &CustomError3{"...", err}
}

Percebo que meus exemplos de código são meio ruins (foo / bar, ack). Mas espero ter ilustrado o que quero dizer com ir com / contra as expectativas existentes. Eu também ficaria muito bem em manter os erros do jeito que estão no Go 1. Mas se uma nova sintaxe for inventada, é necessário pensar cuidadosamente sobre como essa sintaxe já é percebida, não apenas no Go. É difícil inventar uma nova sintaxe sem que ela já signifique algo, então geralmente é melhor seguir as expectativas existentes do que ir contra elas.

Talvez algum tipo de encadeamento como você pode encadear métodos, mas para erros? Não tenho certeza de como seria ou se funcionaria, apenas uma ideia maluca.
Você pode fazer uma espécie de cadeia agora para reduzir o número de verificações de erro, mantendo algum tipo de valor de erro dentro de uma estrutura e extraindo-o no final da cadeia.

É uma situação muito curiosa porque, embora haja um pouco de clichê, não tenho certeza de como simplificá-lo ainda mais fazendo sentido.

O código de amostra de @thejerf se parece com isso com a proposta de @lukescott :

func NewClient(...) (*Client, error) {
    listener, try err := net.Listen("tcp4", listenAddr)
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    return nil, err
}

Vai de 59 para 47 linhas.

Este é o mesmo comprimento, mas acho que é um pouco mais claro do que usar defer :

func NewClient(...) (*Client, error) {
    var openedListener, openedConn bool
    listener, try err := net.Listen("tcp4", listenAddr)
    openedListener = true

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    openedConn = true

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    if openedConn {
        conn.Close()
    }
    if openedListener {
        listener.Close()
    }
    return nil, err
}

Esse exemplo provavelmente seria mais fácil de seguir com deferifnotnil ou algo assim.
Mas isso meio que remonta a uma linha inteira, se é que muitas dessas sugestões se referem.

Depois de brincar um pouco com o código de exemplo, agora sou contra a variante try(label) name . Eu acho que se você tem várias coisas fantásticas para fazer, basta usar o sistema atual de if err != nil { ... } . Se você estiver fazendo basicamente a mesma coisa, como definir uma mensagem de erro personalizada, poderá fazer o seguinte:

func WriteFooBar(w io.Writer) (err error) {
    msg := "thing1 went wrong"
    _, try err = io.WriteString(w, "foo")
    msg = "thing2 went wrong"
    _, try err = w.Write([]byte{','})
    msg = "thing3 went wrong"
    _, try err = io.WriteString(w, "bar")
    return nil

catch:
    return &CustomError{msg, err}
}

Se alguém já usou Ruby, isso se parece muito com sua sintaxe rescue , que eu acho que lê razoavelmente bem.

Uma coisa que poderia ser feita é tornar nil um valor falso e avaliar outros valores como verdadeiros, então você acaba com:

err := doSomething()
if err { return err }

Mas eu não tenho certeza se isso realmente funcionaria e apenas elimina alguns personagens.
Eu digitei muitas coisas, mas acho que nunca digitei != nil .

Tornar as interfaces truthy / falsey já foi mencionado antes, e eu disse que isso tornaria os bugs com sinalizadores mais comuns:

verbose := flag.Bool("v", false, "verbose logging")
flag.Parse()
if verbose { ... } // should be *verbose!

@carlmjohnson , no exemplo que você forneceu acima, há mensagens de erro intercaladas com código de caminho feliz, o que é um pouco estranho para mim. Se você precisar formatar essas strings, estará fazendo muito trabalho extra, independentemente de algo dar errado ou não:

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    msg := fmt.Sprintf("While writing %s, thing1 went wrong", f.foo)
    _, try err = io.WriteString(w, f.foo)
    msg = fmt.Sprintf("While writing %s, thing2 went wrong", f.separator)
    _, try err = w.Write(f.separator)
    msg = fmt.Sprintf("While writing %s, thing3 went wrong", f.bar)
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    return &CustomError{msg, err}
}

@ object88 , acho que a análise SSA deve ser capaz de descobrir se certas atribuições não são usadas e reorganizá-las para que não ocorram se não forem necessárias (otimista demais?). Se isso for verdade, isso deve ser eficiente:

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    var format string, args []interface{}

    msg = "While writing %s, thing1 went wrong", 
    args = []interface{f.foo}
    _, try err = io.WriteString(w, f.foo)

    format = "While writing %s, thing2 went wrong"
    args = []interface{f.separator}
    _, try err = w.Write(f.separator)

    format = "While writing %s, thing3 went wrong"
    args = []interface{f.bar}
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    msg := fmt.Sprintf(format, args...)
    return &CustomError{msg, err}
}

Isso seria legal?

func Foo() error {
catch:
    try _ = doThing()
    return nil
}

Acho que deve ser executado um loop até que doThing() retorne nulo, mas posso estar convencido do contrário.

@carlmjohnson

Tendo brincado um pouco com o código de exemplo, agora sou contra a variante de nome try (rótulo).

Sim, eu não tinha certeza sobre a sintaxe. Eu não gosto porque faz try parecer uma chamada de função. Mas eu percebi o valor de especificar um rótulo diferente.

Isso seria legal?

Eu diria que sim porque try deve ser somente para frente. Se você quisesse fazer isso, eu diria que você precisa fazer assim:

func Foo() error {
tryAgain:
    if err := doThing(); err != nil {
        goto tryAgain
    }
    return nil
}

Ou assim:

func Foo() error {
    for doThing() != nil {}
    return nil
}

@Azareal

Uma coisa que poderia ser feita é tornar nil um valor falso e avaliar outros valores como verdadeiros, então você acaba com: err := doSomething() if err { return err }

Acho que vale a pena encurtá-lo. No entanto, não acho que deva se aplicar a zero em todas as situações. Talvez pudesse haver uma nova interface como esta:

interface Truthy {
  True() bool
}

Então, qualquer valor que implemente essa interface pode ser usado conforme você propôs.

Isso funcionaria contanto que o erro implementasse a interface:

err := doSomething()
if err { return err }

Mas isso não funcionaria:

err := doSomething()
if err == true { return err } // err is not true

Eu sou realmente novo no golang, mas como você pensa sobre a introdução de delegador condicional como a seguir?

func someFunc() error {

    errorHandler := delegator(arg1 Arg1, err error) error if err != nil {
        // ...
        return err // or modifiedErr
    }

    ret, err := doSomething()
    delegate errorHandler(ret, err)

    ret, err := doAnotherThing()
    delegate errorHandler(ret, err)

    return nil
}

delegador funciona como coisas, mas

  • Seu return significa return from its caller context . (o tipo de retorno deve ser o mesmo do chamador)
  • Opcionalmente leva if antes de { , no exemplo acima é if err != nil .
  • Deve ser delegado pelo chamador com delegate palavra-chave

Pode ser capaz de omitir delegate para delegar, mas acho que torna difícil ler o fluxo da função.

E talvez seja útil não apenas para tratamento de erros, mas não tenho certeza agora.

É bonito adicionar cheque , mas você pode fazer mais antes de retornar:

result, err := openFile(f);
if err != nil {
        log.Println(..., err)
    return 0, err 
}

torna-se

result, err := openFile(f);
check err

`` `Go
resultado, errar: = openFile (f);
check err {
log.Println (..., err)
}

```Go
reslt, _ := check openFile(f)
// If err is not nil direct return, does not execute the next step.

`` `Go
resultado, errar: = verificar openFile (f) {
log.Println (..., err)
}

It also attempts simplifying the error handling (#26712):
```Go
result, err := openFile(f);
check !err {
    // err is an interface with value nil or holds a nil pointer
    // it is unusable
    result.C...()
}

Ele também tenta simplificar o tratamento de erros (por alguns considerado tedioso) (# 21161). Seria:

result, err := openFile(f);
check err {
   // handle error and return
    log.Println(..., err)
}

Claro, você pode usar try e outras palavras-chave em vez de check , se estiver mais claro.

reslt, _ := try openFile(f)
// If err is not nil direct return, does not execute the next step.

`` `Go
resultado, errar: = openFile (f);
tente errar {
// trata o erro e retorna
log.Println (..., err)
}

Reference:

A plain idea, with support for error decoration, but requiring a more drastic language change (obviously not for go1.10) is the introduction of a new check keyword.

It would have two forms: check A and check A, B.

Both A and B need to be error. The second form would only be used when error-decorating; people that do not need or wish to decorate their errors will use the simpler form.

1st form (check A)
check A evaluates A. If nil, it does nothing. If not nil, check acts like a return {<zero>}*, A.

Examples

If a function just returns an error, it can be used inline with check, so
```Go
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

torna-se

check UpdateDB()

Para uma função com vários valores de retorno, você precisará atribuir, como fazemos agora.

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

torna-se

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

// use a and b

2º formulário (marque A, B)
verifique A, B avalia A. Se nulo, não faz nada. Se não for nulo, o cheque atua como um retorno {} *, B.

Isso é para necessidades de decoração de erro. Ainda verificamos A, mas é B que é usado no retorno implícito.

Exemplo

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

torna-se

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

Notas
É um erro de compilação para

use a instrução de verificação em coisas que não avaliam como erro
use verificação em uma função com valores de retorno que não estejam na forma {type} *, erro
A verificação de forma two-expr A, B está em curto-circuito. B não é avaliado se A for nulo.

Notas sobre praticidade
Há suporte para erros de decoração, mas você paga pela verificação mais pesada da sintaxe A, B apenas quando realmente precisa decorar erros.

Para o boilerplate if err != nil { return nil, nil, err } (que é muito comum), verifique se errar é tão breve quanto poderia ser, sem sacrificar a clareza (consulte a nota sobre a sintaxe abaixo).

Notas sobre sintaxe
Eu diria que esse tipo de sintaxe (verificar .., no início da linha, semelhante a um retorno) é uma boa maneira de eliminar o clichê de verificação de erros sem esconder a interrupção do fluxo de controle que os retornos implícitos apresentam.

Uma desvantagem de ideias como||epegaracima, ou o a, b = foo ()? proposto em outro thread, é que eles ocultam a modificação do fluxo de controle de uma forma que torna o fluxo mais difícil de seguir; o primeiro com ||maquinário anexado ao final de uma linha de aparência simples, a última com um pequeno símbolo que pode aparecer em todos os lugares, inclusive no meio e no final de uma linha de código de aparência simples, possivelmente várias vezes.

Uma instrução de verificação sempre será de nível superior no bloco atual, tendo o mesmo destaque de outras instruções que modificam o fluxo de controle (por exemplo, um retorno antecipado).

Aqui está outro pensamento.

Imagine uma instrução again que define uma macro com um rótulo. A instrução de instrução que ela rotula pode ser expandida novamente por substituição textual mais tarde na função (uma reminiscência de const / iota, com tons de goto: -]).

Por exemplo:

func(foo int) (int, error) {
    err := f(foo)
again check:
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    check
    x, err := h()
    check
    return x, nil
}

seria exatamente equivalente a:

func(foo int) (int, error) {
    err := f(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    x, err := h()
    if err != nil {
        return 0, errors.Wrap(err)
    }
    return x, nil
}

Observe que a expansão da macro não tem argumentos - isso significa que deve haver menos confusão sobre o fato de que é uma macro, porque o compilador não gosta de símbolos por conta própria .

Como a instrução goto, o escopo do rótulo está dentro da função atual.

Idéia interessante. Gostei da ideia do rótulo catch, mas não acho que seja um bom ajuste com escopos Go (com Go atual, você não pode goto um rótulo com novas variáveis ​​definidas em seu escopo). A ideia again corrige esse problema porque o rótulo vem antes da introdução de novos escopos.

Aqui está o mega-exemplo novamente:

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
    catch {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, try err = net.Listen("tcp4", listenAddr)

    conn, try err = ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

Aqui está uma versão mais próxima da proposta de Rog (não gosto tanto):

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
again:
    if err != nil {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, err = net.Listen("tcp4", listenAddr)
    check

    conn, err = ConnectionManager{}.connect(server, tlsConfig)
    check

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    check

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    check

    session, err := communicationProtocol.FinalProtocol(conn)
    check
    client.session = session

    return client, nil
}

@carlmjohnson Para que

Além disso, eu sugeriria que o exemplo acima não ilustra seu uso muito bem - não há nada na instrução novamente rotulada acima que não pudesse ser feita em uma instrução defer. No exemplo try / catch, esse código não pode (por exemplo) envolver o erro com informações sobre o local de origem do retorno do erro. Também não funcionará AFAICS se você adicionar um "try" dentro de uma dessas instruções if (por exemplo, para verificar o erro retornado por GetRuntimeEnvironment), porque o "err" referido pela instrução catch está em um escopo diferente daquele declarado dentro do bloco.

Acho que meu único problema com a palavra-chave check é que todas as saídas de uma função devem ser return (ou pelo menos ter _algumas_ conotações do tipo "Vou deixar a função"). Nós _podemos_ obter become (para TCO), pelo menos become tem algum tipo de "Estamos nos tornando uma função diferente" ... mas a palavra "verificar" realmente não parece vai ser uma saída para a função.

O ponto de saída de uma função é extremamente importante, e não tenho certeza se check realmente tem aquela sensação de "ponto de saída". Fora isso, eu realmente gosto da ideia do que check faz, permite um tratamento de erros muito mais compacto, mas ainda permite tratar cada erro de maneira diferente, ou embrulhar o erro como você desejar.

Posso adicionar uma sugestão também?
Que tal algo assim:

func Open(filename string) os.File onerror (string, error) {
       f, e := os.Open(filename)
       if e != nil { 
              fail "some reason", e // instead of return keyword to go on the onerror 
       }
      return f
}

f := Open(somefile) onerror reason, e {
      log.Prinln(reason)
      // try to recover from error and reasign 'f' on success
      nf = os.Create(somefile) onerror err {
             panic(err)
      }
      return nf // return here must return whatever Open returns
}

A atribuição de erro pode ter qualquer forma, até mesmo ser algo estúpido como

f := Open(name) =: e

Ou retorne um conjunto diferente de valores em caso de erro, não apenas erros, e também um bloco try catch seria bom.

try {
    f := Open("file1") // can fail here
    defer f.Close()
    f1 := Open("file2") // can fail here
    defer f1.Close()
    // do something with the files
} onerror err {
     log.Println(err)
}

@cthackers Eu pessoalmente acredito que é muito bom para erros no Go não ter um tratamento especial. Eles são simplesmente valores, e acho que devem continuar assim.

Além disso, try-catch (e construções semelhantes) é apenas uma construção ruim que incentiva práticas inadequadas. Cada erro deve ser tratado separadamente, não por algum manipulador de erros "pega-tudo".

https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md
isso é muito complicado.

minha ideia: |err| significa verificar erro: if err! = nil {}

// common util func
func parseInt(s string) (i int64, err error){
    return strconv.ParseInt(s, 10, 64)
}

// expression check err 1 : check and use err variable
func parseAndSum(a string ,b string) (int64,error) {
    sum := parseInt(a) + parseInt(b)  |err| return 0,err
    return sum,nil
} 

// expression check err 2 : unuse variable 
func parseAndSum(a string , b string) (int64,error) {
    a,err := parseInt(a) |_| return 0, fmt.Errorf("parseInt error: %s", a)
    b,err := parseInt(b) |_| { println(b); return 0,fmt.Errorf("parseInt error: %s", b);}
    return a+b,nil
} 

// block check err 
func parseAndSum(a string , b string) (  int64,  error) {
    {
      a := parseInt(a)  
      b := parseInt(b)  
      return a+b,nil
    }|err| return 0,err
} 

@ chen56 e todos os comentaristas futuros: consulte https://go.googlesource.com/proposal/+/master/design/go2draft.md .

Suspeito que isso torne este tópico obsoleto agora e não faz sentido continuar aqui. A página de feedback do Wiki é onde as coisas provavelmente devem ir no futuro.

O mega exemplo usando a proposta Go 2:

func NewClient(...) (*Client, error) {
    var (
        listener net.Listener
        conn     net.Conn
    )
    handle err {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener = check net.Listen("tcp4", listenAddr)

    conn = check ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    check toServer.Send(&client.serverConfig)

    check toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session := check communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

Acho que isso é tão limpo quanto podemos esperar. O bloco handle tem as boas qualidades do rótulo again ou da palavra-chave rescue Ruby. As únicas dúvidas que restam em minha mente são se devo usar pontuação ou uma palavra-chave (acho que palavra-chave) e se devo permitir obter o erro sem retorná-lo.

Estou tentando entender a proposta - parece que há apenas um bloco de manuseio por função, em vez da capacidade de criar diferentes respostas para diferentes erros ao longo dos processos de execução da função. Isso parece uma verdadeira fraqueza.

Também estou me perguntando se estamos negligenciando a necessidade crítica de desenvolver equipamentos de teste em nossos sistemas também. Considerar como vamos exercitar caminhos de erro durante os testes deve fazer parte da discussão, mas também não vejo isso,

@sdwarwick Não acho que este seja o melhor lugar para discutir o rascunho do design descrito em https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md . Uma abordagem melhor é adicionar um link para um artigo na página wiki em https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback .

Dito isso, esse rascunho de design permite vários blocos de alça em uma função.

Esse problema começou como uma proposta específica. Não vamos adotar essa proposta. Tem havido muita discussão sobre esse assunto e espero que as pessoas coloquem as boas idéias em propostas separadas e na discussão do recente esboço do projeto. Vou encerrar esse problema. Obrigado por toda a discussão.

Se falar no conjunto destes exemplos:

r, err := os.Open(src)
    if err != nil {
        return err
    }

Que eu gostaria de escrever em uma linha aproximadamente:

r, err := os.Open(src) try ("blah-blah: %v", err)

Em vez de "experimentar", coloque qualquer palavra bonita e adequada.

Com essa sintaxe, o erro retornaria e o resto seriam alguns valores padrão dependendo do tipo. Se eu precisar retornar junto com um erro e algo mais específico, em vez do padrão, ninguém cancela a opção clássica de mais linhas.

Ainda mais resumidamente (sem adicionar algum tipo de tratamento de erros):

r, err := os.Open(src) try

)
PS Com licença do meu inglês))

Minha variante:

func CopyFile(src, dst string) string, error {
    r := check os.Open(src) // return nil, error
    defer r.Close()

    // if error: run 1 defer and retun error message
    w := check os.Create(dst) // return nil, error
    defer w.Close()

    // if error: run 2, 1 defer and retun error message
    if check io.Copy(w, r) // return nil, error

}

func StartCopyFile() error {
  res := check CopyFile("1.txt", "2.txt")

  return nil
}

func main() {
  err := StartCopyFile()
  if err!= nil{
    fmt.printLn(err)
  }
}

Olá,

Eu tenho uma ideia simples, que é vagamente baseada em como o tratamento de erros funciona no shell, assim como a proposta inicial era. No shell, os erros são comunicados por valores de retorno diferentes de zero. O valor de retorno do último comando / chamada é armazenado em $? na casca. Além do nome da variável fornecido pelo usuário, podemos armazenar automaticamente o valor do erro da última chamada em uma variável predefinida e fazer com que seja verificada por uma sintaxe predefinida. Eu escolhi ? como sintaxe para fazer referência ao último valor de erro, que foi retornado de uma chamada de função no escopo atual. Eu escolhi ! como uma abreviação de if? ! = nulo {}. A escolha para? é influenciado pela concha, mas também porque parece fazer sentido. Se ocorrer um erro, você naturalmente estará interessado no que aconteceu. Isso está levantando uma questão. ? é o sinal comum para uma questão levantada e, portanto, o usamos para fazer referência ao último valor de erro gerado no mesmo escopo.
! é usado como uma abreviação de if? ! = nulo, porque significa que é necessário prestar atenção no caso de algo dar errado. ! significa: se algo deu errado, faça isso. ? faz referência ao valor de erro mais recente. Como de costume, o valor de? é igual a nulo se não houver erro.

val, err := someFunc(param)
! { return &SpecialError("someFunc", param, ?) }

Para tornar a sintaxe mais atraente, eu permitiria colocar o! linha diretamente atrás da chamada, bem como omitindo as chaves.
Com esta proposta, você também pode lidar com erros sem usar um identificador definido pelo programador.

Isso seria permitido:

val, _ := someFunc(param)
! return &SpecialError("someFunc", param, ?)

Isso seria permitido

val, _ := someFunc(param) ! return &SpecialError("someFunc", param, ?)

De acordo com esta proposta, você não precisa retornar da função quando ocorrer um erro
e você pode tentar se recuperar do erro.

val, _ := someFunc(param)
! {
val, _ := someFunc(paramAlternative)
  !{ return &SpecialError("someFunc alternative try failed too", paramAlternative, ?) }}

Sob esta proposta você poderia usar! em um loop for para várias tentativas como esta.

val, _ := someFunc(param)
for i :=0; ! && i <5; i++ {
  // Sleep or make a change or both
  val, _ := someFunc(param)
} ! { return &SpecialError("someFunc", param, ? }

Estou ciente disso! é usado principalmente para negação de expressões, portanto, a sintaxe proposta pode causar confusão nos não iniciados. A ideia é essa! por si só se expande para? ! = nil quando é usado em uma expressão condicional em um caso como o exemplo superior demonstra, onde não é anexado a nenhuma expressão específica. A linha superior for não pode ser compilada com o go atual, porque não faz sentido sem contexto. Sob esta proposta! por si só é verdadeiro, quando um erro ocorreu na chamada de função mais recente, que pode retornar um erro.

A instrução return para retornar o erro é mantida, porque como outros comentaram aqui é desejável ver rapidamente onde sua função retorna. Você pode usar essa sintaxe em um cenário em que um erro não exija que você saia da função.

Esta proposta é mais simples do que algumas outras propostas, uma vez que não há esforço para criar uma variante da sintaxe de bloco try e catch conhecida de outras linguagens. Ele mantém a filosofia atual de lidar com erros diretamente onde eles ocorrem e torna mais sucinto fazê-lo.

@tobimensch , poste novas sugestões para uma essência e wiki de comentários sobre Tratamento de erros do

Se você ainda não viu, você pode querer ler o Projeto de rascunho de tratamento de erros do

E você pode estar interessado em Requisitos a serem considerados para o tratamento de erros do Go 2 .

Pode ser um pouco tarde para apontar, mas qualquer coisa que pareça mágica do javascript me incomoda. Estou falando sobre o operador || que deve, de alguma forma, funcionar magicamente com uma interface error . Eu não gosto disso

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