0

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.go trong /tools.

  • Tổ chức tài liệu thiết kế kiến trúc (ADRs, RFCs) trong /docs và 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.go cùng thư mục với file code nguồn (trong /internal hoặ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/testdata cho 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.go trong /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ặc migrate.

    • Kỹ thuật ghim phiên bản tool vào file go.mod mà 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 get trực tiếp (hoặc các submodule C/C++ dùng cho CGO).

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 allowed củ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 A import Package B, nhưng B lạ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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí