GO語言————4.2 Go程式的基本結構和要素

FLy_鵬程萬里發表於2018-06-26

4.2 Go 程式的基本結構和要素

示例 4.1 hello_world.go

package main

import "fmt"

func main() {
	fmt.Println("hello, world")
}

4.2.1 包的概念、匯入與可見性

包是結構化程式碼的一種方式:每個程式都由包(通常簡稱為 pkg)的概念組成,可以使用自身的包或者從其它包中匯入內容。

如同其它一些程式語言中的類庫或名稱空間的概念,每個 Go 檔案都屬於且僅屬於一個包。一個包可以由許多以 .go 為副檔名的原始檔組成,因此檔名和包名一般來說都是不相同的。

你必須在原始檔中非註釋的第一行指明這個檔案屬於哪個包,如:package mainpackage main表示一個可獨立執行的程式,每個 Go 應用程式都包含一個名為 main 的包。

一個應用程式可以包含不同的包,而且即使你只使用 main 包也不必把所有的程式碼都寫在一個巨大的檔案裡:你可以用一些較小的檔案,並且在每個檔案非註釋的第一行都使用 package main 來指明這些檔案都屬於 main 包。如果你打算編譯包名不是為 main 的原始檔,如 pack1,編譯後產生的物件檔案將會是 pack1.a 而不是可執行程式。另外要注意的是,所有的包名都應該使用小寫字母。

標準庫

在 Go 的安裝檔案裡包含了一些可以直接使用的包,即標準庫。在 Windows 下,標準庫的位置在 Go 根目錄下的子目錄pkg\windows_386 中;在 Linux 下,標準庫在 Go 根目錄下的子目錄 pkg\linux_amd64 中(如果是安裝的是 32 位,則在linux_386 目錄中)。一般情況下,標準包會存放在 $GOROOT/pkg/$GOOS_$GOARCH/ 目錄下。

Go 的標準庫包含了大量的包(如:fmt 和 os),但是你也可以建立自己的包(第 8 章)。

如果想要構建一個程式,則包和包內的檔案都必須以正確的順序進行編譯。包的依賴關係決定了其構建順序。

屬於同一個包的原始檔必須全部被一起編譯,一個包即是編譯時的一個單元,因此根據慣例,每個目錄都只包含一個包。

如果對一個包進行更改或重新編譯,所有引用了這個包的客戶端程式都必須全部重新編譯。

Go 中的包模型採用了顯式依賴關係的機制來達到快速編譯的目的,編譯器會從字尾名為 .o 的物件檔案(需要且只需要這個檔案)中提取傳遞依賴型別的資訊。

如果 A.go 依賴 B.go,而 B.go 又依賴 C.go

  • 編譯 C.goB.go, 然後是 A.go.
  • 為了編譯 A.go, 編譯器讀取的是 B.o 而不是 C.o.

這種機制對於編譯大型的專案時可以顯著地提升編譯速度。

每一段程式碼只會被編譯一次

一個 Go 程式是通過 import 關鍵字將一組包連結在一起。

import "fmt" 告訴 Go 編譯器這個程式需要使用 fmt 包(的函式,或其他元素),fmt 包實現了格式化 IO(輸入/輸出)的函式。包名被封閉在半形雙引號 "" 中。如果你打算從已編譯的包中匯入並載入公開宣告的方法,不需要插入已編譯包的原始碼。

如果需要多個包,它們可以被分別匯入:

import "fmt"
import "os"
或:
import "fmt"; import "os"

但是還有更短且更優雅的方法(被稱為因式分解關鍵字,該方法同樣適用於 const、var 和 type 的宣告或定義):

import (
   "fmt"
   "os"
)

它甚至還可以更短的形式,但使用 gofmt 後將會被強制換行:

import ("fmt"; "os")

當你匯入多個包時,最好按照字母順序排列包名,這樣做更加清晰易讀。

如果包名不是以 . 或 / 開頭,如 "fmt" 或者 "container/list",則 Go 會在全域性檔案進行查詢;如果包名以 ./ 開頭,則 Go 會在相對目錄中查詢;如果包名以 / 開頭(在 Windows 下也可以這樣使用),則會在系統的絕對路徑中查詢。

匯入包即等同於包含了這個包的所有的程式碼物件。

除了符號 _,包中所有程式碼物件的識別符號必須是唯一的,以避免名稱衝突。但是相同的識別符號可以在不同的包中使用,因為可以使用包名來區分它們。

包通過下面這個被編譯器強制執行的規則來決定是否將自身的程式碼物件暴露給外部檔案:

可見性規則

當識別符號(包括常量、變數、型別、函式名、結構欄位等等)以一個大寫字母開頭,如:Group1,那麼使用這種形式的識別符號的物件就可以被外部包的程式碼所使用(客戶端程式需要先匯入這個包),這被稱為匯出(像面嚮物件語言中的 public);識別符號如果以小寫字母開頭,則對包外是不可見的,但是他們在整個包的內部是可見並且可用的(像面嚮物件語言中的 private )。

(大寫字母可以使用任何 Unicode 編碼的字元,比如希臘文,不僅僅是 ASCII 碼中的大寫字母)。

因此,在匯入一個外部包後,能夠且只能夠訪問該包中匯出的物件。

假設在包 pack1 中我們有一個變數或函式叫做 Thing(以 T 開頭,所以它能夠被匯出),那麼在當前包中匯入 pack1 包,Thing 就可以像面嚮物件語言那樣使用點標記來呼叫:pack1.Thing(pack1 在這裡是不可以省略的)。

因此包也可以作為名稱空間使用,幫助避免命名衝突(名稱衝突):兩個包中的同名變數的區別在於他們的包名,例如pack1.Thing 和 pack2.Thing

你可以通過使用包的別名來解決包名之間的名稱衝突,或者說根據你的個人喜好對包名進行重新設定,如:import fm "fmt"。下面的程式碼展示瞭如何使用包的別名:

示例 4.2 alias.go

package main

import fm "fmt" // alias3

func main() {
   fm.Println("hello, world")
}

注意事項

如果你匯入了一個包卻沒有使用它,則會在構建程式時引發錯誤,如 imported and not used: os,這正是遵循了 Go 的格言:“沒有不必要的程式碼!“。

包的分級宣告和初始化

你可以在使用 import 匯入包之後定義或宣告 0 個或多個常量(const)、變數(var)和型別(type),這些物件的作用域都是全域性的(在本包範圍內),所以可以被本包中所有的函式呼叫(如 gotemplate.go 原始檔中的 c 和 v),然後宣告一個或多個函式(func)。

4.2.2 函式

這是定義一個函式最簡單的格式:

func functionName()

你可以在括號 () 中寫入 0 個或多個函式的引數(使用逗號 , 分隔),每個引數的名稱後面必須緊跟著該引數的型別。

main 函式是每一個可執行程式所必須包含的,一般來說都是在啟動後第一個執行的函式(如果有 init() 函式則會先執行該函式)。如果你的 main 包的原始碼沒有包含 main 函式,則會引發構建錯誤 undefined: main.main。main 函式既沒有引數,也沒有返回型別(與 C 家族中的其它語言恰好相反)。如果你不小心為 main 函式新增了引數或者返回型別,將會引發構建錯誤:

func main must have no arguments and no return values results.

在程式開始執行並完成初始化後,第一個呼叫(程式的入口點)的函式是 main.main()(如:C 語言),該函式一旦返回就表示程式已成功執行並立即退出。

函式裡的程式碼(函式體)使用大括號 {} 括起來。

左大括號 { 必須與方法的宣告放在同一行,這是編譯器的強制規定,否則你在使用 gofmt 時就會出現錯誤提示:

`build-error: syntax error: unexpected semicolon or newline before {`

(這是因為編譯器會產生 func main() ; 這樣的結果,很明顯這錯誤的)

Go 語言雖然看起來不使用分號作為語句的結束,但實際上這一過程是由編譯器自動完成,因此才會引發像上面這樣的錯誤

右大括號 } 需要被放在緊接著函式體的下一行。如果你的函式非常簡短,你也可以將它們放在同一行:

func Sum(a, b int) int { return a + b }

對於大括號 {} 的使用規則在任何時候都是相同的(如:if 語句等)。

因此符合規範的函式一般寫成如下的形式:

func functionName(parameter_list) (return_value_list) {
   …
}

其中:

  • parameter_list 的形式為 (param1 type1, param2 type2, …)
  • return_value_list 的形式為 (ret1 type1, ret2 type2, …)

只有當某個函式需要被外部包呼叫的時候才使用大寫字母開頭,並遵循 Pascal 命名法;否則就遵循駱駝命名法,即第一個單詞的首字母小寫,其餘單詞的首字母大寫。

下面這一行呼叫了 fmt 包中的 Println 函式,可以將字串輸出到控制檯,並在最後自動增加換行字元 \n

fmt.Println"hello, world"

使用 fmt.Print("hello, world\n") 可以得到相同的結果。

Print 和 Println 這兩個函式也支援使用變數,如:fmt.Println(arr)。如果沒有特別指定,它們會以預設的列印格式將變數 arr 輸出到控制檯。

單純地列印一個字串或變數甚至可以使用預定義的方法來實現,如:printprintln:print("ABC")println("ABC")println(i)(帶一個變數 i)。

這些函式只可以用於除錯階段,在部署程式的時候務必將它們替換成 fmt 中的相關函式。

當被呼叫函式的程式碼執行到結束符 } 或返回語句時就會返回,然後程式繼續執行呼叫該函式之後的程式碼。

程式正常退出的程式碼為 0 即 Program exited with code 0;如果程式因為異常而被終止,則會返回非零值,如:1。這個數值可以用來測試是否成功執行一個程式。

4.2.3 註釋

示例 4.2 hello_world2.go

package main

import "fmt" // Package implementing formatted I/O.

func main() {
   fmt.Printf("Καλημέρα κόσμε; or こんにちは 世界\n")
}

上面這個例子通過列印 Καλημέρα κόσμε; or こんにちは 世界 展示瞭如何在 Go 中使用國際化字元,以及如何使用註釋。

註釋不會被編譯,但可以通過 godoc 來使用(第 3.6 節)。

單行註釋是最常見的註釋形式,你可以在任何地方使用以 // 開頭的單行註釋。多行註釋也叫塊註釋,均已以 /* 開頭,並以*/ 結尾,且不可以巢狀使用,多行註釋一般用於包的文件描述或註釋成塊的程式碼片段。

每一個包應該有相關注釋,在 package 語句之前的塊註釋將被預設認為是這個包的文件說明,其中應該提供一些相關資訊並對整體功能做簡要的介紹。一個包可以分散在多個檔案中,但是隻需要在其中一個進行註釋說明即可。當開發人員需要了解包的一些情況時,自然會用 godoc 來顯示包的文件說明,在首行的簡要註釋之後可以用成段的註釋來進行更詳細的說明,而不必擁擠在一起。另外,在多段註釋之間應以空行分隔加以區分。

示例:

// Package superman implements methods for saving the world.
//
// Experience has shown that a small number of procedures can prove
// helpful when attempting to save the world.
package superman

幾乎所有全域性作用域的型別、常量、變數、函式和被匯出的物件都應該有一個合理的註釋。如果這種註釋(稱為文件註釋)出現在函式前面,例如函式 Abcd,則要以 "Abcd..." 作為開頭。

示例:

// enterOrbit causes Superman to fly into low Earth orbit, a position
// that presents several possibilities for planet salvation.
func enterOrbit() error {
   ...
}

godoc 工具(第 3.6 節)會收集這些註釋併產生一個技術文件。

4.2.4 型別

可以包含資料的變數(或常量),可以使用不同的資料型別或型別來儲存資料。使用 var 宣告的變數的值會自動初始化為該型別的零值。型別定義了某個變數的值的集合與可對其進行操作的集合。

型別可以是基本型別,如:int、float、bool、string;結構化的(複合的),如:struct、array、slice、map、channel;只描述型別的行為的,如:interface。

結構化的型別沒有真正的值,它使用 nil 作為預設值(在 Objective-C 中是 nil,在 Java 中是 null,在 C 和 C++ 中是NULL或 0)。值得注意的是,Go 語言中不存在型別繼承。

函式也可以是一個確定的型別,就是以函式作為返回型別。這種型別的宣告要寫在函式名和可選的引數列表之後,例如:

func FunctionName (a typea, b typeb) typeFunc

你可以在函式體中的某處返回使用型別為 typeFunc 的變數 var:

return var

一個函式可以擁有多返回值,返回型別之間需要使用逗號分割,並使用小括號 () 將它們括起來,如:

func FunctionName (a typea, b typeb) (t1 type1, t2 type2)

示例: 函式 Atoi (第 4.7 節):func Atoi(s string) (i int, err error)

返回的形式:

return var1, var2

這種多返回值一般用於判斷某個函式是否執行成功(true/false)或與其它返回值一同返回錯誤訊息(詳見之後的並行賦值)。

