Go語言核心36講(新年彩蛋)--學習筆記

MingsonZheng發表於2021-12-27

新年彩蛋 | 完整版思考題答案

基礎概念篇

  1. Go 語言在多個工作區中查詢依賴包的時候是以怎樣的順序進行的?

答:你設定的環境變數GOPATH的值決定了這個順序。如果你在GOPATH中設定了多個工作區,那麼這種查詢會以從左到右的順序在這些工作區中進行。

你可以通過試驗來確定這個問題的答案。例如:先在一個原始碼檔案中匯入一個在你的機器上並不存在的程式碼包,然後編譯這個程式碼檔案。最後,將輸出的編譯錯誤資訊與GOPATH的值進行對比。

  1. 如果在多個工作區中都存在匯入路徑相同的程式碼包會產生衝突嗎?

答:不會產生衝突。因為程式碼包的查詢是按照已給定的順序逐一地在多個工作區中進行的。

  1. 預設情況下,我們可以讓命令原始碼檔案接受哪些型別的引數值?

答:這個問題通過檢視flag程式碼包的文件就可以回答了。概括來講,有布林型別、整數型別、浮點數型別、字串型別,以及time.Duration型別。

  1. 我們可以把自定義的資料型別作為引數值的型別嗎?如果可以,怎樣做?

答:狹義上講是不可以的,但是廣義上講是可以的。這需要一些定製化的工作,並且被給定的引數值只能是序列化的。具體可參見flag程式碼包文件中的例子。

  1. 如果你需要匯入兩個程式碼包,而這兩個程式碼包的匯入路徑的最後一級是相同的,比如:dep/lib/flag和flag,那麼會產生衝突嗎?

答:這會產生衝突。因為代表兩個程式碼包的識別符號重複了,都是flag。

  1. 如果會產生衝突,那麼怎樣解決這種衝突?有幾種方式?

答:接上一個問題。很簡單,匯入程式碼包的時候給它起一個別名就可以了,比如: import libflag "dep/lib/flag"。或者,以本地化的方式匯入程式碼包,如:import . "dep/lib/flag"。

  1. 如果與當前的變數重名的是外層程式碼塊中的變數,那麼意味著什麼?

答:這意味著這兩個變數成為了“可重名變數”。在內層的變數所處的那個程式碼塊以及更深層次的程式碼塊中,這個變數會“遮蔽”掉外層程式碼塊中的那個變數。

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

答:這兩個變數會成為“可重名變數”。雖然這兩個變數在這種情況下的作用域都是當前程式碼包的當前檔案,但是它們所處的程式碼塊是不同的。

前檔案中的變數處在該檔案所代表的程式碼塊中,而被匯入的程式碼包中的變數卻處在宣告它的那個檔案所代表的程式碼塊中。當然,我們也可以說被匯入的程式碼包所代表的程式碼塊包含了這個變數。

在當前檔案中,本地的變數會“遮蔽”掉被匯入的變數。

  1. 除了《程式實體的那些事兒 3》一文中提及的那些,你還認為型別轉換規則中有哪些值得注意的地方?

答:簡單來說,我們在進行型別轉換的時候需要注意各種符號的優先順序。具體可參見 Go 語言規範中的轉換部分。

  1. 你能具體說說別名型別在程式碼重構過程中可以起到的哪些作用嗎?

答:簡單來說,我們可以通過別名型別實現外界無感知的程式碼重構。具體可參見 Go 語言官方的文件 Proposal: Type Aliases。

資料型別和語句篇

  1. 如果有多個切片指向了同一個底層陣列,那麼你認為應該注意些什麼?答:我們需要特別注意的是,當操作其中一個切片的時候是否會影響到其他指向同一個底層陣列的切片。

如果是,那麼問一下自己,這是你想要的結果嗎?無論如何,通過這種方式來組織或共享資料是不正確的。你需要做的是,要麼徹底切斷這些切片的底層聯絡,要麼立即為所有的相關操作加鎖。

  1. 怎樣沿用“擴容”的思想對切片進行“縮容”?

答:關於切片的“縮容”,可參看官方的相關 wiki。不過,如果你需要頻繁的“縮容”,那麼就可能需要考慮其他的資料結構了,比如:container/list程式碼包中的List。

  1. container/ring包中的迴圈連結串列的適用場景都有哪些?

