《Go語言聖經》學習筆記 第六章 方法

《Go語言聖經》學習筆記 第六章 方法


目錄

  1. 方法聲明
  2. 基於指針對象的方法
  3. 通過嵌入結構體來擴展類型
  4. 方法值和方法表達式
  5. 示例:Bit數組
  6. 封裝

注:學習《Go語言聖經》筆記,PDF點擊下載,建議看書。
Go語言小白學習筆記,書上的內容照搬,大佬看了勿噴,以後熟悉了會總結成自己的讀書筆記。


  1. 從90年代早期開始, 面向對象編程(OOP)就成爲了稱霸工程界和教育界的編程範式, 所以之後幾乎所有大規模被應用的語言都包含了對OOP的支持, go語言也不例外。
  2. 儘管沒有被大衆所接受的明確的OOP的定義, 從我們的理解來講, 一個對象其實也就是一個簡單的值或者一個變量, 在這個對象中會包含一些方法, 而一個方法則是一個一個和特殊類型關聯的函數。 一個面向對象的程序會用方法來表達其屬性和對應的操作, 這樣使用這個對象的用戶就不需要直接去操作對象, 而是藉助方法來做這些事情。
  3. 在早些的章節中, 我們已經使用了標準庫提供的一些方法, 比如time.Duration這個類型的Seconds方法:
    在這裏插入圖片描述
  4. 並且在2.5節中, 我們定義了一個自己的方法, Celsius類型的String方法:
    5.
  5. 在本章中, OOP編程的第一方面, 我們會向你展示如何有效地定義和使用方法。 我們會覆蓋到OOP編程的兩個關鍵點, 封裝和組合。

1. 方法聲明

  1. 在函數聲明時, 在其名字之前放上一個變量, 即是一個方法。 這個附加的參數會將該函數附加到這種類型上, 即相當於爲這種類型定義了一個獨佔的方法。
  2. 下面來寫我們第一個方法的例子, 這個例子在package geometry下:
  3. gopl.io/ch6/geometry
    在這裏插入圖片描述
  4. 上面的代碼裏那個附加的參數p, 叫做方法的接收器(receiver), 早期的面嚮對象語言留下的遺產將調用一個方法稱爲“向一個對象發送消息”。
  5. 在Go語言中, 我們並不會像其它語言那樣用this或者self作爲接收器; 我們可以任意的選擇接收器的名字。 由於接收器的名字經常會被使用到, 所以保持其在方法間傳遞時的一致性和簡短性是不錯的主意。 這裏的建議是可以使用其類型的第一個字母, 比如這裏使用了Point的首字母p。
  6. 在方法調用過程中, 接收器參數一般會在方法名之前出現。 這和方法聲明是一樣的, 都是接收器參數在方法名字之前。 下面是例子:
    7.
  7. 可以看到, 上面的兩個函數調用都是Distance, 但是卻沒有發生衝突。 第一個Distance的調用實際上用的是包級別的函數geometry.Distance, 而第二個則是使用剛剛聲明的Point, 調用的是Point類下聲明的Point.Distance方法。
  8. 這種p.Distance的表達式叫做選擇器, 因爲他會選擇合適的對應p這個對象的Distance方法來執行。 選擇器也會被用來選擇一個struct類型的字段, 比如p.X。 由於方法和字段都是在同一命名空間, 所以如果我們在這裏聲明一個X方法的話, 編譯器會報錯, 因爲在調用p.X時會有歧義(譯註: 這裏確實挺奇怪的)。
  9. 因爲每種類型都有其方法的命名空間, 我們在用Distance這個名字的時候, 不同的Distance調用指向了不同類型裏的Distance方法。 讓我們來定義一個Path類型, 這個Path代表一個線段的集合, 並且也給這個Path定義一個叫Distance的方法。
    在這裏插入圖片描述
  10. Path是一個命名的slice類型, 而不是Point那樣的struct類型, 然而我們依然可以爲它定義方法。 在能夠給任意類型定義方法這一點上, Go和很多其它的面向對象的語言不太一樣。 因此在Go語言裏, 我們爲一些簡單的數值、 字符串、 slice、 map來定義一些附加行爲很方便。 方法可以被聲明到任意類型, 只要不是一個指針或者一個interface。
  11. 兩個Distance方法有不同的類型。 他們兩個方法之間沒有任何關係, 儘管Path的Distance方法會在內部調用Point.Distance方法來計算每個連接鄰接點的線段的長度。
  12. 讓我們來調用一個新方法, 計算三角形的周長:
    在這裏插入圖片描述
  13. 在上面兩個對Distance名字的方法的調用中, 編譯器會根據方法的名字以及接收器來決定具體調用的是哪一個函數。 第一個例子中path[i-1]數組中的類型是Point, 因此Point.Distance這個方法被調用; 在第二個例子中perim的類型是Path, 因此Distance調用的是Path.Distance。
  14. 對於一個給定的類型, 其內部的方法都必須有唯一的方法名, 但是不同的類型卻可以有同樣的方法名, 比如我們這裏Point和Path就都有Distance這個名字的方法; 所以我們沒有必要非在方法名之前加類型名來消除歧義, 比如PathDistance。 這裏我們已經看到了方法比之函數的一些好處: 方法名可以簡短。 當我們在包外調用的時候這種好處就會被放大, 因爲我們可以使用這個短名字, 而可以省略掉包的名字, 下面是例子:
    在這裏插入圖片描述
  15. 譯註: 如果我們要用方法去計算perim的distance, 還需要去寫全geometry的包名, 和其函數名, 但是因爲Path這個變量定義了一個可以直接用的Distance方法, 所以我們可以直接寫perim.Distance()。 相當於可以少打很多字, 作者應該是這個意思。 因爲在Go裏包外調用函數需要帶上包名, 還是挺麻煩的。

