《go語言從入門到進階實戰》_徐波


摘錄

  • Go語言是Google公司開發的一種靜態型、編譯型並自帶垃圾回收和並發的編程語言。
  • Go語言不使用虛擬機,只有運行時(runtime)提供垃圾回收和goroutine調度等。
  • Go語言使用自己的鏈接器,不依賴任何系統提供的編譯器、鏈接器。因此編譯出的可執行文件可以直接運行在幾乎所有的操作系統和環境中。
  • 從Go 1.5版本之后,Go語言實現自舉,實現了使用Go語言編寫Go語言編譯器及所有工具鏈的功能。
  • Go語言可以利用自己的特性實現並發編譯,並發編譯的最小元素是包。從Go 1.9版本開始,最小並發編譯元素縮小到函數,整體編譯速度提高了20%。
  • Go語言的並發是基於goroutine,goroutine類似於線程,但並非線程。可以將goroutine理解為一種虛擬線程。Go語言運行時會參與調度goroutine,並將goroutine合理地分配到每個CPU中,最大限度地使用CPU性能。
  • 在Go語言中,自增操作符不再是一個操作符,而是一個語句。因此,在Go語言中自增只有一種寫法:i++如果寫成前置自增“++i”,或者賦值后自增“a=i++”都將導致編譯錯誤。
  • 在多個短變量聲明和賦值中,至少有一個新聲明的變量出現在左值中,即便其他變量名可能是重復聲明的,編譯器也不會報錯。
  • 布爾型無法參與數值運算,也無法與其他類型進行轉換。
  • 切片發生越界時,運行時會報出宕機,並打出堆棧,而原始指針只會崩潰。
  • 變量、指針和地址三者的關系是:每個變量都擁有地址,指針的值就是地址。
  • 變量、指針地址、指針變量、取地址、取值的相互關系和特性如下:
    • 對變量進行取地址(&)操作,可以獲得這個變量的指針變量。
    • 指針變量的值是指針地址。
    • 對指針變量進行取值(*)操作,可以獲得指針變量指向的原變量的值。
  • “*”操作符的根本意義就是操作指針指向的變量。當操作在右值時,就是取指向變量的值;當操作在左值時,就是將值設置給指向的變量。
  • 堆分配內存和棧分配內存相比,堆適合不可預知大小的內存分配。但是為此付出的代價是分配速度較慢,而且會形成內存碎片。
  • 編譯器覺得變量應該分配在堆和棧上的原則是:
    • 變量是否被取地址。
    • 變量是否發生逃逸。
  • ASCII字符串長度使用len()函數。
  • Unicode字符串長度使用utf8.RuneCountInString()函數。
  • ASCII字符串遍歷直接使用下標。
  • Unicode字符串遍歷用for range。
  • Go語言的字符串是不可變的。
  • 修改字符串時,可以將字符串轉換為[]byte進行修改。
  • []byte和string可以通過強制類型轉換互轉。
  • 一般情況下,這個過程可以交給編譯器,讓編譯器在編譯時,根據元素個數確定數組大小。var team = [...]string{"hammer", "soldier", "mum"}“...”表示讓編譯器確定數組大小。上面例子中,編譯器會自動為這個數組設置元素個數為3。
  • 從數組或切片生成新的切片擁有如下特性。
    • 取出的元素數量為:結束位置-開始位置。
    • 取出元素不包含結束位置對應的索引,切片最后一個元素使用slice[len(slice)]獲取。
    • 當缺省開始位置時,表示從連續區域開頭到結束位置。
    • 當缺省結束位置時,表示從開始位置到整個連續區域末尾。
    • 兩者同時缺省時,與切片本身等效。
    • 兩者同時為0時,等效於空切片,一般用於切片復位。
    • 根據索引位置取切片slice元素值時,取值范圍是(0~len(slice)-1),超界會報運行時錯誤。生成切片時,結束位置可以填寫len(slice)但不會報錯。
  • 切片有點像C語言里的指針。指針可以做運算,但代價是內存操作越界。切片在指針的基礎上增加了大小,約束了切片對應的內存區域,切片使用中無法對切片內部的地址和大小進行手動調整,因此切片比指針更安全、強大。
  • 切片是動態結構,只能與nil判定相等,不能互相判等時。
  • 切片已經被分配到了內存,但沒有元素,因此和nil比較時是false。
  • 使用make()函數生成的切片一定發生了內存分配操作。但給定開始與結束位置(包括切片復位)的切片只是將新的切片結構指向已經分配好的內存區域,設定開始與結束位置,不會發生內存分配操作。切片不一定必須經過make()函數才能使用。生成切片、聲明后使用append()函數均可以正常使用切片。
  • append()函數除了添加一個元素外,也可以一次性添加很多元素。
  • Go語言中切片刪除元素的本質是:以被刪除元素為分界點,將前后兩個部分的內存重新連接起來。
  • 大多數語言中映射關系容器使用兩種算法:散列表和平衡樹。
  • 散列表可以簡單描述為一個數組(俗稱“桶”),數組的每個元素是一個列表。
  • 根據散列函數獲得每個元素的特征值,將特征值作為映射的鍵。如果特征值重復,表示元素發生碰撞。碰撞的元素將被放在同一個特征值的列表中進行保存。散列表查找復雜度為O(1),和數組一致。最壞的情況為O(n),n為元素總數。散列需要盡量避免元素碰撞以提高查找效率,這樣就需要對“桶”進行擴容,每次擴容,元素需要重新放入桶中,較為耗時。
  • 平衡樹類似於有父子關系的一棵數據樹,每個元素在放入樹時,都要與一些節點進行比較。平衡樹的查找復雜度始終為O(log n)。
  • sort.Strings的作用是對傳入的字符串切片進行字符串字符的升序排列。
  • Go語言中並沒有為map提供任何清空所有元素的函數、方法。清空map的唯一辦法就是重新make一個新的map。不用擔心垃圾回收的效率,Go語言中的並行垃圾回收效率比寫一個清空函數高效多了。
  • Go語言中的map在並發情況下,只讀是線程安全的,同時讀寫線程不安全。
  • 也就是說使用了兩個並發函數不斷地對map進行讀和寫而發生了競態問題。map內部會對這種並發操作進行檢查並提前發現。
  • sync.Map有以下特性:
    • 無須初始化,直接聲明即可。
    • sync.Map不能使用map的方式進行取值和設置等操作,而是使用sync.Map的方法進行調用。Store表示存儲,Load表示獲取,Delete表示刪除。
    • 使用Range配合一個回調函數進行遍歷操作,通過回調函數返回內部遍歷出來的值。Range參數中的回調函數的返回值功能是:需要繼續迭代遍歷時,返回true;終止迭代遍歷時,返回false。
  • sync.Map沒有提供獲取map數量的方法,替代方法是獲取時遍歷自行計算數量。sync.Map為了保證並發安全有一些性能損失,因此在非並發情況下,使用map相比使用sync.Map會有更好的性能。
  • 列表是一種非連續存儲的容器,由多個節點組成,節點通過一些變量記錄彼此之間的關系。列表有多種實現方法,如單鏈表、雙鏈表等。
  • 在Go語言中,將列表使用container/list包來實現,內部的實現原理是雙鏈表。列表能夠高效地進行任意位置的元素插入和刪除操作。
  • list的初始化有兩種方法:New和聲明。兩種方法的初始化效果都是一致的。
  • 列表與切片和map不同的是,列表並沒有具體元素類型的限制。因此,列表的元素可以是任意類型。這既帶來遍歷,也會引來一些問題。給一個列表放入了非期望類型的值,在取出值后,將interface{}轉換為期望類型時將會發生宕機。
  • Go語言規定與if匹配的左括號“{”必須與if和表達式放在同一行,如果嘗試將“{”放在其他位置,將會觸發編譯錯誤。與else匹配的“{”也必須與else在同一行,else也必須與上一個if或else if的右邊的大括號在一行。
  • 通過for range遍歷的返回值有一定的規律:
    • 數組、切片、字符串返回索引和值。
    • map返回鍵和值。
    • 通道(channel)只返回通道內的值。
  • 對map遍歷時,遍歷輸出的鍵值是無序的,如果需要有序的鍵值對輸出,需要對結果進行排序。
  • 在Go語言中的switch,不僅可以基於常量進行判斷,還可以基於表達式進行判斷。
  • 函數是組織好的、可重復使用的、用來實現單一或相關聯功能的代碼段,其可以提高應用的模塊性和代碼的重復利用率。
  • Go語言支持普通函數、匿名函數和閉包,從設計上對函數進行了優化和改進,讓函數使用起來更加方便。
  • Go語言的函數屬於“一等公民”(first-class),也就是說:
    • 函數本身可以作為值進行傳遞。
    • 支持匿名函數和閉包(closure)。
    • 函數可以滿足接口。
  • golang中,函數可以為單返回值或多返回值,當函數為多返回值時,需要使用括號對返回值列表進行約束;函數的返回值可以為不同類型,並且可以在定義時指定返回值的變量名,並且在函數體中進行顯式賦值,在函數內return時,對於已經顯式賦值的返回值,在return時可以不帶參數,也可以將部分返回值放在return語句中,此時return語句需要寫出完整的返回值列表。
  • 同一種類型返回值和命名返回值兩種形式只能二選一,混用時將會發生編譯錯誤。
  • 函數內的局部變量只能在函數體中使用,函數調用結束后,這些局部變量都會被釋放並且失效。
  • Go語言中傳入和返回參數在調用和返回時都使用值傳遞,這里需要注意的是指針、切片和map等引用型對象指向的內容在參數傳遞中不會發生復制,而是將指針進行復制,類似於創建一次引用。

5.6 閉包(Closure)——引用了外部變量的匿名函數

  • 閉包是引用了自由變量的函數,被引用的自由變量和函數一同存在,即使已經離開了自由變量的環境也不會被釋放或者刪除,在閉包中可以繼續使用這個自由變量。
  • 一個函數類型就像結構體一樣,可以被實例化。函數本身不存儲任何信息,只有與引用環境結合后形成的閉包才具有“記憶性”。函數是編譯期靜態的概念,而閉包是運行期動態的概念。
  • 閉包(Closure)在某些編程語言中也被稱為Lambda表達式。閉包對環境中變量的引用過程,也可以被稱為“捕獲”,在C++ 11標准中,捕獲有兩種類型:引用和復制,可以改變引用的原值叫做“引用捕獲”,捕獲的過程值被復制到閉包中使用叫做“復制捕獲”。在Lua語言中,將被捕獲的變量起了一個名字叫做Upvalue,因為捕獲過程總是對閉包上方定義過的自由變量進行引用。閉包在各種語言中的實現也是不盡相同的。在Lua語言中,無論閉包還是函數都屬於Prototype概念,被捕獲的變量以Upvalue的形式引用到閉包中。C++與C#中為閉包創建了一個類,而被捕獲的變量在編譯時放到類中的成員中,閉包在訪問被捕獲的變量時,實際上訪問的是閉包隱藏類的成員。
  • 被捕獲到閉包中的變量讓閉包本身擁有了記憶效應,閉包中的邏輯可以修改閉包捕獲的變量,變量會跟隨閉包生命期一直存在,閉包本身就如同變量一樣擁有了記憶效應。

5.7 可變參數——參數數量不固定的函數形式

  • 可變參數一般被放置在函數列表的末尾,前面是固定參數列表,當沒有固定參數時,所有變量就將是可變參數。
  • v為可變參數變量,類型為[]T,也就是擁有多個T元素的T類型切片,v和T之間由“...”即3個點組成。
  • T為可變參數的類型,當T為interface{}時,傳入的可以是任意類型。

5.8 延遲執行語句(defer)

  • Go語言的defer語句會將其后面跟隨的語句進行延遲處理。在defer歸屬的函數即將返回時,將延遲處理的語句按defer的逆序進行執行,也就是說,先被defer的語句最后被執行,最后被defer的語句,最先被執行。
  • 延遲調用是在defer所在函數結束時進行,函數結束可以是正常返回時,也可以是發生宕機時。
5.8.2 使用延遲執行語句在函數退出時釋放資源
  • 處理業務或邏輯中涉及成對的操作是一件比較煩瑣的事情,比如打開和關閉文件、接收請求和回復請求、加鎖和解鎖等。在這些操作中,最容易忽略的就是在每個函數退出處正確地釋放和關閉資源。
  • defer語句正好是在函數退出時執行的語句,所以使用defer能非常方便地處理資源釋放問題。

5.10 宕機(panic)——程序終止運行

  • 宕機不是一件很好的事情,可能造成體驗停止、服務中斷,就像沒有人希望在取錢時遇到ATM機藍屏一樣。但是,如果在損失發生時,程序沒有因為宕機而停止,那么用戶將會付出更大的代價,這種代價可以是金錢、時間甚至生命。因此,宕機有時是一種合理的止損方法。
  • Go語言可以在程序中手動觸發宕機,讓程序崩潰,這樣開發者可以及時地發現錯誤,同時減少可能的損失。Go語言程序在宕機時,會將堆棧和goroutine信息輸出到控制台,所以宕機也可以方便地知曉發生錯誤的位置。
  • 當panic()觸發的宕機發生時,panic()后面的代碼將不會被運行,但是在panic()函數前面已經運行過的defer語句依然會在宕機發生時發生作用。

5.11 宕機恢復(recover)——防止程序崩潰

  • 無論是代碼運行錯誤由Runtime層拋出的panic崩潰,還是主動觸發的panic崩潰,都可以配合defer和recover實現錯誤捕捉和恢復,讓代碼在發生崩潰后允許繼續運行。
  • panic和recover的關系,panic和defer的組合有如下幾個特性。
    • 有panic沒recover,程序宕機。
    • 有panic也有recover捕獲,程序不會宕機。執行完對應的defer后,從宕機點退出當前函數后繼續執行。
  • 在panic觸發的defer函數內,可以繼續調用panic,進一步將錯誤外拋直到程序整體崩潰。如果想在捕獲錯誤時設置當前函數的返回值,可以對返回值使用命名返回值方式直接進行設置。

第6章 結構體(struct)

  • Go語言通過用自定義的方式形成新的類型,結構體是類型中帶有成員的復合類型。Go語言使用結構體和結構體成員來描述真實世界的實體和實體對應的各種屬性。
  • Go語言中的類型可以被實例化,使用new或“&”構造的類型實例的類型是類型的指針。
  • 結構體成員是由一系列的成員變量構成,這些成員變量也被稱為“字段”。字段有以下特性:
    • 字段擁有自己的類型和值。
    • 字段名必須唯一。
    • 字段的類型也可以是結構體,甚至是字段所在結構體的類型。

6.2 實例化結構體——為結構體分配內存並初始化

  • 結構體的定義只是一種內存布局的描述,只有當結構體實例化時,才會真正地分配內存。因此必須在定義結構體並實例化后才能使用結構體的字段。
  • 實例化就是根據結構體定義的格式創建一份與格式一致的內存區域,結構體實例與實例間的內存是完全獨立的。
  • 創建指針類型的結構體
    • Go語言中,還可以使用new關鍵字對類型(包括結構體、整型、浮點數、字符串等)進行實例化,結構體在實例化后會形成指針類型的結構體。
    • 在C/C++語言中,使用new實例化類型后,訪問其成員變量時必須使用“->”操作符。在Go語言中,訪問結構體指針的成員變量時可以繼續使用“.”。這是因為Go語言為了方便開發者訪問結構體指針的成員變量,使用了語法糖(Syntactic sugar)技術,將ins.Name形式轉換為(*ins).Name。
    • 在計算機中,小對象由於值復制時的速度較快,所以適合使用非指針接收器。大對象因為復制性能較低,適合使用指針接收器,在接收器和參數間傳遞時不進行復制,只是傳遞指針。

第7章 接口(interface)

  • Go語言的接口在命名時,一般會在單詞后面添加er,如有寫操作的接口叫Writer,有字符串功能的接口叫Stringer,有關閉功能的接口叫Closer等。

7.2.1 接口被實現的條件一:接口的方法與實現接口的類型方法格式一致

  • 當一個接口中有多個方法時,只有這些方法都被實現了,接口才能被正確編譯並使用。

第9章 並發

  • 並發指在同一時間內可以執行多個任務。並發編程含義比較廣泛,包含多線程編程、多進程編程及分布式程序等。本章講解的並發含義屬於多線程編程。
  • Go語言通過編譯器運行時(runtime),從語言上支持了並發的特性。Go語言的並發通過goroutine特性完成。goroutine類似於線程,但是可以根據需要創建多個goroutine並發工作。goroutine是由Go語言的運行時調度完成,而線程是由操作系統調度完成。
  • Go程序中使用go關鍵字為一個函數創建一個goroutine。一個函數可以被創建多個goroutine,一個goroutine必定對應一個函數。
  • 所有goroutine在main()函數結束時會一同結束。
  • goroutine雖然類似於線程概念,但是從調度性能上沒有線程細致,而細致程度取決於Go程序的goroutine調度器的實現和運行環境。
  • 並發和並行之間的區別。
    • 並發(concurrency):把任務在不同的時間點交給處理器進行處理。在同一時間點,任務並不會同時運行。
    • 並行(parallelism):把每一個任務分配給每一個處理器獨立完成。在同一時間點,任務一定是同時運行。
    • 兩個概念的區別是:任務是否同時執行。
  • 同步——保證並發環境下數據訪問的正確性
  • Go程序可以使用通道進行多個goroutine間的數據交換,但這僅僅是數據同步中的一種方法。通道內部的實現依然使用了各種鎖,因此優雅代碼的代價是性能。在某些輕量級的場合,原子訪問(atomic包)、互斥鎖(sync.Mutex)以及等待組(sync.Wait Group)能最大程度滿足需求。

第10章 反射

  • 反射是指在程序運行期對程序本身進行訪問和修改的能力。程序在編譯時,變量被轉換為內存地址,變量名不會被編譯器寫入到可執行部分。在運行程序時,程序無法獲取自身的信息。支持反射的語言可以在程序編譯期將變量的反射信息,如字段名稱、類型信息、結構體信息等整合到可執行文件中,並給程序提供接口訪問反射信息,這樣就可以在程序運行期獲取類型的反射信息,並且有能力修改它們。
  • Go程序在運行期使用reflect包訪問程序的反射信息。

10.1 反射的類型對象(reflect.Type)

  • 在Go程序中,使用reflect.Type Of()函數可以獲得任意值的類型對象(reflect.Type),程序通過類型對象可以訪問任意值的類型信息。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM