Go語言核心36講(Go語言基礎知識五)--學習筆記

MingsonZheng發表於2021-10-16

05 | 程式實體的那些事兒(中)

在前文中,我解釋過程式碼塊的含義。Go 語言的程式碼塊是一層套一層的,就像大圓套小圓。

一個程式碼塊可以有若干個子程式碼塊;但對於每個程式碼塊,最多隻會有一個直接包含它的程式碼塊(後者可以簡稱為前者的外層程式碼塊)。

這種程式碼塊的劃分,也間接地決定了程式實體的作用域。我們今天就來看看它們之間的關係。

我先說說作用域是什麼?大家都知道,一個程式實體被創造出來,是為了讓別的程式碼引用的。那麼,哪裡的程式碼可以引用它呢,這就涉及了它的作用域。

我在前面說過,程式實體的訪問許可權有三種:包級私有的、模組級私有的和公開的。這其實就是 Go 語言在語言層面,依據程式碼塊對程式實體作用域進行的定義。

包級私有和模組級私有訪問許可權對應的都是程式碼包程式碼塊,公開的訪問許可權對應的是全域程式碼塊。然而,這個顆粒度是比較粗的,我們往往需要利用程式碼塊再細化程式實體的作用域。

比如,我在一個函式中宣告瞭一個變數,那麼在通常情況下,這個變數是無法被這個函式以外的程式碼引用的。這裡的函式就是一個程式碼塊,而變數的作用域被限制在了該程式碼塊中。當然了,還有例外的情況,這部分內容,我留到講函式的時候再說。

總之,請記住,一個程式實體的作用域總是會被限制在某個程式碼塊中,而這個作用域最大的用處,就是對程式實體的訪問許可權的控制。對“高內聚,低耦合”這種程式設計思想的實踐,恰恰可以從這裡開始。

今天的問題是:如果一個變數與其外層程式碼塊中的變數重名會出現什麼狀況?

我把此題的程式碼存到了 demo10.go 檔案中了。你可以在“Golang_Puzzlers”專案的puzzlers/article5/q1包中找到它。

package main

import "fmt"

var block = "package"

func main() {
  block := "function"
  {
    block := "inner"
    fmt.Printf("The block is %s.\n", block)
  }
  fmt.Printf("The block is %s.\n", block)
}

這個命令原始碼檔案中有四個程式碼塊,它們是:全域程式碼塊、main包代表的程式碼塊、main函式代表的程式碼塊,以及在main函式中的一個用花括號包起來的程式碼塊。

我在後三個程式碼塊中分別宣告瞭一個名為block的變數,並分別把字串值"package"、"function"和"inner"賦給了它們。此外,我在後兩個程式碼塊的最後分別嘗試用fmt.Printf函式列印出“The block is %s.”。這裡的“%s”只是為了佔位,程式會用block變數的實際值替換掉。

具體的問題是:該原始碼檔案中的程式碼能通過編譯嗎?如果不能,原因是什麼?如果能,執行它後會列印出什麼內容?

典型回答

能通過編譯。執行後列印出的內容是:

The block is inner.
The block is function.

問題解析

初看這道題,你可能會認為它無法通過編譯,因為三處程式碼都宣告瞭相同名稱的變數。的確,宣告重名的變數是無法通過編譯的,用短變數宣告對已有變數進行重宣告除外,但這只是對於同一個程式碼塊而言的。

對於不同的程式碼塊來說,其中的變數重名沒什麼大不了,照樣可以通過編譯。即使這些程式碼塊有直接的巢狀關係也是如此,就像 demo10.go 中的main包程式碼塊、main函式程式碼塊和那個最內層的程式碼塊那樣。

這樣規定顯然很方便也很合理,否則我們會每天為了選擇變數名而煩惱。但是這會導致另外一個問題,我引用變數時到底用的是哪一個?這也是這道題的第二個考點。

這其實有一個很有畫面感的查詢過程。這個查詢過程不只針對於變數,還適用於任何程式實體。如下面所示。

  • 首先,程式碼引用變數的時候總會最優先查詢當前程式碼塊中的那個變數。注意,這裡的“當前程式碼塊”僅僅是引用變數的程式碼所在的那個程式碼塊,並不包含任何子程式碼塊。
  • 其次,如果當前程式碼塊中沒有宣告以此為名的變數,那麼程式會沿著程式碼塊的巢狀關係,從直接包含當前程式碼塊的那個程式碼塊開始,一層一層地查詢。
  • 一般情況下,程式會一直查到當前程式碼包代表的程式碼塊。如果仍然找不到,那麼 Go 語言的編譯器就會報錯了。

好了,當你明白了上述過程之後,再去看 demo10.go 中的程式碼。是不是感覺清晰了很多?