2. 基於指針對象的方法

  1. 當調用一個函數時, 會對其每一個參數值進行拷貝, 如果一個函數需要更新一個變量, 或者函數的其中一個參數實在太大我們希望能夠避免進行這種默認的拷貝, 這種情況下我們就需要用到指針了。 對應到我們這裏用來更新接收器的對象的方法, 當這個接受者變量本身比較大時, 我們就可以用其指針而不是對象來聲明方法, 如下:
    在這裏插入圖片描述
  2. 這個方法的名字是 (*Point).ScaleBy 。 這裏的括號是必須的; 沒有括號的話這個表達式可能會被理解爲 *(Point.ScaleBy) 。
  3. 在現實的程序裏, 一般會約定如果Point這個類有一個指針作爲接收器的方法, 那麼所有Point的方法都必須有一個指針接收器, 即使是那些並不需要這個指針接收器的函數。 我們在這裏打破了這個約定只是爲了展示一下兩種方法的異同而已。
  4. 只有類型(Point)和指向他們的指針(*Point), 纔是可能會出現在接收器聲明裏的兩種接收器。此外, 爲了避免歧義, 在聲明方法時, 如果一個類型名本身是一個指針的話, 是不允許其出現在接收器中的, 比如下面這個例子:
    在這裏插入圖片描述
  5. 想要調用指針類型方法 (*Point).ScaleBy , 只要提供一個Point類型的指針即可, 像下面這樣。
    在這裏插入圖片描述
  6. 或者這樣:
    在這裏插入圖片描述
  7. 或者這樣:
    在這裏插入圖片描述
  8. 不過後面兩種方法有些笨拙。 幸運的是, go語言本身在這種地方會幫到我們。 如果接收器p是一個Point類型的變量, 並且其方法需要一個Point指針作爲接收器, 我們可以用下面這種簡短的寫法:
    在這裏插入圖片描述
  9. 編譯器會隱式地幫我們用&p去調用ScaleBy這個方法。 這種簡寫方法只適用於“變量”, 包括struct裏的字段比如p.X, 以及array和slice內的元素比如perim[0]。 我們不能通過一個無法取到地址的接收器來調用指針方法, 比如臨時變量的內存地址就無法獲取得到:
    在這裏插入圖片描述
  10. 但是我們可以用一個 *Point 這樣的接收器來調用Point的方法, 因爲我們可以通過地址來找到這個變量, 只要用解引用符號 * 來取到該變量即可。 編譯器在這裏也會給我們隱式地插入 * 這個操作符, 所以下面這兩種寫法等價的:
    在這裏插入圖片描述
  11. 這裏的幾個例子可能讓你有些困惑, 所以我們總結一下: 在每一個合法的方法調用表達式中, 也就是下面三種情況裏的任意一種情況都是可以的:
  12. 不論是接收器的實際參數和其接收器的形式參數相同, 比如兩者都是類型T或者都是類型 *T :
    在這裏插入圖片描述
  13. 或者接收器形參是類型T, 但接收器實參是類型 *T , 這種情況下編譯器會隱式地爲我們取變量的地址:
    在這裏插入圖片描述
  14. 或者接收器形參是類型 *T , 實參是類型T。 編譯器會隱式地爲我們解引用, 取到指針指向的實際變量:
    在這裏插入圖片描述
  15. 如果類型T的所有方法都是用T類型自己來做接收器(而不是 *T ), 那麼拷貝這種類型的實例就是安全的; 調用他的任何一個方法也就會產生一個值的拷貝。 比如time.Duration的這個類型, 在調用其方法時就會被全部拷貝一份, 包括在作爲參數傳入函數的時候。 但是如果一個方法使用指針作爲接收器, 你需要避免對其進行拷貝, 因爲這樣可能會破壞掉該類型內部的不變性。 比如你對bytes.Buffer對象進行了拷貝, 那麼可能會引起原始對象和拷貝對象只是別名而已, 但實際上其指向的對象是一致的。 緊接着對拷貝後的變量進行修改可能會有讓你意外的結果。

