Go: 建议:Go 2:删除裸返回

创建于 2017-08-03  ·  52评论  ·  资料来源: golang/go

(我找不到与此相关的现有问题,因此如果这是重复的并且我错过了,请关闭。)

我建议摆脱裸回报。 命名返回值很好,保留这些。 光秃秃的回报比它们的价值更麻烦,消除它们。

Go2 LanguageChange NeedsDecision Proposal

最有用的评论

到处都返回 nils 有点冗长,你不觉得吗?

简单胜过冗长。 一堆return nil比一堆return更容易理解,其中必须追逐变量的最新值并确保它没有被隐藏(有意或无意)错误。)

所有52条评论

我会反对这种改变,有几个原因。

1)这是一个相对显着的句法变化。 这可能会引起开发人员的混淆,并导致 Go 陷入与 Python 一样的烂摊子。

2)我不认为它们没用,为什么它们比它们的价值更麻烦? 到处都是return nil有点冗长,你不觉得吗?

@awnumar for 2 这将与 #21182 完美搭配

到处都返回 nils 有点冗长,你不觉得吗?

简单胜过冗长。 一堆return nil比一堆return更容易理解,其中必须追逐变量的最新值并确保它没有被隐藏(有意或无意)错误。)

请注意,裸返回允许您做一件事,否则您无法完成:更改延迟中的返回值。 引用代码审查评论维基

最后,在某些情况下,您需要命名一个结果参数,以便在延迟闭包中更改它。 那总是好的。

一个消除裸回报的完整提案应该解释这种用途应该如何写,以及为什么替代形式更可取(或至少可以接受)。 明显的重写往往涉及大量样板和重复,虽然它们通常涉及错误处理,但机制是通用的。

我不认为这

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

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

,对不起。

OTOH - 是的,阴影很糟糕。 完全取缔它是没有意义的(我想到了生成的代码),但我很乐意投票支持至少禁止返回值名称阴影。 哦,让编译器在所有其他情况下生成一些诊断信息也会很好:)

编辑:显然有 #377 谈论它

@josharian我假设延迟函数仍然可以修改返回变量。 这种语义似乎或多或少与 return 语句的语法正交。

@josharian您需要命名的返回参数才能在延迟函数中修改它们。 您不需要为此使用回报。 带有显式值的 return 将在延迟函数运行之前将值复制到命名的返回值中。

@bcmills @dominikh对,我傻。 精神错乱; 谢谢你让我直截了当。

没有返回值的 func 应该仍然能够“裸露” return

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

我花了 1.5 分钟查看 Go 标准库中的以下 Go 代码(来自4 年前,/cc @bradfitz),难以置信地认为可能有一个坏错误:

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

具体来说,这部分:

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

乍一看,在我看来,在第一行,新变量tcerr是使用短变量声明语法声明的,与命名的err error返回不同多变的。 然后,在我看来,裸返回实际上是返回nil, nil而不是nil, err应该是。

经过大约 90 秒的认真思考,我意识到它实际上是正确的代码。 由于命名的err error返回值与函数体在同一个块中,短变量声明tc, err := ...只将tc声明为一个新变量,而没有声明一个新的err变量,所以命名的err error返回变量是通过调用ln.AcceptTCP() ,所以裸return实际上返回一个非零错误,因为它应该。

(如果它在一个新块中,它实际上会是一个编译错误“错误在返回期间被隐藏”,请参见此处。)

我认为这将是更清晰易读的代码:

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

我想分享这个故事,因为我认为这是浪费程序员时间的裸回报的一个很好的例子。 在我看来,裸返回稍微容易编写(只需节省几次按键),但与使用完整返回(非裸)的等效代码相比,通常会导致代码更难阅读。 Go 通常会为阅读优化做正确的事情,因为这比写作更频繁地(由更多人)完成。 在我看来,裸返回功能往往会降低可读性,因此在 Go 2 中删除它可能会使语言更好。

免责声明:在我最常编写和阅读的 Go 代码中,我倾向于避免直接返回(因为我认为它们的可读性较差)。 但这意味着我阅读/理解裸返回的经验较少,这可能会对我阅读/解析它们的能力产生负面影响。 这有点catch-22。

@shurcooL ,有趣的例子也让我失望。 对我来说,事实是有两个连接变量tcc 。 我认为你在复制粘贴代码时犯了一个错误,而不是c应该是tc 。 我花了大约相同的时间才意识到该代码的作用。

从文章:

请注意,如果您不喜欢或不喜欢 Golang 提供的裸回报,您可以使用return oi同时仍然获得相同的好处,如下所示:

命名返回值很棒! 命名返回值的裸返回是问题所在。 :-)

@awnumar ,这不相关。 查看我对 Hacker News 的评论: https : =14668595

我认为删除这些将遵循显式错误处理的方法。 我不使用它们。

我在 golang-nuts 上查看的带有裸返回的代码并不难理解,但解析变量范围对读者来说是不必要的额外工作。

我真的很喜欢裸返回,尤其是当一个函数有多个返回时。 它只是让代码更简洁。 最大的原因,我选择了用go。

Go tool vet shadow 可以检测潜在的阴影变量。 如果 func 具有较小的圈复杂度,则加上一些测试用例。 我看不出它会遇到什么麻烦。 我可能是错的,但我希望看到一些例子来证明裸露的回报有多糟糕。

我希望看到一些例子来证明裸露的回报有多糟糕。

@kelwang您是否从上面的 7 条评论中看到了我关于ln.AcceptTCP()示例?

嗨, @shurcooL ,谢谢

是的,我认为你提出了一个很好的观点。
但就像你说的,已经4年了。 我有一种感觉,也许你已经习惯了。

我认为这不是裸露回归的真正问题。 但是在多个 vars 初始化中存在混淆。

对我来说,shadow vet 工具通常效果很好。 我从来没有真正担心过。
也许我们应该在 go linter 中填写一张票以避免这种混乱。 是否重命名错误,或首先声明 tc 。 我觉得 linter 建议应该足够好了。

@kelwang在我看来,如果一个函数有多个返回到 return 语句变得丑陋的点,那么应该返回一个结构体/指向结构体的指针,而不是直接返回。

既然提到了#377,我认为ln.AcceptTCP示例中混淆的根源更多是关于:=背后的魔法,而不是裸返回本身。

我认为ln.AcceptTCP情况对于更明确的简短声明形式不会那么糟糕(在引用的问题中提出):

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

仅通过查看多变量:=就无法判断正在声明哪些变量:您需要考虑块的所有前面部分才能知道这一点。
对块边界位置的疏忽,最终可能会导致难以发现的错误。
此外,您无法修复多变量:=以使其完全声明您想要的内容; 你被迫放弃简短的声明或重新组织代码。

我已经看到许多提案试图解决这一问题的具体后果,但我认为问题的根源在于:=缺乏明确性(我还认为,每当阴影被认为是一个陷阱时,它真的只是多变量:=的错)。

我并不是说裸回报一定是值得的,我只是说我们所看到的是一个复合问题。

无论可读性参数如何,我认为裸返回在逻辑上不那么一致和优雅。 这在某种程度上是一个不稳定的立场。 虽然,显然我不是唯一一个主观的人。

@tandr我认为返回一个小的 struct 对象比具有三个以上的返回值要清楚得多。

替代纯粹的返回,尽管在 #21182 中进行了辩论并放弃了 #19642:

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

虽然我赞成在 Go2 中删除它,但我让 #28160(基本上)通过gofmt在 Go1 中删除它。 那里的人们评论将不胜感激! 😄

我发现裸返回在 DB 层中出奇地有帮助。

这是一个真正的编辑生产代码 -

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

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

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

这里有 6 个 return 语句。 (实际上还有 2 个,我只是为了简洁起见缩短了代码)。 但我的观点是当发生错误时,调用者只会检查err变量。 如果我只需要写return err ,我仍然可以,但是为了匹配返回签名,我必须一遍又一遍地写return st, err 。 如果你有更多的变量要返回,这会线性增长; 你的退货单越来越大。

然而,如果有命名的返回参数,我只调用返回,知道它也会返回任何其他状态的变量。 这让我很高兴,我认为这是一个很棒的功能。

@agnivade不确定这是否是因为它被编辑了,但看起来你的生产代码会在 defer 中发生恐慌,如果行为 nil,如果查询失败会发生这种情况。

@agnivade在即将到来的 Go2 错误处理方案下,你的六个回报中的五个应该消失:-)

有关反馈 wiki 的更多

@nhooyr - 哦,亲爱的 .. 看起来我们今天正在做一个刺激性的推动 .. :man_facepalming:: :sweat_smile:

