1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 7264字,需瀏覽 15分鐘

         ·

        2024-07-01 19:51

        寫在正文之前

        閱讀本文之前我們來(lái)先熟悉以下的代碼原則,如果你已經(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)所替代,而無(wú)需更改你的代碼。

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

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

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

        too many interfaces

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

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

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

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

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

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

        too many methods

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

        The bigger the interface the weaker the abstraction,接口越大,抽象越弱,這實(shí)際上是 Go 的諺語(yǔ)之一。正如 Rob Pike 所說(shuō),這是接口最重要的一點(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)語(yǔ)言中,諸如 User(用戶)、Request(請(qǐng)求)等名詞性接口非常常見(jiàn)。而在 Go 語(yǔ)言中,大多數(shù)接口都有 er 后綴:Reader、Writer、Closer 等。這是因?yàn)?,?Go 中,接口暴露了行為,而它們的名稱則指向該行為。

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

        Effective Go[1] 中也有提到過(guò):

        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 "的接口,問(wèn)問(wèn)自己為什么這個(gè) "Thing "不是一個(gè)結(jié)構(gòu)體 ??。

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

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

        但是,也許客戶并不想使用生產(chǎn)者接口中的所有方法。請(qǐng)記住 "接口隔離原則 "中的一句話:"不應(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)??蛻舳舜a不再需要導(dǎo)入某個(gè)接口并編寫實(shí)現(xiàn),因?yàn)?Go 中沒(méi)有這樣的關(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)榻涌诓还_(kāi)而無(wú)法使用,這是很惱人的。這種限制可能是有原因的,但并非總是如此。下面是一個(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
        }

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

        shape := NewCircle(5)

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

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

        // 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

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

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

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

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

        比方說(shuō),你有一個(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),你的代碼看起來(lái)就像這樣:

        package users

        type User struct {
            Name  string
            Email string
        }

        在這種情況下,您的代碼仍然可以編譯和運(yùn)行,但輸出結(jié)果將是 {John Doe [email protected]}。沒(méi)有任何反饋執(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)

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

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

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

        凡事沒(méi)有絕對(duì),我們?cè)趯懘a時(shí)還是要具體情況具體分析,本文只是分享一些通識(shí),歡迎大家廣開(kāi)討論。

        參考資料
        [1]

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


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

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产精品免费观看 | 少妇一级淫片免费放正片 | 91成人 在线观看学生 | 在线免费视频一区二区 | 欧洲成人毛片 | 91精品国产色综合久久不卞臂 | 国产三男一女免费视频观看 | 成人区无码高潮AV在现观看 | 黄片日韩 | 国产又黄又爽日韩精品 |