第一章 ---- **Tell me and I forget. Teach me and I remember . Involve me and I leran** 告訴我,我忘了。教我,我記得。 讓我加入一起,我學習 這也是在錯誤中學習的一種主旨,深入其中才能學得更好 **Go is simple but not easy。** Go是簡單的但其實也不簡單,雖然簡單也不好專精。 雖然Goroutine 和 channel 很簡單,但有統計說使用channel的case很多,甚至比使用mutx還多 **The bigger interface , the weaker the abstraction** 雖然一個接口的方法多,他的抽象能力越低,就像read/write強大是因為只有一個方法。 此書把錯誤分成七種 1. Bugs : 比較偏向邏輯錯誤、數據、洩漏、 這個錯誤比較好處理通常可以透過單元測試 2. Needless complexity : 不必要的複雜性,我們常常會因為思考這功能的未來性,把這功能搞得很複雜,例如: ``` type shape interface { Area() float64 } type Circle struct { Radius float 64 } func (c Circle) Area() float64 { return 3.14 * c.Radius * c.Radius } ``` ``` func CircleArea(radius float64)float64{ return 3.14 * radius *radius } ``` 一個複雜一個不複雜,雖然複雜的在未來發展上可以比好擴充,但如果到頭來只做了這個功能,那這豈不是做得很複雜嗎 3.Weaker readability 可讀性的問題,在<<Clean Code: A Handbook of Agile Software Craftsmanship>>書中有說道我們花在讀跟寫時間差不多 10:1.5,但如果你不好好地寫會導致幾年後十年後的維護上的困難,錯誤在嵌套程式碼(巢式迴圈是一種)、資料類型表示、等等。 4.Suboptimal or unidiomatic 項目的結構例如cmd目錄放主要應用程序入口,Package方可重用的包這樣有助於把程式跟模塊分開來,並且避免過多嵌套,公開和私有的,Go通常使用大小寫來判別是否公開(就是在包外訪問) util包通常拿來放置輔助函數,這樣其實定義範圍很大,因為輔助這兩字太大,init函數會在調用時候就馬上使用,如果用太多又會導致可預測性跟效能下降。所以清晰的命名是很重要的。 5.Lack of API convenience 怎麼讓API便利且有好,可讀性跟表達性應該要高,像是結構不該複雜和很多就像一個人身分訊息好幾個結構,或者說在函數使用上,應該具有方便導入性讓單元測試簡單,還有就是其實傳統的繼承概念在Go語言會看起來更複雜,因為Go是一種interface而不是OOP語言,像是結構內有結構這種改善方法 ``` type Animal interface { // 動物的行為接口 Speak() string // 定義 Speak 方法,所有動物都要實作 } type Dog struct { // Dog 結構體,表示狗的品種 Breed string // 品種屬性 } func (d Dog) Speak() string { // 將 Speak 方法綁定到 Dog 結構體 return "Woof!" // 狗的叫聲 } // API 使用接口來表示行為,不限於具體類型 func AnimalSound(a Animal) string { return a.Speak() // 調用傳入的 Animal 介面的 Speak 方法 } ``` ------------------------------------------------------------------ Animal 是一個規範,它規定了所有實作這個介面的類型必須擁有某些行為(方法)。介面本身不關心具體的類型,它只關心方法的簽名是否匹配。 如果你需要傳入多個參數,介面的定義必須提前考慮這一點。介面不能「隨意」更改方法簽名,因為所有實作這個介面的類型都需要完全遵守它的規範。如果方法簽名不一致,編譯器會報錯。 這樣的設計讓 Go 程式碼更加清晰和可維護,也讓不同的類型可以在不暴露具體實作的情況下被統一處理。 6.Under-optimized code 欠優化的程式碼,其實也是一個錯誤並行話(,並實現stroe街口,同時又有return store街口會導致包依賴在client包 進而引起1 加到100 不應該開100個直接加這樣,應該分組1~20 20~40 分開運算,這樣才是合理的並行,或者是不斷分配內存,應該採取重用或者預分配不然導致頻繁內存分配和垃圾回收影響效能,還有個內存深入問題, ![image](https://hackmd.io/_uploads/HkJjAnAAA.png) 這邊參考GPT 4.0解,大致上意思是類似研究所考試的內存分配問題,可能要多出一些空間使用,[內碎外碎](https://ithelp.ithome.com.tw/articles/10210329)那個,這裡應該是外碎因為這4用不到。 7.Lack of productivity 缺乏生產力,我們工作效率最高的語言,要完全熟悉語言工作原理跟充分利用他其實蠻難的 第二章 --------- ## 1.Unintended variable shadowing (意外變數陰影變數) ``` var client *http.Client if tracing { client, err := createClientWithTracing() // 這裡遮蔽了外部變量 client if err != nil { return err } log.Println(client) } else { client, err := createDefaultClient() // 同樣,使外部變量 client 欲入 nil if err != nil { return err } log.Println(client) } // 使用 client 會發現它仍然是 nil ``` 上面code展示了標題shadowing variable,宣告了Global變client之後又在條件句中又宣告了client := 使用短變亮聲明運算子,這樣會導致外面的Global 變數實際上只有nil,因為條件句中client實際操作只影響內部局部便亮。其實平常在使用就會提醒編譯錯誤了 ## 2.Unnecessary nested code Align the happy path to the left; you should quickly be able to scan down one column to see the expected execution flow. Code 對齊是很重要的,這樣可讀性才高尤其是nested code(嵌套) ``` func join(s1, s2 string, max int) (string, error) { if s1 == "" { return "", errors.New("s1 is empty") } else { if s2 == "" { return "", errors.New("s2 is empty") } else { concat, err := concatenate(s1, s2) if err != nil { return "", err } else { if len(concat) > max { return concat[:max], nil } else { return concat, nil } } } } } func concatenate(s1 string, s2 string) (string, error) { // 實現內容... } ``` --------------------------------------- ``` func join(s1, s2 string, max int) (string, error) { if s1 == "" { return "", errors.New("s1 is empty") } if s2 == "" { return "", errors.New("s2 is empty") } concat, err := concatenate(s1, s2) if err != nil { return "", err } if len(concat) > max { return concat[:max], nil } return concat, nil } func concatenate(s1 string, s2 string) (string, error) { // 實現內容... } ``` --------------------------------------- 以上兩段code 明顯可讀性差蠻多的 還有程式應該盡量不要else 可以更好讀 ## 3.Misusing init function 誤用init其實蠻嚴重的還會導致管理不擅和Code 難以理解 **Init Function ** init函數是初始化應用程式狀態的一種函數,他不接受任何參數,也不返回任何結果,當包被初始化時,包內所有償術和變數聲明都會被評估。 ``` package main import "fmt" var a = func() int { fmt.Println("var") return 0 }() func init() { fmt.Println("init") } func main() { fmt.Println("main") } ``` 輸出: var init main ``` package main import ( "fmt" "redis" ) func init() { // ... } func main() { err := redis.Store("foo", "bar") // ... } ``` 此code 因為main包依賴redis包 ,所以Redis包的init優先執行,接著才執行main包的init,包中定義多個init是依靠文件名稱字母決定順序的如a.go和b.go會先執行a.go ![image](https://hackmd.io/_uploads/H1XqC0AC0.png) init()函數也不能被直接調用 init()在錯誤上因為本身不回傳,會直接Panic,雖然Panic有時候不錯,但有一種失去自型定義錯誤的感覺,Panic也是直接死機。 在單元測試上也會因為init()會自動調用而出問題,還有還要先宣告全域變數,那控管上的風險也很麻。 這樣說起來init()一點好處都沒用,但也有好用地方就像 ``` func init() { redirect := func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusFound) } http.HandleFunc("/blog", redirect) http.HandleFunc("/blog/", redirect) static := http.FileServer(http.Dir("static")) http.Handle("/favicon.ico", static) http.Handle("/fonts.css", static) http.Handle("/fonts/", static) http.Handle("/lib/godoc/", http.StripPrefix("/lib/godoc/", http.HandlerFunc(staticHandler))) } ``` 上面程式碼不用特別檢查err,也沒全域變數,也不影響單元測試。 ## 4.Overusings and setters 在Go中不像其他語言一樣有自動封裝getters 跟 setters ,但go還是可以實現封裝功能 ![image](https://hackmd.io/_uploads/BJrEt6yJkl.png) 另一方面,使用 Getters 和 Setters 也有一些優點,包括以下幾點: * 它們封裝了與取得或設置欄位相關的行為,使我們能夠在日後增加新功能(例如,對欄位進行驗證、返回計算值,或將欄位存取包裹在互斥鎖裡)。 * 它們隱藏了內部表示方式,讓我們有更多靈活性來決定公開哪些內容。 * 它們提供了一個除錯攔截點,當屬性在運行時發生變化時,可以更容易進行調試。 在go中除非必要不然盡量別用,因為主旨是為了保持整潔度,不然就是命名上要有一個規範 ![image](https://hackmd.io/_uploads/HkPIc6kyye.png) ## 5.Interface polllutuin 汙染介面也是個問題,如果我們用很多不必要的抽象在code上。會讓這code很難讀 GO 在跟其他語言的implement是採取隱式的,看下面例子 ![image](https://hackmd.io/_uploads/r1mJ2akJ1e.png) ![image](https://hackmd.io/_uploads/S1Dx2Tyykl.png) ![image](https://hackmd.io/_uploads/S1yLh6yJye.png) Go的介面通常有較少方法,這樣可以讓介面具體變簡單,靈活性高就像io.reader 和 io.writer 的介面各一個方法,這樣可以讓不同類型選擇最適合他們的介面 總結: * 隱式介面實作:Go 不需要 implements 關鍵字,讓程式碼更加簡潔靈活。 * 抽象化的威力:通過介面,我們可以撰寫更加通用的程式碼,並且在測試時可以使用不同的資料來源和目標,讓程式碼更具適應性和可測試性。 * 細粒度介面設計:介面的設計應該保持簡單,避免介面過於龐大,使得實作更加靈活且專注。 所以這邊有一句話 The bigger the interface, the weaker the abstraction. —Rob Pike 那到底何時該建立interface勒 分為三大點 * Common behavior * Decoupling * Restricting behavior 1.Common behavior 介面上有很強的可重用性 ![image](https://hackmd.io/_uploads/rkrSIA1J1l.png) 2.Decoupling ![image](https://hackmd.io/_uploads/S1YYcR1y1x.png) 給我的感覺比較像是為了解決單位測試再依賴其他包上的麻煩,而設計的。 3.Restricting behavior ![image](https://hackmd.io/_uploads/rJRT9AJyyx.png) 感覺起來是為了限制只能執行某些行為而刻intConfiGetter只有一個Get()函數 type Foo 因為綁定成員到intConfigGetter上,進而限制他只能做intconfigGetter有的事情,NewFoo比較像是透過傳入的intconfigGetter的格式資料,賦予值,而最後的Bar()方法,就只能讀取 背後的設計邏輯: * Decoupling(解耦合): 這段程式碼將 Foo 的邏輯與具體的配置實現分離(也就是解耦合)。Foo 不需要知道 threshold 的具體實現,它只關心 threshold 提供的讀取行為。 * * Restricting Behavior(限制行為): 透過將 threshold 定義為 intConfigGetter,強制 Foo 只能讀取配置,這在某些情況下是有意義的。例如,你可能想保證一個配置值是只讀的,以避免某些敏感數據被無意修改。 ![image](https://hackmd.io/_uploads/rJ4IR0k1ke.png) 在污染介面,其實主要就是過度使用接口介面問題,而且效能上其實也會影響,畢竟是透過hash table數據結構查找街口指向的具體類型,盡管開銷微乎其微,但沒有總比友好,所以創建應該以抽象為主,言而總之,能不用接口就不要用接口 > Don’t design with interfaces, discover them. > —Rob Pike 在創建街口介面時應該考慮到,會不會用到,或者能證明他是有效的,不然別塞都進來 ## 6.Returning interfaces 返回接口其實問題蠻多的。如果包含store 接口的client 包和實現接口的store包,如果再inMemortSotre 結構體,並實現stroe街口,同時又有return store接口的函數newinmemorystore會導致包依賴在client包進而引起,循環依賴client包不能調用newinmemorystroe函數,不能可能導致循環,這樣導致設計困難,返回接口還會限制靈魂性,因為沒考慮到所有各自需求。 > Be conservative in what you do, be liberal in what you accept from others. > —Transmission Control Protocol 再行動中保持保守,在接受中保持寬容 應該要返回具體類型而不是接口會更簡單。 ## 7.any says nothing In Go var i any(or interface{}),可以有效降低複雜性,但用這方式也會導致使用者要特別查詢any真正的型態是啥,還有類型安全的問題,像以下這Code ``` package store type Customer struct { // 一些字段 } type Contract struct { // 一些字段 } type Store struct{} func (s *Store) Get(id string) (any, error) { // ... 实现 } func (s *Store) Set(id string, v any) error { // ... 实现 } s := store.Store{} s.Set("foo", 42) // 編譯上肯定不會出錯但邏輯上可能出錯 ``` 解決方法: ``` func (s *Store) GetContract(id string) (Contract, error) { // ... 实现 } func (s *Store) SetContract(id string, contract Contract) error { // ... 实现 } func (s *Store) GetCustomer(id string) (Customer, error) { // ... 实现 } func (s *Store) SetCustomer(id string, customer Customer) error { // ... 实现 } ``` 看起來好像any沒那麼好,但其實得看情況,如果是在encoding/json包中的Marshal函數那any那就真的方便 ``` func Marshal(v any) ([]byte, error) { // ... } ``` 或者database/mysql ``` func (c *Conn) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error) { // ... } ``` ## 8.Being confused about when to use generics ![image](https://hackmd.io/_uploads/r1dJ5Qg11l.png) 上面code是一種Go的泛型運作,getKeys是函數名稱,[k comparable, V any]示範型類型參數定義,k 代表key類型 ,comparable是一個約束代表這一類型的值可以比較(能作為鍵使用),V就值,[]K是返回一個切片類型是K 如果不了Go的泛型可以參考[GO泛型解釋](https://www.cnblogs.com/insipid/p/17772581.html) 蠻詳細的 ![image](https://hackmd.io/_uploads/BkfxqdmJye.png) 再靈活性挺方便的,就像函數定義int固定,但泛型可以讓你int float都用 ![image](https://hackmd.io/_uploads/rkz89O7yye.png) 但都要記住要傳入實際參數,但是go還可以自動幫你,但實際還是有傳入實際參數 ![image](https://hackmd.io/_uploads/ByF1ouQy1l.png) 如果泛型在map上的話細節要注意 type m[K comparable, V any](map[K]V) var m map[[]byte]int 這會出錯,因為comparable 不能是slice要可以== 或者 != 才行 在Go 1.18之後interface分為兩種類型 * 基本interface(裡面只有方法) * 一般interface(裡面有形態 int|string) ## 9.Common users and misuses 常見使用方法跟誤用 * Data structures—We can use generics to factor out the element type if we implement a binary tree, a linked list, or a heap, for example. * Functions working with slices, maps, and channels of any type—A function to merge * two channels would work with any channel type, for example. Hence, we could * use type parameters to factor out the channel type: ``` func merge[T any](ch1, ch2 <-chan T) <-chan T { // ... } ``` * Factoring out behaviors instead of types—The sort package, for example, contains * a sort.Interface interface with three methods: * ![image](https://hackmd.io/_uploads/H14knFXJke.png) 圖片中說滿足Sort的interface,主要是隱性滿足,因為Len() Less() Swap() 這在sort interface 中都滿足,所以認為這個類行也滿足sort.interface 如果使用泛型不會讓code簡潔,反而變複雜感覺別用了。 ## 10.Not being aware of the possible problems with type embedding 蝦用類型embedding可能問題很多 ![image](https://hackmd.io/_uploads/r1kbNcQyJl.png) 上圖看起來要foo.bar.baz 才能更新baz值,但Go自帶簡潔效果,只要foo.baz也可以 如果要使用mutx 放入結構中應該要注意一下細節 ![image](https://hackmd.io/_uploads/rJ1pZiXyye.png) 就像上面應該把sync.mutex 做一個普通字段,這樣客戶端才不能直接訪問如果i.unlock()這樣就不好 ![image](https://hackmd.io/_uploads/SyhjoiQykl.png) 這種結構寫法雖然不如下面寫法簡潔 ![image](https://hackmd.io/_uploads/rkphioX1ye.png) 但還是有必要性的這些 * 接口的隱藏性: * 嵌入結構體的方式可能導致接口的行為不明確,對於閱讀代碼的人來說,他們必須查看嵌入的結構來發現這些方法。這在大型項目中可能會影響可讀性。 * 方法的衝突: * 如果嵌入的結構體和包內的其他方法存在同名衝突,這可能會導致混淆。為了解決這個問題,您必須仔細管理命名,避免使用相同的名稱。 ![image](https://hackmd.io/_uploads/Hy3El3X1yg.png) * 擴展性問題: * 若將來需要在 Logger 中添加其他方法或屬性,可能會造成結構的複雜性增加。如果嵌入的結構體改變,則可能影響到現有的代碼。 * 無法更換基類: * 一旦使用嵌入,若果未來想要更換 io.WriteCloser 的具體實現,則需要重新編寫代碼。這可能會導致在大型項目中進行重構時更加困難。 * 測試和模擬問題: * 當使用嵌入結構時,對該包的單元測試可能更加複雜,尤其是在需要模擬 io.WriteCloser 的情況下。這需要確保測試用例要涵蓋嵌入結構的行為。 * 缺乏額外的邏輯: * 如果您的 Logger 將來需要增加一些特定的邏輯(例如,加上前綴、格式化等),那麼您可能會失去嵌入的優勢,這樣需要額外寫代碼來處理這些邏輯。 用好嵌入 第一種看起來更簡便 ![image](https://hackmd.io/_uploads/BJGat27kkl.png) 第二種避免曝露太多 ![image](https://hackmd.io/_uploads/BkVbqhm1Jl.png) ## 11.Not using the functional options patter 在Go中的高階函數使用,其實很方便,可以一次傳入一堆一堆function做使用只需要 func 函數名稱(options.....你type定義 函數類型名稱)。 就可以傳入多個函數做使用蠻酷的 ``` type ServerConfig struct { Port int Timeout int } type ServerOption func(*ServerConfig) func WithPort(port int) ServerOption { return func(c *ServerConfig) { c.Port = port } } func WithTimeout(timeout int) ServerOption { return func(c *ServerConfig) { c.Timeout = timeout } } func NewServer(options ...ServerOption) *ServerConfig { // 預設配置 config := &ServerConfig{ Port: 8080, Timeout: 30, } // 應用每個選項函數 for _, opt := range options { opt(config) } return config } func main() { // 不傳任何選項,使用默認值 server1 := NewServer() fmt.Println(server1) // {8080 30} // 傳入一個選項 server2 := NewServer(WithPort(9090)) fmt.Println(server2) // {9090 30} // 傳入多個選項 server3 := NewServer(WithPort(9090), WithTimeout(60)) fmt.Println(server3) // {9090 60} } ``` 簡單描述一下上面code在說啥,首先建立結構ServerConfig然後定義函數類型ServerOption,在製作出三個函數,這三個函數回傳函數func(c *ServerConfig)隱性用法,他會檢查回傳值是否符合你函數宣告的回傳值類型(返回參數還要一樣),返回內容可以直接返回參數而不用返回名稱這叫做Go的隱性,而且要強制用匿名函數,因為你不匿名又變成重新指定函數傳入參數了,除非你在宣告一個變數:= 它這樣,但這樣太笨了,最後就是一次使用這三個函數方法,函數名稱(options...定義的函數名稱),這個options可以當成一種切片裡面放了函數,這樣就可以實現平常在使用的server架設的一次完成函數版本。 以上是功能選項模式(Functional Options Pattern) 有兩種模式,一種是功能選項模式,另一種則是建造者模式(Builder Pattern) 建造者模式code: ``` type Config struct { Port int } type ConfigBuilder struct { port *int } func (b *ConfigBuilder) Port(port int) *ConfigBuilder { b.port = &port return b } func (b *ConfigBuilder) Build() (Config, error) { cfg := Config{} if b.port == nil { cfg.Port = defaultHTTPPort } else { if *b.port == 0 { cfg.Port = randomPort() } else if *b.port < 0 { return Config{}, errors.New("port should be positive") } else { cfg.Port = *b.port } } return cfg, nil } func NewServer(addr string, config Config) (*http.Server, error) { // ... } builder := ConfigBuilder{} builder.Port(8080) // 設置端口為 8080 cfg, err := builder.Build() // 調用 Build 方法來生成 Config if err != nil { return err } server, err := NewServer("localhost", cfg) // 使用生成的 Config 啟動伺服器 if err != nil { return err } ``` ![image](https://hackmd.io/_uploads/B1evtXB11e.png) ![image](https://hackmd.io/_uploads/HJ8_FXBJ1x.png) 以上是GPT 對於建造者模式 以及 函數選項模式看法 ## 12.Project misorganization 此錯誤比較像是在說檔案路徑的配置,就是資料夾,簡單來說不多就精簡,名子分配要合理。 此書作者的檔案路徑配置 ![image](https://hackmd.io/_uploads/BkwA5QByyl.png) ## 13.Creating utility packages 在創造一些範圍很大的包像是utils,會有問題例如這樣 ![image](https://hackmd.io/_uploads/BJw23mSkke.png) 這裡客戶使用了utils.NewStringSet,此書作者覺得這樣utils是個啥沒意義阿,不如取個叫做stringGet ![image](https://hackmd.io/_uploads/ByF5a7S1kg.png) 作者這裡是採用前墜搭配後墜,這樣就能直接連貫使用直接看也挺直接的。 ![image](https://hackmd.io/_uploads/B1UdAQHyyl.png) 直接綁定成方法好像更不錯,set := stringset.New("c", "a", "b") 所以這個set 同等於 用stringset的包 裡的new 函數 進行創造出Set的結構,然後這set 是Set的這個結構,Sort又跟Set這個結構做了綁定所以又可以直接使用sort函數 從這邊就能得知包的命名有多重要 ## 14.Ignoring package name collisions ![image](https://hackmd.io/_uploads/rJNofNrykg.png) ![image](https://hackmd.io/_uploads/HJrmzNrJ1x.png) 在這裡redis := redis.NewClinent() 變數名稱跟包名稱相同,儘管可以這樣取,但也該避免,免得不知道在操作啥 ![image](https://hackmd.io/_uploads/SJFCz4H11l.png) 改成這樣就蠻好的,不然也可以幫包取別名(alias) ![image](https://hackmd.io/_uploads/rJGHX4H1Jx.png) 但其實不推薦這樣,避免產生混亂 ## 15.Missing code documentation ![image](https://hackmd.io/_uploads/HyfLNVHyJx.png) 其實code配上註釋也是非常重要的,就算是自己一個人,自己有一天也會忘記這啥吧,註釋也應該在強調函數用法,而不是強調他如何做到的 ![image](https://hackmd.io/_uploads/SJODrEBJ1g.png) 這函數沒在用最好打上Deprecated:節省時間。 最好再包名也說明一下這包在幹嗎的,然後註釋統一應該都在code 上方!!!! ## 16.Not useing linter 不使用linter會導致很多問題跟麻煩,靜態分析工具(linters)是一種自動化工具,用來分析程式碼,捕捉錯誤。例如,Go 語言中,vet 是一個標準的 linter,它可以幫助檢測變數遮蔽(variable shadowing)等問題。當變數名稱在不同範圍內重複使用時,這可能會導致不小心引用錯誤的變數。 最後章節總結來說: * 避免變數遮蔽:避免變數重複宣告,這樣可以防止引用錯誤的變數或混淆讀者。 * 避免過度嵌套:避免程式碼中的過度嵌套,並保持「快樂路徑」對齊在程式碼的左側,這樣更容易建立程式碼的心智模型。 * 變數初始化:在初始化變數時要記得 init 函數的錯誤處理能力有限,會讓狀態管理和測試變得更複雜。在大多數情況下,應該使用具體的函數來處理初始化。 * 強制使用 getter 和 setter 並不符合 Go 語言的慣用做法:應該根據實際情況,找到在效率和遵循慣用法之間的平衡點。 * 抽象應該被發現,而不是創造:為了避免不必要的複雜性,應在需要時才創建介面,而不是預見可能會需要時創建它;或者至少要能證明這個抽象是有效的。 * 將介面保留在客戶端:這樣可以避免不必要的抽象。 * 函數返回具體實現,而不是介面:這樣可以避免靈活性受限,而函數應儘量接受介面作為參數。 * 僅在必要時使用 any:僅在需要接受或返回任意類型(如 json.Marshal)時才使用 any,否則 any 不提供有意義的信息,並且可能導致編譯時的問題,允許調用者傳入任意類型的數據。 * 依賴泛型和型別參數可以減少重複代碼:但不要過早使用型別參數,只有在確實需要時才使用它們,否則會引入不必要的抽象和複雜性。 * 使用型別嵌入可以避免重複代碼:但要確保這樣做不會導致可見性問題,尤其是某些欄位應該保持隱藏。 * 使用功能選項模式來方便地處理選項,並且讓 API 更友好。 * 採用專案佈局標準:例如 project-layout,這是個開始構建 Go 專案結構的好方式,特別是如果你在尋找標準化新專案的現有慣例。 * 命名是應用設計中的關鍵部分:創建像 common、util 和 shared 這樣的包對讀者沒有太大價值。應將這些包重構為有意義且具體的名稱。 * 避免變數與包名稱的命名衝突:這會導致混淆或甚至是錯誤。應為每個變數使用唯一的名稱。如果無法做到,則使用導入別名來區分包名稱與變數名稱,或者考慮一個更好的命名。 * 為了幫助客戶端和維護者理解你的程式碼目的,應該為匯出的元素添加文件注釋。 * 為了提升程式碼的質量和一致性,應使用 linters 和格式化工具。 第三章 ------ 這章主要在資料處理上的問題。 ## 17.Creating confusion with octal literals ![image](https://hackmd.io/_uploads/Sk3s9VUk1x.png) 看起來是100 + 010 = 110 但其實實際上 是 100 + 8 (010 八進制) = 108 要考慮這些: * Binary—Uses a 0b or 0B prefix (for example, 0b100 is equal to 4 in base 10) * Hexadecimal—Uses an 0x or 0X prefix (for example, 0xF is equal to 15 in base 10) * Imaginary—Uses an i suffix (for example, 3i) ![image](https://hackmd.io/_uploads/Skx2iVUkkg.png) 在Go中還可以使用_ 來區別更好識別大小。 ## 18.Neglecting integer overflows ![image](https://hackmd.io/_uploads/Bkjw6NIJyl.png) 在Go中 int就有10種其中八種如上圖,另外兩種是根據系統 32 或 64 (直接int的那種) ![image](https://hackmd.io/_uploads/ryWiaELkke.png) 雖然超出不會panic但是會溢出變成負的,編譯有錯,但run time 還是可以執行,要是怕錯誤其實也可以特別用一個func來檢測 ![image](https://hackmd.io/_uploads/Hye9zSU1yg.png) 加法檢測溢出 ![image](https://hackmd.io/_uploads/BJPBQr81kl.png) 乘法檢測溢出 ![image](https://hackmd.io/_uploads/SyIFQBIJ1e.png) ## 19.Not understanding floating points 在Go 中float 是遵循IEEE-754 原則 如果是32 bit(1bit sign 8 bit 指數 23 bit 尾數) 64 (1 11 52) ![image](https://hackmd.io/_uploads/S1v6ErIk1x.png) 這圖看起來是 +1.0 * 2^0 * 1.000100016593933 所以我們1.0001實際上不是真的1.0001,這如果在比較兩個浮點數是否相等會出大事 ![image](https://hackmd.io/_uploads/Hk19HSIyyx.png) Nan是唯一可以用 != 的 ![image](https://hackmd.io/_uploads/Hk5lvS8kyl.png) 記住先處理誤差小的在處理誤差大,乘法和除法誤差較小,除非是連續相似數加法,不然都要先乘在加 ## 20.Not understanding slice length and capacity 切片使用要是蠻多要的注意 make([]int , 3 ,6) ![image](https://hackmd.io/_uploads/r10y_8UJye.png) 3.2圖是創造出的結果因為一開始只有指定初始化3長度,然後空間6,所以他初始化前三格為int格式,如果我今天s[1] = 1 那就變成圖3.3,如果我今天要s[4]那就會出現panic因為,雖然有空間容量但是沒這位置必須要s = append(s, 2) 才能讓s[4] = 2,突然我們又s = append(s,3,4,5) 會導致空間問題在加入5時候就會變圖3.5 ![image](https://hackmd.io/_uploads/ry6roIIJJe.png) 這邊要注意1024之前都是加倍擴充,但1024之後就是增加25%而已 ![image](https://hackmd.io/_uploads/rJnI28IyJl.png) 如果今天s2 是 s1裡面宣告出來的哩,那他就會是(int , 2 , 5),如果我們對s2 = append(s2,2)s1怎樣哩,s1會沒變 ![image](https://hackmd.io/_uploads/HkCMaLLykg.png) 這裡如果fmt pritf s1 = 0 1 0 s2 = 1 0 2 他們這時還都共用一個陣列呢 ![image](https://hackmd.io/_uploads/r13HpIUkJe.png) ![image](https://hackmd.io/_uploads/HyVU6LIk1l.png) 當加超過陣列容量,讓他就會像3.9圖那樣自己開出一個空間來存了,關於這個我覺得可以參考[billbill影片](https://www.bilibili.com/video/BV18qnZexEYy?spm_id_from=333.788.videopod.episodes&vd_source=cc871b6b447463409bf4ba7548e59133)講得蠻好的 ## 21.inefficient slice initalization ![image](https://hackmd.io/_uploads/BJs0CU8J1g.png) 在Go 切片容量中,特別重要的效能問題,如果你容量一開始沒弄好,那一直append()一直擴容一直複製(Go是採去超過容量時候會複製到新的slice)那效能就爆炸拉,而且GC還要一直把舊的清掉 ![image](https://hackmd.io/_uploads/HJmuJvIJJl.png) 有兩種CODE方式可以解決,一種是你把容量等於要複製的一樣,另一種是長度,只是你要記住有了長度就不是append了,因為不是增加,是從已存的陣列(例如初始化的0)改變值了。 ![image](https://hackmd.io/_uploads/ry9NfD81ke.png) 那你一定想知道三種哪種方式最好 看起來是第三種直接附值最好,但其實沒有一錠第一種就爛,因為如果我們實際上沒有複製那麼多哩,有個判斷式99%都沒要新增勒。 ## 22.Being confused ablout nill vs. empty slices ![image](https://hackmd.io/_uploads/r1C7UPIyyl.png) 第一種跟第二種是真正的nil 不占內存,但三和四是真的有站,在print上其實也有一個是真的空一個會有[]出現切片。 ![image](https://hackmd.io/_uploads/B1e2IPUJkl.png) 這張圖可以明確知道,不知道最終長度可以式空的,[]型態(nil)也可以創造nil的,長度已知可以直接建造 ## 23.Not properly checking if a slice is empty 判斷切片是不是空最簡單方法感覺是,直接len(切片)看是不是0最快 舉個反例: * If the slice is nil, len(operations) != 0 is false. * If the slice isn’t nil but empty, len(operations) != 0 is also false ![image](https://hackmd.io/_uploads/Skdn6P8kkx.png) ## 24.Not making slice copies correctly ![image](https://hackmd.io/_uploads/SJ3GxuLyyg.png) 這code會發生什麼哩,可能會根本沒複製出東西,如果dst是個0長度的,就會導致0複製 copy 只要記住 dst 跟 src 取最小值。 ![image](https://hackmd.io/_uploads/S1H1ZO8yye.png) 不然這樣也是可以,感覺更裝靈活只是不直觀。 ## 25.Unexpected side effects using slice append ![image](https://hackmd.io/_uploads/SkfNf_Ik1e.png) s1 (int , 3 , 3) s2 (int , 1 , 2) s3(int , 2 ,2) 在printf 就變成s1=[1 2 10], s2=[2], s3=[2 10] 只能說切片這塊用多才會熟,我的感覺是擴充超過你的容量時候就會複製新的,使用指標指的時候也要看自己有沒有設定最大容量。 ## 26.Slices and memory leaks 在某些情況下切片或數組可能導致記憶體洩漏問題 ![image](https://hackmd.io/_uploads/rk6DZoIkkx.png) 這邊有個問題msg他如果有存1M 資料然後你函數用指標指前面五個0~4雖然你只要這些,但是你沒考慮到後果,就是一千筆時候你以為只需要5000B的一千個前五個,但實際上是5000M因為你都要保持這些,所以最好改成複製前面就好。 ![image](https://hackmd.io/_uploads/ryukzoIk1e.png) ![image](https://hackmd.io/_uploads/HyKh3o8yye.png) 這邊他想利用完整切片來解決,但其實就算強制GC也是無法解決,所以最好還是複製一個新的來存吧 ![image](https://hackmd.io/_uploads/By2yRjI1yg.png) ![image](https://hackmd.io/_uploads/r17gRo8kyx.png) 即使我們用共享切片指定兩個而已,也沒用,因為他只是不能被訪問,但他還是會被保存在記憶體中,那有辦法嗎? ![image](https://hackmd.io/_uploads/rJw4RjI1Je.png) 這樣可以讓GC 知道FOO沒在用被引用 ![image](https://hackmd.io/_uploads/HylwCs8k1x.png) 那如果想要保留FOO的998個,可以考慮以上CODE,把後面998個裡面的資料變成nil,這樣GC也會知道要回收誰 ![image](https://hackmd.io/_uploads/r16Ko3LkJx.png) 至於該怎麼選擇可以考慮以上圖,考慮第一種的0到i操作多,還是i~n操作多決定吧。 ## 27.Ineffcient map initialization 先來談談map映射原理八,採取hash map的,採取每個hash8個桶,不夠就是用以下圖方式擴充 ![image](https://hackmd.io/_uploads/SJs-xhDJkg.png) ![image](https://hackmd.io/_uploads/r1gtg3Pkye.png) 可以參考這邊bucket overflow 情況,用separate chaining 的方法 GO的map有使用負載因子 元素數量 / 桶子數量(Mode 4 有4個) , 目前是超過6.5就會翻倍進行Rehashing,讓桶子數量變成8 ( mode 8),所以這也反映在如果你一開始數量沒設定好,預設只有桶內空間8格用完可能一直Rehashing,影響到效能問題。 ![image](https://hackmd.io/_uploads/H1I9Vavykl.png) ## 28.Maps and memory leaks ![image](https://hackmd.io/_uploads/SJ2-C1dkye.png) ![image](https://hackmd.io/_uploads/H1KfRkOkJe.png) 我們先建立map 在加入元素,然後刪掉,為啥沒全刪,主要是因為只刪掉key沒刪掉桶結構跟內存配置,這樣可以讓我們要在配置時候可以加速,而且Go 的GC其實也會保留內存放配,防止頻繁分配問題。 案例感覺是如果有個高峰期建立大量桶,然後時間過了還殘留著問題。 ![image](https://hackmd.io/_uploads/rJ1DBx_JJg.png) ![image](https://hackmd.io/_uploads/rJgRVZdkJg.png) 雖然透過改用指針,來解決,但我覺得指針指過去的位置還是有記憶體問題,都不會有洩漏問題,然後指針在更改跟刪除比較方便更快,注意這裡有提到當Go的NOTE If a key or a value is over 128 bytes, Go won’t store it directly in the map bucket. Instead, Go stores a pointer to reference the key or the value. 他就會直接用指針使用,節省空間,然後Go的空間是不能縮小的,除非你重新創一個複製進去 ## 29.Comparing values incorrtectly ![image](https://hackmd.io/_uploads/rJsRtb_kke.png) 以上是可以comparable 如果是不可比較,就只能用Reflection方式了,Go有DeepEqual,透過遍歷慢慢去對比元素是否相等,而且他是比較嚴格的,就像s1 = []int 和 s2 = make([]int){0} 會是false。 摘要: * 在閱讀現有程式碼時,請注意以 0 開頭的整數字面量是八進位數字。為了提高可讀性,請用 0o 前綴來明確表示八進位整數。 * 由於 Go 中的整數溢位和下溢是靜默處理的,您可以實作自己的函數來捕捉這些情況。 * 進行浮點數比較時,應在給定的誤差範圍內進行,這樣可以確保程式的可移植性。 * 當進行加法或減法時,將相似數量級的運算群組在一起以提高精確度。此外,應先進行乘法和除法,再進行加法和減法。 * 了解 slice 的長度和容量的差異是 Go 開發者的核心知識。slice 的長度是指可用元素的數量,而容量是指底層陣列中可容納的元素數量。 * 當建立 slice 時,若已知其長度,應先初始化其長度或容量。這能減少配置次數並提升效能。相同的邏輯也適用於 map,應初始化其大小。 * 使用 copy 或完整的 slice 表達式是防止不同函數使用相同底層陣列而導致 append 衝突的方法。然而,只有 slice 複製能避免記憶體洩漏,特別是在縮小大型 slice 時。 * 使用內建的 copy 函數來複製 slice 時,記得複製的元素數量是兩個 slice 長度中的最小值。 * 當處理指針 slice 或具有指針欄位的結構時,可以將被排除的元素設為 nil 來避免記憶體洩漏。 * 為避免常見混淆,例如在使用 encoding/json 或 reflect 套件時,需了解 nil 和空 slice 之間的差異。兩者皆為零長度、零容量的 slice,但只有 nil slice 不需要配置記憶體。 * 若要檢查 slice 是否不含任何元素,可以檢查其長度。此檢查無論 slice 是 nil 還是空都適用。map 也可以這樣檢查。 * 為了設計明確的 API,不應區分 nil 和空 slice。 * map 的記憶體只會增長不會縮小。因此,若此情況導致記憶體問題,您可以嘗試一些方法,例如強制 Go 重新建立 map 或使用指針。 * 若要在 Go 中比較類型,若兩個類型是可比較的,可以使用 == 和 != 運算子。布林值、數字、字串、指針、通道和全部由可比較類型組成的結構體都是可比較的類型。否則,您可以使用 reflect.DeepEqual,但需承擔反射的效能成本,或使用自定義的實作和庫。 第四章 ---- ## 30.ignoring the fact that element are copied in rage loops for range在使用要小心他是複製本,不是直接使用,如果你想改變導入的值記得用指數或者不複製直接用 ![1000002111](https://hackmd.io/_uploads/By3dmnu1yg.jpg) 以上是想改裡面值時候 ## 31.Ignoring how arguments are evaluated in range loops 這裡有個很重要的東西如果a:=[3]{1,2,3}這叫做值類型(數組類型),a:=[]{1,2,3}這是引用類型(切片類型),差別在於如果在for range上前者只有副本,你要改值還要再處理偏麻煩或者前者可用指針達到相同效果,[引用類型和值類型](https://blog.csdn.net/luduoyuan/article/details/135396996) ![image](https://hackmd.io/_uploads/r1dfTUoykl.png) 這裡提供兩種解決方法對值類型,但第二個不會導致複製超大數組。 ## 32.Ignoring the impackt of using pointer element in range loops ![image](https://hackmd.io/_uploads/B1bHdwokyg.png) 雖然map是引用類型,但是他的value 是Struct值類型,要直接修改還是要用指標,而key就不需要主要是他的不可變性 ![image](https://hackmd.io/_uploads/B11UKwoJ1x.png) 現在可以來討論這標題的真正pointer問題了 ![image](https://hackmd.io/_uploads/HJ8noDiyJl.png) 這裡可以明顯看到都只有ID3,這是為甚麼呢?因為Go的內存管理,在range使用的臨時變數地址是不變的,他只是把下一個切片值賦予臨時變數,所以你如果想要必須傳入每個切片位址。 ![image](https://hackmd.io/_uploads/Sk292wjJJe.png) ![image](https://hackmd.io/_uploads/r1mw3vokkx.png) ## 33. Making wrong assumptions map iterations 這裡必須先知道Go的map 是無任何排序的,並且也沒防止錯序 ![image](https://hackmd.io/_uploads/SJi6D_oy1g.png) 這看起來會印出aczdey 實際上不會,他是acdeyz很像是字母大小?其實沒有他只是剛剛好。 那為甚麼Go要這樣搞呢參考GPT的回答 ![image](https://hackmd.io/_uploads/r1HAOuoJye.png) ![image](https://hackmd.io/_uploads/SkJQj_o11g.png) 如果像這樣range 一個map要注意,他不是像slice在range時候就會看它的長度決定range次數,而是有動態性的,可能因為妳插入一個新的map然後哈希表改變,所以阿你如果只想原本長度最好弄一個副本出來確保不會隨機。 ![image](https://hackmd.io/_uploads/rJAKiusyyg.png) 總結來說,當我們在使用 map 時,不應該依賴以下幾點: * 數據按鍵排序:map 中的數據不會按鍵的大小或其他順序自動排序。 * 插入順序的保留:插入 map 的元素順序不會被保留,這意味著迭代時的順序與插入的順序可能不同。 * 確定性的迭代順序:每次迭代 map 時,元素的迭代順序都是不可預測的,可能會隨著插入或刪除而變動。 * 在同一次迭代中生成的元素:不應假設在迭代中添加的元素會在同一次迭代中被生成或訪問。 ## 34.Ignoring how the breaj statement works ![image](https://hackmd.io/_uploads/BkNtvYjk1l.png) 看起來是跳出迴圈,其實在Go中指中斷了2,他根本沒跳出迴圈,這裡要記住Switch、Select、for使用break都是解決方法其實也很簡單使用label ![image](https://hackmd.io/_uploads/HkQluFi1yl.png) ![image](https://hackmd.io/_uploads/rkVXuFjJyg.png) 標籤跟goto有啥不同,標籤可以明確標示出自己目的。 ![image](https://hackmd.io/_uploads/HyNXtKs1kl.png) select打斷範例 有Break label ,當然也有continue label接去另一個lable,這樣其實蠻像組譯語言 ## 35.Using defer inside a loop ![image](https://hackmd.io/_uploads/ryCLe9jyJl.png) 如果這個函數都沒return 這file就都沒被關閉,這樣可能導致洩漏 ![image](https://hackmd.io/_uploads/H1numqiyJl.png) 可以使用這方式有點像有始有終,有使用就要關閉 ![image](https://hackmd.io/_uploads/HknzSciyyg.png) 當然也可以做成閉包 摘要: * 在範圍循環中,值元素是複製的。因此,若要改變一個結構體,可以透過其索引或使用傳統的 for 循環(除非要修改的元素或字段是指針)。 * 了解範圍運算符所傳遞的表達式僅在循環開始之前評估一次,能幫助您避免在通道或切片迭代中常見的低效賦值錯誤。 * 使用局部變數或透過索引訪問元素,可以防止在循環中錯誤地複製指針。 * 為了確保使用地圖時輸出可預測,請記住地圖數據結構: 不按鍵排序數據 不保留插入順序 沒有確定的迭代順序 不保證在迭代期間添加的元素會在該次迭代中生成 * 使用帶標籤的 break 或 continue 可以強制退出特定語句。在循環中,這對 switch 或 select 語句特別有用。 * 將循環邏輯提取到函數中會導致在每次迭代結束時執行 defer 語句。 第五章 ---- * Understanding the fundamental concept of the * rune in Go * Preventing common mistakes with string iteration and trimming * Avoiding inefficient code due to string * concatenations or useless conversions * Avoiding memory leaks with substrings 在Go中 字串是一個不可變的資料結構他有指向不可變的位元組序列指針,和序列中位元組的總數 ![image](https://hackmd.io/_uploads/B1r3n5iJ1l.png) ## 36.Not understanding the concept of a rune 字符集*(charset) 跟 編碼(encoding),charset 是一個字符的集合,包含了特定環境的所有字符,例如Unicode字符就有2^21個字符,幾乎所有國際字符了,encoding則是將charset中的每個字符轉乘二進字的一種方法,例如UTF-8用1~4個byte對unicode進行編碼,Go的rune是一個Unicode 的別名 type rune = int32 ,這表示rune類型的變亮可以表示任何unicode的值,並使用32位的字節表示,要注意在Go中不一定是UTF-8的,本質上是任意字節序列,基本上非UTF-8近來也會保持不會自動幫你轉。 一般來說字母是1BYTE漢字是3BYTE,Len(字串)返回是byte數不是字數 ## 37.Inaccurate string iteration ![image](https://hackmd.io/_uploads/rJvsHpo1Jg.png) 這就是我們上個錯誤提到的,因為len是看byte不是字數所以是6,不是5,除非你這樣fmt.Println(utf8.RuneCountInString(s)) // 才能是5。 ![image](https://hackmd.io/_uploads/S10tITikkl.png) 或者這種解法不是導入s[i] 而是直接返回r,另一種方式就是用[]rune(s)將s字串轉成符文片段,但這種方式經過rune轉換是要O(n)的時間複雜度的,如果有要反覆運算還是推薦上一種方法 ![image](https://hackmd.io/_uploads/r1g4vTjyye.png) 如果s不是a~z 或正常組成,可以透過rune[s[x]] 來輸出 例如"你好"的 好可以rune[s[1]] ## 38.Misusing trim functions TrimRight 和 TrimSuffix 這兩個函數 ![image](https://hackmd.io/_uploads/ry54menyke.png) TrimRight的使用他會重複操作刪掉集合的東西就像xo 被刪掉又刪了一個x ![image](https://hackmd.io/_uploads/BkrQNghykg.png) fmt.Println(strings.TrimSuffix("123oxo", "xo")) TrimSuffix則是 扣掉後面的字串變成"123o",這種平常應該比較常用 ![image](https://hackmd.io/_uploads/Skms1W3JJx.png) ![image](https://hackmd.io/_uploads/HkYBebnJ1e.png) ## 39.Under-optimized string concatenation 字的連結 ![image](https://hackmd.io/_uploads/r1inebnkyl.png) 這concat 乍看之下沒怎樣,但別忘了String的不變性,每一次做+=都是對字串行進重新分配新字串,這樣可是會影響效能了 ![image](https://hackmd.io/_uploads/B1n7kz3kyl.png) 這邊有這方法可以解決 Strings.Builder{} 這方法是建立一個內部緩存這樣在寫入時候才不用一直複製字串,只是寫入要配合 變數.Writexxx(寫入東西),WriteString為啥返回值都是_ _ 第一個是反回字數 第二個是錯誤,但這方法幾乎根本不會錯誤畢竟他只是寫入內存而已,但為了符合Go的介面設計才故意用的 ![image](https://hackmd.io/_uploads/ByRV-fnJkg.png) Strings.builder內部運作機制,是保存一個byte切片,當呼叫writestring時候就會把這東西加進去切片中,這實際上是用了append(),由於append()是會改變切片長度跟容量的,所以這不能再goroutine中使用,不然會有競跑 ![image](https://hackmd.io/_uploads/Hkc8ffnyke.png) 如果能知道切片要多大,最好是使用Grow(長度)事先分配好空間 ![image](https://hackmd.io/_uploads/SJunGzh1ke.png) 這邊一比較就知道 += 效率多差 ,要是能預分配那效能差更多了,但其實也是只有在迴圈上使用比較快而已,平常只需要名子之類的直接+=或者fmt.sprintf就好了 ## 40.Useless string conversions 很多程式設計師都喜歡用字串,但大多數的I/O實際上是使用[]BYTE,這樣還要進行轉換 ![image](https://hackmd.io/_uploads/rkFZ9ohyyg.png) ![image](https://hackmd.io/_uploads/rkxG9i31Jg.png) ![image](https://hackmd.io/_uploads/BkuG9j2yke.png) 這裡GetBytes 是傳[]byte給sanitize 做字串消除空白,但是這函數又只能用string格式,所以又要先轉string這過程又多一筆創造空間的消耗,這樣導致性能和記憶體的浪費 ![image](https://hackmd.io/_uploads/SkQ0osh1ye.png) 上面可以證明string的immutability,這邊重點是確認能不能直接用[]byte就好了,可以省下轉換消耗。 ## 41.Substring and memory leaks in mistake 之前在#26的錯誤有看到切片洩漏記憶體的問題,這在字串其實也有 ![image](https://hackmd.io/_uploads/Sy42-g6J1l.png) 這裡也有之前提到的錯誤,就是切片是取byte所以如果是特殊e 這樣取出來不會hello,所以字串要切記得使用[]rune()再切 ![image](https://hackmd.io/_uploads/S1RZml61kg.png) 這裡是GPT老哥的見解,大致上跟書上差不多,都是再說字串洩漏問題,當你從一個巨大字串裡擷取一小段會導致洩漏,因為Go是用指針共享記憶體的,所以你要解決都必須獨力複製一份出來才是最理想。 ![image](https://hackmd.io/_uploads/r1UDwlp1kg.png) 如果只是1byte其實也可以考慮接這樣。 在Go 1.18開始多了一個東西strings.clone 可以做到我們上面說的轉換簡單直觀,感覺記這個比較實際。 章節總結: * 瞭解 rune 對應的是 Unicode 代碼點的概念,並且它可以由多個字節組成,應該是 Go 開發者在處理字串時的核心知識,以準確操作字串。 * 使用 range 遍歷字串時,會遍歷每個 rune,而索引則對應於該 rune 的字節序列的起始位置。若要存取特定 rune 的索引(例如第三個 rune),需要將字串轉換為 []rune。 * strings.TrimRight 和 strings.TrimLeft 移除字串中所有尾部或首部包含在給定集合中的 rune,而 strings.TrimSuffix 和 strings.TrimPrefix 則返回去除指定後綴或前綴的字串。 * 字串串接應該使用 strings.Builder,以避免每次迭代時都分配新的字串。 * 記住 bytes 包提供了與 strings 包相同的操作,這可以避免額外的字節/字串轉換。 * 使用拷貝而不是子字串來避免記憶體洩漏,因為子字串操作返回的字串將會與原來的字節陣列共用內存。 ----- [第六章之後點這](https://hackmd.io/@Xb1nH7gpQi-pztNr0Nalsw/BkFJolT1yx)