Go?代碼塊作用域變量遮蔽問(wèn)題解析
一、引入
首先我們從一個(gè) Go 變量遮蔽(Variable Shadowing
)的問(wèn)題說(shuō)起。
什么是變量遮蔽呢?
變量遮蔽(Variable Shadowing)是指在程序中一個(gè)作用域內(nèi)的變量名(或標(biāo)識(shí)符)隱藏(遮蔽)了外部作用域中相同名稱的變量。這會(huì)導(dǎo)致在遮蔽內(nèi)部作用域內(nèi),無(wú)法直接訪問(wèn)外部作用域的變量,因?yàn)榫幾g器或解釋器將優(yōu)先選擇內(nèi)部作用域的變量,而不是外部的。
我們來(lái)看下面這段示例代碼:
package main import "fmt" var x = 10 // 包級(jí)作用域的變量 func main() { x := 5 // 函數(shù)內(nèi)的局部變量,遮蔽了包級(jí)作用域的 x fmt.Println(x) // 輸出:5 } func anotherFunction() { fmt.Println(x) // 在這個(gè)函數(shù)中,外部包級(jí)作用域的 x 是可見(jiàn)的,輸出:10 }
你可以看到,在這段代碼中,函數(shù)main
內(nèi)部有一個(gè)局部變量 x
,它遮蔽了包級(jí)作用域的 x
。因此,在main
函數(shù)內(nèi)部,通過(guò)變量 x
訪問(wèn)的是局部變量,而不是外部包級(jí)作用域的變量。然而,在anotherFunction
中,沒(méi)有局部變量 x
,因此外部包級(jí)作用域的 x
是可見(jiàn)的。
二、代碼塊 (Block)
2.1 代碼塊介紹
在Go語(yǔ)言中,代碼塊是包裹在一對(duì)大括號(hào){}
包圍的聲明和語(yǔ)句序列。
2.2 顯式代碼塊
這些代碼塊是你在代碼中明確可見(jiàn)的,由一對(duì)大括號(hào) {}
包圍。比如函數(shù)的函數(shù)體、for循環(huán)的循環(huán)體、以及其他控制結(jié)構(gòu)內(nèi)部的代碼塊。這些代碼塊明確定義了它們的作用域,包括變量的可見(jiàn)性:
func Foo() { // 這里是顯式代碼塊,包裹在函數(shù)的函數(shù)體內(nèi) // ... for { // 這里是顯式代碼塊,包裹在for循環(huán)體內(nèi) // 該代碼塊也是嵌套在函數(shù)體顯式代碼塊內(nèi)部的代碼塊 // ... } if true { // 這里是顯式代碼塊,包裹在if語(yǔ)句的true分支內(nèi) // 該代碼塊也是嵌套在函數(shù)體顯式代碼塊內(nèi)部的代碼塊 // ... } }
2.3 隱式代碼塊
隱式代碼塊沒(méi)有顯式代碼塊那樣的肉眼可見(jiàn)的配對(duì)大括號(hào)包裹,我們無(wú)法通過(guò)大括號(hào)來(lái)識(shí)別隱式代碼塊。
雖然隱式代碼塊身著“隱身衣”,但我們也不是沒(méi)有方法來(lái)識(shí)別它,因?yàn)?Go 語(yǔ)言規(guī)范對(duì)現(xiàn)存的幾類隱式代碼塊做了明確的定義,我們可以看下這張圖:
我們按代碼塊范圍從大到小,逐一說(shuō)明:
- 宇宙(
Universe
)代碼塊:它囊括的范圍最大,所有 Go 源碼都在這個(gè)隱式代碼塊中,你也可以將該隱式代碼塊想象為在所有 Go 代碼的最外層加一對(duì)大括號(hào),就像圖中最外層的那對(duì)大括號(hào)那樣。 - 包代碼塊:在宇宙代碼塊內(nèi)部嵌套了包代碼塊(Package Block),每個(gè) Go 包都對(duì)應(yīng)一個(gè)隱式包代碼塊,每個(gè)包代碼塊包含了該包中的所有 Go 源碼,不管這些代碼分布在這個(gè)包里的多少個(gè)的源文件中。
- 文件代碼塊:在包代碼塊的內(nèi)部嵌套著若干文件代碼塊(File Block),每個(gè) Go 源文件都對(duì)應(yīng)著一個(gè)文件代碼塊,也就是說(shuō)一個(gè) Go 包如果有多個(gè)源文件,那么就會(huì)有多個(gè)對(duì)應(yīng)的文件代碼塊。
- 再下一個(gè)級(jí)別的隱式代碼塊就在控制語(yǔ)句層面了,包括
if
、for
與switch
。我們可以把每個(gè)控制語(yǔ)句都視為在它自己的隱式代碼塊里。不過(guò)你要注意,這里的控制語(yǔ)句隱式代碼塊與控制語(yǔ)句使用大括號(hào)包裹的顯式代碼塊并不是一個(gè)代碼塊。你再看一下前面的圖,switch
控制語(yǔ)句的隱式代碼塊的位置是在它顯式代碼塊的外面的。 - 最后,位于最內(nèi)層的隱式代碼塊是
switch
或select
語(yǔ)句的每個(gè)case/default
子句中,雖然沒(méi)有大括號(hào)包裹,但實(shí)質(zhì)上,每個(gè)子句都自成一個(gè)代碼塊。
2.4 空代碼塊
如果一對(duì)大括號(hào)內(nèi)部沒(méi)有任何聲明或其他語(yǔ)句,我們就把它叫做空代碼塊。
空代碼塊在Go語(yǔ)言中是有效的,并且在某些情況下可以有一定的用途,尤其是在控制結(jié)構(gòu)中,如if語(yǔ)句、for循環(huán)或switch語(yǔ)句的特定分支。它們充當(dāng)了占位符,允許你將來(lái)添加代碼而不需要改變代碼的結(jié)構(gòu)。
以下是一個(gè)示例,演示了空代碼塊的使用:
func main() { x := 10 if x > 5 { // 非空代碼塊 fmt.Println("x 大于 5") } else { // 空代碼塊,什么都不做 } for i := 0; i < 5; i++ { // 空代碼塊,什么都不做 } }
2.5 支持嵌套代碼塊
Go 代碼塊支持嵌套,我們可以在一個(gè)代碼塊中嵌入多個(gè)層次的代碼塊,如下面示例代碼所示:
func foo() { //代碼塊1 { // 代碼塊2 { // 代碼塊3 { // 代碼塊4 } } } }
三、作用域 (Scope)
3.1 作用域介紹
作用域的概念是針對(duì)標(biāo)識(shí)符的,不局限于變量。每個(gè)標(biāo)識(shí)符都有自己的作用域,而一個(gè)標(biāo)識(shí)符的作用域就是指這個(gè)標(biāo)識(shí)符在被聲明后可以被有效使用的源碼區(qū)域。
顯然,作用域是一個(gè)編譯期的概念,也就是說(shuō),編譯器在編譯過(guò)程中會(huì)對(duì)每個(gè)標(biāo)識(shí)符的作用域進(jìn)行檢查,對(duì)于在標(biāo)識(shí)符作用域外使用該標(biāo)識(shí)符的行為會(huì)給出編譯錯(cuò)誤的報(bào)錯(cuò)。
3.2 作用域劃定原則
我們可以使用代碼塊的概念來(lái)劃定每個(gè)標(biāo)識(shí)符的作用域。一般劃定原則就是聲明于外層代碼塊中的標(biāo)識(shí)符,其作用域包括所有內(nèi)層代碼塊。而且,這一原則同時(shí)適于顯式代碼塊與隱式代碼塊。
3.3 標(biāo)識(shí)符的作用域范圍
3.3.1 預(yù)定義標(biāo)識(shí)符作用域
首先,我們來(lái)看看位于最外層的宇宙隱式代碼塊的標(biāo)識(shí)符。這一區(qū)域是 Go 語(yǔ)言預(yù)定義標(biāo)識(shí)符的自留地。你可以看看下面這張表是Go 語(yǔ)言當(dāng)前版本定義里的所有預(yù)定義標(biāo)識(shí)符:
由于這些預(yù)定義標(biāo)識(shí)符位于包代碼塊的外層,所以它們的作用域是范圍最大的,對(duì)于開(kāi)發(fā)者而言,它們的作用域就是源代碼中的任何位置。不過(guò),這些預(yù)定義標(biāo)識(shí)符不是關(guān)鍵字,我們同樣可以在內(nèi)層代碼塊中聲明同名的標(biāo)識(shí)符。
3.3.2 包代碼塊級(jí)作用域
包頂層聲明中的常量、類型、變量或函數(shù)(不包括方法)對(duì)應(yīng)的標(biāo)識(shí)符的作用域是包代碼塊。
不過(guò),對(duì)于作用域?yàn)榘a塊的標(biāo)識(shí)符,我需要你知道一個(gè)特殊情況。那就是當(dāng)一個(gè)包 A
導(dǎo)入另外一個(gè)包 B
后,包 A
僅可以使用被導(dǎo)入包包 B
中的導(dǎo)出標(biāo)識(shí)符(Exported Identifier)。
按照 Go 語(yǔ)言定義,一個(gè)標(biāo)識(shí)符要成為導(dǎo)出標(biāo)識(shí)符需同時(shí)具備兩個(gè)條件:一是這個(gè)標(biāo)識(shí)符聲明在包代碼塊中,或者它是一個(gè)字段名或方法名;二是它名字第一個(gè)字符是一個(gè)大寫的 Unicode 字符。這兩個(gè)條件缺一不可。
// 包 A package A import "B" func SomeFunction() { // 可以訪問(wèn)包 B 中的導(dǎo)出標(biāo)識(shí)符 B.ExportFunction() } // 這里無(wú)法訪問(wèn)包 B 中的非導(dǎo)出標(biāo)識(shí)符
3.3.3 文件代碼塊作用域(包的導(dǎo)入作用域)
在Go語(yǔ)言中,除了大多數(shù)在包頂層聲明的標(biāo)識(shí)符具有包代碼塊范圍的作用域外,還有一個(gè)特殊情況,即導(dǎo)入的包名。導(dǎo)入的包名的作用域是文件代碼塊范圍,這意味著它在包含它的源代碼文件中可見(jiàn),但對(duì)其他源文件不可見(jiàn)。
考慮以下示例,其中一個(gè)包A有兩個(gè)源文件,它們都依賴包B中的標(biāo)識(shí)符:
// 文件1:source1.go package A import "B" func FunctionInSource1() { B.SomeFunctionFromB() // 可以使用導(dǎo)入的包名 B }
// 文件2:source2.go package A import "B" func FunctionInSource2() { B.AnotherFunctionFromB() // 可以使用導(dǎo)入的包名 B }
在這個(gè)示例中,兩個(gè)源文件都導(dǎo)入了包B,但每個(gè)文件內(nèi)的包名 B
在文件級(jí)別可見(jiàn)。這意味著FunctionInSource1
和FunctionInSource2
函數(shù)都可以訪問(wèn)B
包中的導(dǎo)出標(biāo)識(shí)符(以大寫字母開(kāi)頭的標(biāo)識(shí)符),但對(duì)于其他包和源文件而言,它們不可見(jiàn)。
3.3.4 函數(shù)體的作用域
函數(shù)體內(nèi)的標(biāo)識(shí)符的作用域被限制在函數(shù)的開(kāi)始和結(jié)束之間。這意味著函數(shù)體內(nèi)的局部變量只能在函數(shù)體內(nèi)部訪問(wèn)。
func exampleFunction() { var localVar = 42 fmt.Println(localVar) // 可以訪問(wèn)局部變量 localVar } fmt.Println(localVar) // 這里無(wú)法訪問(wèn)局部變量 localVar
3.3.5 流程控制作用域
流程控制結(jié)構(gòu),如if語(yǔ)句、for循環(huán)和switch語(yǔ)句,也會(huì)引入新的作用域。在這些結(jié)構(gòu)中聲明的局部變量的作用域限制在結(jié)構(gòu)內(nèi)部,不會(huì)泄漏到外部。
if x := 10; x > 5 { // x 只能在 if 語(yǔ)句塊內(nèi)訪問(wèn) fmt.Println(x) } fmt.Println(x) // 這里無(wú)法訪問(wèn) x
在上面的示例中,變量 x
在if語(yǔ)句內(nèi)部有一個(gè)新的局部作用域,因此它只在if語(yǔ)句塊內(nèi)可見(jiàn)。
四、避免變量遮蔽的原則
4.1 變量遮蔽的根本原因
變量是標(biāo)識(shí)符的一種,通過(guò)以上我們知道,一個(gè)變量的作用域起始于其聲明所在的代碼塊,并且可以一直擴(kuò)展到嵌入到該代碼塊中的所有內(nèi)層代碼塊,而正是這樣的作用域規(guī)則,成為了滋生“變量遮蔽問(wèn)題”的土壤。
變量遮蔽問(wèn)題的根本原因,就是內(nèi)層代碼塊中聲明了一個(gè)與外層代碼塊同名且同類型的變量,這樣,內(nèi)層代碼塊中的同名變量就會(huì)替代那個(gè)外層變量,參與此層代碼塊內(nèi)的相關(guān)計(jì)算,我們也就說(shuō)內(nèi)層變量遮蔽了外層同名變量?,F(xiàn)在,我們先來(lái)看一下這個(gè)示例代碼,它就存在著多種變量遮蔽的問(wèn)題:
... ... var a int = 2020 func checkYear() error { err := errors.New("wrong year") switch a, err := getYear(); a { case 2020: fmt.Println("it is", a, err) case 2021: fmt.Println("it is", a) err = nil } fmt.Println("after check, it is", a) return err } type new int func getYear() (new, error) { var b int16 = 2021 return new(b), nil } func main() { err := checkYear() if err != nil { fmt.Println("call checkYear error:", err) return } fmt.Println("call checkYear ok") }
這個(gè)變量遮蔽的例子還是有點(diǎn)復(fù)雜的,我們首先運(yùn)行一下這個(gè)例子:
$go run complex.go it is 2021 after check, it is 2020 call checkYear error: wrong year
我們可以看到,第 20 行定義的 getYear 函數(shù)返回了正確的年份 (2021),但是 checkYear 在結(jié)尾卻輸出“after check, it is 2020”,并且返回的 err 并非為 nil,這顯然是變量遮蔽的“鍋”!
根據(jù)我們前面給出的變量遮蔽的根本原因,看看上面這段代碼究竟有幾處變量遮蔽問(wèn)題(包括標(biāo)識(shí)符遮蔽問(wèn)題)。
4.2 變量遮蔽問(wèn)題分析
4.2.1 第一個(gè)問(wèn)題:遮蔽預(yù)定義標(biāo)識(shí)符
面對(duì)上面代碼,我們一眼就看到了位于第 18 行的 new,這本是 Go 語(yǔ)言的一個(gè)預(yù)定義標(biāo)識(shí)符,但上面示例代碼呢,卻用 new 這個(gè)名字定義了一個(gè)新類型,于是 new 這個(gè)標(biāo)識(shí)符就被遮蔽了。如果這個(gè)時(shí)候你在 main 函數(shù)下方放上下面代碼:
p := new(int) *p = 11
你就會(huì)收到 Go 編譯器的錯(cuò)誤提示:“type int is not an expression
”,如果沒(méi)有意識(shí)到 new
被遮蔽掉,這個(gè)提示就會(huì)讓你不知所措。不過(guò),在上面示例代碼中,遮蔽 new
并不是示例未按預(yù)期輸出結(jié)果的真實(shí)原因,我們還得繼續(xù)往下看。
4.2.2 第二個(gè)問(wèn)題:遮蔽包代碼塊中的變量
你看,位于第 7 行的 switch 語(yǔ)句在它自身的隱式代碼塊中,通過(guò)短變量聲明形式重新聲明了一個(gè)變量 a,這個(gè)變量 a 就遮蔽了外層包代碼塊中的包級(jí)變量 a,這就是打印“after check, it is 2020
”的原因。包級(jí)變量 a 沒(méi)有如預(yù)期那樣被 getYear
的返回值賦值為正確的年份 2021,2021 被賦值給了遮蔽它的 switch
語(yǔ)句隱式代碼塊中的那個(gè)新聲明的 a。
4.2.3 第三個(gè)問(wèn)題:遮蔽外層顯式代碼塊中的變量
同樣還是第 7 行的 switch
語(yǔ)句,除了聲明一個(gè)新的變量 a 之外,它還聲明了一個(gè)名為 err
的變量,這個(gè)變量就遮蔽了第 4 行 checkYear 函數(shù)在顯式代碼塊中聲明的 err
變量,這導(dǎo)致第 12 行的 nil 賦值動(dòng)作作用到了 switch
隱式代碼塊中的 err 變量上,而不是外層 checkYear
聲明的本地變量 err 變量上,后者并非 nil,這樣 checkYear
雖然從 getYear 得到了正確的年份值,但卻返回了一個(gè)錯(cuò)誤給 main 函數(shù),這直接導(dǎo)致了 main 函數(shù)打印了錯(cuò)誤:“call checkYear error: wrong year
”。
通過(guò)這個(gè)示例,我們也可以看到,短變量聲明與控制語(yǔ)句的結(jié)合十分容易導(dǎo)致變量遮蔽問(wèn)題,并且很不容易識(shí)別,因此在日常 go 代碼開(kāi)發(fā)中你要尤其注意兩者結(jié)合使用的地方。
五、利用工具檢測(cè)變量遮蔽問(wèn)題
依靠肉眼識(shí)別變量遮蔽問(wèn)題終歸不是長(zhǎng)久之計(jì),所以Go 官方提供了 go
vet
工具可以用于對(duì) Go 源碼做一系列靜態(tài)檢查,在 Go 1.14 版以前默認(rèn)支持變量遮蔽檢查,Go 1.14 版之后,變量遮蔽檢查的插件就需要我們單獨(dú)安裝了,安裝方法如下:
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
安裝成功后,我們就可以通過(guò) go vet 掃描代碼并檢查這里面有沒(méi)有變量遮蔽的問(wèn)題了。我們檢查一下前面的示例代碼,看看效果怎么樣。執(zhí)行檢查的命令如下:
$go vet -vettool=$(which shadow) -strict complex.go ./complex.go:13:12: declaration of "err" shadows declaration at line 11
以上就是Go 代碼塊作用域變量遮蔽問(wèn)題解析的詳細(xì)內(nèi)容,更多關(guān)于Go作用域變量遮蔽的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go開(kāi)源項(xiàng)目分布式唯一ID生成系統(tǒng)
這篇文章主要為大家介紹了Go開(kāi)源項(xiàng)目分布式唯一ID生成系統(tǒng)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06golang基于errgroup實(shí)現(xiàn)并發(fā)調(diào)用的方法
這篇文章主要介紹了golang基于errgroup實(shí)現(xiàn)并發(fā)調(diào)用,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-09-09go語(yǔ)言fasthttp使用實(shí)例小結(jié)
fasthttp?是一個(gè)使用?Go?語(yǔ)言開(kāi)發(fā)的?HTTP?包,主打高性能,針對(duì)?HTTP?請(qǐng)求響應(yīng)流程中的?hot?path?代碼進(jìn)行了優(yōu)化,下面我們就來(lái)介紹go語(yǔ)言fasthttp使用實(shí)例小結(jié),感興趣的朋友跟隨小編一起看看吧2024-03-03Golang打印復(fù)雜結(jié)構(gòu)體兩種方法詳解
在?Golang?語(yǔ)言開(kāi)發(fā)中,我們經(jīng)常會(huì)使用結(jié)構(gòu)體類型,如果我們使用的結(jié)構(gòu)體類型的變量包含指針類型的字段,我們?cè)谟涗浫罩镜臅r(shí)候,指針類型的字段的值是指針地址,將會(huì)給我們?debug?代碼造成不便2022-10-10使用Golang快速構(gòu)建出命令行應(yīng)用程序
在日常開(kāi)發(fā)中,大家對(duì)命令行工具(CLI)想必特別熟悉了,如果說(shuō)你不知道命令工具,那你可能是個(gè)假開(kāi)發(fā)。每天都會(huì)使用大量的命令行工具,例如最常用的Git、Go、Docker等,這篇文章主要介紹了使用Golang快速構(gòu)建出命令行應(yīng)用程序,需要的朋友可以參考下2023-02-02Golang學(xué)習(xí)之反射機(jī)制的用法詳解
反射的本質(zhì)就是在程序運(yùn)行的時(shí)候,獲取對(duì)象的類型信息和內(nèi)存結(jié)語(yǔ)構(gòu),反射是把雙刃劍,功能強(qiáng)大但可讀性差。本文將詳細(xì)講講Golang中的反射機(jī)制,感興趣的可以了解一下2022-06-06Go網(wǎng)絡(luò)編程TCP抓包實(shí)操示例探究
作為一名軟件開(kāi)發(fā)者,網(wǎng)絡(luò)編程是必備知識(shí),本文通過(guò)?Go?語(yǔ)言實(shí)現(xiàn)?TCP?套接字編程,并結(jié)合?tcpdump?工具,展示它的三次握手、數(shù)據(jù)傳輸以及四次揮手的過(guò)程,幫助讀者更好地理解?TCP?協(xié)議與?Go?網(wǎng)絡(luò)編程2024-01-01golang的基礎(chǔ)語(yǔ)法和常用開(kāi)發(fā)工具詳解
這篇文章主要介紹了golang的基礎(chǔ)語(yǔ)法和常用開(kāi)發(fā)工具,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12