1. Nil也是一個合法的接收器類型

  1. 就像一些函數允許nil指針作爲參數一樣, 方法理論上也可以用nil指針作爲其接收器, 尤其當nil對於對象來說是合法的零值時, 比如map或者slice。 在下面的簡單int鏈表的例子裏, nil代表的是空鏈表:
    在這裏插入圖片描述
  2. 當你定義一個允許nil作爲接收器值的方法的類型時, 在類型前面的註釋中指出nil變量代表的意義是很有必要的, 就像我們上面例子裏做的這樣。
  3. 下面是net/url包裏Values類型定義的一部分。
  4. net/url
    在這裏插入圖片描述
  5. 這個定義向外部暴露了一個map的類型的變量, 並且提供了一些能夠簡單操作這個map的方法。 這個map的value字段是一個string的slice, 所以這個Values是一個多維map。 客戶端使用這個變量的時候可以使用map固有的一些操作(make, 切片, m[key]等等), 也可以使用這裏提供的操作方法, 或者兩者並用, 都是可以的:
  6. gopl.io/ch6/urlvalues
    在這裏插入圖片描述
  7. 對Get的最後一次調用中, nil接收器的行爲即是一個空map的行爲。 我們可以等價地將這個操作寫成Value(nil).Get(“item”), 但是如果你直接寫nil.Get(“item”)的話是無法通過編譯的, 因爲nil的字面量編譯器無法判斷其準備類型。 所以相比之下, 最後的那行m.Add的調用就會產生一個panic, 因爲他嘗試更新一個空map。
  8. 由於url.Values是一個map類型, 並且間接引用了其key/value對, 因此url.Values.Add對這個map裏的元素做任何的更新、 刪除操作對調用方都是可見的。 實際上, 就像在普通函數中一樣, 雖然可以通過引用來操作內部值, 但在方法想要修改引用本身是不會影響原始值的, 比如把他置爲nil, 或者讓這個引用指向了其它的對象, 調用方都不會受影響。 ( 譯註: 因爲傳入的是存儲了內存地址的變量, 你改變這個變量是影響不了原始的變量的, 想想C語言, 是差不多的)

3. 通過嵌入結構體來擴展類型

  1. 來看看ColoredPoint這個類型:
  2. gopl.io/ch6/coloredpoint
    在這裏插入圖片描述
  3. 我們完全可以將ColoredPoint定義爲一個有三個字段的struct, 但是我們卻將Point這個類型嵌入到ColoredPoint來提供X和Y這兩個字段。 像我們在4.4節中看到的那樣, 內嵌可以使我們在定義ColoredPoint時得到一種句法上的簡寫形式, 並使其包含Point類型所具有的一切字段,然後再定義一些自己的。 如果我們想要的話, 我們可以直接認爲通過嵌入的字段就是ColoredPoint自身的字段, 而完全不需要在調用時指出Point, 比如下面這樣。
    4.
  4. 對於Point中的方法我們也有類似的用法, 我們可以把ColoredPoint類型當作接收器來調用Point裏的方法, 即使ColoredPoint裏沒有聲明這些方法:
    在這裏插入圖片描述
  5. Point類的方法也被引入了ColoredPoint。 用這種方式, 內嵌可以使我們定義字段特別多的複雜類型, 我們可以將字段先按小類型分組, 然後定義小類型的方法, 之後再把它們組合起來。
  6. 讀者如果對基於類來實現面向對象的語言比較熟悉的話, 可能會傾向於將Point看作一個基類, 而ColoredPoint看作其子類或者繼承類, 或者將ColoredPoint看作"is a" Point類型。 但這是錯誤的理解。 請注意上面例子中對Distance方法的調用。 Distance有一個參數是Point類型, 但q並不是一個Point類, 所以儘管q有着Point這個內嵌類型, 我們也必須要顯式地選擇它。 嘗試直接傳q的話你會看到下面這樣的錯誤:
    在這裏插入圖片描述
  7. 一個ColoredPoint並不是一個Point, 但他"has a"Point, 並且它有從Point類裏引入的Distance和ScaleBy方法。 如果你喜歡從實現的角度來考慮問題, 內嵌字段會指導編譯器去生成額外的包裝方法來委託已經聲明好的方法, 和下面的形式是等價的:
    在這裏插入圖片描述
  8. 當Point.Distance被第一個包裝方法調用時, 它的接收器值是p.Point, 而不是p, 當然了, 在Point類的方法裏, 你是訪問不到ColoredPoint的任何字段的。
  9. 在類型中內嵌的匿名字段也可能是一個命名類型的指針, 這種情況下字段和方法會被間接地引入到當前的類型中(譯註: 訪問需要通過該指針指向的對象去取)。 添加這一層間接關係讓我們可以共享通用的結構並動態地改變對象之間的關係。 下面這個ColoredPoint的聲明內嵌了一個*Point的指針。
    在這裏插入圖片描述
  10. 一個struct類型也可能會有多個匿名字段。 我們將ColoredPoint定義爲下面這樣:
    11.
  11. 然後這種類型的值便會擁有Point和RGBA類型的所有方法, 以及直接定義在ColoredPoint中的方法。 當編譯器解析一個選擇器到方法時, 比如p.ScaleBy, 它會首先去找直接定義在這個類型裏的ScaleBy方法, 然後找被ColoredPoint的內嵌字段們引入的方法, 然後去找Point和RGBA的內嵌字段引入的方法, 然後一直遞歸向下找。 如果選擇器有二義性的話編譯器會報錯, 比如你在同一級裏有兩個同名的方法。
  12. 方法只能在命名類型(像Point)或者指向類型的指針上定義, 但是多虧了內嵌, 有些時候我們給匿名struct類型來定義方法也有了手段。
  13. 下面是一個小trick。 這個例子展示了簡單的cache, 其使用兩個包級別的變量來實現, 一個mutex互斥量(§9.2)和它所操作的cache:
    在這裏插入圖片描述
  14. 下面這個版本在功能上是一致的, 但將兩個包級吧的變量放在了cache這個struct一組內:
    在這裏插入圖片描述
  15. 我們給新的變量起了一個更具表達性的名字: cache。 因爲sync.Mutex字段也被嵌入到了這個struct裏, 其Lock和Unlock方法也就都被引入到了這個匿名結構中了, 這讓我們能夠以一個簡單明瞭的語法來對其進行加鎖解鎖操作。

4. 方法值和方法表達式

  1. 我們經常選擇一個方法, 並且在同一個表達式裏執行, 比如常見的p.Distance()形式, 實際上將其分成兩步來執行也是可能的。 p.Distance叫作“選擇器”, 選擇器會返回一個方法"值"->一個將方法(Point.Distance)綁定到特定接收器變量的函數。 這個函數可以不通過指定其接收器即可被調用; 即調用時不需要指定接收器(譯註: 因爲已經在前文中指定過了), 只要傳入函數的參數即可:
    在這裏插入圖片描述
  2. 在一個包的API需要一個函數值、 且調用方希望操作的是某一個綁定了對象的方法的話, 方法"值"會非常實用(=_=真是繞)。 舉例來說, 下面例子中的time.AfterFunc這個函數的功能是在指定的延遲時間之後來執行一個(譯註: 另外的)函數。 且這個函數操作的是一個Rocket對象r
    3.
  3. 直接用方法"值"傳入AfterFunc的話可以更爲簡短:
    在這裏插入圖片描述
  4. 譯註: 省掉了上面那個例子裏的匿名函數。
  5. 和方法"值"相關的還有方法表達式。 當調用一個方法時, 與調用一個普通的函數相比, 我們必須要用選擇器(p.Distance)語法來指定方法的接收器。
  6. 當T是一個類型時, 方法表達式可能會寫作T.f或者(*T).f, 會返回一個函數"值", 這種函數會將其第一個參數用作接收器, 所以可以用通常(譯註: 不寫選擇器)的方式來對其進行調用:
    在這裏插入圖片描述
  7. 當你根據一個變量來決定調用同一個類型的哪個函數時, 方法表達式就顯得很有用了。 你可以根據選擇來調用接收器各不相同的方法。 下面的例子, 變量op代表Point類型的addition或者subtraction方法, Path.TranslateBy方法會爲其Path數組中的每一個Point來調用對應的方法:
    在這裏插入圖片描述

