GO的第三天,包和做用域

今天的內容看起來簡單,其實深刻了解的話仍是蠻難的!不過,咱們也是GO的初學者的一個身份,咱們先從簡單的理解開始吧。模塊化

什麼是包?Go語言中的包和其餘語言的庫或模塊的概念相似,目的都是爲了支持模塊化、封裝、單獨編譯和代碼重用。函數

import "fmt"

上面的代碼中,fmt 就是一個包,是經過關鍵詞import導入的。那麼,咱們怎麼製做本身的包呢?一個包的源代碼保存在 一個或多個 以.go爲文件後綴名的源文件中,一般一個包所在 目錄路徑 的後綴是包的導入路徑。每一個源文件都是以包的 聲明語句 開始,用來指明 包的名字 。若是你寫了一個本身的包,使用 相對路徑 來引用,那麼這個包通常放在GO的工做目錄下的 src 文件夾下,才能夠被引入使用。工具

每一個包都對應一個獨立的名字空間。例如,在image包中的Decode函數和在unicode/utf16包中的 Decode函數是不一樣的。要在外部引用該函數,必須顯式使用image.Decode或utf16.Decode形式訪問。日誌

包還可讓咱們經過控制哪些名字是外部可見的來隱藏內部實現信息。在Go語言中,一個簡單的規則是:若是一個名字是大寫字母開頭的,那麼該名字是能夠導出的。code

在每一個源文件的包聲明前緊跟着的註釋是包註釋。一般,包註釋的第一句應該先是包的功能概要說明。一個包一般只有一個源文件有包註釋(若是有多個包註釋,目前的文檔工具會根據源文件名的前後順序將它們連接爲一個包註釋)。若是包註釋很大,一般會放到一個獨立的 doc.go 文件中。排序

導入包

在Go語言程序中,每一個包都有一個全局惟一的導入路徑。Go語言的規範並無定義這些字符串的具體含義或包來自哪裏,它們是由 構建工具 來解釋的。當使用Go語言自帶的go工具箱時,一個導入路徑表明一個目錄中的一個或多個Go源文件。生命週期

除了包的導入路徑,每一個包還有一個包名,包名通常是短小的名字(並不要求包名是惟一的),包名在包的聲明處指定。 按照慣例,一個包的名字和包的導入路徑的最後一個字段相同。作用域

若是導入了一個包,可是又沒有使用該包將被看成一個編譯錯誤處理。unicode

包的初始化

包的初始化首先是解決包級變量的依賴順序,而後按照包級變量聲明出現的順序依次初始化:文檔

var a = b + c // a 第三個初始化, 爲 3
var b = f()   // b 第二個初始化, 爲 2, 經過調用 f (依賴c)
var c = 1     // c 第一個初始化, 爲 1

func f() int { return c + 1 }

若是包中含有多個.go源文件,它們將按照發給編譯器的順序進行初始化,Go語言的構建工具首先會將.go文件根據文件名排序,而後依次調用編譯器編譯。

對於在包級別聲明的變量,若是有初始化表達式則用表達式初始化,還有一些沒有初始化表達式的,例如某些表格數據初始化並非一個簡單的賦值過程。在這種狀況下,咱們能夠用一個特殊的 init初始化函數 來簡化初始化工做。每一個文件都 能夠包含多個init初始化函數

func init() { /* ... */ }

這樣的init初始化函數除了 不能被調用或引用外,其餘行爲和普通函數相似。在每一個文件中的init初始化函數,在程序開始執行時按照它們聲明的順序被自動調用。

每一個包在解決依賴的前提下,以導入聲明的順序初始化,每一個包只會被初始化一次。所以,若是一個p包導入了q包,那麼在p包初始化的時候能夠認爲q包必然已經初始化過了。初始化工做是自下而上進行的,main包最後被初始化。以這種方式,能夠確保在main函數執行以前,全部依賴的包都已經完成初始化工做了。

做用域

做用域咱們能夠套用PHP的做用域概念來理解,簡單歸納就是 函數內部聲明的變量不能被外部使用,內部聲明屏蔽了外部同名的聲明

不要將做用域和生命週期混爲一談。聲明語句的 做用域 對應的是 一個源代碼 的文本區域;它是一個編譯時的 屬性。一個變量的生命週期是指程序運行時變量存在的 有效時間段,在此時間區域內它能夠被程序的其餘部分引用;是一個 運行時的概念

聲明語句對應的詞法域決定了做用域範圍的大小。對於內置的類型、函數和常量,好比int、len和true等是在 全局做用域 的,所以能夠在整個程序中直接使用。任何在在函數外部(也就是包級語法域)聲明的名字能夠在 同一個包 的任何源文件中訪問的。

一個程序可能包含多個同名的聲明,只要它們在不一樣的詞法域就沒有關係。例如,你能夠聲明一個局部變量,和包級的變量同名。可是物極必反,若是濫用不一樣詞法域可重名的特性的話,可能致使程序很難閱讀。

當編譯器遇到一個名字引用時,若是它看起來像一個聲明,它首先從最內層的詞法域向全局的做用域查找。若是查找失敗,則報告「未聲明的名字」這樣的錯誤。若是該名字在內部和外部的塊分別聲明過,則內部塊的聲明首先被找到。在這種狀況下,內部聲明屏蔽了外部同名的聲明,讓外部的聲明的名字沒法被訪問

隱式和顯示

下面的例子一樣有三個不一樣的x變量,每一個聲明在不一樣的詞法域,一個在函數體詞法域,一個在for隱式的初始化詞法域,一個在for循環體詞法域;只有兩個塊是顯式建立的:

func main() {
    x := "hello"
    for _, x := range x {   // 隱式
        x := x + 'A' - 'a'
        fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
    }
}

在這個程序中:

if f, err := os.Open(fname); err != nil {
    return err
}
f.ReadByte() // compile error: undefined f
f.Close()    // compile error: undefined f

變量f的做用域只有在if語句內,所以後面的語句將沒法引入它,這將致使編譯錯誤。你可能會收到一個局部變量f沒有聲明的錯誤提示,具體錯誤信息依賴編譯器的實現。

一般須要在if以前聲明變量,這樣能夠確保後面的語句依然能夠訪問變量:

f, err := os.Open(fname)
if err != nil {
    return err
}
f.ReadByte()
f.Close()

短變量語句做用域

要特別注意短變量聲明語句的做用域範圍,考慮下面的程序,它的目的是獲取當前的工做目錄而後保存到一個包級的變量中。這能夠原本經過直接調用os.Getwd完成,可是將這個從主邏輯中分離出來可能會更好,特別是在須要處理錯誤的時候。函數log.Fatalf用於打印日誌信息,而後調用os.Exit(1)終止程序。

var cwd string

func init() {
    cwd, err := os.Getwd() // compile error: unused: cwd
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

雖然cwd在外部已經聲明過,可是:=語句仍是將cwd和err 從新聲明 爲新的局部變量。由於內部聲明的cwd將屏蔽外部的聲明,所以上面的代碼並不會正確更新包級聲明的cwd變量。

因爲當前的編譯器會檢測到局部聲明的cwd並無本使用,而後報告這多是一個錯誤,可是這種檢測並不可靠。由於一些小的代碼變動,例如增長一個局部cwd的打印語句,就可能致使這種檢測失效。

有許多方式能夠避免出現相似潛在的問題。最直接的方法是經過單獨聲明err變量,來避免使用:=的簡短聲明方式:

var cwd string

func init() {
    var err error
    cwd, err = os.Getwd()
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

參考

《GO語言聖經》

相關文章
相關標籤/搜索