0

Go Zero to Hero - Bài 44: Package và cấu trúc thư mục chuẩn của một dự án Go

Trong lập trình Go, việc tổ chức mã nguồn thành các thư mục và package chuẩn mực là yếu tố cốt lõi giúp dự án scale lớn mà không rơi vào trạng thái hỗn loạn.


1. Package là gì? Quy tắc Viết Hoa sinh tử

Trong Go, mọi file code đều phải thuộc về một Package. Dòng đầu tiên của file luôn luôn là package ten_goi. Package thực chất là một cách để gom nhóm các file .go nằm chung trong một thư mục.

Quy tắc Exported (Public) và Unexported (Private):

Khác với Java hay C++ dùng từ khóa public, private, Go sử dụng một triết lý tinh gọn đến mức cực đoan: Dựa vào chữ cái đầu tiên của tên biến/hàm/struct.

package ticket

// Chữ T viết HOA -> Public. Các package khác CÓ THỂ gọi được.
type Transaction struct {
    ID     string  // Public field
    amount float64 // Chữ a viết thường -> Private field, package khác KHÔNG THỂ truy cập
}

// Chữ p viết thường -> Private. Chỉ các file nằm cùng thư mục 'ticket' mới gọi được hàm này.
func processInternal() { /* ... */ }

// Chữ V viết HOA -> Public. Các package khác gọi được bằng cách: ticket.Validate()
func Validate() { /* ... */ }

2. Go Modules: Trái tim của việc quản lý Dependencies

Trước khi tạo thư mục, mọi dự án Go hiện đại đều phải được khởi tạo bằng Go Modules. Nó thay thế hoàn toàn hệ thống $GOPATH cũ kỹ.

Khi bạn chạy lệnh:

go mod init github.com/username/afc-system

Hệ thống sẽ sinh ra file go.mod. Đây là "chứng minh thư" của dự án, nó ghi nhận tên module gốc và quản lý toàn bộ các thư viện bên thứ 3 (như GORM, Gin, Redis) cùng với phiên bản chính xác của chúng, đảm bảo code chạy trên máy bạn cũng sẽ chạy y hệt trên Server Production.


3. Cấu trúc thư mục "Chuẩn quốc dân" (Standard Layout)

Dù Go không bắt buộc bạn phải đặt file ở đâu, nhưng cộng đồng Backend Go trên thế giới đã thống nhất một bộ khung chuẩn mực (dựa trên repo golang-standards/project-layout).

Dưới đây là 3 thư mục tối quan trọng bạn phải có:

A. Thư mục /cmd (Trạm chỉ huy)

Đây là nơi chứa các điểm bắt đầu (Entry points) của ứng dụng, tức là các hàm main().

Nếu dự án của bạn có nhiều khối chạy độc lập (ví dụ: một API Server, một Worker chạy ngầm, một Cronjob), bạn sẽ chia nó ra:

  • cmd/api-server/main.go
  • cmd/sync-worker/main.go

💡 Lưu ý: File main.go ở đây chỉ làm nhiệm vụ đọc config, khởi tạo kết nối DB, và kích hoạt các module khác. Tuyệt đối không viết logic nghiệp vụ ở đây.

B. Thư mục /internal (Pháo đài bất khả xâm phạm)

Đây là một tính năng đặc biệt được trình biên dịch Go bảo vệ. Bất kỳ đoạn code nào nằm trong thư mục internal/ sẽ chỉ có thể được import bởi các code nằm cùng dự án (cùng module). Nếu người khác tải thư viện của bạn về qua GitHub, họ không thể gọi được các hàm trong internal.

Đây là nơi chứa 90% logic nghiệp vụ của bạn (Service, Repository, UseCase).

C. Thư mục /pkg (Thư viện công cộng)

Trái ngược với internal, code để trong /pkg là code mang tính chất tiện ích chung (như hàm mã hóa thẻ, hàm tính toán tọa độ GPS) mà bạn cho phép các dự án khác import và sử dụng.


4. Thực chiến: Layout kết hợp Clean Architecture

Để hình dung rõ nhất, hãy thử map cấu trúc thư mục này vào một hệ thống trạm thu phí tự động (AFC), nơi chia rạch ròi các tầng: Domain, Usecase, Repository, và Delivery.

afc-system/
├── go.mod                  # Quản lý thư viện
├── cmd/
│   └── engine/
│       └── main.go         # Entry point: Khởi động Gin server, kết nối Postgres
├── internal/
│   ├── domain/             # TẦNG CORE: Chứa Struct và Interface
│   │   ├── ticket.go       # Struct TicketTransaction, TicketRepository (Interface)
│   │   └── error.go        # Chứa các Sentinel Errors
│   │
│   ├── repository/         # TẦNG DATABASE: Giao tiếp với các Storage
│   │   ├── postgres/
│   │   │   └── ticket_pg.go # Implement TicketRepository bằng GORM
│   │   └── redis/
│   │       └── cache.go    # Quản lý TTL keyspace notification
│   │
│   ├── usecase/            # TẦNG LOGIC NGHIỆP VỤ: Xử lý trừ tiền, kiểm tra thẻ
│   │   └── ticket_uc.go
│   │
│   └── delivery/           # TẦNG GIAO TIẾP VỚI NGOẠI VI
│       ├── http/           # RESTful API Controllers
│       │   └── handler.go
│       └── rmq/            # Lắng nghe message từ RabbitMQ/Kafka
│           └── consumer.go
└── pkg/                    # THƯ VIỆN DÙNG CHUNG
    ├── logger/             # Hàm ghi log tùy biến
    └── hash/               # Thuật toán mã hóa chuỗi

5. Hai quy luật "Máu và Nước mắt" khi chia Package

  • Quy luật 1: Tránh xa tên package là utils hay common Gopher chân chính rất ghét package tên là utils. Đừng gom đũa, muỗng, dao, thớt vào một cái hộp ghi chữ "đồ linh tinh". Hãy chia nhỏ theo đúng công năng: hàm xử lý thời gian thì để ở package timeutil, hàm xử lý chuỗi thì để ở package strutil, hàm ghi log thì để ở logger.
  • Quy luật 2: Đặt tên Package theo thứ nó CUNG CẤP, không phải thứ nó CHỨA Thay vì đặt tên package models (vì nó chứa các model Struct), hãy thiết kế nó thành package domain hoặc package ticket để khi gọi hàm ở nơi khác, ta đọc được thành những câu có nghĩa:
    • Tốt: ticket.NewTransaction()
    • Xấu: models.NewTicketTransaction()

Bằng cách quy hoạch codebase theo chuẩn cmd/, internal/ và kết hợp với Clean Architecture, dự án Go của bạn sẽ trở nên cực kỳ gọn gàng. Các kỹ sư mới (Onboarding) chỉ cần nhìn vào cây thư mục là biết ngay luồng dữ liệu chạy từ đâu đến đâu, và tình trạng Phụ thuộc vòng (Circular Dependency) sẽ bị tiêu diệt hoàn toàn.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.