從作用域的角度也可以說,雖然通過var block = "package"宣告的變數作用域是整個main程式碼包,但是在main函式中,它卻被那兩個同名的變數“遮蔽”了。

相似的,雖然main函式首先宣告的block的作用域,是整個main函式,但是在最內層的那個程式碼塊中,它卻是不可能被引用到的。反過來講,最內層程式碼塊中的block也不可能被該塊之外的程式碼引用到,這也是列印內容的第二行是“The block is function.”的另一半原因。

你現在應該知道了,這道題看似簡單,但是它考察以及可延展的範圍並不窄。

知識擴充套件

不同程式碼塊中的重名變數與變數重宣告中的變數區別到底在哪兒?

為了方便描述,我就把不同程式碼塊中的重名變數叫做“可重名變數”吧。注意,在同一個程式碼塊中不允許出現重名的變數,這違背了 Go 語言的語法。關於這兩者的表象和機理,我們已經討論得足夠充分了。你現在可以說出幾條區別?請想一想,然後再看下面的列表。

  • 變數重宣告中的變數一定是在某一個程式碼塊內的。注意,這裡的“某一個程式碼塊內”並不包含它的任何子程式碼塊,否則就變成了“多個程式碼塊之間”。而可重名變數指的正是在多個程式碼塊之間由相同的識別符號代表的變數。
  • 變數重宣告是對同一個變數的多次宣告,這裡的變數只有一個。而可重名變數中涉及的變數肯定是有多個的。
  • 不論對變數重宣告多少次,其型別必須始終一致,具體遵從它第一次被宣告時給定的型別。而可重名變數之間不存在類似的限制,它們的型別可以是任意的。
  • 如果可重名變數所在的程式碼塊之間,存在直接或間接的巢狀關係,那麼它們之間一定會存在“遮蔽”的現象。但是這種現象絕對不會在變數重宣告的場景下出現。

image

以上 4 大區別中的第 3 條需要你再注意一下。既然可重名變數的型別可以是任意的,那麼當它們之間存在“遮蔽”時你就更需要注意了。

不同型別的值大都有著不同的特性和用法。當你在某一種型別的值上施加只有在其他型別值上才能做的操作時,Go 語言編譯器一定會告訴你:“這不可以”。

具體到不同型別的可重名變數的問題上,讓我們先來看一下puzzlers/article5/q2包中的原始碼檔案 demo11.go。它是一個很典型的例子。

package main

import "fmt"

var container = []string{"zero", "one", "two"}

func main() {
  container := map[int]string{0: "zero", 1: "one", 2: "two"}
  fmt.Printf("The element is %q.\n", container[1])
}

在 demo11.go 中,有兩個都叫做container的變數,分別位於main包程式碼塊和main函式程式碼塊。main包程式碼塊中的變數是切片(slice)型別的,另一個是字典(map)型別的。在main函式的最後,我試圖列印出container變數的值中索引為1的那個元素。

如果你熟悉這兩個型別肯定會知道,在它們的值上我們都可以施加索引表示式,比如container[0]。只要中括號裡的整數在有效範圍之內(這裡是[0, 2]),它就可以把值中的某一個元素取出來。

如果container的型別不是陣列、切片或字典型別,那麼索引表示式就會引發編譯錯誤。這正是利用 Go 語言語法,幫我們約束程式的一個例子;但是當我們想知道 container 確切型別的時候,利用索引表示式的方式就不夠了。

總結

我們先討論了程式碼塊,並且也談到了它與程式實體的作用域,以及訪問許可權控制之間的巧妙關係。Go 語言本身對程式實體提供了相對粗粒度的訪問控制。但我們自己可以利用程式碼塊和作用域精細化控制它們。

如果在具有巢狀關係的不同程式碼塊中存在重名的變數,那麼我們應該特別小心,它們之間可能會發生“遮蔽”的現象。這樣你在不同程式碼塊中引用到變數很可能是不同的。具體的鑑別方式需要參考 Go 語言查詢(代表了程式實體的)識別符號的過程。

另外,請記住變數重宣告與可重名變數之間的區別以及它們的重要特徵。其中最容易產生隱晦問題的一點是,可重名變數可以各有各的型別。這時候我們往往應該在真正使用它們之前先對其型別進行檢查。利用 Go 語言的語法、規範和命令做輔助的檢查是很好的辦法,但有些時候並不充分。

思考題

我們在討論 Go 語言查詢識別符號時的範圍的時候,提到過import . XXX這種匯入程式碼包的方式。這裡有個思考題:

如果通過這種方式匯入的程式碼包中的變數與當前程式碼包中的變數重名了,那麼 Go 語言是會把它們當做“可重名變數”看待還是會報錯呢?

知識共享許可協議

本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。

歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。

相關文章