Vários valores em contexto de valor único

110

Devido ao tratamento de erros no Go, muitas vezes acabo com funções de vários valores. Até agora, a maneira como gerenciei isso tem sido muito confusa e estou procurando as melhores práticas para escrever um código mais limpo.

Digamos que eu tenha a seguinte função:

type Item struct {
   Value int
   Name string
}

func Get(value int) (Item, error) {
  // some code

  return item, nil
}

Como posso atribuir uma nova variável a item.Valueelegantemente. Antes de introduzir o tratamento de erros, minha função acabou de retornar iteme eu poderia simplesmente fazer isso:

val := Get(1).Value

Agora eu faço isso:

item, _ := Get(1)
val := item.Value

Não existe uma maneira de acessar diretamente a primeira variável retornada?

Spearfisher
fonte
3
itemnormalmente ocorrerá nilem caso de erro. Sem verificar se há um erro primeiro, seu código irá travar nesse caso.
Thomas

Respostas:

83

No caso de uma função de retorno de vários valores, você não pode se referir a campos ou métodos de um valor específico do resultado ao chamar a função.

E se um deles for um error, ele está lá por um motivo (que é a função pode falhar) e você não deve ignorá-lo porque, se o fizer, seu código subsequente também pode falhar miseravelmente (por exemplo, resultando em pânico no tempo de execução).

No entanto, pode haver situações em que você sabe que o código não falhará em nenhuma circunstância. Nesses casos, você pode fornecer uma função auxiliar (ou método) que descartará o error(ou gerará um pânico de tempo de execução, se ainda ocorrer).
Esse pode ser o caso se você fornecer os valores de entrada para uma função a partir do código e souber que eles funcionam.
Ótimos exemplos disso são os pacotes templatee regexp: se você fornecer um modelo ou regexp válido em tempo de compilação, você pode ter certeza de que eles sempre podem ser analisados ​​sem erros em tempo de execução. Por esse motivo, o templatepacote fornece a Must(t *Template, err error) *Templatefunção e o regexppacote fornece os s, porque o uso pretendido é onde a entrada tem garantia de validade.MustCompile(str string) *Regexp função: eles não retornamerror

Exemplos:

// "text" is a valid template, parsing it will not fail
var t = template.Must(template.New("name").Parse("text"))

// `^[a-z]+\[[0-9]+\]$` is a valid regexp, always compiles
var validID = regexp.MustCompile(`^[a-z]+\[[0-9]+\]$`)

De volta ao seu caso

SE você tiver certeza de Get()que não produzirá errorpara determinados valores de entrada, pode criar uma Must()função auxiliar que não retornaria o, errormas geraria um pânico de tempo de execução se ainda ocorrer:

func Must(i Item, err error) Item {
    if err != nil {
        panic(err)
    }
    return i
}

Mas você não deve usar isso em todos os casos, apenas quando tiver certeza de que funciona. Uso:

val := Must(Get(1)).Value

Alternativa / Simplificação

Você pode até simplificar ainda mais se incorporar a Get()chamada à sua função auxiliar, vamos chamá-la MustGet:

func MustGet(value int) Item {
    i, err := Get(value)
    if err != nil {
        panic(err)
    }
    return i
}

Uso:

val := MustGet(1).Value

Veja algumas questões interessantes / relacionadas:

como analisar retornos múltiplos em golang

Retorna o mapa como 'ok' no Golang nas funções normais

icza
fonte
7

Não, mas isso é bom, pois você deve sempre lidar com seus erros.

Existem técnicas que você pode empregar para adiar o tratamento de erros, consulte Erros são valores de Rob Pike.

ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
    return ew.err
}

Neste exemplo da postagem do blog, ele ilustra como você pode criar um errWritertipo que adia o tratamento de erros até terminar de chamar write.

Jmaloney
fonte
6

Sim existe.

Surpreendente, hein? Você pode obter um valor específico de um retorno múltiplo usando uma mutefunção simples :

package main

import "fmt"
import "strings"

func µ(a ...interface{}) []interface{} {
    return a
}

type A struct {
    B string
    C func()(string)
}

func main() {
    a := A {
        B:strings.TrimSpace(µ(E())[1].(string)),
        C:µ(G())[0].(func()(string)),
    }

    fmt.Printf ("%s says %s\n", a.B, a.C())
}

func E() (bool, string) {
    return false, "F"
}

func G() (func()(string), bool) {
    return func() string { return "Hello" }, true
}

https://play.golang.org/p/IwqmoKwVm-

Observe como você seleciona o número do valor exatamente como faria em uma fatia / matriz e, em seguida, o tipo para obter o valor real.

Você pode ler mais sobre a ciência por trás disso neste artigo . Créditos ao autor.

Kesarion
fonte
5

Não, você não pode acessar diretamente o primeiro valor.

Suponho que um hack para isso seria retornar uma matriz de valores em vez de "item" e "err", e então apenas fazer, item, _ := Get(1)[0] mas eu não recomendaria isso.

Antimônio
fonte
3

Que tal assim?

package main

import (
    "fmt"
    "errors"
)

type Item struct {
    Value int
    Name string
}

var items []Item = []Item{{Value:0, Name:"zero"}, 
                        {Value:1, Name:"one"}, 
                        {Value:2, Name:"two"}}

func main() {
    var err error
    v := Get(3, &err).Value
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(v)

}

func Get(value int, err *error) Item {
    if value > (len(items) - 1) {
        *err = errors.New("error")
        return Item{}
    } else {
        return items[value]
    }
}
Jaehoon
fonte