在函数返回期间返回和修改原始值是安全的吗?

huangapple go评论69阅读模式
英文:

Is it safe to return and modify primitive values during func return

问题

《Go语言规范》(https://go.dev/ref/spec#Order_of_evaluation)提供了一些示例,显示在分配给切片和映射时,原始类型的求值顺序与函数调用无关。但是,这些示例中没有提到多值返回,所以我不确定它们是否适用于这种情况。

规范中的示例:

a := 1
f := func() int { a++; return a }
x := []int{a, f()}            // x可能是[1, 2]或[2, 2]:a和f()之间的求值顺序未指定
m := map[int]int{a: 1, a: 2}  // m可能是{2: 1}或{2: 2}:两个映射赋值之间的求值顺序未指定
n := map[int]int{a: f()}      // n可能是{2: 3}或{3: 3}:键和值之间的求值顺序未指定

根据规范的语言,这个函数的返回值是否也是未指定的:

func run() (int, int) {
	a := 1
	f := func() int { a++; return a }
	return a, f() // 总是返回1,2或总是返回2,2或可以返回任意值?
}

如果求值顺序未指定,那么像下面这个非玩具示例在将来的某个时候可能会出现问题,如果编译器更新了:

func CountRows(ctx context.Context, db *pgxpool.Pool) (int, error) {
  row := db.QueryRow(ctx, "SELECT COUNT(*)")
  var count int
  return count, row.Scan(&count)
}

原始问题

我不清楚Go语言规范是否明确指出从函数返回的值是逐个返回还是在所有表达式求值完成后一次性返回。

也就是说,这段代码是否保证始终输出10 <nil>(就像在playground中一样),还是可能输出0 <nil>

package main

import "fmt"

func main() {
	fmt.Println(run())
	// Output: 10 <nil>
}

func run() (int, error) {
	var i int
	return i, inc(&i)
}

func inc(i *int) error {
	*i = *i + 10
	return nil
}

编辑

这个相关问题表明规范中未指定原始类型返回值的求值顺序。

英文:

(question reworded based on discussion, original question below)

The Go Language Specification gives examples showing that order of evaluation for primitives is unspecified with respect to function calls during assignment to slices and maps. None of the examples mention multi-value return though, so I'm not sure if they apply in this case or not.

Examples from the spec

a := 1
f := func() int { a++; return a }
x := []int{a, f()}            // x may be [1, 2] or [2, 2]: evaluation order between a and f() is not specified
m := map[int]int{a: 1, a: 2}  // m may be {2: 1} or {2: 2}: evaluation order between the two map assignments is not specified
n := map[int]int{a: f()}      // n may be {2: 3} or {3: 3}: evaluation order between the key and the value is not specified

Using the language of the spec, is this functions return also unspecified:

func run() (int, int) {
	a := 1
	f := func() int { a++; return a }
	return a, f() // always return 1,2 OR always returns 2,2 OR can return either?
}

If the order of evaluation is not specified then a non-toy example like the following could break at some time in the future if the compiler is updated:

func CountRows(ctx context.Context, db *pgxpool.Pool) (int, error) {
  row := db.QueryRow(ctx, &quot;SELECT COUNT(*)&quot;)
  var count int
  return count, row.Scan(&amp;count)
}

Original Question

I'm unclear whether the go language specification is clear on whether values returned from funcs are "returned" one at a time or once all expressions are evaluated.

Aka is this code guaranteed to always output 10 &lt;nil&gt; (as it does in the playground) or can it ever output 0 &lt;nil&gt;?

package main

import &quot;fmt&quot;

func main() {
	fmt.Println(run())
	// Output: 10 &lt;nil&gt;
}

func run() (int, error) {
	var i int
	return i, inc(&amp;i)
}

func inc(i *int) error {
	*i = *i + 10
	return nil
}

Edit

This related question suggests that order of evaluation of the primitive return value is not specified by the specification

答案1

得分: 1

  1. "这个函数的返回值也是未指定的吗?"

是的。正如你所发现的,语言规定了某些情况下的求值顺序:

在求值表达式、赋值语句或返回语句的操作数时,所有的函数调用方法调用通信操作都按照从左到右的词法顺序进行求值。

自然地,对于表达式、赋值语句或返回语句中不是函数调用、方法调用或通信操作的部分,其求值顺序是未指定的。

  1. "像下面这个非玩具般的例子,如果编译器更新了,它可能在将来的某个时候出错"

是的,即使编译器没有更新。如果你期望它有任何依赖于求值顺序的特定结果,那么从这个意义上说,代码已经是错误的,因为语言并不保证代码会按照你认为的方式执行。

  1. "从函数返回的值是逐个返回还是在所有表达式求值完成后一次性返回?"

没有这样的区别需要进行。

  1. "这段代码是否保证始终输出 10 <nil>(就像在 playground 中一样),还是可能输出 0 <nil>?"

是的,它可以输出 0, <nil>。这本质上与之前的 run 示例相同,只是现在将闭包 f 重构为一个名为 inc 的函数。

英文:
  1. "Is this function return also unspecified?"

Yes. As you found, the language specifies the evaluation order for some things:
> when evaluating the operands of an expression, assignment, or return statement, all function calls, method calls, and communication operations are evaluated in lexical left-to-right order.

Naturally, anything that is not a function call, method call or communication operation is left unspecified with respect to evaluation order in an expression, assignment or return statement.

  1. "a non-toy example like the following could break at some time in the future if the compiler is updated"

Yes. Even if the compiler doesn't update. If you expect it to have any particular result that's reliant on evaluation order, then the code is already broken in the sense that the language does not say the code will do what you think it will do.

  1. "are values returned from funcs "returned" one at a time or once all expressions are evaluated."

There is no such distinction to be made.

  1. "is this code guaranteed to always output 10 &lt;nil&gt; (as it does in the playground) or can it ever output 0 &lt;nil&gt;?"

Yes, it can output 0, &lt;nil&gt;. This is essentially the same as the previous run example, where now the closure f has been refactored as a function called inc.

答案2

得分: -1

是的!这是一个非常好的问题。

另一种编写这段代码的方式是:

package main

import "fmt"

func main() {
    fmt.Println(run())
    // 输出:10 <nil>
}

func run() (int, error) {
    var i int
    err := inc(&i)
    return i, err
}

func inc(i *int) error {
    *i = *i + 10
    return nil
}

这是为什么你可以这样做的原因:

func main() {
    callIt()
}

func callIt() (int, error) {
    return multiReturn()
}

func multiReturn() (int, error) {
    return 0, nil
}

此外,你还可以这样做,如果你在错误处理程序中包装了一个数据库事务(例如),这可能会很有用:

func main() {
    result, err := assignInReturn()
    if err != nil {
        panic(err)
    }

    // 这里的结果,假设没有发生错误,将是1...
    fmt.Println(result)
}

func assignInReturn() (int, error) {
    var i int
    return i, wrapErrFunc(func() error {
        // 数据库调用
        if err := dbCall(); err != nil {
            return err
        }

        i = 1 // ...因为这个。这会在`assignInReturn`返回给其调用者之前将`i`设置为1
        return nil
    })
}

func wrapErrFunc(fn func() error) error {
    return fn()
}

func dbCall() error {
    // 数据库查询
    return nil
}

在这些情况下,你可以确保顶层的return中的项在返回给调用者之前会被评估。

英文:

Yes! This is a really great question.

Another way to write this code is

package main

import &quot;fmt&quot;

func main() {
    fmt.Println(run())
    // Output: 10 &lt;nil&gt;
}

func run() (int, error) {
    var i int
    err := inc(&amp;i)
    return i, err
}

func inc(i *int) error {
    *i = *i + 10
    return nil
}

This is the same reason why you can do the following

func main() {
	callIt()
}

func callIt() (int, error) {
	return multiReturn()
}

func multiReturn() (int, error) {
	return 0, nil
}

And additionally, you can do this as well, which can be useful if you wrap a db transaction (for example) in an error handler

func main() {
	result, err := assignInReturn()
	if err != nil {
		panic(err)
	}

    // the result here, assuming no error occurred, will be 1...
	fmt.Println(result)
}

func assignInReturn() (int, error) {
	var i int
	return i, wrapErrFunc(func() error {
		// db call
		if err := dbCall(); err != nil {
			return err
		}

		i = 1 // ...because of this. This will set `i` to 1 before `assignInReturn` returns to its caller
		return nil
	})
}

func wrapErrFunc(fn func() error) error {
	return fn()
}

func dbCall() error {
	// db query
	return nil
}

In these situations, you can be certain that the items in the top-level return will evaluate prior to being returned to its caller

huangapple
  • 本文由 发表于 2023年2月17日 00:01:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/75474870.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定