Go 語言 作用域

2023-03-14 16:51 更新

原文鏈接:https://gopl-zh.github.io/ch2/ch2-07.html


2.7. 作用域

一個聲明語句將程序中的實體和一個名字關聯(lián),比如一個函數(shù)或一個變量。聲明語句的作用域是指源代碼中可以有效使用這個名字的范圍。

不要將作用域和生命周期混為一談。聲明語句的作用域?qū)氖且粋€源代碼的文本區(qū)域;它是一個編譯時的屬性。一個變量的生命周期是指程序運行時變量存在的有效時間段,在此時間區(qū)域內(nèi)它可以被程序的其他部分引用;是一個運行時的概念。

句法塊是由花括弧所包含的一系列語句,就像函數(shù)體或循環(huán)體花括弧包裹的內(nèi)容一樣。句法塊內(nèi)部聲明的名字是無法被外部塊訪問的。這個塊決定了內(nèi)部聲明的名字的作用域范圍。我們可以把塊(block)的概念推廣到包括其他聲明的群組,這些聲明在代碼中并未顯式地使用花括號包裹起來,我們稱之為詞法塊。對全局的源代碼來說,存在一個整體的詞法塊,稱為全局詞法塊;對于每個包;每個for、if和switch語句,也都有對應詞法塊;每個switch或select的分支也有獨立的詞法塊;當然也包括顯式書寫的詞法塊(花括弧包含的語句)。

聲明語句對應的詞法域決定了作用域范圍的大小。對于內(nèi)置的類型、函數(shù)和常量,比如int、len和true等是在全局作用域的,因此可以在整個程序中直接使用。任何在函數(shù)外部(也就是包級語法域)聲明的名字可以在同一個包的任何源文件中訪問的。對于導入的包,例如tempconv導入的fmt包,則是對應源文件級的作用域,因此只能在當前的文件中訪問導入的fmt包,當前包的其它源文件無法訪問在當前源文件導入的包。還有許多聲明語句,比如tempconv.CToF函數(shù)中的變量c,則是局部作用域的,它只能在函數(shù)內(nèi)部(甚至只能是局部的某些部分)訪問。

控制流標號,就是break、continue或goto語句后面跟著的那種標號,則是函數(shù)級的作用域。

一個程序可能包含多個同名的聲明,只要它們在不同的詞法域就沒有關系。例如,你可以聲明一個局部變量,和包級的變量同名。或者是像2.3.3節(jié)的例子那樣,你可以將一個函數(shù)參數(shù)的名字聲明為new,雖然內(nèi)置的new是全局作用域的。但是物極必反,如果濫用不同詞法域可重名的特性的話,可能導致程序很難閱讀。

當編譯器遇到一個名字引用時,它會對其定義進行查找,查找過程從最內(nèi)層的詞法域向全局的作用域進行。如果查找失敗,則報告“未聲明的名字”這樣的錯誤。如果該名字在內(nèi)部和外部的塊分別聲明過,則內(nèi)部塊的聲明首先被找到。在這種情況下,內(nèi)部聲明屏蔽了外部同名的聲明,讓外部的聲明的名字無法被訪問:

func f() {}

var g = "g"

func main() {
    f := "f"
    fmt.Println(f) // "f"; local var f shadows package-level func f
    fmt.Println(g) // "g"; package-level var
    fmt.Println(h) // compile error: undefined: h
}

在函數(shù)中詞法域可以深度嵌套,因此內(nèi)部的一個聲明可能屏蔽外部的聲明。還有許多語法塊是if或for等控制流語句構造的。下面的代碼有三個不同的變量x,因為它們是定義在不同的詞法域(這個例子只是為了演示作用域規(guī)則,但不是好的編程風格)。

func main() {
    x := "hello!"
    for i := 0; i < len(x); i++ {
        x := x[i]
        if x != '!' {
            x := x + 'A' - 'a'
            fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
        }
    }
}

x[i]x + 'A' - 'a'聲明語句的初始化的表達式中都引用了外部作用域聲明的x變量,稍后我們會解釋這個。(注意,后面的表達式與unicode.ToUpper并不等價。)

