如果你已經寫了一些Go代碼,你應該知道,Go代碼風格不能太隨意。 具體說來,我們不能隨意在某個空格或者符號字符處斷行。 本文余下的部分將列出Go代碼中的詳細斷行規(guī)則。
我們在Go編程中常遵循的一個規(guī)則是:一個顯式代碼塊的起始左大括號{
不放在下一行。 比如,下面這個for
循環(huán)代碼塊編譯將失敗。
for i := 5; i > 0; i--
{ // error: 未預料到的新行
}
為了讓上面這個for
循環(huán)代碼塊編譯成功,我們不能在起始左大括號{
前斷行,而應該像下面這樣進行修改:
for i := 5; i > 0; i-- {
}
然而,有時候起始左大括號{
卻可以放在一個新行上,比如下面這個for
循環(huán)代編譯時沒有問題的。
for
{
// do something ...
}
那么,Go代碼中的根本性換行規(guī)則究竟是如何定義的呢? 在回答這個問題之前,我們應該知道一個事實:正式的Go語法是使用(英文)分號;
做為結尾標識符的。 但是,我們很少在Go代碼中使用和看到分號。為什么呢?原因是大多數分號都是可選的,因此它們常常被省略。 在編譯時刻,Go編譯器會自動插入這些省略的分號。
比如,下面這個程序中的十個分號都是可以被省略掉的。
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
將此程序中的不必要的分號去除掉。 在編譯時刻,編譯器會自動此插入這些去除掉的分號(至此文件的內存中的版本)。
自動插入分號的規(guī)則是什么呢?Go白皮書這樣描述:
)
或者右大括號}
之前。對于上述第一條規(guī)則描述的情形,我們當然也可以手動插入這些分號,就像此前的例子中所示。換句話說,這些分號在編程時是可選的。
上述第二條規(guī)則允許我們寫出如下的代碼:
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 (MyInt int; T struct{x bool; y int32;};);
type I interface{m1(int) int; m2() string;};
func f() {print("a"); panic(nil);};
編譯器不會為其它任何情形插入分號。如果其它任何情形需要一個分號,我們必須手動插入此分號。 比如,上例中的每行中的第一個分號必須手動插入。下例中的分號也都需要手動插入。
var a = 1; var b = true
a++; b = !b
print(a); print(b)
從以上兩條規(guī)則可以看出,一個分號永遠不會插入在for
關鍵字后,這就是為什么上面的裸for
循環(huán)例子是合法的。
分號自動插入規(guī)則導致的一個結果是:自增和自減運算必須呈現為單獨的語句,它們不能被當作表達式使用。 比如,下面的代碼是編譯不通過的:
func f() {
a := 0
println(a++)
println(a--)
}
上面代碼編譯不通過的原因是它等價于下面的代碼:
func f() {
a := 0
println(a++;)
println(a--;)
}
分號自動插入規(guī)則導致的另一個結果是:我們不能在選擇器中的句點.
之前斷行。 在選擇器中的句點之后斷行是允許的,比如:
anObject.
MethodA().
MethodB().
MethodC()
而下面這樣是非法的:
anObject
.MethodA()
.MethodB()
.MethodC()
此代碼片段是非法的原因是編譯器將自動在每個右小括號)
后插入一個分號,如下面所示:
anObject;
.MethodA();
.MethodB();
.MethodC();
上述分號自動插入規(guī)則可以讓我們寫出更簡潔的代碼,同時也允許我們寫出一些合法的但看上去有些怪異的代碼,比如:
package main
import "fmt"
func alwaysFalse() bool {return false}
func main() {
for
i := 0
i < 6
i++ {
// 使用i ...
}
if x := alwaysFalse()
!x {
// ...
}
switch alwaysFalse()
{
case true: fmt.Println("true")
case false: fmt.Println("false")
}
}
上例中所有的流程控制代碼塊都是合法的。編譯器將在這些行的行尾自動插入一個分號:第9行、第10行、第15行和第20行。
注意,上例中的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
和go vet
命令來格式化和發(fā)現可能的邏輯錯誤是一個好習慣。
下面是一個很少見的情形,此情形中所示的代碼看上去是合法的,但是實際上是編譯不通過的。
func f() {
switch x {
case 1:
{
goto A
A: // 這里編譯沒問題
}
case 2:
goto B
B: // syntax error: 跳轉標簽后缺少語句
case 0:
goto C
C: // 這里編譯沒問題
}
}
編譯錯誤信息表明跳轉標簽的聲明之后必須跟一條語句。 但是,看上去,上例中的三個標簽聲明沒什么不同,它們都沒有跟隨一條語句。 那為什么只有B:
標簽聲明是不合法的呢? 原因是,根據上述第二條分號自動插入規(guī)則,編譯器將在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:
標簽聲明之后手動插入一個分號使之變得合法。
一些包含多個類似項目的語法形式多用逗號,
來做為這些項目之間的分割符,比如組合字面量和函數參數列表等。 在這樣的一個語法形式中,最后一個項目后總可以跟一個可選的逗號。 如果此逗號為它所在代碼行的最后一個有效字符,則此逗號是必需的;否則,此逗號可以省略。 編譯器在任何情況下都不會自動插入逗號。
比如,下面的代碼是合法的:
func f1(a int, b string,) (x bool, y int,) {
return true, 789
}
var f2 func (a int, b string) (x bool, y int)
var f3 func (a int, b string, // 最后一個逗號是必需的
) (x bool, y int, // 最后一個逗號是必需的
)
var _ = []int{2, 3, 5, 7, 9,} // 最后一個逗號是可選的
var _ = []int{2, 3, 5, 7, 9, // 最后一個逗號是必需的
}
var _ = []int{2, 3, 5, 7, 9}
var _, _ = f1(123, "Go",) // 最后一個逗號是可選的
var _, _ = f1(123, "Go", // 最后一個逗號是必需的
)
var _, _ = f1(123, "Go")
// 對于顯式轉換也是一樣的:
var _ = string(65,) // 最后一個逗號是可選的
var _ = string(65, // 最后一個逗號是必需的
)
而下面這段代碼是不合法的,因為編譯器將自動在每一行的行尾插入一個分號(除了第二行)。 其中三行在插入分號后將導致編譯錯誤。
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
)
最后,根據上面的解釋,在這里描述一下Go代碼中的斷行規(guī)則。
在Go代碼中,以下斷行是沒問題的(不影響程序行為的):
break
、continue
和return
這幾個跳轉關鍵字之外的任何關鍵字之后斷行,或者在不跟隨標簽的break
和continue
關鍵字以及不跟隨返回值的return
關鍵字之后斷行;
;
之后斷行;
和很多Go中的其它設計細節(jié)一樣,Go代碼斷行規(guī)則設計的評價也是褒貶不一。 有些程序員不太喜歡這樣的斷行規(guī)則,因為這樣的規(guī)則限制了代碼風格的自由度。 但是這些規(guī)則不但使得代碼編譯速度大大提高,另一方面也使得不同Go程序員寫出的代碼風格大體一致,從而相互可以比較輕松地讀懂對方的代碼。
更多建議: