DRY(Don’t Repeat Yourself,不要重複你自己)

DRY(Don’t Repeat Yourself,不要重複你自己)是軟體開發中最重要的核心原則之一 [ExplainThis 寫程式必備原則]。

它的核心精神是:「系統中的每一個知識或邏輯,都必須有一個單一、明確且權威的代表。

💡 核心觀念

  • 不只是程式碼長相:DRY 指的是「商業邏輯/知識」的重複,而不是單純的字元重複。如果兩段程式碼長得一模一樣,但分別代表完全無關的業務意義(未來不會同步修改),那就不算違反 DRY 原則。
  • 單一事實來源(SSOT):當需求變更時,開發者應該只需要修改一個地方,整個系統就會自動同步,不需要在多個檔案中手動搜尋並重複修改。

⚠️ 常見的違反情況

  • 複製貼上(Copy-Paste):在多個不同的檔案中,寫了相同的資料驗證或邏輯運算。
  • 魔法數字與字串:相同的設定值(如 API 網址、系統權限代碼、打折趴數)直接寫死(Hardcode)在各個程式碼角落。
  • 文件與程式碼脫節:邏輯改了,但註解或規格書沒改,導致兩邊資訊不一致。

🛠️ 如何實踐?

  • 封裝共用邏輯:將重複的商業逻辑抽取出來,做成獨立的函式(Function)、類別(Class)或工具包(Utility)。
  • 提取常數與設定檔:將固定不變的值統一集中到 configconstants 檔案中管理。

🛑 避免過度使用(Over-engineering)

  • 不要為了 DRY 而 DRY:如果硬要把兩個「只是剛好長得像,但未來發展完全不同」的邏輯綁在一起,會導致程式碼過度耦合(Coupling),反而讓系統變得很難維護。
  • 搭配 YAGNI 原則:通常會遵循「事不過三」原則(Rule of Three),當同樣的邏輯真的出現第三次時,才是最適合動手重構的時機。

這裡為您提供重構計畫以及乾淨的 Go 實作程式碼,徹底消除這三個地方的重複邏輯。

🛠️ 重構計畫

  1. 建立共享工具包:新增 internal/pkg/netutil 目錄,專門存放網路相關的共用邏輯。
  2. 統一核心邏輯:將 IP 萃取(解析 HTTP Header)與私有 IP 判定(檢查網路區段)移入該工具包。
  3. 引入並取代:刪除上述三個檔案中的重複程式碼,並統一 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.go
  • internal/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 統一引用。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *