Bài 04: Xử lý các thư mục hỗ trợ: /docs, /tools, /test, /third_party
1. Mục tiêu bài học
-
Phân loại rõ ràng giữa Unit Test (đi kèm code nguồn) và Integration/E2E Test (nằm trong
/test). -
Quản lý phiên bản các công cụ phát triển (linter, mock generator, migration CLI) thông qua kỹ thuật
tools.gotrong/tools. -
Tổ chức tài liệu thiết kế kiến trúc (ADRs, RFCs) trong
/docsvà quản lý thư viện vendor ngoại vi với/third_party.
2. Nội dung chi tiết
-
Phần 1: Kiến trúc kiểm thử với
/test-
Nguyên tắc Unit Test trong Go: Luôn đặt file
*_test.gocùng thư mục với file code nguồn (trong/internalhoặc/pkg). -
Vai trò của
/test: Chứa các kịch bản kiểm thử tích hợp (Integration Tests), kiểm thử hiệu năng tải (Load/Benchmark Tests), và kiểm thử đầu cuối (End-to-End Tests). -
Sử dụng thư mục
/test/testdatacho fixture data (JSON payloads, mock SSL certificates, SQL dumps). -
Phân loại test bằng Go Build Tags:
//go:build integration.
-
-
Phần 2: Quản lý công cụ phát triển bằng
tools.gotrong/tools-
Vấn đề "Works on my machine" khi các lập trình viên cài khác phiên bản
mockgen,golangci-lint, hoặcmigrate. -
Kỹ thuật ghim phiên bản tool vào file
go.modmà không làm phình binary release bằng build tag//go:build tools.
-
-
Phần 3: Lưu trữ tài liệu kỹ thuật trong
/docs-
Cấu trúc Architecture Decision Records (ADR): Ghi chép lý do lựa chọn công nghệ và thiết kế kiến trúc.
-
Sơ đồ phân rã hệ thống (C4 Model, Sequence diagrams) và tài liệu quy trình onboard cho developer mới.
-
-
Phần 4: Vai trò của
/third_party- Nơi chứa các thư viện, tiện ích, hoặc code fork bên ngoài bị sửa đổi nhưng không thể cài qua
go gettrực tiếp (hoặc các submodule C/C++ dùng cho CGO).
- Nơi chứa các thư viện, tiện ích, hoặc code fork bên ngoài bị sửa đổi nhưng không thể cài qua
3. Cấu trúc thư mục minh họa
Plaintext
enterprise-service/
├── docs/
│ ├── adr/
│ │ └── 0001-use-postgresql-over-mongodb.md
│ └── architecture.png
├── test/
│ ├── e2e/
│ │ └── payment_e2e_test.go
│ └── testdata/
│ └── sample_invoice.json
├── tools/
│ └── tools.go # Quản lý tool dependencies
├── third_party/
│ └── swagger-ui/ # Static asset của Swagger UI bên thứ ba
├── internal/
│ └── payment/
│ ├── payment.go
│ └── payment_test.go # Unit test đặt tại đây
├── go.mod
└── go.sum
4. Ví dụ code minh họa
tools/tools.go (Ghim phiên bản công cụ vào go.mod):
Go
//go:build tools
// +build tools
package tools
import (
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
_ "go.uber.org/mock/mockgen"
)
test/e2e/payment_e2e_test.go (Integration/E2E test tách biệt hoàn toàn với build tag):
Go
//go:build integration
package e2e
import (
"net/http"
"os"
"testing"
)
func TestPaymentEndToEnd(t *testing.T) {
baseURL := os.Getenv("SERVICE_BASE_URL")
if baseURL == "" {
baseURL = "http://localhost:8080"
}
payload, err := os.ReadFile("../testdata/sample_invoice.json")
if err != nil {
t.Fatalf("Không thể đọc file fixture test: %v", err)
}
resp, err := http.Post(baseURL+"/v1/checkout", "application/json", bytes.NewBuffer(payload))
if err != nil {
t.Fatalf("Lỗi gọi request E2E: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Kỳ vọng mã HTTP 200, thực tế nhận được: %d", resp.StatusCode)
}
}
Bài 08: Anti-Patterns trong Go layout: Tránh common/utils và vòng lặp import cycle
1. Mục tiêu bài học
-
Nhận diện và loại bỏ các bẫy thiết kế phổ biến nhất khiến cấu trúc dự án Go sụp đổ khi mở rộng.
-
Hiểu sâu cơ chế kiểm tra
import cycle not allowedcủa Go compiler và cách khắc phục triệt để. -
Chuyển đổi tư duy từ "Package by Layer" (kiểu Java/Spring) sang "Package by Feature / Domain" trong Go.
2. Nội dung chi tiết
-
Phần 1: Cái bẫy "bãi rác"
common,util,helpers-
Tại sao các package này vi phạm triết lý Go: Tên mờ nhạt, thiếu ngữ cảnh, không mô tả mục đích sử dụng (gây ra
util.Process(),common.Format()). -
Giải pháp tái cấu trúc: Chia nhỏ thành các package cụ thể theo đúng danh từ hoặc chức năng (
cryptoutil,stringconv,httperr).
-
-
Phần 2: Nguồn gốc và giải pháp xử lý
import cycle-
Nguyên nhân: Package
Aimport PackageB, nhưngBlại cần kiểu dữ liệu từA. -
Cách khắc phục 1: Interface Segregation – Đảo ngược phụ thuộc (Dependency Inversion), định nghĩa interface ngay tại nơi tiêu thụ (Consumer-defined interfaces).
-
Cách khắc phục 2: Trích xuất kiểu dữ liệu dùng chung (Shared Domain Types) vào một package nền tảng độc lập.
-
-
Phần 3: Anti-pattern "Package by Layer" (Controller - Service - Repository)
-
Sai lầm khi tạo các thư mục:
/controllers,/services,/modelsở root (khiến toàn bộ domain bị xáo trộn, dễ dính circular dependency). -
Chuyển đổi sang kiến trúc module hóa theo domain:
/internal/user,/internal/order.
-
3. Sơ đồ minh họa Import Cycle & Cách khắc phục
Plaintext
[LỖI: Import Cycle]
internal/user ------(cần OrderService)-----> internal/order
^ |
|---------------(cần User model)---------------|
[KHẮC PHỤC: Sử dụng Interface tại nơi tiêu thụ]
internal/user (định nghĩa interface OrderTracker)
^
| (implement interface)
internal/order
4. Ví dụ code minh họa
Tình huống lỗi (Anti-Pattern - Circular Dependency):
internal/order/service.go:
Go
package order
import "enterprise-service/internal/user" // Order import User
type Service struct{}
func (s *Service) CreateOrder(userID string) {
u := user.GetUser(userID) // Sử dụng hàm từ user
_ = u
}
internal/user/service.go:
Go
package user
import "enterprise-service/internal/order" // LỖI: User lại import Order -> COMPILE ERROR
type Service struct {
orderSvc *order.Service
}
Giải pháp chuẩn Go (Áp dụng Consumer-defined Interface):
internal/user/service.go (Không phụ thuộc vào order, tự định nghĩa hợp đồng nó cần):
Go
package user
// OrderCreator là interface được định nghĩa bởi consumer (user package)
type OrderCreator interface {
CreateOrder(userID string) error
}
type Service struct {
orderCreator OrderCreator // Phụ thuộc vào abstraction
}
func NewService(oc OrderCreator) *Service {
return &Service{orderCreator: oc}
}
func (s *Service) RegisterAndCreateFirstOrder(userID string) error {
// Xử lý tạo user...
return s.orderCreator.CreateOrder(userID)
}
cmd/api/main.go (Lớp liên kết - Dependency Injection resolver):
Go
package main
import (
"enterprise-service/internal/order"
"enterprise-service/internal/user"
)
func main() {
// Khởi tạo các thành phần độc lập
orderService := order.NewService()
// orderService tự động thỏa mãn interface OrderCreator của user package
userService := user.NewService(orderService)
_ = userService.RegisterAndCreateFirstOrder("USER-123")
}
All rights reserved