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