Like Share Discussion Bookmark Smile

J.J. Huang   2025-06-12   Getting Started Golang 07.套件   瀏覽次數:次   DMCA.com Protection Status

Go | 如何處理循環引用問題

💬 簡介

當專案成長到一定規模後,各模組間開始互相依賴邏輯與資料,這時最容易發生的問題之一就是 循環引用(circular import)

舉例來說:

  • user 套件需要用到 order 的函式
  • 同時 order 也需要 user 的資料結構

這樣的雙向引用會讓 Go 編譯器直接報錯,導致模組無法建置。

本篇將解析循環引用的形成原因與錯誤範例,並提供實務解法,例如 介面抽離、共用套件重構、邏輯解耦 等技巧。

圖片來源:Gophers


❗ 錯誤示範:循環引用報錯

  • 📁 專案結構

    1
    2
    3
    4
    5
    6
    project/
    ├── main.go
    ├── user/
    │ └── user.go
    ├── order/
    │ └── order.go
  • user/user.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package user

    import "project/order"

    type User struct {
    ID string
    }

    func (u *User) GetOrders() []order.Order {
    return order.GetOrdersByUserID(u.ID)
    }
  • order/order.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package order

    import "project/user"

    type Order struct {
    ID string
    UserID string
    }

    func GetOrdersByUserID(userID string) []Order {
    // 模擬查詢資料
    return []Order{
    {ID: "A1", UserID: userID},
    {ID: "B2", UserID: userID},
    }
    }

    func (o *Order) GetUser() *user.User {
    return &user.User{ID: o.UserID}
    }
  • ⛔ 編譯錯誤訊息:

    1
    import cycle not allowed

📌 問題解析

  • 🔁 形成循環的關係圖

    1
    2
    user ➜ 引用了 ➜ order
    order ➜ 又引用了 ➜ user

    ⚠️ Go 不允許模組間形成環狀引用,這樣會導致編譯器無法解析依賴順序。


🛠️ 解決方法

  • ✅ 解法一:介面抽離法

    將「需要回傳的結構」抽象為 interface,由下層模組定義 interface,不引用上層模組。
  • 重構後目錄結構:

    1
    2
    3
    4
    5
    6
    7
    8
    project/
    ├── main.go
    ├── user/
    │ └── user.go
    ├── order/
    │ └── order.go
    ├── contract/
    │ └── user_contract.go
  • contract/user_contract.go

    1
    2
    3
    4
    5
    package contract

    type User interface {
    GetID() string
    }
  • user/user.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package user

    type User struct {
    ID string
    }

    func (u *User) GetID() string {
    return u.ID
    }
  • order/order.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package order

    import "project/contract"

    type Order struct {
    ID string
    UserID string
    }

    func GetOrdersByUser(user contract.User) []Order {
    return []Order{
    {ID: "A1", UserID: user.GetID()},
    {ID: "B2", UserID: user.GetID()},
    }
    }
  • main.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package main

    import (
    "fmt"
    "project/order"
    "project/user"
    )

    func main() {
    u := &user.User{ID: "U123"}
    orders := order.GetOrdersByUser(u)
    fmt.Println("訂單數量:", len(orders))
    }

    📝 使用 interface 解耦雙向依賴,contract 套件僅定義協定,不帶實作邏輯。

  • ✅ 解法二:抽取共用結構至共用套件

    如果只是因為共用某些結構(例如 UserOrder)而互相引用,可以抽出來放到共用資料套件中。
  • 📁 重構後:

    1
    2
    3
    project/
    ├── model/
    │ └── model.go (包含 User, Order 結構)
    如此 user 和 order 可同時引用 model,但不會彼此直接依賴。
  • ✅ 解法三:打破耦合,重新拆分邏輯

    若邏輯耦合過深,建議重構職責:
    • 讓某一方變成工具(如轉為 repository 套件)
    • 合併功能相近的模組(視需求可接受)

🎯 總結

循環引用是 Go 模組設計中常見但容易忽略的陷阱。面對這類錯誤,推薦採取以下解法:

  • ✅ 使用介面將邏輯反轉,避免直接依賴
  • ✅ 抽出共用型別至 modelcontract 套件
  • ✅ 重構功能與邏輯,降低模組間耦合性

最後建議回顧一下 Go | 菜鳥教學 目錄,了解其章節內容。


註:以上參考了
Go