Go 語(yǔ)言 作用域

2023-03-14 16:51 更新

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


2.7. 作用域

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

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

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

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

控制流標(biāo)號(hào),就是break、continue或goto語(yǔ)句后面跟著的那種標(biāo)號(hào),則是函數(shù)級(jí)的作用域。

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

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

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)部的一個(gè)聲明可能屏蔽外部的聲明。還有許多語(yǔ)法塊是if或for等控制流語(yǔ)句構(gòu)造的。下面的代碼有三個(gè)不同的變量x,因?yàn)樗鼈兪嵌x在不同的詞法域(這個(gè)例子只是為了演示作用域規(guī)則,但不是好的編程風(fēng)格)。

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'聲明語(yǔ)句的初始化的表達(dá)式中都引用了外部作用域聲明的x變量,稍后我們會(huì)解釋這個(gè)。(注意,后面的表達(dá)式與unicode.ToUpper并不等價(jià)。)

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

下面的例子同樣有三個(gè)不同的x變量,每個(gè)聲明在不同的詞法域,一個(gè)在函數(shù)體詞法域,一個(gè)在for隱式的初始化詞法域,一個(gè)在for循環(huán)體詞法域;只有兩個(gè)塊是顯式創(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語(yǔ)句也會(huì)在條件部分創(chuàng)建隱式詞法域,還有它們對(duì)應(yīng)的執(zhí)行體詞法域。下面的if-else測(cè)試鏈演示了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

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

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

在這個(gè)程序中:

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語(yǔ)句內(nèi),因此后面的語(yǔ)句將無(wú)法引入它,這將導(dǎo)致編譯錯(cuò)誤。你可能會(huì)收到一個(gè)局部變量f沒(méi)有聲明的錯(cuò)誤提示,具體錯(cuò)誤信息依賴編譯器的實(shí)現(xiàn)。

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

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

你可能會(huì)考慮通過(guò)將ReadByte和Close移動(dòng)到if的else塊來(lái)解決這個(gè)問(wèn)題:

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

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

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

由于當(dāng)前的編譯器會(huì)檢測(cè)到局部聲明的cwd并沒(méi)有使用,然后報(bào)告這可能是一個(gè)錯(cuò)誤,但是這種檢測(cè)并不可靠。因?yàn)橐恍┬〉拇a變更,例如增加一個(gè)局部cwd的打印語(yǔ)句,就可能導(dǎo)致這種檢測(cè)失效。

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變量依然是沒(méi)有被正確初始化的,而且看似正常的日志輸出更是讓這個(gè)BUG更加隱晦。

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

var cwd string

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

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



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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)