🎯 什麼情境該想到我

當你「發現同一組資料項目老是成群結隊地出現在多個函式的參數清單裡」的時候——把它們組成一個物件一起傳。這不只讓參數變短,更揭示了這群資料本來就屬於同一個概念,還能把處理它們的行為搬進去。

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

意圖:把常一起出現的一組參數組成一個物件來傳遞。

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

  1. 若還沒有合適的結構,先建立一個新類別(或記錄)來代表這組資料,給它一個能表達整體概念的名字。
  2. 用改變函式宣告 Change Function Declaration把這個新物件加進函式的參數清單。
  3. 逐一調整呼叫端,改成建立並傳入這個新物件。
  4. 把函式內原本使用個別參數的地方,改成從新物件取值。
  5. 移除已被取代的舊個別參數,測試。
  6. 後續可把「操作這組資料的邏輯」搬進這個新類別,讓它從資料袋長成有行為的物件。

★ before / after(Go)

// before:start / end 這兩個參數在整個 codebase 一直黏在一起出現
func ReadingsOutsideRange(station Station, min, max float64) []Reading
func AmountInvoiced(startDate, endDate time.Time) float64
func Recipe(startDate, endDate time.Time) []Item
 
// after
type DateRange struct{ Start, End time.Time }
 
func AmountInvoiced(r DateRange) float64
func Recipe(r DateRange) []Item

★★ 真正的收益在第二步:新型別會長出方法

參數物件本身只是「少打幾個字」。價值在於現在有地方放邏輯了:

func (r DateRange) Contains(t time.Time) bool {
    return !t.Before(r.Start) && !t.After(r.End)
}
func (r DateRange) Days() int { return int(r.End.Sub(r.Start).Hours() / 24) }
func (r DateRange) Valid() error {
    if r.End.Before(r.Start) { return errors.New("end before start") }
    return nil
}

原本散落在五個呼叫點的 if t.After(start) && t.Before(end)(而且有兩處把 >= 寫成 >)
全部收斂到一個方法。→ 這條路的終點是 值物件。

Go 的三個選項與判準

做法什麼時候用
直接多個參數≤3 個、型別各異、不會一起出現在別處
★ params struct這組參數總是一起出現、且大多是必填。func Do(p Params)
★ Functional Options大多是選項(有合理預設)、未來還會增加。func Do(x string, opts ...Option)

★ 判準:這些欄位是「必填的一組資料」還是「可選的調整」?
資料 → struct(而且該長出方法);選項 → options(見 工具-Go函式參數太多怎麼辦)。

⚠️ params struct 的零值陷阱

type Query struct {
    Limit  int
    Offset int
}
Search(Query{Offset: 20})     // ★ Limit 是 0 —— 你是想要 0 筆,還是忘了填?

Go 的 struct 分不出「沒給」和「給了零值」。 三種解法:

// ① 建構函式塞預設值
func NewQuery(opts ...QueryOpt) Query { q := Query{Limit: 50}; … }
 
// ② 零值就是合理預設(★ 最 Go 的做法)
if q.Limit == 0 { q.Limit = 50 }     // 讓 0 的意思等於「用預設」
 
// ③ 真的需要分辨時才用指標
type Query struct{ Limit *int }      // ★ 代價:呼叫端要寫 &n,而且到處都要判 nil

TypeScript

TS 用物件字面量當「具名參數」是主流寫法,而且 ? 天然分得出「沒給」:

function search({ limit = 50, offset = 0 }: { limit?: number; offset?: number }) { … }
search({ offset: 20 });        // ★ limit 自動是 50,不會有 Go 的零值歧義

→ 這個手法在 TS 幾乎是免費的,所以 TS 的痛點不是「參數太多」而是「物件型別散落」(見 工具-satisfies與型別標註)。

工具支援

⚠️ gopls 沒有這個功能。Extract parameter struct 自 2024 年列為計畫,
到 2026-09-25 查證仍未實作(go.dev/issue/65552)→ 在 Go 只能手動做。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 若這些參數只是偶爾湊在一起、彼此沒有概念上的關聯,硬包成物件反而增加無意義的間接。

  • ★ 別把不相關的參數硬綁成一包。判準是「這幾個參數在別的函式簽章裡也總是一起出現」(那就是 資料泥團)。只在這一個函式裡同時出現,不算。

  • 停在「只有欄位的 struct」就白做了。沒有長出方法的參數物件,只是把長參數列換成長欄位列——問題沒解決。

  • ★ params struct 的零值歧義(見上)。這是 Go 用 struct 當參數最常出的 bug。

  • 會被大量呼叫的熱路徑:大 struct 傳值會複製。用指標或 profile 過再說——但不要為了猜測的效能提前用指標,指標會帶來 nil 與別名(aliasing)的新問題。

  • 參數物件會變成上帝物件。RequestContext 塞了三十個欄位,每個呼叫者只用三個——那已經退化成 全域資料。

🔗 相關工具