Go語(yǔ)言 代碼斷行規(guī)則

2023-02-16 17:38 更新

如果你已經(jīng)寫了一些Go代碼,你應(yīng)該知道,Go代碼風(fēng)格不能太隨意。 具體說來,我們不能隨意在某個(gè)空格或者符號(hào)字符處斷行。 本文余下的部分將列出Go代碼中的詳細(xì)斷行規(guī)則。

分號(hào)插入規(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白皮書這樣描述

  1. 在Go代碼中,注釋除外,如果一個(gè)代碼行的最后一個(gè)語(yǔ)法詞段(token)為下列所示之一,則一個(gè)分號(hào)將自動(dòng)插入在此字段后(即行尾):
    • 一個(gè)標(biāo)識(shí)符
    • 一個(gè)整數(shù)、浮點(diǎn)數(shù)、虛部、碼點(diǎn)或者字符串字面量;
    • 這幾個(gè)跳轉(zhuǎn)關(guān)鍵字之一:breakcontinue、fallthroughreturn;
    • 自增運(yùn)算符++或者自減運(yùn)算符--
    • 一個(gè)右括號(hào):)、]}。
  2. 為了允許一條復(fù)雜語(yǔ)句完全顯示在一個(gè)代碼行中,分號(hào)可能被插入在一個(gè)右小括號(hào))或者右大括號(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 fmtgo 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)使之變得合法。

逗號(hào),從不會(huì)被自動(dòng)插入

一些包含多個(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
)

結(jié)束語(yǔ)

最后,根據(jù)上面的解釋,在這里描述一下Go代碼中的斷行規(guī)則。

在Go代碼中,以下斷行是沒問題的(不影響程序行為的):

  • 在除了break、continuereturn這幾個(gè)跳轉(zhuǎn)關(guān)鍵字之外的任何關(guān)鍵字之后斷行,或者在不跟隨標(biāo)簽的breakcontinue關(guān)鍵字以及不跟隨返回值的return關(guān)鍵字之后斷行;
  • 在(顯式輸入的或者隱式被編譯器插入的)分號(hào);之后斷行;
  • 在不會(huì)導(dǎo)致新的隱式分號(hào)被編譯器插入的情況下斷行。

和很多Go中的其它設(shè)計(jì)細(xì)節(jié)一樣,Go代碼斷行規(guī)則設(shè)計(jì)的評(píng)價(jià)也是褒貶不一。 有些程序員不太喜歡這樣的斷行規(guī)則,因?yàn)檫@樣的規(guī)則限制了代碼風(fēng)格的自由度。 但是這些規(guī)則不但使得代碼編譯速度大大提高,另一方面也使得不同Go程序員寫出的代碼風(fēng)格大體一致,從而相互可以比較輕松地讀懂對(duì)方的代碼。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)