@networkimprov - 是的。 我没有想到新的错误处理。 就个人而言,我会接受这个_when_新的错误处理领域。 但即便如此,我认为这是一个非常重大的变化,它必然会破坏很多代码库。 可以说,我们可以使用自动化工具解决这个问题。 但我想知道 Go 2 应该做出多少这些重大改变。

Go2 被授予不向后兼容 Go1 的许可,不是吗?

我同意可以制作工具来升级现有的 Go1 代码以与 Go2 中的此更改兼容。

Go2 被授予不向后兼容 Go1 的许可,不是吗?

几乎可以肯定我们不会使用上述许可证。 打破向后兼容性在生态系统方面非常困难且风险很大。

如果当前没有计划进行重大更改,我同意此提案不值得中断。 我希望如果/当计划进行重大更改时,我们会考虑该提案。 更新go fix以适应这个提议应该是微不足道的。

用于 Go2 错误处理的新关键字已摆在桌面上,这会破坏一些代码。

对于 Go 2,我们可以在值得的时候破坏代码,但它会带来沉重的成本。 新关键字的成本低于删除语言功能的代码,因为诊断和修复很简单。 可以想象,new 关键字的好处将超过必须稍微重写使用与关键字相同的标识符的代码的成本。

这种情况比新关键字更难重写,但它是可行的。 我认为最重要的问题是:人们在写裸回归时是否误解了这意味着什么? 由于使用了裸返回,程序中是否存在真正的错误? 即使没有真正的错误,当人们编写代码时,他们是否会错误地使用裸返回来犯错误?

由于使用了裸返回,程序中是否存在真正的错误?

我不确定它是否会导致生产错误,但对我来说,当我看到赤裸裸的回报时,通常会让我停下来试图弄清楚发生了什么。 我对隐藏的err以一种意想不到的方式工作感到偏执,因为我对他们如何互动的理解不是 100% 有信心。 它使阅读代码的速度慢得多。

也就是说,我认为 #28160 可能是一个更好的解决方案,因为它向后兼容并且 gofmt 已经有了一些小的变化。

我对以意想不到的方式工作的影子错误感到偏执

我最近才发现,您实际上不需要像我想象的那样担心阴影,至少return发生时被隐藏,则使用裸返回实际上是一个编译时错误。

在任何人说任何话之前,是的,我知道这个例子还有很多其他问题。 我只是把一些东西放在一起来演示。

28160 已关闭,在我看来,在 Go1 中修复此问题的大门已关闭。

好吧,无论如何,这种变化永远不会发生在 Go 1 中,无论人们如何接近它。

也许,也许,问题出在:= ,我们应该有删除它的建议。

这不仅仅是关于阴影。 这是关于让读者携带更多不必要的上下文。

使用这里的代码示例: https :

在第一个示例中,读者只需跟踪err的值即可确切知道返回的内容。

在第二个示例中,读者需要跟踪三个附加变量的值。

换一种方式

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

对比

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

(返回值为 nil 的变量 vs 返回 nil)

你喜欢哪个?

我更喜欢后者,因为乍一看它很明确且更容易理解。 去除裸回报将鼓励明确的回报值,而不是鼓励必须推导出的回报值。

认知负担的另一个例子是,当您查看裸返回时,您无法区分空函数 ( func(arg T) ) 和非空函数 ( func(arg T) ret R )。 可能是该函数没有返回值,也可能是它具有正在返回的命名值。 您必须记住函数签名才能知道返回的实际含义。 可以肯定的是,这不是一个大问题,但要记住的另一件事。

在某些情况下,裸 return 语句可能会令人困惑。 但也有一些特定情况下它们是有用的。 而且,当然,它们今天有效,这是保留它们的有力论据。

这里最好的解决方案可能是在语言中保留裸返回,但考虑添加一个 lint 检查,警告在发生阴影的复杂情况下使用它们。

这是我开始阅读 Go 代码库时的确切想法。
对于想要快速编码、到处使用语法并毒害受害计算机的黑客来说,简短版本应该很好。
但是在企业应用方面,很少的按键节省可能会花费您数百万的时间来检测

编辑:我想澄清一下,我的建议是禁用返回值的函数的裸返回,并保留空函数的裸返回。

所以我遇到了一些从 2017 年开始根本没有返回的示例代码。在 go 1.12.5 (windows/amd64) 中,这在 go build 中被标记为缺少返回。 这是同样的问题吗? 过去允许隐性回报,现在无效吗? 我无法通过光返回来修复它。 类型是 uintptr 所以我返回 0(nill 不会 lint)。

