🎯 什麼情境該想到我

當你「有一筆被許多地方直接讀寫的可變資料(例如全域變數、共享狀態)」的時候——用取值/設值函式把它包起來,讓所有存取都經過同一個入口,之後要加驗證、記錄,或改變它的內部結構都只改一處。

⚙️ 怎麼用(步驟 / 公式)

意圖:用封裝函式(取值/設值)包住可變資料,集中控管對它的存取與修改。

動作要點(小步安全進行):

  1. 建立取值(get)與設值(set)函式來讀寫這筆資料。
  2. 逐一把每個直接存取處改成呼叫這些封裝函式,每次都測試。
  3. 全部改完後,限制這筆資料的可見性(例如設為私有),確保外界只能透過封裝函式存取。
  4. 若連值本身也要保護不被外部竄改,可在取值時回傳副本、或封裝該筆記錄(見以物件取代基本型別、封裝記錄)。

★ 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。

🔗 相關工具