答:比如:可重用的資源(快取等)的儲存,或者需要靈活組織的資源池,等等。

  1. container/heap包中的堆的適用場景又有哪些呢?

答:它最重要的用途就是構建優先順序佇列,並且這裡的“優先順序”可以很靈活。所以,想象空間很大。

  1. 字典型別的值是併發安全的嗎?如果不是,那麼在我們只在字典上新增或刪除鍵 - 元素對的情況下,依然不安全嗎?

答:字典型別的值不是併發安全的,即使我們只是增減其中的鍵值對也是如此。其根本原因是,字典值內部有時候會根據需要進行儲存方面的調整。

  1. 通道的長度代表著什麼?

它在什麼時候會通道的容量相同?通道的長度代表它當前包含的元素值的個數。當通道已滿時,其長度會與容量相同。

  1. 元素值在經過通道傳遞時會被複制,那麼這個複製是淺表複製還是深層複製呢?

答:淺表複製。實際上,在 Go 語言中並不存在深層次的複製,除非我們自己來做。

  1. 如果在select語句中發現某個通道已關閉,那麼應該怎樣遮蔽掉它所在的分支?

答:很簡單,把nil賦給代表了這個通道的變數就可以了。如此一來,對於這個通道(那個變數)的傳送操作和接收操作就會永遠被阻塞。

  1. 在select語句與for語句聯用時,怎樣直接退出外層的for語句?

答:這一般會用到goto語句和標籤(label),具體請參看 Go 語言規範的這部分。

  1. complexArray1被傳入函式的話,這個函式中對該引數值的修改會影響到它的原值嗎?

答:文中complexArray1變數的宣告如下:

complexArray1 := [3][]string{
  []string{"d", "e", "f"},
  []string{"g", "h", "i"},
  []string{"j", "k", "l"},
}

這要看怎樣修改了。雖然complexArray1本身是一個陣列,但是其中的元素卻都是切片。如果對complexArray1中的元素進行增減,那麼原值就不會受到影響。但若要修改它已有的元素值,那麼原值也會跟著改變。

  1. 函式真正拿到的引數值其實只是它們的副本,那麼函式返回給呼叫方的結果值也會被複制嗎?

答:函式返回給呼叫方的結果值也會被複制。不過,在一般情況下,我們不用太在意。但如果函式在返回結果值之後依然保持執行並會對結果值進行修改,那麼我們就需要注意了。

  1. 我們可以在結構體型別中嵌入某個型別的指標型別嗎?如果可以,有哪些注意事項?

答:當然可以。在這時,我們依然需要注意各種“遮蔽”現象。由於某個型別的指標型別會包含與前者有關聯的所有方法,所以我們更要注意。

另外,我們在嵌入和引用這樣的欄位的時候還需要注意一些衝突方面的問題,具體請參看 Go 語言規範的這一部分。

  1. 字面量struct{}代表了什麼?又有什麼用處?

答:字面量struct{}代表了空的結構體型別。這樣的型別既不包含任何欄位也沒有任何方法。該型別的值所需的儲存空間幾乎可以忽略不計。

因此,我們可以把這樣的值作為佔位值來使用。比如:在同一個應用場景下,map[int] [int]bool型別的值佔用更少的儲存空間。

  1. 如果我們把一個值為nil的某個實現型別的變數賦給了介面變數,那麼在這個介面變數上仍然可以呼叫該介面的方法嗎?

如果可以,有哪些注意事項?如果不可以,原因是什麼?答:可以呼叫。但是請注意,這個被呼叫的方法在此時所持有的接收者的值是nil。因此,如果該方法引用了其接收者的某個欄位,那麼就會引發 panic!

  1. 引用型別的值的指標值是有意義的嗎?如果沒有意義,為什麼?如果有意義,意義在哪裡?

答:從儲存和傳遞的角度看,沒有意義。因為引用型別的值已經相當於指向某個底層資料結構的指標了。當然,引用型別的值不只是指標那麼簡單。

  1. 用什麼手段可以對 goroutine 的啟用數量加以限制?

答:一個很簡單且很常用的方法是,使用一個通道儲存一些令牌。只有先拿到一個令牌,才能啟用一個 goroutine。另外在go函式即將執行結束的時候還需要把令牌及時歸還給那個通道。

更高階的手段就需要比較完整的設計了。比如,任務分發器 + 任務管道(單層的通道)+ 固定個數的 goroutine。又比如,動態任務池(多層的通道)+ 動態 goroutine 池(可由前述的那個令牌方案演化而來)。等等。

  1. runtime包中提供了哪些與模型三要素 G、P 和 M 相關的函式?

答:關於這個問題,我相信你一查文件便知。不過光知道還不夠,還要會用。

  1. 在型別switch語句中,我們怎樣對被判斷型別的那個值做相應的型別轉換?

答:其實這個事情可以讓 Go 語言自己來做,例如:

switch t := x.(type) {
// cases
}

當流程進入到某個case子句的時候,變數t的值就已經被自動地轉換為相應型別的值了。

  1. 在if語句中,初始化子句宣告的變數的作用域是什麼?

答:如果這個變數是新的變數,那麼它的作用域就是當前if語句所代表的程式碼塊。注意,後續的else if子句和else子句也包含在當前的if語句代表的程式碼塊之內。

  1. 請列舉出你經常用到或者看到的 3 個錯誤型別,它們所在的錯誤型別體系都是怎樣的?你能畫出一棵樹來描述它們嗎?

答:略。這需要你自己去做,我代替不了你。

  1. 請列舉出你經常用到或者看到的 3 個錯誤值,它們分別在哪個錯誤值列表裡?這些錯誤值列表分別包含的是哪個種類的錯誤?

答:略。這需要你自己去做,我代替不了你。

  1. 一個函式怎樣才能把 panic 轉化為error型別值,並將其作為函式的結果值返回給呼叫方?

答:可以這樣編寫:

func doSomething() (err error) {
  defer func() {
    p := recover()
    err = fmt.Errorf("FATAL ERROR: %s", p)
  }()
  panic("Oops!!")
}
  1. 我們可以在defer函式中恢復 panic,那麼可以在其中引發 panic 嗎?

答:當然可以。這樣做可以把原先的 panic 包裝一下再丟擲去。Go 程式的測試

  1. 除了本文中提到的,你還知道或用過testing.T型別和testing.B型別的哪些方法?它們都是做什麼用的?

答:略。這需要你自己去做,我代替不了你。

  1. 在編寫示例測試函式的時候,我們怎樣指定預期的列印內容?

答:這個問題的答案就在testing程式碼包的文件中。

  1. -benchmem標記和-benchtime標記的作用分別是什麼?

答:-benchmem標記的作用是在效能測試完成後列印記憶體分配統計資訊。-benchtime標記的作用是設定測試函式的執行時間上限。具體請看這裡的文件。

  1. 怎樣在測試的時候開啟測試覆蓋度分析?如果開啟,會有什麼副作用嗎?

答:go test命令可以接受-cover標記。該標記的作用就是開啟測試覆蓋度分析。不過,由於覆蓋度分析開啟之後go test命令可能會在程式被編譯之前註釋掉一部分原始碼,所以,若程式編譯或測試失敗,那麼錯誤報告可能會記錄下與原始的原始碼不對應的行號。

標準庫的用法

  1. 你知道互斥鎖和讀寫鎖的指標型別都實現了哪一個介面嗎?

答:它們都實現了sync.Locker介面。

  1. 怎樣獲取讀寫鎖中的讀鎖?

答:sync.RWMutex型別有一個名為RLocker的指標方法可以獲取其讀鎖。

  1. *sync.Cond型別的值可以被傳遞嗎?那sync.Cond型別的值呢?

答:sync.Cond型別的值一旦被使用就不應該再被傳遞了,傳遞往往意味著拷貝。拷貝一個已經被使用的sync.Cond值會引發 panic。但是它的指標值是可以被拷貝的。

  1. sync.Cond型別中的公開欄位L是做什麼用的?我們可以在使用條件變數的過程中改變這個欄位的值嗎?

答:這個欄位代表的是當前的sync.Cond值所持有的那個鎖。我們可以在使用條件變數的過程中改變該欄位的值,但是在改變之前一定要搞清楚這樣做的影響。

  1. 如果要對原子值和互斥鎖進行二選一,你認為最重要的三個決策條件應該是什麼?

答:我覺得首先需要考慮下面幾個問題。

  • 被保護的資料是什麼型別的
  • 是值型別的還是引用型別的?
  • 操作被保護資料的方式是怎樣的?是簡單的讀和寫還是更復雜的操作?操作被保護資料的程式碼是集中的還是分散的?如果是分散的,是否可以變為集中的?

