TL;DR
Go sẽ không biên dịch khi hai hoặc nhiều package import lẫn nhau — trực tiếp hoặc qua một chuỗi phụ thuộc. Cách sửa luôn mang tính cấu trúc: tách code dùng chung vào một package thứ ba mà cả hai package xung đột đều không import, hoặc gộp chúng lại nếu chúng quá gắn kết để tách rời.
Lỗi trông như thế nào
$ go build ./...
package myapp/service
imports myapp/repository
imports myapp/service: import cycle not allowed
Go in ra toàn bộ chuỗi import để bạn xác định chính xác vòng lặp đóng ở đâu. Đọc từ dưới lên: package được liệt kê cuối cùng hoàn thành vòng tròn bằng cách import package đầu tiên.
Tại sao Go cấm import cycle
Go biên dịch các package theo thứ tự phụ thuộc nghiêm ngặt. Nếu package A cần package B và B cần A, trình biên dịch sẽ rơi vào deadlock ở giai đoạn kiểm tra kiểu — không có thứ tự biên dịch hợp lệ nào tồn tại.
Các ngôn ngữ như Java hay Python xử lý vấn đề này ở giai đoạn liên kết. Go thì không. Đây là lỗi cứng tại thời điểm biên dịch và không có cách thoát ở cấp độ ngôn ngữ. Bạn phải sửa cấu trúc, không có ngoại lệ.
Phát hiện vòng lặp
Thông báo lỗi đã hiển thị chuỗi phụ thuộc. Với các dự án lớn có đồ thị phức tạp, một số công cụ bổ sung sẽ hữu ích:
# Hiển thị tất cả phụ thuộc của một package
go list -f '{{ .Imports }}' ./internal/service
# Tìm tất cả vòng lặp trong module
go build ./... 2>&1 | grep 'import cycle'
# Tạo đồ thị phụ thuộc trực quan (yêu cầu Graphviz)
go install github.com/kisielk/godepgraph@latest
godepgraph -s ./... | dot -Tpng -o deps.png
Các tình huống phổ biến và cách sửa
Tình huống 1: Service và repository import lẫn nhau
Chín trong mười lỗi cycle trong Go trông như thế này. Tầng service gọi repository — bình thường. Repository sau đó tham chiếu một kiểu được định nghĩa trong package service — không ổn.
// SAI: myapp/repository/user.go
import "myapp/service" // cycle!
func (r *UserRepo) Save(u service.User) error { ... }
Cách sửa: Chuyển các kiểu dùng chung như User vào một package chuyên biệt models hoặc domain. Cả service lẫn repository đều không import nhau — cả hai đều import models.
// myapp/models/user.go — package lá, không có import nội bộ
package models
type User struct {
ID int
Email string
}
// myapp/repository/user.go
import "myapp/models" // OK
func (r *UserRepo) Save(u models.User) error { ... }
// myapp/service/user.go
import (
"myapp/models" // OK
"myapp/repository" // OK
)
func CreateUser(repo *repository.UserRepo, email string) error {
u := models.User{Email: email}
return repo.Save(u)
}
Tình huống 2: Package utility kéo vào logic ứng dụng
Ai đó thêm một helper vào utils cần dùng một kiểu từ config. Bình thường — cho đến khi config import utils cho mục đích khác.
// myapp/utils/helpers.go
import "myapp/config" // cycle nếu config import utils
Cách sửa: Package utility không nên có bất kỳ import nội bộ nào. Truyền giá trị qua tham số thay vì kéo về từ package sở hữu chúng.
// TRƯỚC — helper tự lấy config
func GetTimeout() time.Duration {
cfg := config.Load()
return cfg.Timeout
}
// SAU — người gọi cung cấp giá trị
func FormatDuration(d time.Duration) string {
return d.String()
}
Tình huống 3: Hai package nên được gộp thành một
Đôi khi vòng lặp là triệu chứng, không phải vấn đề. Nếu packageA và packageB liên tục gọi nhau và không có điểm tách sạch nào, chúng thuộc về cùng một package.
# Trước — hai package không thể sống thiếu nhau
myapp/auth/
myapp/session/
# Sau — gộp thành một package gắn kết
myapp/auth/ # chứa cả logic auth lẫn session
Tình huống 4: Dùng interface để đảo ngược phụ thuộc
Khi bạn thực sự cần giao tiếp hai chiều, hãy định nghĩa interface trong package cấp thấp hơn. Package cấp cao hơn sẽ implement nó. Không cần import chung — phụ thuộc chỉ chảy một chiều.
// myapp/repository/notifier.go
package repository
// Notifier được implement bởi tầng service.
// repository không bao giờ import service.
type Notifier interface {
OnUserSaved(id int)
}
type UserRepo struct {
notifier Notifier
}
func NewUserRepo(n Notifier) *UserRepo {
return &UserRepo{notifier: n}
}
func (r *UserRepo) Save(u User) error {
// ... logic lưu ...
r.notifier.OnUserSaved(u.ID)
return nil
}
// myapp/service/user.go — implement repository.Notifier
package service
import "myapp/repository"
type UserService struct{}
func (s *UserService) OnUserSaved(id int) {
// xử lý thông báo sau khi lưu
}
func SetupRepo(svc *UserService) *repository.UserRepo {
return repository.NewUserRepo(svc) // service được truyền vào như interface
}
Các quy tắc cấu trúc giúp ngăn cycle
- Phụ thuộc chảy vào trong:
cmd→service→repository→models. Các tầng thấp hơn không bao giờ import tầng cao hơn. Vẽ mũi tên này lên bảng trước khi bắt đầu — các vi phạm sẽ hiện ra rõ ràng. - Kiểu dùng chung nằm trong package lá:
models,domain, hoặctypes— các package không có import nội bộ. Mọi package khác đều có thể import chúng an toàn. - Interface thuộc về phía sử dụng: Định nghĩa interface trong package dùng nó, không phải package implement nó. Đây là phong cách Go thông thường và tự nhiên ngăn chặn cycle trước khi chúng hình thành.
- Chú ý bẫy
internal/common: Một packagecommontích lũy các helper không liên quan giải quyết cycle về mặt cơ học nhưng tạo ra mớ hỗn độn khó bảo trì. Hãy cân nhắc kỹ những gì thực sự thuộc về đó.
Xác nhận đã sửa xong
# Thoát sạch, không có output = vòng lặp đã biến mất
go build ./...
# Đảm bảo không có gì bị hỏng
go test ./...
# Kiểm tra thêm với vet
go vet ./...
# Tùy chọn: tái tạo đồ thị để thấy trước/sau
godepgraph -s ./... | dot -Tpng -o deps-after.png
go build ./... thoát với mã 0 và không có output là xác nhận của bạn. Nếu vẫn còn output ồn ào nghĩa là vẫn còn việc phải làm.
Danh sách kiểm tra nhanh khi gặp lỗi này
- Đọc toàn bộ chuỗi cycle — xác định package nào là mắt xích bất thường.
- Import đó chỉ cần cho một kiểu hoặc hàm? Chuyển kiểu đó sang package lá dùng chung.
- Hai package liên tục gọi nhau? Gộp chúng lại.
- Có thể thay thế import bằng interface định nghĩa cục bộ không? Hãy làm vậy.
- Không bao giờ import package tầng cao từ package tầng thấp — không có ngoại lệ.

