# interface 與組合

> Go 的 interface 隱式滿足，型別有方法就算數，小介面加 embedding 取代繼承，對介面寫程式配 adapter 就能抽換實作，gortex 換 OTel tracing 就是例子。

原文：https://sheng.page/posts/go-interfaces-and-composition/ · 發布：2025-05-25 · 作者：Sheng

Go 沒有 `implements` 關鍵字，介面是隱式滿足的。這讓「組合勝繼承」變得自然，也讓 gortex 能把 tracing 從 in-memory 換成 OTel 而不動業務碼。

## 小介面與隱式滿足

一個型別只要有介面要的方法，就自動滿足那個介面，不用宣告：

```go
type Stringer interface { String() string }

type Point struct{ X, Y int }
func (p Point) String() string { return fmt.Sprintf("(%d,%d)", p.X, p.Y) }
// Point 自動是 Stringer，沒寫任何 implements
```

慣例是介面要小。標準庫的 `io.Reader`、`io.Writer` 都只有一個方法，所以到處能組。兩條 Go 諺語講得很準：「介面越大，抽象越弱」、「接受介面，回傳具體型別」。函式參數收小介面，呼叫端能塞的東西就最多。

## 組合勝繼承：embedding

Go 沒有繼承，用 embedding（嵌入）做組合 + 方法提升：

```go
type Server struct {
    *log.Logger // 嵌入；Server 直接有 Logger 的 Printf 等方法
}

s := &Server{Logger: log.Default()}
s.Printf("up") // 來自嵌入的 Logger

type ReadWriter interface { // 介面也能嵌
    io.Reader
    io.Writer
}
```

這不是 subclass，只是把嵌入型別的方法集提升到外層。要覆蓋就在外層定義同名方法蓋掉，沒有 `super` 那套。

## 依賴反轉與 adapter

對小介面寫程式、而不是對具體型別，實作就能換。adapter 則是把外來型別包一層，拼出你要的介面。

gortex 的 tracing 就是現成範例：框架只依賴一個 `Tracer` 介面，預設掛 in-memory 的 `SimpleTracer`；要上 OpenTelemetry 就換一個 OTel adapter 實作同一組介面，業務碼一行都不用改（見〈[可觀測性：三態健康檢查與 tracing 抽象層](/posts/gortex-observability-health-tracing/)〉）。

## Effective Go 精選：Interfaces、Embedding

Effective Go 的 [Interfaces and methods](https://go.dev/doc/effective_go#interfaces_and_methods) 講介面就是一組方法、一個型別可同時滿足多個介面。[Embedding](https://go.dev/doc/effective_go#embedding) 講介面與 struct 的嵌入、方法如何提升、以及為什麼這不是繼承。兩節合起來，就是 Go 組織程式碼的主軸：用組合與小介面，而不是型別階層。


## 補充筆記

- 介面隱式滿足：型別有方法就算數，不用宣告 implements。
- 小介面最有用：「介面越大，抽象越弱」。
- embedding 是組合 + 方法提升，不是繼承；同名方法可覆蓋。
- 對介面寫程式 + adapter，讓實作可抽換；gortex 的 `Tracer` 就是這套（見可觀測性那篇）。
- 介面驅動設計貫穿整個框架，後面框架篇會一直看到。

延伸：本系列接著用這些觀念拆 gortex 框架，原始碼見 [yshengliao/gortex](https://github.com/yshengliao/gortex)。

---

> 想法與技術判斷出自 Sheng，和 Claude 一起起草 · 環境 Go 1.24（當時最新 release）· 本系列為事後回填整理 · 列入 20260613 blog 翻新計劃，新漆未乾。