在搞清楚上述問題(以及你關注的其他問題)之後,優先使用原子值。

  1. 在使用WaitGroup值實現一對多的 goroutine 協作流程時,怎樣才能讓分發子任務的 goroutine 獲得各個子任務的具體執行結果?

答:可以考慮使用鎖 + 容器(陣列、切片或字典等),也可以考慮使用通道。另外,你或許也可以用上golang.org/x/sync/errgroup程式碼包中的程式實體,相應的文件在這裡。

  1. Context值在傳達撤銷訊號的時候是廣度優先的還是深度優先的?其優勢和劣勢都是什麼?

答:它是深度優先的。其優勢和劣勢都是:直接分支的產生時間越早,其中的所有子節點就會越先接收到訊號。至於什麼時候是優勢、什麼時候是劣勢還要看具體的應用場景。

例如,如果子節點的存續時間與資源的消耗是正相關的,那麼這可能就是一個優勢。但是,如果每個分支中的子節點都很多,而且各個分支中的子節點的產生順序並不依從於分支的產生順序,那麼這種優勢就很可能會變成劣勢。最終的定論還是要看測試的結果。

  1. 怎樣保證一個臨時物件池中總有比較充足的臨時物件?

答:首先,我們應該事先向臨時物件池中放入足夠多的臨時物件。其次,在用完臨時物件之後,我們需要及時地把它歸還給臨時物件池。

最後,我們應該保證它的New欄位所代表的值是可用的。雖然New函式返回的臨時物件並不會被放入池中,但是起碼能夠保證池的Get方法總能返回一個臨時物件。

  1. 關於保證併發安全字典中的鍵和值的型別正確性,你還能想到其他的方案嗎?

答:這是一道開放的問題,需要你自己去思考。其實怎樣做完全取決於你的應用場景。不過,我們應該儘量避免使用反射,因為它對程式效能還是有一定的影響的。

  1. 判斷一個 Unicode 字元是否為單位元組字元通常有幾種方式?

答:unicode/utf8程式碼包中有幾個可以做此判斷的函式,比如:RuneLen函式、EncodeRune函式等。我們需要根據輸入的不同來選擇和使用它們。具體可以檢視該程式碼包的文件。

  1. strings.Builder和strings.Reader都分別實現了哪些介面?這樣做有什麼好處嗎?

答:strings.Builder型別實現了 3 個介面,分別是:fmt.Stringer、io.Writer和io.ByteWriter。而strings.Reader型別則實現了 8 個介面,即:io.Reader、io.ReaderAt、io.ByteReader、io.RuneReader、io.Seeker、io.ByteScanner、io.RuneScanner和io.WriterTo。

好處是顯而易見的。實現的介面越多,它們的用途就越廣。它們會適用於那些要求引數的型別為這些介面型別的地方。

  1. 對比strings.Builder和bytes.Buffer的String方法,並判斷哪一個更高效?原因是什麼?

答:strings.Builder的String方法更高效。因為該方法只對其所屬值的內容容器(那個位元組切片)做了簡單的型別轉換,並且直接使用了底層的值(或者說記憶體空間)。它的原始碼如下:

// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}

陣列值和字串值在底層的儲存方式其實是一樣的。所以從切片值到字串值的指標值的轉換可以是直截了當的。又由於字串值是不可變的,所以這樣做也是安全的。

不過,由於一些歷史、結構和功能方面的原因,bytes.Buffer的String方法卻不能這樣做。

  1. io包中的同步記憶體管道的運作機制是什麼?

答:我們實際上已經在正文中做了基本的說明。

io.Pipe函式會返回一個io.PipeReader型別的值和一個io.PipeWriter型別的值,並將它們分別作為管道的兩端。而這兩個值在底層其實只是代理了同一個*io.pipe型別值的功能而已。

io.pipe型別通過無緩衝的通道實現了讀操作與寫操作之間的同步,並且通過互斥鎖實現了寫操作之間的序列化。另外,它還使用原子值來處理錯誤。這些共同保證了這個同步記憶體管道的併發安全性。

  1. bufio.Scanner型別的主要功用是什麼?它有哪些特點?

答:bufio.Scanner型別俗稱帶快取的掃描器。它的功能還是比較強大的。

