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

2023-02-16 17:38 更新

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

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

  1. 在Go代碼中,注釋除外,如果一個代碼行的最后一個語法詞段(token)為下列所示之一,則一個分號將自動插入在此字段后(即行尾):
    • 一個標識符;
    • 一個整數、浮點數、虛部、碼點或者字符串字面量;
    • 這幾個跳轉關鍵字之一:breakcontinue、fallthroughreturn;
    • 自增運算符++或者自減運算符--
    • 一個右括號:)、]}。
  2. 為了允許一條復雜語句完全顯示在一個代碼行中,分號可能被插入在一個右小括號)或者右大括號}之前。

對于上述第一條規(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 fmtgo 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、continuereturn這幾個跳轉關鍵字之外的任何關鍵字之后斷行,或者在不跟隨標簽的breakcontinue關鍵字以及不跟隨返回值的return關鍵字之后斷行;
  • 在(顯式輸入的或者隱式被編譯器插入的)分號;之后斷行;
  • 在不會導致新的隱式分號被編譯器插入的情況下斷行。

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


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號