在C/ c++(以及该家族的许多语言)中,根据条件声明和初始化变量的常用习语使用三元条件操作符:

int index = val > 0 ? val : -val

Go没有条件运算符。实现上面同一段代码的最惯用的方法是什么?我想出了下面的解决方案,但它似乎相当啰嗦

var index int

if val > 0 {
    index = val
} else {
    index = -val
}

还有更好的办法吗?


当前回答

正如其他人所注意到的,golang没有三元运算符或任何等价的运算符。这是一个经过深思熟虑的决定,旨在提高可读性。

这最近让我遇到了一个场景,以一种非常有效的方式构建位掩码,在习惯地编写时变得很难阅读,或者在封装为函数时非常低效,或者两者兼有,因为代码产生分支:

package lib

func maskIfTrue(mask uint64, predicate bool) uint64 {
  if predicate {
    return mask
  }
  return 0
}

生产:

        text    "".maskIfTrue(SB), NOSPLIT|ABIInternal, $0-24
        funcdata        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        funcdata        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        movblzx "".predicate+16(SP), AX
        testb   AL, AL
        jeq     maskIfTrue_pc20
        movq    "".mask+8(SP), AX
        movq    AX, "".~r2+24(SP)
        ret
maskIfTrue_pc20:
        movq    $0, "".~r2+24(SP)
        ret

我从中学到的是多利用一点围棋;在函数中使用一个命名的结果(result int)为我节省了在函数中声明它的一行(你可以用capture做同样的事情),但是编译器也识别这个习惯用法(只分配一个值IF),并在可能的情况下将其替换为条件指令。

func zeroOrOne(predicate bool) (result int) {
  if predicate {
    result = 1
  }
  return
}

产生无分支的结果:

    movblzx "".predicate+8(SP), AX
    movq    AX, "".result+16(SP)
    ret

然后自由内联。

package lib

func zeroOrOne(predicate bool) (result int) {
  if predicate {
    result = 1
  }
  return
}

type Vendor1 struct {
    Property1 int
    Property2 float32
    Property3 bool
}

// Vendor2 bit positions.
const (
    Property1Bit = 2
    Property2Bit = 3
    Property3Bit = 5
)

func Convert1To2(v1 Vendor1) (result int) {
    result |= zeroOrOne(v1.Property1 == 1) << Property1Bit
    result |= zeroOrOne(v1.Property2 < 0.0) << Property2Bit
    result |= zeroOrOne(v1.Property3) << Property3Bit
    return
}

生产https://go.godbolt.org/z/eKbK17

    movq    "".v1+8(SP), AX
    cmpq    AX, $1
    seteq   AL
    xorps   X0, X0
    movss   "".v1+16(SP), X1
    ucomiss X1, X0
    sethi   CL
    movblzx AL, AX
    shlq    $2, AX
    movblzx CL, CX
    shlq    $3, CX
    orq     CX, AX
    movblzx "".v1+20(SP), CX
    shlq    $5, CX
    orq     AX, CX
    movq    CX, "".result+24(SP)
    ret

其他回答

如果你所有的分支都产生了副作用,或者计算成本很高,下面是一个语义保留的重构:

index := func() int {
    if val > 0 {
        return printPositiveAndReturn(val)
    } else {
        return slowlyReturn(-val)  // or slowlyNegate(val)
    }
}();  # exactly one branch will be evaluated

通常没有开销(内联),最重要的是,不会因为只使用一次的辅助函数而使名称空间混乱(这会影响可读性和维护)。生活的例子

注意,如果你天真地应用Gustavo的方法:

    index := printPositiveAndReturn(val);
    if val <= 0 {
        index = slowlyReturn(-val);  // or slowlyNegate(val)
    }

你会得到一个有着不同行为的程序;在val <= 0的情况下,程序将输出一个非正值,而它不应该!(类似地,如果反转分支,就会不必要地调用缓慢的函数,从而引入开销。)

没有括号,map三元很容易读:

c := map[bool]int{true: 1, false: 0} [5 > 4]

我已经编译了一些项目并比较了速度。

/*
go test ternary_op_test.go -v -bench="^BenchmarkTernaryOperator" -run=none -benchmem
*/
package _test

import (
    "testing"
)

func BenchmarkTernaryOperatorIfElse(b *testing.B) {
    for i := 0; i < b.N; i++ {
        if i%2 == 0 {
            _ = i
        } else {
            _ = -i
        }
    }
}

// https://stackoverflow.com/a/45886594/9935654
func Ternary(statement bool, a, b interface{}) interface{} {
    if statement {
        return a
    }
    return b
}

func BenchmarkTernaryOperatorTernaryFunc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = Ternary(i%2 == 0, i, -i).(int)
    }
}

// https://stackoverflow.com/a/34636594/9935654
func BenchmarkTernaryOperatorWithFunc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = func() int {
            if i%2 == 0 {
                return i
            } else {
                return -i
            }
        }
    }
}

// https://stackoverflow.com/a/31483763/9935654
func BenchmarkTernaryOperatorMap(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = map[bool]int{true: i, false: -i}[i%2 == 0]
    }
}

输出

goos: windows
goarch: amd64
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
BenchmarkTernaryOperatorIfElse
BenchmarkTernaryOperatorIfElse-8                1000000000               0.4460 ns/op          0 B/op          0 allocs/op
BenchmarkTernaryOperatorTernaryFunc
BenchmarkTernaryOperatorTernaryFunc-8           1000000000               0.3602 ns/op          0 B/op          0 allocs/op
BenchmarkTernaryOperatorWithFunc
BenchmarkTernaryOperatorWithFunc-8              659517496                1.642 ns/op           0 B/op          0 allocs/op
BenchmarkTernaryOperatorMap
BenchmarkTernaryOperatorMap-8                   13429532                82.48 ns/op            0 B/op          0 allocs/op
PASS
ok      command-line-arguments  4.365s

埃尔德的回答既有趣又有创意,甚至可以说很聪明。

但是,建议改为:

var index int
if val > 0 {
    index = printPositiveAndReturn(val)
} else {
    index = slowlyReturn(-val)  // or slowlyNegate(val)
}

是的,它们都编译成本质上相同的程序集,但是这段代码比调用匿名函数只返回一个可以首先写入变量的值要清晰得多。

基本上,简单清晰的代码比有创意的代码更好。

此外,任何使用map文字的代码都不是一个好主意,因为在Go中映射根本不是轻量级的。自Go 1.3以来,小型地图的随机迭代顺序得到了保证,为了加强这一点,小型地图的内存效率大大降低。

As a result, making and removing numerous small maps is both space-consuming and time-consuming. I had a piece of code that used a small map (two or three keys, are likely, but common use case was only one entry) But the code was dog slow. We're talking at least 3 orders of magnitude slower than the same code rewritten to use a dual slice key[index]=>data[index] map. And likely was more. As some operations that were previously taking a couple of minutes to run, started completing in milliseconds.\

正如所指出的(希望这并不令人意外),使用if+else确实是在Go中执行条件语句的惯用方式。

除了完整的var+if+else代码块之外,这种拼写也经常被使用:

index := val
if val <= 0 {
    index = -val
}

如果你有一段重复的代码,比如int value = a <= b ?A: b,你可以创建一个函数来保存它:

func min(a, b int) int {
    if a <= b {
        return a
    }
    return b
}

...

value := min(a, b)

编译器将内联这些简单的函数,所以它更快,更清楚,更简短。