使用 type 關鍵字可以定義你自己的型別,你可能想要定義一個結構體(第 10 章),但是也可以定義一個已經存在的型別的別名,如:

type IZ int

這裡並不是真正意義上的別名,因為使用這種方法定義之後的型別可以擁有更多的特性,且在型別轉換時必須顯式轉換。

然後我們可以使用下面的方式宣告變數:

var a IZ = 5

這裡我們可以看到 int 是變數 a 的底層型別,這也使得它們之間存在相互轉換的可能(第 4.2.6 節)。

如果你有多個型別需要定義,可以使用因式分解關鍵字的方式,例如:

type (
   IZ int
   FZ float64
   STR string
)

每個值都必須在經過編譯後屬於某個型別(編譯器必須能夠推斷出所有值的型別),因為 Go 語言是一種靜態型別語言。

4.2.5 Go 程式的一般結構

下面的程式可以被順利編譯但什麼都做不了,不過這很好地展示了一個 Go 程式的首選結構。這種結構並沒有被強制要求,編譯器也不關心 main 函式在前還是變數的宣告在前,但使用統一的結構能夠在從上至下閱讀 Go 程式碼時有更好的體驗。

所有的結構將在這一章或接下來的章節中進一步地解釋說明,但總體思路如下:

  • 在完成包的 import 之後,開始對常量、變數和型別的定義或宣告。
  • 如果存在 init 函式的話,則對該函式進行定義(這是一個特殊的函式,每個含有該函式的包都會首先執行這個函式)。
  • 如果當前包是 main 包,則定義 main 函式。
  • 然後定義其餘的函式,首先是型別的方法,接著是按照 main 函式中先後呼叫的順序來定義相關函式,如果有很多函式,則可以按照字母順序來進行排序。

示例 4.4 gotemplate.go

package main

import (
   "fmt"
)

const c = "C"

var v int = 5

type T struct{}

func init() { // initialization of package
}

func main() {
   var a int
   Func1()
   // ...
   fmt.Println(a)
}

func (t T) Method1() {
   //...
}

func Func1() { // exported function Func1
   //...
}

Go 程式的執行(程式啟動)順序如下:

  1. 按順序匯入所有被 main 包引用的其它包,然後在每個包中執行如下流程:
  2. 如果該包又匯入了其它的包,則從第一步開始遞迴執行,但是每個包只會被匯入一次。
  3. 然後以相反的順序在每個包中初始化常量和變數,如果該包含有 init 函式的話,則呼叫該函式。
  4. 在完成這一切之後,main 也執行同樣的過程,最後呼叫 main 函式開始執行程式。

4.2.6 型別轉換

在必要以及可行的情況下,一個型別的值可以被轉換成另一種型別的值。由於 Go 語言不存在隱式型別轉換,因此所有的轉換都必須顯式說明,就像呼叫一個函式一樣(型別在這裡的作用可以看作是一種函式):

valueOfTypeB = typeB(valueOfTypeA)

型別 B 的值 = 型別 B(型別 A 的值)

示例:

a := 5.0
b := int(a)

但這隻能在定義正確的情況下轉換成功,例如從一個取值範圍較小的型別轉換到一個取值範圍較大的型別(例如將 int16 轉換為 int32)。當從一個取值範圍較大的轉換到取值範圍較小的型別時(例如將 int32 轉換為 int16 或將 float32 轉換為 int),會發生精度丟失(截斷)的情況。當編譯器捕捉到非法的型別轉換時會引發編譯時錯誤,否則將引發執行時錯誤。

具有相同底層型別的變數之間可以相互轉換:

var a IZ = 5
c := int(a)
d := IZ(c)

4.2.7 Go 命名規範

乾淨、可讀的程式碼和簡潔性是 Go 追求的主要目標。通過 gofmt 來強制實現統一的程式碼風格。Go 語言中物件的命名也應該是簡潔且有意義的。像 Java 和 Python 中那樣使用混合著大小寫和下劃線的冗長的名稱會嚴重降低程式碼的可讀性。名稱不需要指出自己所屬的包,因為在呼叫的時候會使用包名作為限定符。返回某個物件的函式或方法的名稱一般都是使用名詞,沒有Get... 之類的字元,如果是用於修改某個物件,則使用 SetName。有必須要的話可以使用大小寫混合的方式,如 MixedCaps 或 mixedCaps,而不是使用下劃線來分割多個名稱。

相關文章