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.go, B.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.Thingpack2.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") 可以得到相同的結果。

PrintPrintln 這兩個函數也支持使用變量,如: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,而不是使用下劃線來分割多個名稱。

鏈接

results matching ""

    No results matching ""