@datatribe ,我想不出任何会改变它的变化。 错过退货一直是一个错误。 提交包含详细信息的新错误?

@datatribe ,我想不出任何会改变它的变化。 错过退货一直是一个错误。 提交包含详细信息的新错误?

示例代码完全有可能不好。 谢谢@bradfitz

我不确定它是否会导致生产错误,但对我来说,当我看到赤裸裸的回报时,通常会让我停下来试图弄清楚发生了什么。 我对隐藏的err以一种意想不到的方式工作感到偏执,因为我对他们如何互动的理解不是 100% 有信心。 它使阅读代码的速度慢得多。

我同意你的这个观点。 它让我停下来试图找出分配给命名参数变量的值。 我不舒服的阅读(特别是长)功能与赤裸裸的回报是我不能轻易地跟踪实际返回的内容。 对于命名参数,每个裸返回实际上意味着return p1, p2, ..., pn ,但有时某些参数的值显然是字面值,如return nil, nilreturn nil, errreturn "", errreturn p, nil而不是return p, err 。 如果我写return nil, errerr仍然可能是nil值,但很明显第一个返回值是nil 。 如果不允许裸返回,如果可能的话,作者更有可能在 return 语句中写入文字值。 实际上,当我要求他们在代码审查期间避免裸返回时,他们确实都使用了文字值。

当作家用赤裸裸的回报写作时,他们往往不会考虑回报什么。 当我要求他们避免裸返回时,现在他们考虑了他们想要返回的实际值,并注意到他们返回了错误的值,例如部分构造的值。 显式不是比隐式更好吗?

我知道建议缩进错误流,但实际上并不是每个人都遵循这种模式,尤其是那些喜欢编写带有大量裸返回的长函数的人。 有时,通过查看许多裸返回很难知道它是否处于错误流中。 我仔细阅读了代码并对其进行了解码,并使用一些文字值将裸返回转换为非裸返回,这样就更容易阅读了。 (有时,如果代码很复杂,则无法计算出文字值)

我知道文档注释块提到了它在某些有意义的条件下返回的内容,但并不是每个人都这样做。 使用带有一些文字值的 return 语句,我可以通过查看代码轻松理解。

我知道我们可以同时使用命名参数和非裸返回,但并不是每个人都知道这一点。 我经常看到类似下面的东西,我认为return io.EOF更好。

err = io.EOF
return

也就是说,我认为 #28160 可能是一个更好的解决方案,因为它向后兼容并且 gofmt 已经有了一些小的变化。

我不同意gofmt应该把它们放回去的另一个提议,即使那是gofmt的工作。 那是因为只是机械地放置一堆return p1, ..., pn不会使代码更易于理解。

lint 工具可能可以检查它( nakedret做到这一点),但 IMO,裸返回弊大于利。 我也同意,去除裸返回的好处可能小于破坏兼容性的危害。 但我想用go fix很容易修复,即使机械go fix不会使代码可读。

在我看来,Go 是一种非常清晰易读的语言。 而裸返回是语言的一小部分,让我很难理解代码。

更改https://golang.org/cl/189778提到这个问题: cmd/go/internal/get: propagate parse errors in parseMetaGoImports

我可能是少数,但实际上我想提出相反的建议。 具有命名返回的函数应始终使用裸返回。 否则,它看起来就像代码在撒谎。 您分配值,然后可能会在返回时默默地覆盖它们。 这是一段糟糕的代码来说明我的意思,

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

如果您只是在那里使用裸返回,则代码再次有意义。

@millerlogic我认为这太过分了。 例如,这样的代码没有任何问题:

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

我还认为您的观点可能最适合作为单独的反提案,因为您提出的建议与当前提案提出的相反。

@mvdan好点,我认为有一个不涉及妖魔化他们的中间立场。 我想我读到了类似的想法:如果您之前分配给返回变量,则不显式返回值。 它甚至可能在某个地方实施,但我从未成功。 所以这意味着我的 hello 示例会在没有裸返回的情况下出错,而您的示例会成功,因为没有分配给 foo 或 bar。

我认为这些应该从 go 之旅中删除,这样就不会鼓励新的 go 开发人员使用这些。 实际上,它们只是一种认知税。 我见过的少数人总是需要我的大脑说“等等什么?!......哦,是的,裸返回和指定的返回值”我知道可能将它们留在巡回演出中以便每个人都知道,但有一整页专门介绍它们.

此页面是否有帮助?
0 / 5 - 0 等级