DRY(Don’t Repeat Yourself,不要重複你自己)是軟體開發中最重要的核心原則之一 [ExplainThis 寫程式必備原則]。
它的核心精神是:「系統中的每一個知識或邏輯,都必須有一個單一、明確且權威的代表。」
💡 核心觀念
- 不只是程式碼長相:DRY 指的是「商業邏輯/知識」的重複,而不是單純的字元重複。如果兩段程式碼長得一模一樣,但分別代表完全無關的業務意義(未來不會同步修改),那就不算違反 DRY 原則。
- 單一事實來源(SSOT):當需求變更時,開發者應該只需要修改一個地方,整個系統就會自動同步,不需要在多個檔案中手動搜尋並重複修改。
⚠️ 常見的違反情況
- 複製貼上(Copy-Paste):在多個不同的檔案中,寫了相同的資料驗證或邏輯運算。
- 魔法數字與字串:相同的設定值(如 API 網址、系統權限代碼、打折趴數)直接寫死(Hardcode)在各個程式碼角落。
- 文件與程式碼脫節:邏輯改了,但註解或規格書沒改,導致兩邊資訊不一致。
🛠️ 如何實踐?
- 封裝共用邏輯:將重複的商業逻辑抽取出來,做成獨立的函式(Function)、類別(Class)或工具包(Utility)。
- 提取常數與設定檔:將固定不變的值統一集中到
config或constants檔案中管理。
🛑 避免過度使用(Over-engineering)
- 不要為了 DRY 而 DRY:如果硬要把兩個「只是剛好長得像,但未來發展完全不同」的邏輯綁在一起,會導致程式碼過度耦合(Coupling),反而讓系統變得很難維護。
- 搭配 YAGNI 原則:通常會遵循「事不過三」原則(Rule of Three),當同樣的邏輯真的出現第三次時,才是最適合動手重構的時機。
這裡為您提供重構計畫以及乾淨的 Go 實作程式碼,徹底消除這三個地方的重複邏輯。
🛠️ 重構計畫
- 建立共享工具包:新增
internal/pkg/netutil目錄,專門存放網路相關的共用邏輯。 - 統一核心邏輯:將 IP 萃取(解析 HTTP Header)與私有 IP 判定(檢查網路區段)移入該工具包。
- 引入並取代:刪除上述三個檔案中的重複程式碼,並統一 import 新的工具函式。
1. 建立共享工具包 (internal/pkg/netutil/ip.go)
這個工具包會安全地解析代理伺服器 Header,並使用 Go 標準庫高效判定 IPv4 與 IPv6 的私有網段。
package netutil
import (
"net"
"net/http"
"strings"
)
// ExtractIP 從 HTTP 請求標頭中萃取用戶端真實 IP。
// 優先級:X-Forwarded-For > X-Real-IP > RemoteAddr。
func ExtractIP(r *http.Request) string {
// 檢查 X-Forwarded-For(可能包含多個代理 IP,取第一個)
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
parts := strings.Split(xff, ",")
if ip := strings.TrimSpace(parts[0]); ip != "" {
return ip
}
}
// 次要備援方案:X-Real-IP
if xrip := r.Header.Get("X-Real-IP"); xrip != "" {
return strings.TrimSpace(xrip)
}
// 最終備援方案:RemoteAddr(若包含連接埠則將其剔除)
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return ip
}
// IsPrivateIP 檢查指定的 IP 字串是否屬於私有網路區段或本地迴圈。
func IsPrivateIP(ipStr string) bool {
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
// 正確處理 IPv4-mapped IPv6 格式
if ip4 := ip.To4(); ip4 != nil {
return ip4[0] == 10 ||
(ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) ||
(ip4[0] == 192 && ip4[1] == 168) ||
ip.IsLoopback()
}
// 標準 IPv6 私有與本地範圍判定
return ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsInterfaceLocalMulticast()
}
2. 更新您的業務元件
請刪除原本在各檔案中重複的邏輯塊,並統一改用以下簡潔的呼叫模式:
package middleware
import (
"net/http"
// 請將 "your_project" 替換為您 go.mod 中定義的實際模組名稱
"your_project/internal/pkg/netutil"
)
func YourMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. 統一萃取 IP
clientIP := netutil.ExtractIP(r)
// 2. 統一判定私有 IP
if netutil.IsPrivateIP(clientIP) {
// 執行您的白名單放行或信任邏輯
}
next.ServeHTTP(w, r)
})
}
請將此 import 結構與呼叫方式同步應用至:
internal/auth/internal/ratelimit/middleware.gointernal/auditlog/middleware.go
為了確保這次重構不會改變您原本系統的行為,如果您願意,可以告訴我:
- 您目前的 Header 檢查順序(例如:是否有使用 Cloudflare 的
CF-Connecting-IP或 AWS 的標頭?) - 您在
go.mod中定義的 Module 名稱( package name)是什麼?
我可以幫您直接調整好完整的導入程式碼。
實際上 claude sonnet 4.6 寫的 go lang code
if xff := r.Header.Get("X-Forwarded-For"); xff != "" && accessLogIsPrivateIP(r.RemoteAddr) {
first := xff
if idx := strings.Index(xff, ","); idx != -1 {
first = xff[:idx]
}
return stripPort(strings.TrimSpace(first))
}
return stripPort(r.RemoteAddr)
}
func accessLogIsPrivateIP(addr string) bool {
host := addr
if h, _, err := net.SplitHostPort(addr); err == nil {
host = h
}
ip := net.ParseIP(strings.TrimSpace(host))
if ip == nil {
return false
}
for _, cidr := range []string{
"127.0.0.0/8", // loopback
"10.0.0.0/8", // RFC 1918
"172.16.0.0/12", // RFC 1918
"192.168.0.0/16", // RFC 1918
"100.64.0.0/10", // RFC 6598 CGNAT / Azure ACA
"::1/128", // IPv6 loopback
"fc00::/7", // IPv6 unique-local
} {
_, network, err := net.ParseCIDR(cidr)
if err != nil {
continue
}
if network.Contains(ip) {
return true
}
}
return false
}
真人 reviewer
The IP extraction logic and private IP identification logic are the same in three files: internal/auth/, internal/ratelimit/middleware.go, and internal/auditlog/middleware.go.
Please extract them as a utility function and import from the same source.
這部分當時是為了避免循環 import 才各自複製一份,但確實違反了 DRY 原則。會建立 internal/netutil package,把 ClientIP() 和 IsPrivateIP() 提取出來,讓 auth、ratelimit、auditlog 三個 package 統一引用。