<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go 使用 interface 時(shí)的 7 個(gè)常見錯(cuò)誤

          共 7264字,需瀏覽 15分鐘

           ·

          2024-07-01 19:51

          寫在正文之前

          閱讀本文之前我們來先熟悉以下的代碼原則,如果你已經(jīng)很熟悉這些內(nèi)容,可以直接跳到正文。

          • 接口隔離原則:絕不能強(qiáng)迫客戶實(shí)現(xiàn)其不使用的接口,也不能強(qiáng)迫客戶依賴其不使用的方法。
          • 多態(tài)性:代碼變化會(huì)根據(jù)接收到的具體數(shù)據(jù)改變其行為。
          • 里氏替換原則:如果你的代碼依賴于一個(gè)抽象概念,那么一個(gè)實(shí)現(xiàn)可以被另一個(gè)實(shí)現(xiàn)所替代,而無需更改你的代碼。

          抽象的目的不是為了含糊不清,而是為了創(chuàng)造一個(gè)新的語義層次,在這個(gè)層次上,我們可以做到絕對精確。- E.W.Dijkstra

          有機(jī)代碼是根據(jù)您在某一時(shí)刻所需的行為而增長的代碼。它不會(huì)強(qiáng)迫你提前考慮類型以及它們之間的關(guān)系,因?yàn)槟愫芸赡軣o法正確地處理它們。這就是為什么說 Go 更傾向于組合而非繼承。與預(yù)先定義由其他類型繼承的類型并希望它們適合問題領(lǐng)域的做法相比,你有一小套行為,可以從中組合出任何你想要的東西。

          理論講得夠多了,讓我們開始正文,看下使用 interface 的時(shí)候最常犯的錯(cuò)誤:

          too many interfaces

          擁有過多接口的術(shù)語叫做接口污染。當(dāng)你在編寫具體類型之前就開始抽象時(shí),就會(huì)出現(xiàn)這種情況。由于無法預(yù)知需要哪些抽象,因此很容易編寫出過多的接口,而這些接口在日后要么是錯(cuò)誤的,要么是無用的。

          Rob Pike 有一個(gè)很好建議,可以幫助我們避免接口污染:

          Don’t design with interfaces, discover them. Rob Pike

          Rob 在這里指出的是,你不需要提前考慮你需要什么樣的抽象。您可以從具體的結(jié)構(gòu)開始設(shè)計(jì),只有在設(shè)計(jì)需要時(shí)才創(chuàng)建接口。這樣,你的代碼就會(huì)按照預(yù)期的設(shè)計(jì)有機(jī)地發(fā)展。

          接口是有代價(jià)的:它是一個(gè)新的概念,你在推理代碼時(shí)需要記住它。正如 Djikstra 所說,理想的接口必須是 "一個(gè)新的語義層次,在這個(gè)層次上,人們可以絕對精確"。

          因此,在創(chuàng)建接口之前,先問問自己:你需要這么多接口嗎?

          too many methods

          在 PHP 項(xiàng)目中,10 個(gè)方法的接口是很常見的。在 Go 中,接口的數(shù)量很少,標(biāo)準(zhǔn)庫中所有接口的平均方法數(shù)量為 2 個(gè)。

          The bigger the interface the weaker the abstraction,接口越大,抽象越弱,這實(shí)際上是 Go 的諺語之一。正如 Rob Pike 所說,這是接口最重要的一點(diǎn),這意味著接口越小越有用。

          接口的實(shí)現(xiàn)越多,通用性就越強(qiáng)。如果一個(gè)接口有一大堆方法,就很難有多個(gè)實(shí)現(xiàn)。方法越多,接口就越具體。接口越具體,不同類型顯示相同行為的可能性就越低。

          io.Reader 和 io.Writer 就是有用接口的一個(gè)很好的例子,它們有數(shù)以百計(jì)的實(shí)現(xiàn)。或者是 error 接口,它非常強(qiáng)大,可以在 Go 中實(shí)現(xiàn)整個(gè)錯(cuò)誤處理。

          我們可以用其他接口組成一個(gè)接口。例如,這里的 ReadWriteCloser 由 3 個(gè)較小的接口組成:

          type ReadWriteCloser interface {
           Reader
           Writer
           Closer
          }

          非行為驅(qū)動(dòng)的接口

          在傳統(tǒng)語言中,諸如 User(用戶)、Request(請求)等名詞性接口非常常見。而在 Go 語言中,大多數(shù)接口都有 er 后綴:Reader、Writer、Closer 等。這是因?yàn)椋?Go 中,接口暴露了行為,而它們的名稱則指向該行為。

          在 Go 中定義接口時(shí),你定義的不是 "某物是什么",而是 "某物提供了什么"--是 "行為",而不是 "事物"!這就是為什么 Go 中沒有 File 接口,但有 Reader 和 Writer:這些都是行為,而 File 是實(shí)現(xiàn) Reader 和 Writer 的事物。

          Effective Go[1] 中也有提到過:

          Interfaces in Go provide a way to specify the behavior of an object: if something can do this, then it can be used here.

          在編寫接口時(shí),盡量考慮動(dòng)作或行為。如果你定義了一個(gè)名為 "Thing "的接口,問問自己為什么這個(gè) "Thing "不是一個(gè)結(jié)構(gòu)體 ??。

          producer 端實(shí)現(xiàn)接口

          經(jīng)常在 code review 中看到這種情況:人們在寫具體實(shí)現(xiàn)的同一個(gè)包中定義接口:

          但是,也許客戶并不想使用生產(chǎn)者接口中的所有方法。請記住 "接口隔離原則 "中的一句話:"不應(yīng)強(qiáng)迫客戶端實(shí)現(xiàn)其不使用的方法"。下面是一個(gè)例子:

          package main

          // ====== producer side

          // This interface is not needed
          type UsersRepository interface {
              GetAllUsers()
              GetUser(id string)
          }

          type UserRepository struct {
          }

          func (UserRepository) GetAllUsers()      {}
          func (UserRepository) GetUser(id string) {}

          // ====== client side

          // Client only needs GetUser and
          // can create this interface implicitly implemented
          // by concrete UserRepository on his side 
          type UserGetter interface {
              GetUser(id string)
          }

          如果客戶想使用生產(chǎn)者的所有方法,可以使用具體的結(jié)構(gòu)體。結(jié)構(gòu)體方法已經(jīng)提供了這些行為。

          即使客戶想要解耦代碼并使用多種實(shí)現(xiàn)方法,他仍然可以在自己這邊創(chuàng)建一個(gè)包含所有方法的接口:

          由于 Go 中的接口是隱式實(shí)現(xiàn)的,所以可以這樣實(shí)現(xiàn)。客戶端代碼不再需要導(dǎo)入某個(gè)接口并編寫實(shí)現(xiàn),因?yàn)?Go 中沒有這樣的關(guān)鍵字。如果實(shí)現(xiàn)(Implementation)與接口(Interface)有相同的方法,那么實(shí)現(xiàn)(Implementation)就已經(jīng)滿足了該接口,可以在客戶代碼中使用。

          返回接口

          如果一個(gè)方法返回的是接口而不是具體的結(jié)構(gòu),那么所有調(diào)用該方法的客戶端都會(huì)被迫使用相同的抽象。你需要讓客戶決定他們需要什么樣的抽象。

          當(dāng)你想使用結(jié)構(gòu)體中的某項(xiàng)功能時(shí),卻因?yàn)榻涌诓还_而無法使用,這是很惱人的。這種限制可能是有原因的,但并非總是如此。下面是一個(gè)人為的例子:

          package main

          import "math"

          type Shape interface {
              Area() float64
              Perimeter() float64
          }

          type Circle struct {
              Radius float64
          }

          func (c Circle) Area() float64 {
              return math.Pi * c.Radius * c.Radius
          }

          func (c Circle) Perimeter() float64 {
              return 2 * math.Pi * c.Radius
          }

          // NewCircle returns an interface instead of struct
          func NewCircle(radius float64) Shape {
              return Circle{Radius: radius}
          }

          func main() {
              circle := NewCircle(5)

              // we lose access to circle.Radius
          }

          在上面的示例中,我們不僅無法訪問 circle.Radius,而且每次要訪問它時(shí)都需要在代碼中添加類型斷言:

          shape := NewCircle(5)

          if circle, ok := shape.(Circle); ok {
              fmt.Println(circle.Radius)
          }

          Dave Cheney 寫的 Practical Go 一書中的一個(gè)例子很有說服力:

          // Save writes the contents of doc to the file f.
          func Save(f *os.File, doc *Document) error

          可以改進(jìn)為:

          // Save writes the contents of doc to the supplied
          // Writer.
          func Save(w io.Writer, doc *Document) error

          粹為測試而創(chuàng)建接口

          接口污染的另一個(gè)原因是:僅僅因?yàn)橄肽M一個(gè)實(shí)現(xiàn),就創(chuàng)建一個(gè)只有一個(gè)實(shí)現(xiàn)的接口。

          如果通過創(chuàng)建許多模擬來濫用接口,最終測試的將是生產(chǎn)中從未使用過的模擬,而不是應(yīng)用程序的實(shí)際邏輯。在您的實(shí)際代碼中,您現(xiàn)在有兩個(gè)概念(如 Djikstra 所說的語義層),而一個(gè)概念就可以了。而這只是為了測試你想要測試的東西。難道你想在每次創(chuàng)建新測試時(shí)都將語義級別加倍嗎?可以使用 testcontainers 來代替模擬數(shù)據(jù)庫。如果 testcontainers 不支持,也可以使用自己的容器。

          沒有驗(yàn)證接口的兼容性

          比方說,你有一個(gè)導(dǎo)出名為 User 的類型的軟件包,你實(shí)現(xiàn)了 Stringer 接口,因?yàn)槌鲇谀撤N原因,當(dāng)你打印時(shí),你不希望顯示電子郵件:

          package users

          type User struct {
              Name  string
              Email string
          }

          func (u User) String() string {
              return u.Name
          }

          客戶端的代碼如下:

          package main

          import (
              "fmt"

              "pkg/users"
          )

          func main() {
              u := users.User{
                 Name:  "John Doe",
                 Email: "[email protected]",
              }
              fmt.Printf("%s", u)
          }

          現(xiàn)在,假設(shè)你進(jìn)行了重構(gòu),不小心刪除或注釋了 String() 的實(shí)現(xiàn),你的代碼看起來就像這樣:

          package users

          type User struct {
              Name  string
              Email string
          }

          在這種情況下,您的代碼仍然可以編譯和運(yùn)行,但輸出結(jié)果將是 {John Doe [email protected]}。沒有任何反饋執(zhí)行你之前的意圖。當(dāng)你的方法接受 User 時(shí),編譯器會(huì)幫助你,但在上述情況下,編譯器不會(huì)幫助你。

          要強(qiáng)制執(zhí)行某個(gè)類型實(shí)現(xiàn)了某個(gè)接口,我們可以這樣做:

          package users

          import "fmt"

          type User struct {
              Name  string
              Email string
          }

          var _ fmt.Stringer = User{} // User implements the fmt.Stringer

          func (u User) String() string {
              return u.Name
          }

          現(xiàn)在,如果我們刪除 String() 方法,就會(huì)在構(gòu)建時(shí)得到如下結(jié)果:

          cannot use User{} (value of type User) as fmt.Stringer value in variable declaration: User does not implement fmt.Stringer (missing method String)

          在該行中,我們試圖將一個(gè)空的 User{} 賦值給一個(gè) fmt.Stringer 類型的變量。由于 User{} 不再實(shí)現(xiàn) fmt.Stringer,我們收到了投訴。我們在變量名中使用了 _,因?yàn)槲覀儾]有真正使用它,所以不會(huì)進(jìn)行分配。

          上面我們看到用戶實(shí)現(xiàn)了界面。User 和 *User 是不同的類型。因此,如果你想讓 *User 實(shí)現(xiàn)它,你可以這樣做:

          var _ fmt.Stringer = (*User)(nil) // *User implements the fmt.Stringer

          凡事沒有絕對,我們在寫代碼時(shí)還是要具體情況具體分析,本文只是分享一些通識,歡迎大家廣開討論。

          參考資料
          [1]

          Effective Go: https://go.dev/doc/effective_go


          瀏覽 156
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲第一狼人综合 | 青青影视99 | 特黄av毛片 | 成人黄色性爱视频 | 激情五月综合网 |