Go代码断行规则
我们在Go编程中常遵循的一个规则是:一个显式代码块的起始左大括号不放在下一行。比如,下面这个for
循环代码块编译将失败。
为了让上面这个for
循环代码块编译成功,我们不能在起始左大括号{
前断行,而应该像下面这样进行修改:
for i := 5; i > 0; i-- {
}
然而,有时候起始左大括号{
却可以放在一个新行上,比如下面这个for
循环代编译时没有问题的。
for
{
// do something ...
}
package main;
import "fmt";
func main() {
var (
i int;
sum int;
);
for i < 6 {
sum += i;
i++;
};
fmt.Println(sum);
};
假设上面这个程序存储在一个semicolons.go
文件中,我们可以运行go fmt semicolons.go
将此程序中的不必要的分号去除掉。在编译时刻,编译器会自动此插入这些去除掉的分号(至此文件的内存中的版本)。
自动插入分号的规则是什么呢?Go白皮书这样描述:
- 在Go代码中,注释除外,如果一个代码行的最后一个语法词段(token)为下列所示之一,则一个分号将自动插入在此字段后(即行尾):
- 一个;
- 一个整数、浮点数、虚部、码点或者字符串字面表示形式;
- 这几个跳转关键字之一:
break
、continue
、fallthrough
和return
; - 自增运算符
++
或者自减运算符—
; - 一个右括号:
)
、]
或}
。
- 为了允许一条复杂语句完全显示在一个代码行中,分号可能被插入在一个右小括号
)
或者右大括号}
之前。
对于上述第一条规则描述的情形,我们当然也可以手动插入这些分号,就像此前的例子中所示。换句话说,这些分号在编程时是可选的。 上述第二条规则允许我们写出如下的代码:
import (_ "math"; "fmt")
var (a int; b string)
const (M = iota; N)
type (MyInt int; T struct{x bool; y int32})
type I interface{m1(int) int; m2() string}
func f() {print("a"); panic(nil)}
编译器在编译时刻将自动插入所需的分号,如下所示:
var (a int; b string;);
const (M = iota; N;);
type I interface{m1(int) int; m2() string;};
func f() {print("a"); panic(nil);};
从以上两条规则可以看出,一个分号永远不会插入在for
关键字后,这就是为什么上面的裸for
循环例子是合法的原因。
分号自动插入规则导致的一个结果是:自增和自减运算必须呈现为单独的语句,它们不能被当作表达式使用。比如,下面的代码是编译不通过的:
func f() {
a := 0
println(a++)
println(a--)
}
上面代码编译不通过的原因是它等价于下面的代码:
func f() {
a := 0
println(a++;)
println(a--;)
}
分号自动插入规则导致的另一个结果是:我们不能在选择器中的句点.
之前断行。在选择器中的句点之后断行是允许的,比如:
anObject.
MethodA().
MethodB().
MethodC()
anObject
.MethodA()
.MethodB()
.MethodC()
此代码片段是非法的原因是编译器将自动在每个右小括号)
后插入一个分号,如下面所示:
上述分号自动插入规则可以让我们写出更简洁的代码,同时也允许我们写出一些合法的但看上去有些怪异的代码,比如: 上例中所有的流程控制代码块都是合法的。编译器将在这些行的行尾自动插入一个分号:第9行、第10行、第15行和第20行。 注意,上例中的
anObject;
.MethodA();
.MethodB();
.MethodC();
switch-case
代码块将输出true
,而不是false
。此代码块和下面这个是不同的:
如果我们使用
switch alwaysFalse() {
case true: fmt.Println("true")
case false: fmt.Println("false")
}
go fmt
命令格式化前者,一个分号将自动添加到alwaysFalse()
函数调用之后,如下所示:
插入此分号后,此代码块将和下者等价:
switch alwaysFalse();
{
case true: fmt.Println("true")
case false: fmt.Println("false")
}
这就是它为什么输出
switch alwaysFalse(); true {
case true: fmt.Println("true")
case false: fmt.Println("false")
}
true
的原因。
常使用go fmt
和命令来格式化和发现可能的逻辑错误是一个好习惯。
下面是一个很少见的情形,此情形中所示的代码看上去是合法的,但是实际上是编译不通过的。
编译错误信息表明跳转标签的声明之后必须跟一条语句。但是,看上去,上例中的三个标签声明没什么不同,它们都没有跟随一条语句。那为什么只有
switch x {
case 1:
{
goto A
A: // 这里编译没问题
}
case 2:
goto B
B: // syntax error: 跳转标签后缺少语句
case 0:
goto C
C: // 这里编译没问题
}
}
B:
标签声明是不合法的呢?原因是,根据上述第二条分号自动插入规则,编译器将在A:
和C:
标签声明之后的右大括号}
字符之前插入一个分号,如下所示:
一个单独的分号实际上表示一条。这就是为什么
func f(x int) {
switch x {
case 1:
{
goto A
A:
;} // 一个分号插入到了这里
case 2:
goto B
B: // syntax error: 跳转标签后缺少语句
case 0:
goto C
C:
;} // 一个分号插入到了这里
}
A:
和C:
标签声明之后确实跟随了一条语句的原因,所以它们是合法的。而B:
标签声明跟随的case 0:
不是一条语句,所以它是不合法的。
我们可以在B:
标签声明之后手动插入一个分号使之变得合法。
### 逗号,从不会被自动插入
一些包含多个类似项目的语法形式多用逗号,
来做为这些项目之间的分割符,比如组合字面值和函数参数列表等。在这样的一个语法形式中,最后一个项目后总可以跟一个可选的逗号。如果此逗号为它所在代码行的最后一个有效字符,则此逗号是必需的;否则,此逗号可以省略。编译器在任何情况下都不会自动插入逗号。
比如,下面的代码是合法的:
而下面这段代码是不合法的,因为编译器将自动在每一行的行尾插入一个分号(除了第二行)。其中三行在插入分号后将导致编译错误。
### 结束语 和很多Go中的其它设计细节一样,Go代码断行规则设计的评价也是褒贬不一。有些程序员不太喜欢这样的断行规则,因为这样的规则限制了代码风格的自由度。但是这些规则不但使得代码编译速度大大提高,另一方面也使得不同Go程序员写出的代码风格大体一致,从而相互可以比较轻松地读懂对方的代码。 Go语言101项目目前同时托管在和Gitlab上。欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。 本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。
func f1(a int, b string,) (x bool, y int // error
) {
return true, 789
}
var
= []int{2, 3, 5, 7, 9 // error: unexpected newline
}
var , = f1(123, "Go" // error: unexpected newline