Go1.20 將禁止匿名介面迴圈匯入!這是一次打破 Go1 相容性承諾的真實案例。。。

煎魚發表於2022-12-15

大家好,我是煎魚。

最近因為臨近新版本釋出節點,我在看 Go1.20 的新特性《spec: disallow anonymous interface cycles》,發現了一個比較騷的操作...以前我都沒想到可以這麼用,還有點意思,分享給大家。

在 Go 規範中是允許將介面型別(interface{})內嵌到其他宣告的介面當中的,也就是著名的套娃神器:組合。

套娃介面型別

Go 標準庫中比較經典的例子如下:

type ReadCloser interface {
    Reader
    Closer
}

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

實際上展開是:

type ReadCloser interface {
    interface {
        Read(p []byte) (n int, err error)
  }
    interface {
        Close() error
    }
}

一切都看起來如此美好,似乎很好的體現了 Go 的優秀之處。

計劃是趕不上變化的。

匿名介面迴圈匯入

在現實程式碼中,這種支援就存在著迴圈引用的用法。如下簡單例子:

type I interface {
    m()
    interface {
        I
  }
}

這段程式碼,宣告瞭介面型別 I,然後又包含了 m(),又包含介面 I。這會是一個 “永動機”,永遠都不會停止。在開源的 GitHub 中,也真實存在著。

如專案 gozelus/zelus_rest 的程式碼:

type MySQLDb interface {
    execSQL
    Table(ctx context.Context, name string) interface {
        whereSQL
        insertSQL
        selectSQL
        findSQL
        orderSQL
        clausesSQL
    }
    Begin() interface {
        MySQLDb
        Rollback()
        Commit()
    }
}

如專案 vetcher/go-astra 的程式碼:

type ComplexInterface interface {
    A(a interface {
        B()
        ComplexInterface
    }) interface {
        C()
        D()
    }
}

這類寫法其實非常迷惑人,這意味可以無限巢狀介面,並使用內在的方法。但作者在寫這個程式碼時,可能目的並不是如此,導致被使用者錯用。

這有沒有問題

對外宣傳簡潔好用瀑布式程式設計的 Go,如此對匿名介面迴圈匯入的支援,是否合規呢?

其實並不然。

早在 2016 年的 Proposal: Type Aliases 中的 Type cycles 部分就對此有所定義:

這之中明確指出:型別別名必須能夠 "擴充套件出來",但沒有辦法 "擴充套件" 出像 T = *T 這樣的別名

套用到現在的問題來,如果上面的 T 就是 I(介面型別),那麼同理可得 I = *I,這個過程是永遠無法終止的。

社群討論

在一番激烈討論後,基於以下幾點,決定接納該提案,也就是在新版本中禁用 Go 匿名介面的迴圈匯入,將其改為有限地擴充套件所有的嵌入式介面。

在禁用後,以下三種類似寫法都會被拒絕。

第一種:

type B interface { I }
type I interface { m() interface { B } }

第二種:

type B = interface{ I }
type I interface{ m() interface{ B } }

第三種:

type B = interface{ I }
type I interface{ m() B }

Go1 相容性承諾

最核心的是 Go1 相容性承諾。從任何角度上來講,禁用這個特性是破壞性變更(無法向後相容),絕對是違反相容性承諾的

大家認為在公共專案庫中,基本沒有人使用這種匿名介面迴圈匯入的方式,用途很少(幾乎為 0)除了上面提到的 gozelus/zelus_rest 專案,並且該模組似乎沒什麼人引用。

rsc 在綜合了利弊後,認為把這個特性幹掉,能更好的提高程式碼簡潔性,確立了該特性的禁用,會和以往一樣的推進節奏。

如下:

  • Go1.20:Go 編譯器預設會拒絕這些介面迴圈,但可以使用 go build -gcflags=all=-d=interfacecycles 來進行構建,以確保舊程式碼的正常編譯。如果在候選釋出期間有人向 Go 團隊報告大量損壞,將會取消此更改。
  • Go1.22:等到 1.22 版本後 -d=interfacecycles 標誌將被刪除,舊程式碼將不再構建該特性。如果有人報告問題,將可以討論或是推遲刪除,給予更多的改造時間。

鏈式呼叫模式

有一種經典的設計模式叫:鏈式呼叫,也有叫方法鏈的。例如在 etcd sdk 中,常常會在 Watch、Next 這類相關介面中見到。

在 Go 中可以這麼寫:

type Nexter interface { 
    Next(Input) (interface { Nexter }, error)
    Done() Output
}

一旦禁用後,就不能如此匿名巢狀了。

會強烈推薦使用如下方式:

type Nexter interface { 
    Next(Input) (Nexter, error)
    Done() Output
}

包括在 Node 這類節點宣告時,也推薦如此:

type Node interface {
    Parent() Node
    FirstChild() Node
    Children() []Node
}

套娃也得套上名字,不能成為 “無名” 者。

總結

原先支援匿名介面的迴圈匯入,本質上違背了 Go 一貫的簡潔明瞭的設計理念。如果在 Go 工程中用的多,不注意就會產生次生影響,禁了也有好處。

目前該特性變更的程式碼已經提交。如果按照 rsc 的計劃我們會在 Go1.20 或 Go1.21 看到這個新特性,Go1.22 或 Go1.24 將會正式移除。

值得關注的一點,Go 團隊為此,應該是首次打破了對 Go1 相容性的承諾,做出了破壞性變更,在推進方式上採取的是漸進式的模式。

這仍然值得我們關注,畢竟...破窗效應?

文章持續更新,可以微信搜【腦子進煎魚了】閱讀,本文 GitHub github.com/eddycjy/blog 已收錄,學習 Go 語言可以看 Go 學習地圖和路線,歡迎 Star 催更。

Go 圖書系列

推薦閱讀

相關文章