比如,我們可以自定義每次掃描的邊界,或者說內容的分段方法。我們在呼叫它的Scan方法對目標進行掃描之前,可以先呼叫其Split方法並傳入一個函式來自定義分段方法。

在預設情況下,掃描器會以行為單位對目標內容進行掃描。bufio程式碼包提供了一些現成的分段方法。實際上,掃描器在預設情況下會使用bufio.ScanLines函式作為分段方法。

又比如,我們還可以在掃描之前自定義快取的載體和快取的最大容量,這需要呼叫它的Buffer方法。在預設情況下,掃描器內部設定的最大快取容量是64K個位元組。

換句話說,目標內容中的每一段都不能超過64K個位元組。否則,掃描器就會使它的Scan方法返回false,並通過其Err方法給予我們一個表示“token too long”的錯誤值。這裡的“token”代表的就是一段內容。

關於bufio.Scanner型別的更多特點和使用注意事項,你可以通過它的文件獲得。

  1. 怎樣通過os包中的 API 建立和操縱一個系統程式?

答:你可以從os包的FindProcess函式和StartProcess函式開始。前者用於通過程式 ID(pid)查詢程式,後者用來基於某個程式啟動一個程式。

這兩者都會返回一個*os.Process型別的值。該型別提供了一些方法,比如,用於殺掉當前程式的Kill方法,又比如,可以給當前程式傳送系統訊號的Signal方法,以及會等待當前程式結束的Wait方法。

與此相關的還有os.ProcAttr型別、os.ProcessState型別、os.Signal型別,等等。你可以通過積極的實踐去探索更多的玩法。

  1. 怎樣在net.Conn型別的值上正確地設定針對讀操作和寫操作的超時時間?

答:net.Conn型別有 3 個可用於設定超時時間的方法,分別是:SetDeadline、SetReadDeadline和SetWriteDeadline。

這三個方法的簽名是一模一樣的,只是名稱不同罷了。它們都接受一個time.Time型別的引數,並都會返回一個error型別的結果。其中的SetDeadline方法是用來同時設定讀操作超時和寫操作超時的。

有一點需要特別注意,這三個方法都會針對任何正在進行以及未來將要進行的相應操作進行超時設定。

因此,如果你要在一個迴圈中進行讀操作或寫操作的話,最好在每次迭代中都進行一次超時設定。

否則,靠後的操作就有可能因觸達超時時間而直接失敗。另外,如果有必要,你應該再次呼叫它們並傳入time.Time型別的零值來表達不再限定超時時間。

  1. 怎樣優雅地停止基於 HTTP 協議的網路服務程式?

答:net/http.Server型別有一個名為Shutdown的指標方法可以實現“優雅的停止”。也就是說,它可以在不中斷任何正處在活動狀態的連線的情況下平滑地關閉當前的伺服器。

它會先關閉所有的空閒連線,並一直等待。只有活動的連線變為空閒之後,它才會關閉它們。當所有的連線都被平滑地關閉之後,它會關閉當前的伺服器並返回。當有錯誤發生時,它還會把相應的錯誤值返回。

另外,你還可以通過呼叫Server值的RegisterOnShutdown方法來註冊可以在伺服器即將關閉時被自動呼叫的函式。

更確切地說,當前伺服器的Shutdown方法會以非同步的方式呼叫如此註冊的所有函式。我們可以利用這樣的函式來通知長連線的客戶端“連線即將關閉”。

  1. runtime/trace程式碼包的功用是什麼?答:簡單來說,這個程式碼包是用來幫助 Go 程式實現內部跟蹤操作的。其中的程式實體可以幫助我們記錄程式中各個 goroutine 的狀態、各種系統呼叫的狀態,與 GC 有關的各種事件,以及記憶體相關和 CPU 相關的變化,等等。

通過它們生成的跟蹤記錄可以通過go tool trace命令來檢視。更具體的說明可以參看runtime/trace程式碼包的文件。

有了runtime/trace程式碼包,我們就可以為 Go 程式加裝上可以滿足個性化需求的跟蹤器了。Go 語言標準庫中有的程式碼包正是通過使用該包實現了自身的功能,例如net/http/pprof包。

筆記原始碼

https://github.com/MingsonZheng/go-core-demo

知識共享許可協議

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

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

相關文章