天天看點

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

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

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

你可以通過試驗來确定這個問題的答案。例如:先在一個源碼檔案中導入一個在你的機器上并不存在的代碼包,然後編譯這個代碼檔案。最後,将輸出的編譯錯誤資訊與GOPATH的值進行對比。

如果在多個工作區中都存在導入路徑相同的代碼包會産生沖突嗎?

答:不會産生沖突。因為代碼包的查找是按照已給定的順序逐一地在多個工作區中進行的。

預設情況下,我們可以讓指令源碼檔案接受哪些類型的參數值?

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

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

答:狹義上講是不可以的,但是廣義上講是可以的。這需要一些定制化的工作,并且被給定的參數值隻能是序列化的。具體可參見flag代碼封包檔中的例子。

如果你需要導入兩個代碼包,而這兩個代碼包的導入路徑的最後一級是相同的,比如:dep/lib/flag和flag,那麼會産生沖突嗎?

答:這會産生沖突。因為代表兩個代碼包的辨別符重複了,都是flag。

如果會産生沖突,那麼怎樣解決這種沖突?有幾種方式?

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

如果與目前的變量重名的是外層代碼塊中的變量,那麼意味着什麼?

答:這意味着這兩個變量成為了“可重名變量”。在内層的變量所處的那個代碼塊以及更深層次的代碼塊中,這個變量會“屏蔽”掉外層代碼塊中的那個變量。

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

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

前檔案中的變量處在該檔案所代表的代碼塊中,而被導入的代碼包中的變量卻處在聲明它的那個檔案所代表的代碼塊中。當然,我們也可以說被導入的代碼包所代表的代碼塊包含了這個變量。

在目前檔案中,本地的變量會“屏蔽”掉被導入的變量。

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

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

你能具體說說别名類型在代碼重構過程中可以起到的哪些作用嗎?

答:簡單來說,我們可以通過别名類型實作外界無感覺的代碼重構。具體可參見 Go 語言官方的文檔 Proposal: Type Aliases。

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

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

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

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

container/ring包中的循環連結清單的适用場景都有哪些?

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

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

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

字典類型的值是并發安全的嗎?如果不是,那麼在我們隻在字典上添加或删除鍵 - 元素對的情況下,依然不安全嗎?

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

通道的長度代表着什麼?

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

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

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

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

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

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

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

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

答:文中complexArray1變量的聲明如下:

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

函數真正拿到的參數值其實隻是它們的副本,那麼函數傳回給調用方的結果值也會被複制嗎?

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

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

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

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

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

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

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

如果我們把一個值為nil的某個實作類型的變量賦給了接口變量,那麼在這個接口變量上仍然可以調用該接口的方法嗎?

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

引用類型的值的指針值是有意義的嗎?如果沒有意義,為什麼?如果有意義,意義在哪裡?

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

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

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

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

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

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

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

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

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

在if語句中,初始化子句聲明的變量的作用域是什麼?

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

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

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

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

一個函數怎樣才能把 panic 轉化為error類型值,并将其作為函數的結果值傳回給調用方?

答:可以這樣編寫:

我們可以在defer函數中恢複 panic,那麼可以在其中引發 panic 嗎?

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

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

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

答:這個問題的答案就在testing代碼包的文檔中。

-benchmem标記和-benchtime标記的作用分别是什麼?

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

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

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

你知道互斥鎖和讀寫鎖的指針類型都實作了哪一個接口嗎?

答:它們都實作了sync.Locker接口。

怎樣擷取讀寫鎖中的讀鎖?

答:sync.RWMutex類型有一個名為RLocker的指針方法可以擷取其讀鎖。

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

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

sync.Cond類型中的公開字段L是做什麼用的?我們可以在使用條件變量的過程中改變這個字段的值嗎?

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

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

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

被保護的資料是什麼類型的

是值類型的還是引用類型的?

操作被保護資料的方式是怎樣的?是簡單的讀和寫還是更複雜的操作?操作被保護資料的代碼是集中的還是分散的?如果是分散的,是否可以變為集中的?

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

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

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

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

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

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

怎樣保證一個臨時對象池中總有比較充足的臨時對象?

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

最後,我們應該保證它的New字段所代表的值是可用的。雖然New函數傳回的臨時對象并不會被放入池中,但是起碼能夠保證池的Get方法總能傳回一個臨時對象。

關于保證并發安全字典中的鍵和值的類型正确性,你還能想到其他的方案嗎?

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

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

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

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。

好處是顯而易見的。實作的接口越多,它們的用途就越廣。它們會适用于那些要求參數的類型為這些接口類型的地方。

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

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

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

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

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

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

io.Pipe函數會傳回一個io.PipeReader類型的值和一個io.PipeWriter類型的值,并将它們分别作為管道的兩端。而這兩個值在底層其實隻是代理了同一個*io.pipe類型值的功能而已。

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

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

答:bufio.Scanner類型俗稱帶緩存的掃描器。它的功能還是比較強大的。

比如,我們可以自定義每次掃描的邊界,或者說内容的分段方法。我們在調用它的Scan方法對目标進行掃描之前,可以先調用其Split方法并傳入一個函數來自定義分段方法。

在預設情況下,掃描器會以行為機關對目标内容進行掃描。bufio代碼包提供了一些現成的分段方法。實際上,掃描器在預設情況下會使用bufio.ScanLines函數作為分段方法。

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

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

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

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

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

這兩者都會傳回一個*os.Process類型的值。該類型提供了一些方法,比如,用于殺掉目前程序的Kill方法,又比如,可以給目前程序發送系統信号的Signal方法,以及會等待目前程序結束的Wait方法。

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

怎樣在net.Conn類型的值上正确地設定針對讀操作和寫操作的逾時時間?

答:net.Conn類型有 3 個可用于設定逾時時間的方法,分别是:SetDeadline、SetReadDeadline和SetWriteDeadline。

這三個方法的簽名是一模一樣的,隻是名稱不同罷了。它們都接受一個time.Time類型的參數,并都會傳回一個error類型的結果。其中的SetDeadline方法是用來同時設定讀操作逾時和寫操作逾時的。

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

是以,如果你要在一個循環中進行讀操作或寫操作的話,最好在每次疊代中都進行一次逾時設定。

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

怎樣優雅地停止基于 HTTP 協定的網絡服務程式?

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

它會先關閉所有的空閑連接配接,并一直等待。隻有活動的連接配接變為空閑之後,它才會關閉它們。當所有的連接配接都被平滑地關閉之後,它會關閉目前的伺服器并傳回。當有錯誤發生時,它還會把相應的錯誤值傳回。

另外,你還可以通過調用Server值的RegisterOnShutdown方法來注冊可以在伺服器即将關閉時被自動調用的函數。

更确切地說,目前伺服器的Shutdown方法會以異步的方式調用如此注冊的所有函數。我們可以利用這樣的函數來通知長連接配接的用戶端“連接配接即将關閉”。

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

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

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

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