5. 示例: Bit數組

  1. Go語言裏的集合一般會用map[T]bool這種形式來表示, T代表元素類型。 集合用map類型來表示雖然非常靈活, 但我們可以以一種更好的形式來表示它。 例如在數據流分析領域, 集合元素通常是一個非負整數, 集合會包含很多元素, 並且集合會經常進行並集、 交集操作, 這種情況下, bit數組會比map表現更加理想。 (譯註: 這裏再補充一個例子, 比如我們執行一個http下載任務, 把文件按照16kb一塊劃分爲很多塊, 需要有一個全局變量來標識哪些塊下載完成了, 這種時候也需要用到bit數組)

  2. 一個bit數組通常會用一個無符號數或者稱之爲“字”的slice或者來表示, 每一個元素的每一位都表示集合裏的一個值。 當集合的第i位被設置時, 我們才說這個集合包含元素i。 下面的這個程序展示了一個簡單的bit數組類型, 並且實現了三個函數來對這個bit數組來進行操作:

  3. gopl.io/ch6/intset
    在這裏插入圖片描述

  4. 因爲每一個字都有64個二進制位, 所以爲了定位x的bit位, 我們用了x/64的商作爲字的下標,並且用x%64得到的值作爲這個字內的bit的所在位置。 UnionWith這個方法裏用到了bit位的“或”邏輯操作符號|來一次完成64個元素的或計算。 (在練習6.5中我們還會程序用到這個64位字的例子。 )

  5. 當前這個實現還缺少了很多必要的特性, 我們把其中一些作爲練習題列在本小節之後。 但是有一個方法如果缺失的話我們的bit數組可能會比較難混: 將IntSet作爲一個字符串來打印。 這裏我們來實現它, 讓我們來給上面的例子添加一個String方法, 類似2.5節中做的那樣:

    // String方法以字符串"{1 2 3}"的形式返回集中
    func (s *IntSet) String() string {
    	var buf bytes.Buffer
    	buf.WriteByte('{')
    	for i, word := range s.words {
    		if word == 0 {
    			continue
    		}
    		for j := 0; j < 64; j++ {
    			if word&(1<<uint(j)) != 0 {
    				if buf.Len() > len("{") {
    					buf.WriteByte(' ')
    				}
    				fmt.Fprintf(&buf, "%d", 64*i+j)
    			}
    		}
    	}
    	buf.WriteByte('}')
    	return buf.String()
    }
    
  6. 這裏留意一下String方法, 是不是和3.5.4節中的intsToString方法很相似; bytes.Buffer在String方法裏經常這麼用。 當你爲一個複雜的類型定義了一個String方法時, fmt包就會特殊對待這種類型的值, 這樣可以讓這些類型在打印的時候看起來更加友好, 而不是直接打印其原始的值。 fmt會直接調用用戶定義的String方法。 這種機制依賴於接口和類型斷言, 在第7章中我們會詳細介紹。

  7. 現在我們就可以在實戰中直接用上面定義好的IntSet了:
    在這裏插入圖片描述

  8. 這裏要注意: 我們聲明的String和Has兩個方法都是以指針類型*IntSet來作爲接收器的, 但實際上對於這兩個類型來說, 把接收器聲明爲指針類型也沒什麼必要。 不過另外兩個函數就不是這樣了, 因爲另外兩個函數操作的是s.words對象, 如果你不把接收器聲明爲指針對象, 那麼實際操作的是拷貝對象, 而不是原來的那個對象。 因此, 因爲我們的String方法定義在IntSet指針上, 所以當我們的變量是IntSet類型而不是IntSet指針時, 可能會有下面這樣讓人意外的情況:
    在這裏插入圖片描述

  9. 在第一個Println中, 我們打印一個*IntSet的指針, 這個類型的指針確實有自定義的String方法。 第二Println, 我們直接調用了x變量的String()方法; 這種情況下編譯器會隱式地在x前插入&操作符, 這樣相當遠我們還是調用的IntSet指針的String方法。 在第三個Println中, 因爲IntSet類型沒有String方法, 所以Println方法會直接以原始的方式理解並打印。 所以在這種情況下&符號是不能忘的。 在我們這種場景下, 你把String方法綁定到IntSet對象上, 而不是IntSet指針上可能會更合適一些, 不過這也需要具體問題具體分析。


