🎯 什麼情境該想到我
當你「有一筆被許多地方直接讀寫的可變資料(例如全域變數、共享狀態)」的時候——用取值/設值函式把它包起來,讓所有存取都經過同一個入口,之後要加驗證、記錄,或改變它的內部結構都只改一處。
⚙️ 怎麼用(步驟 / 公式)
意圖:用封裝函式(取值/設值)包住可變資料,集中控管對它的存取與修改。
動作要點(小步安全進行):
- 建立取值(get)與設值(set)函式來讀寫這筆資料。
- 逐一把每個直接存取處改成呼叫這些封裝函式,每次都測試。
- 全部改完後,限制這筆資料的可見性(例如設為私有),確保外界只能透過封裝函式存取。
- 若連值本身也要保護不被外部竄改,可在取值時回傳副本、或封裝該筆記錄(見以物件取代基本型別、封裝記錄)。
★ before / after(Go)
before——資料被到處直接讀寫,沒有任何地方能放檢查或鎖:
package cfg
var DefaultTimeout = 30 * time.Second // ★ 任何人可以在任何時間改它cfg.DefaultTimeout = -1 // ★ 沒人阻止你;而且如果是併發改,就是資料競爭after:
package cfg
var (
mu sync.RWMutex
defaultTimeout = 30 * time.Second // ★ 小寫 = 套件外看不到
)
func DefaultTimeout() time.Duration { // ★ Go 慣例:getter 不叫 GetXxx
mu.RLock(); defer mu.RUnlock()
return defaultTimeout
}
func SetDefaultTimeout(d time.Duration) error {
if d <= 0 { return errors.New("timeout must be positive") } // ★ 現在有地方放檢查了
mu.Lock(); defer mu.Unlock()
defaultTimeout = d
return nil
}★ 真正的收益不是「藏起來」,是「有一個地方可以放東西」——驗證、鎖、日誌、
快取失效、feature flag、deprecation 警告。封裝之前,這些無處可放。
Go 的三個慣例(和 Java 不一樣)
| Java/C# | ★ Go |
|---|---|
getName() / setName() | Name() / SetName() —— getter 不加 Get 前綴 |
| 每個欄位配一組 getter/setter | 只在需要時才包。同套件內直接存取小寫欄位是正常的 |
private 逐欄位 | 封裝的邊界是 package,不是 struct —— 同套件內看得到全部小寫欄位 |
→ 見 工具-Go命名慣例。
★ 封裝的深淺:三層
type Order struct{ items []Item }
// ① 淺封裝:欄位變小寫,但 getter 回傳內部切片 —— ★ 呼叫端照樣能改
func (o *Order) Items() []Item { return o.items }
o.Items()[0].Price = 0 // ★ 生效了!封裝是假的
// ② 回傳副本
func (o *Order) Items() []Item { return slices.Clone(o.items) } // ★ 每次呼叫一次配置
// ③ 只給迭代器(Go 1.23+)—— 不洩漏切片,也不複製
func (o *Order) Items() iter.Seq[Item] { return slices.Values(o.items) }→ 完整討論見 封裝集合。
⚠️ Go 特有:struct 複製不等於封裝
type Counter struct {
mu sync.Mutex
n int
}
func (c Counter) Value() int { return c.n } // ★ 值接收者 —— 每次呼叫都複製了 mutex實測 Go 1.26.4(2026-09-25),go vet 會報:
c.go:13:9: Value passes lock by value: inltest.Counter contains sync.Mutex
⚠️ 但 go test 只跑 vet 的一個子集,不含 copylocks —— CI 裡要單獨跑 go vet ./... 才抓得到。
→ 含 sync.Mutex 的型別,方法一律用指標接收者。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
若這筆資料是不可變的(immutable),就沒有封裝存取的必要,不需硬包。
-
使用範圍極小、只在單一函式內的臨時資料,封裝反而多此一舉。
-
★ 回傳內部 slice / map / 指標 = 封裝失敗。編譯器不會提醒你(見上的三層)。
-
★ 全域可變狀態就算封裝了也還是全域可變狀態。封裝只是讓它可控;真正的解法通常是把它變成依賴注入的欄位(見 全域資料)。
-
不要為每個欄位機械式地產生 getter/setter。一個只有 getter/setter 的型別就是 純資料類別——換了個壞味道而已。
-
同套件內不必封裝。Go 的可見性邊界是 package;在 package 內部直接用小寫欄位,比繞一層方法更清楚。
-
加鎖不等於執行緒安全。
Get後判斷再Set的呼叫端,中間仍有競爭窗口——這種要提供CompareAndSwap式的單一原子操作,而不是分開的 getter/setter。
🔗 相關工具
- 封裝記錄 Encapsulate Record(把裸資料記錄升級為類別以控管欄位存取)
- 封裝集合 Encapsulate Collection(集合欄位的封裝特例)
- 以物件取代基本型別 Replace Primitive with Object(把被封裝的值進一步升級成有行為的物件)
- 重構
- 封裝集合 —— 欄位是 slice/map 時的專門處理
- 封裝記錄 —— 整個資料結構的封裝
- 全域資料 —— 最需要封裝的壞味道
- 純資料類別 —— 只有 getter/setter 的反效果
- 工具-Go命名慣例 —— getter 不加 Get 前綴
- 封裝全域引用 —— 遺留碼裡先封裝再打破依賴的版本