Go代码断行规则

如果你已经写了一些Go代码,你应该知道,Go代码风格不能太随意。 具体说来,我们不能随意在某个空格或者符号字符处断行。 本文余下的部分将列出Go代码中的详细断行规则。

分号插入规则

我们在Go编程中常遵循的一个规则是:一个显式代码块的起始左大括号{不放在下一行。 比如,下面这个for循环代码块编译将失败。

  1. for i := 5; i > 0; i--
  2. { // error: 未预料到的新行
  3. }

为了让上面这个for循环代码块编译成功,我们不能在起始左大括号{前断行,而应该像下面这样进行修改:

  1. for i := 5; i > 0; i-- {
  2. }

然而,有时候起始左大括号{却可以放在一个新行上,比如下面这个for循环代编译时没有问题的。

  1. for
  2. {
  3. // do something ...
  4. }

那么,Go代码中的根本性换行规则究竟是如何定义的呢? 在回答这个问题之前,我们应该知道一个事实:正式的Go语法是使用(英文)分号;做为结尾标识符的。 但是,我们很少在Go代码中使用和看到分号。为什么呢?原因是大多数分号都是可选的,因此它们常常被省略。 在编译时刻,Go编译器会自动插入这些省略的分号。

比如,下面这个程序中的十个分号都是可以被省略掉的。

  1. package main;
  2. import "fmt";
  3. func main() {
  4. var (
  5. i int;
  6. sum int;
  7. );
  8. for i < 6 {
  9. sum += i;
  10. i++;
  11. };
  12. fmt.Println(sum);
  13. };

假设上面这个程序存储在一个semicolons.go文件中,我们可以运行go fmt semicolons.go将此程序中的不必要的分号去除掉。 在编译时刻,编译器会自动此插入这些去除掉的分号(至此文件的内存中的版本)。

自动插入分号的规则是什么呢?Go白皮书这样描述

  1. 在Go代码中,注释除外,如果一个代码行的最后一个语法词段(token)为下列所示之一,则一个分号将自动插入在此字段后(即行尾):
    • 一个标识符
    • 一个整数、浮点数、虚部、码点或者字符串字面量
    • 这几个跳转关键字之一:breakcontinuefallthroughreturn
    • 自增运算符++或者自减运算符--
    • 一个右括号:)]}
  2. 为了允许一条复杂语句完全显示在一个代码行中,分号可能被插入在一个右小括号)或者右大括号}之前。

对于上述第一条规则描述的情形,我们当然也可以手动插入这些分号,就像此前的例子中所示。换句话说,这些分号在编程时是可选的。

上述第二条规则允许我们写出如下的代码:

  1. import (_ "math"; "fmt")
  2. var (a int; b string)
  3. const (M = iota; N)
  4. type (MyInt int; T struct{x bool; y int32})
  5. type I interface{m1(int) int; m2() string}
  6. func f() {print("a"); panic(nil)}

编译器在编译时刻将自动插入所需的分号,如下所示:

  1. var (a int; b string;);
  2. const (M = iota; N;);
  3. type (MyInt int; T struct{x bool; y int32;};);
  4. type I interface{m1(int) int; m2() string;};
  5. func f() {print("a"); panic(nil);};

编译器不会为其它任何情形插入分号。如果其它任何情形需要一个分号,我们必须手动插入此分号。 比如,上例中的每行中的第一个分号必须手动插入。下例中的分号也都需要手动插入。

  1. var a = 1; var b = true
  2. a++; b = !b
  3. print(a); print(b)

从以上两条规则可以看出,一个分号永远不会插入在for关键字后,这就是为什么上面的裸for循环例子是合法的。

分号自动插入规则导致的一个结果是:自增和自减运算必须呈现为单独的语句,它们不能被当作表达式使用。 比如,下面的代码是编译不通过的:

  1. func f() {
  2. a := 0
  3. println(a++)
  4. println(a--)
  5. }

上面代码编译不通过的原因是它等价于下面的代码:

  1. func f() {
  2. a := 0
  3. println(a++;)
  4. println(a--;)
  5. }

分号自动插入规则导致的另一个结果是:我们不能在选择器中的句点.之前断行。 在选择器中的句点之后断行是允许的,比如:

  1. anObject.
  2. MethodA().
  3. MethodB().
  4. MethodC()

而下面这样是非法的:

  1. anObject
  2. .MethodA()
  3. .MethodB()
  4. .MethodC()

此代码片段是非法的原因是编译器将自动在每个右小括号)后插入一个分号,如下面所示:

  1. anObject;
  2. .MethodA();
  3. .MethodB();
  4. .MethodC();

上述分号自动插入规则可以让我们写出更简洁的代码,同时也允许我们写出一些合法的但看上去有些怪异的代码,比如:

  1. package main
  2. import "fmt"
  3. func alwaysFalse() bool {return false}
  4. func main() {
  5. for
  6. i := 0
  7. i < 6
  8. i++ {
  9. // 使用i ...
  10. }
  11. if x := alwaysFalse()
  12. !x {
  13. // ...
  14. }
  15. switch alwaysFalse()
  16. {
  17. case true: fmt.Println("true")
  18. case false: fmt.Println("false")
  19. }
  20. }