6. 封裝

  1. 一個對象的變量或者方法如果對調用方是不可見的話, 一般就被定義爲“封裝”。 封裝有時候也被叫做信息隱藏, 同時也是面向對象編程最關鍵的一個方面。
  2. Go語言只有一種控制可見性的手段: 大寫首字母的標識符會從定義它們的包中被導出, 小寫字母的則不會。 這種限制包內成員的方式同樣適用於struct或者一個類型的方法。 因而如果我們想要封裝一個對象, 我們必須將其定義爲一個struct。
  3. 這也就是前面的小節中IntSet被定義爲struct類型的原因, 儘管它只有一個字段:
    在這裏插入圖片描述
  4. 當然, 我們也可以把IntSet定義爲一個slice類型, 儘管這樣我們就需要把代碼中所有方法裏用到的s.words用*s替換掉了:
    在這裏插入圖片描述
  5. 儘管這個版本的IntSet在本質上是一樣的, 他也可以允許其它包中可以直接讀取並編輯這個slice。 換句話說, 相對*s這個表達式會出現在所有的包中, s.words只需要在定義IntSet的包中出現(譯註: 所以還是推薦後者吧的意思)。
  6. 這種基於名字的手段使得在語言中最小的封裝單元是package, 而不是像其它語言一樣的類型。 一個struct類型的字段對同一個包的所有代碼都有可見性, 無論你的代碼是寫在一個函數還是一個方法裏。
  7. 封裝提供了三方面的優點。 首先, 因爲調用方不能直接修改對象的變量值, 其只需要關注少量的語句並且只要弄懂少量變量的可能的值即可.
  8. 第二, 隱藏實現的細節, 可以防止調用方依賴那些可能變化的具體實現, 這樣使設計包的程序員在不破壞對外的api情況下能得到更大的自由。
  9. 把bytes.Buffer這個類型作爲例子來考慮。 這個類型在做短字符串疊加的時候很常用, 所以在設計的時候可以做一些預先的優化, 比如提前預留一部分空間, 來避免反覆的內存分配。 又因爲Buffer是一個struct類型, 這些額外的空間可以用附加的字節數組來保存, 且放在一個小寫字母開頭的字段中。 這樣在外部的調用方只能看到性能的提升, 但並不會得到這個附加變量。 Buffer和其增長算法我們列在這裏, 爲了簡潔性稍微做了一些精簡:
    在這裏插入圖片描述
  10. 封裝的第三個優點也是最重要的優點, 是阻止了外部調用方對對象內部的值任意地進行修改。 因爲對象內部變量只可以被同一個包內的函數修改, 所以包的作者可以讓這些函數確保對象內部的一些值的不變性。 比如下面的Counter類型允許調用方來增加counter變量的值, 並且允許將這個值reset爲0, 但是不允許隨便設置這個值(譯註: 因爲壓根就訪問不到):
    在這裏插入圖片描述
  11. 只用來訪問或修改內部變量的函數被稱爲setter或者getter, 例子如下, 比如log包裏的Logger類型對應的一些函數。 在命名一個getter方法時, 我們通常會省略掉前面的Get前綴。 這種簡潔上的偏好也可以推廣到各種類型的前綴比如Fetch, Find或者Lookup。
    在這裏插入圖片描述
  12. Go的編碼風格不禁止直接導出字段。 當然, 一旦進行了導出, 就沒有辦法在保證API兼容的情況下去除對其的導出, 所以在一開始的選擇一定要經過深思熟慮並且要考慮到包內部的一些不變量的保證, 未來可能的變化, 以及調用方的代碼質量是否會因爲包的一點修改而變差。
  13. 封裝並不總是理想的。 雖然封裝在有些情況是必要的, 但有時候我們也需要暴露一些內部內容, 比如: time.Duration將其表現暴露爲一個int64數字的納秒, 使得我們可以用一般的數值操作來對時間進行對比, 甚至可以定義這種類型的常量:
    在這裏插入圖片描述
  14. 另一個例子, 將IntSet和本章開頭的geometry.Path進行對比。 Path被定義爲一個slice類型,這允許其調用slice的字面方法來對其內部的points用range進行迭代遍歷; 在這一點上, IntSet是沒有辦法讓你這麼做的。
  15. 這兩種類型決定性的不同: geometry.Path的本質是一個座標點的序列, 不多也不少, 我們可以預見到之後也並不會給他增加額外的字段, 所以在geometry包中將Path暴露爲一個slice。相比之下, IntSet僅僅是在這裏用了一個[]uint64的slice。 這個類型還可以用[]uint類型來表示, 或者我們甚至可以用其它完全不同的佔用更小內存空間的東西來表示這個集合, 所以我們可能還會需要額外的字段來在這個類型中記錄元素的個數。 也正是因爲這些原因, 我們讓IntSet對調用方透明。
  16. 在這章中, 我們學到了如何將方法與命名類型進行組合, 並且知道了如何調用這些方法。 儘管方法對於OOP編程來說至關重要, 但他們只是OOP編程裏的半邊天。 爲了完成OOP, 我們還需要接口。 Go裏的接口會在下一章中介紹。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章