正如上面例子所示,并不是所有的詞法域都顯式地對應到由花括弧包含的語句;還有一些隱含的規(guī)則。上面的for語句創(chuàng)建了兩個詞法域:花括弧包含的是顯式的部分,是for的循環(huán)體部分詞法域,另外一個隱式的部分則是循環(huán)的初始化部分,比如用于迭代變量i的初始化。隱式的詞法域部分的作用域還包含條件測試部分和循環(huán)后的迭代部分(i++),當然也包含循環(huán)體詞法域。

下面的例子同樣有三個不同的x變量,每個聲明在不同的詞法域,一個在函數(shù)體詞法域,一個在for隱式的初始化詞法域,一個在for循環(huán)體詞法域;只有兩個塊是顯式創(chuàng)建的:

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

和for循環(huán)類似,if和switch語句也會在條件部分創(chuàng)建隱式詞法域,還有它們對應的執(zhí)行體詞法域。下面的if-else測試鏈演示了x和y的有效作用域范圍:

if x := f(); x == 0 {
    fmt.Println(x)
} else if y := g(x); x == y {
    fmt.Println(x, y)
} else {
    fmt.Println(x, y)
}
fmt.Println(x, y) // compile error: x and y are not visible here

第二個if語句嵌套在第一個內(nèi)部,因此第一個if語句條件初始化詞法域聲明的變量在第二個if中也可以訪問。switch語句的每個分支也有類似的詞法域規(guī)則:條件部分為一個隱式詞法域,然后是每個分支的詞法域。

在包級別,聲明的順序并不會影響作用域范圍,因此一個先聲明的可以引用它自身或者是引用后面的一個聲明,這可以讓我們定義一些相互嵌套或遞歸的類型或函數(shù)。但是如果一個變量或常量遞歸引用了自身,則會產(chǎn)生編譯錯誤。

在這個程序中:

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

變量f的作用域只在if語句內(nèi),因此后面的語句將無法引入它,這將導致編譯錯誤。你可能會收到一個局部變量f沒有聲明的錯誤提示,具體錯誤信息依賴編譯器的實現(xiàn)。

通常需要在if之前聲明變量,這樣可以確保后面的語句依然可以訪問變量:

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

你可能會考慮通過將ReadByte和Close移動到if的else塊來解決這個問題:

if f, err := os.Open(fname); err != nil {
    return err
} else {
    // f and err are visible here too
    f.ReadByte()
    f.Close()
}

但這不是Go語言推薦的做法,Go語言的習慣是在if中處理錯誤然后直接返回,這樣可以確保正常執(zhí)行的語句不需要代碼縮進。

要特別注意短變量聲明語句的作用域范圍,考慮下面的程序,它的目的是獲取當前的工作目錄然后保存到一個包級的變量中。這本來可以通過直接調(diào)用os.Getwd完成,但是將這個從主邏輯中分離出來可能會更好,特別是在需要處理錯誤的時候。函數(shù)log.Fatalf用于打印日志信息,然后調(diào)用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在外部已經(jīng)聲明過,但是:=語句還是將cwd和err重新聲明為新的局部變量。因為內(nèi)部聲明的cwd將屏蔽外部的聲明,因此上面的代碼并不會正確更新包級聲明的cwd變量。

由于當前的編譯器會檢測到局部聲明的cwd并沒有使用,然后報告這可能是一個錯誤,但是這種檢測并不可靠。因為一些小的代碼變更,例如增加一個局部cwd的打印語句,就可能導致這種檢測失效。

var cwd string

func init() {
    cwd, err := os.Getwd() // NOTE: wrong!
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
    log.Printf("Working directory = %s", cwd)
}

全局的cwd變量依然是沒有被正確初始化的,而且看似正常的日志輸出更是讓這個BUG更加隱晦。

有許多方式可以避免出現(xiàn)類似潛在的問題。最直接的方法是通過單獨聲明err變量,來避免使用:=的簡短聲明方式:

var cwd string

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

我們已經(jīng)看到包、文件、聲明和語句如何來表達一個程序結構。在下面的兩個章節(jié),我們將探討數(shù)據(jù)的結構。



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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號