上例中所有的流程控制代码块都是合法的。编译器将在这些行的行尾自动插入一个分号:第9行、第10行、第15行和第20行。

注意,上例中的switch-case代码块将输出true,而不是false。 此代码块和下面这个是不同的:

  1. switch alwaysFalse() {
  2. case true: fmt.Println("true")
  3. case false: fmt.Println("false")
  4. }

如果我们使用go fmt命令格式化前者,一个分号将自动添加到alwaysFalse()函数调用之后,如下所示:

  1. switch alwaysFalse();
  2. {
  3. case true: fmt.Println("true")
  4. case false: fmt.Println("false")
  5. }

插入此分号后,此代码块将和下者等价:

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

这就是它输出true的原因。

常使用go fmtgo vet命令来格式化和发现可能的逻辑错误是一个好习惯。

下面是一个很少见的情形,此情形中所示的代码看上去是合法的,但是实际上是编译不通过的。

  1. func f() {
  2. switch x {
  3. case 1:
  4. {
  5. goto A
  6. A: // 这里编译没问题
  7. }
  8. case 2:
  9. goto B
  10. B: // syntax error: 跳转标签后缺少语句
  11. case 0:
  12. goto C
  13. C: // 这里编译没问题
  14. }
  15. }

编译错误信息表明跳转标签的声明之后必须跟一条语句。 但是,看上去,上例中的三个标签声明没什么不同,它们都没有跟随一条语句。 那为什么只有B:标签声明是不合法的呢? 原因是,根据上述第二条分号自动插入规则,编译器将在A:C:标签声明之后的右大括号}字符之前插入一个分号,如下所示:

  1. func f(x int) {
  2. switch x {
  3. case 1:
  4. {
  5. goto A
  6. A:
  7. ;} // 一个分号插入到了这里
  8. case 2:
  9. goto B
  10. B: // syntax error: 跳转标签后缺少语句
  11. case 0:
  12. goto C
  13. C:
  14. ;} // 一个分号插入到了这里
  15. }

一个单独的分号实际上表示一条空语句。 这就意味着A:C:标签声明之后确实跟随了一条语句,所以它们是合法的。 而B:标签声明跟随的case 0:不是一条语句,所以它是不合法的。

我们可以在B:标签声明之后手动插入一个分号使之变得合法。

逗号,从不会被自动插入

一些包含多个类似项目的语法形式多用逗号,来做为这些项目之间的分割符,比如组合字面量和函数参数列表等。 在这样的一个语法形式中,最后一个项目后总可以跟一个可选的逗号。 如果此逗号为它所在代码行的最后一个有效字符,则此逗号是必需的;否则,此逗号可以省略。 编译器在任何情况下都不会自动插入逗号。

比如,下面的代码是合法的:

  1. func f1(a int, b string,) (x bool, y int,) {
  2. return true, 789
  3. }
  4. var f2 func (a int, b string) (x bool, y int)
  5. var f3 func (a int, b string, // 最后一个逗号是必需的
  6. ) (x bool, y int, // 最后一个逗号是必需的
  7. )
  8. var _ = []int{2, 3, 5, 7, 9,} // 最后一个逗号是可选的
  9. var _ = []int{2, 3, 5, 7, 9, // 最后一个逗号是必需的
  10. }
  11. var _ = []int{2, 3, 5, 7, 9}
  12. var _, _ = f1(123, "Go",) // 最后一个逗号是可选的
  13. var _, _ = f1(123, "Go", // 最后一个逗号是必需的
  14. )
  15. var _, _ = f1(123, "Go")
  16. // 对于显式转换也是一样的:
  17. var _ = string(65,) // 最后一个逗号是可选的
  18. var _ = string(65, // 最后一个逗号是必需的
  19. )

而下面这段代码是不合法的,因为编译器将自动在每一行的行尾插入一个分号(除了第二行)。 其中三行在插入分号后将导致编译错误。

  1. func f1(a int, b string,) (x bool, y int // error
  2. ) {
  3. return true, 789
  4. }
  5. var _ = []int{2, 3, 5, 7, 9 // error: unexpected newline
  6. }
  7. var _, _ = f1(123, "Go" // error: unexpected newline
  8. )

结束语

最后,根据上面的解释,在这里描述一下Go代码中的断行规则。

在Go代码中,以下断行是没问题的(不影响程序行为的):

  • 在除了breakcontinuereturn这几个跳转关键字之外的任何关键字之后断行,或者在不跟随标签的breakcontinue关键字以及不跟随返回值的return关键字之后断行;
  • 在(显式输入的或者隐式被编译器插入的)分号;之后断行;
  • 在不会导致新的隐式分号被编译器插入的情况下断行。

和很多Go中的其它设计细节一样,Go代码断行规则设计的评价也是褒贬不一。 有些程序员不太喜欢这样的断行规则,因为这样的规则限制了代码风格的自由度。 但是这些规则不但使得代码编译速度大大提高,另一方面也使得不同Go程序员写出的代码风格大体一致,从而相互可以比较轻松地读懂对方的代码。