Golang中这个switch语句能编译通过,应该允许吗?

Golang中这个switch语句能编译通过,应该允许吗? 今天遇到了一个关于Go语言switch语句的有趣陷阱。

考虑以下代码:

package main

import "fmt"

func f() bool {
  return false
}

func main() {
  switch f()
  {
  case false:
    fmt.Println(1)
  case f():
    fmt.Println(2)
  case true:
    fmt.Println(3)
  default:
    fmt.Println(4)
  }
}

这段代码输出3。

这个陷阱(大多数编辑器不会捕捉到)在于switch语句后面的大括号被放在了下一行。我想知道为什么这会被允许?以及这些case语句是否仍然与它们前面的switch语句相关联。

谢谢


更多关于Golang中这个switch语句能编译通过,应该允许吗?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

太棒了!!!我纯粹出于好奇,刚刚把这个工具加入了我的工具箱……!!!谢谢!!!

更多关于Golang中这个switch语句能编译通过,应该允许吗?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我不是“反编译”专家,但我猜想 switch 语句只是编写一堆嵌套 if…else…if 的“简写”方式。

这段代码:

 switch f(); {
case false:
    fmt.Println(1)
case f():
    fmt.Println(2)
case true:
    fmt.Println(3)
default:
    fmt.Println(4)
}

其运行方式相当于:

 f()
if false {
fmt.Println(1)
} else if f() {
fmt.Println(2)
} else if true {
fmt.Println(3)
} else {
fmt.Println(4)
}

只需在 f 函数中添加一个 Println 来检查它被执行了多少次(2次)。

func f() bool {
 fmt.Println("f() Called")
return false
}

你可以在这个网站上(它展示生成的汇编代码)进一步研究并反馈回来 😉 (我已经在里面编辑了你的示例并使用了ARM64 Go编译器…)

Compiler Explorer Logo

Compiler Explorer - Go (ARM64 gccgo 13.2.0)

package main

import (
	"fmt"
)

func f() bool {
	return false
}

func doswitch() {
switch f(); {
	case false:
		fmt.Println(1)
	case f():
		fmt.Println(2)
	case true:
		fmt.Println(3)
	default:
		fmt.Println(4)
	}
}

func doifelse() {
    f()
	if...

在实际工作中,我从未见过由类似情况引发的错误。而且在实际开发中,工具会对你的代码执行 go fmt。例如,尝试将以下代码粘贴到 Go Playground 中,一旦你点击“运行”(它也会格式化你的代码),问题就会变得显而易见。来自 Go 语言之旅

Go 的 switch 语句类似于 C、C++、Java、JavaScript 和 PHP 中的 switch,不同之处在于 Go 只运行选中的 case,而不是之后所有的 case。实际上,这些语言中每个 case 末尾所需的 break 语句在 Go 中是自动提供的。另一个重要区别是,Go 的 switch case 不必是常量,所涉及的值也不必是整数。

正如我所说——我从未见过由你上面所写的那种情况导致的错误。然而,我见过很多在 Go 中被避免的错误,正是因为 Go 隐式地添加了 break 语句。无论如何,如果你在 Go Playground 上运行你的代码,你就会明白发生了什么。本质上,你的代码等同于 没有条件的 switch

没有条件的 switch 等同于 switch true

另请参阅:

https://gobyexample.com/switch

这是一个关于Go语言语法解析的经典案例。你观察到的现象是正确的,这段代码能够编译通过,并且输出3

原因分析

Go语言的分号自动插入规则导致了这个问题。根据Go语言规范,当一行以特定标记结束时(如标识符、数字、字符串字面量或breakcontinue等关键字),解析器会自动插入分号。

在你的代码中:

switch f()
{
// ...
}

解析器将switch f()解析为一个完整的语句,并在右括号后自动插入分号,相当于:

switch f();
{
// ...
}

这实际上创建了两个独立的语句:

  1. 一个没有case的switch f()语句
  2. 一个独立的代码块(复合语句)

实际解析结果

你的代码被解析为:

switch f();  // 空的switch语句,没有case,不执行任何操作
{
case false:  // 这是一个标签语句,不是switch的case
    fmt.Println(1)
case f():    // 另一个标签语句
    fmt.Println(2)
case true:   // 标签语句,程序会执行到这里
    fmt.Println(3)
default:     // 默认标签
    fmt.Println(4)
}

大括号内的casedefault被解析为标签语句,而不是switch语句的一部分。在Go中,标签可以用于breakcontinuegoto语句。

验证示例

这里有一个更清晰的示例来展示这个现象:

package main

import "fmt"

func main() {
    // 示例1:你遇到的情况
    fmt.Println("示例1:")
    switch f()
    {
    case false:
        fmt.Println("不会执行")
    case true:
        fmt.Println("会执行 - 作为标签")
    }
    
    // 示例2:正确的写法
    fmt.Println("\n示例2:")
    switch f() {
    case false:
        fmt.Println("会执行 - 作为switch的case")
    case true:
        fmt.Println("不会执行")
    }
    
    // 示例3:展示标签的实际用途
    fmt.Println("\n示例3:")
    i := 0
Loop:
    for i < 3 {
        switch i {
        case 1:
            break Loop  // 使用标签跳出外层循环
        }
        fmt.Println(i)
        i++
    }
}

func f() bool {
    return false
}

输出:

示例1:
会执行 - 作为标签
示例2:
会执行 - 作为switch的case
示例3:
0

结论

  1. 允许的原因:这是Go语言分号自动插入规则的副作用,语法上完全合法。
  2. 关联性:大括号内的case语句与前面的switch语句没有关联,它们是独立的标签语句。
  3. 最佳实践:始终将switch语句的左大括号放在同一行,这是Go语言的官方代码风格。

这种语法虽然合法,但容易引起误解,因此Go的gofmt工具会强制将左大括号放在同一行,避免这种混淆。

回到顶部