Bài 01: Triết lý Go & Giới thiệu về Standard Project Layout
1. Mục tiêu bài học
-
Hiểu cách tiếp cận "Simple & Pragmatic" của Go đối với tổ chức thư mục.
-
Nhận diện sự khác biệt giữa chuẩn chính thức từ Go Core Team và repository cộng đồng
golang-standards/project-layout. -
Nắm vững các tiêu chí quyết định khi nào nên dùng cấu trúc phẳng (Flat) và khi nào cần chuyển sang Standard Layout.
2. Nội dung chi tiết
-
Phần 1: Nguồn gốc và tranh luận quanh Standard Layout
-
Sự thật về repo
golang-standards/project-layout: Chuẩn cộng đồng thực tế (de facto) chứ không phải khuyến nghị chính thức từ Go team. -
Góc nhìn của Russ Cox (Go Team): Ưu tiên sự tối giản (
package mainvà cấu trúc phẳng ở giai đoạn đầu). -
Tại sao các hệ thống enterprise lớn (Kubernetes, Docker, Prometheus) vẫn áp dụng cấu trúc thư mục phân tầng.
-
-
Phần 2: Bản đồ thư mục cốt lõi
-
Nhóm ứng dụng:
/cmd,/internal,/pkg. -
Nhóm hỗ trợ:
/configs,/api,/deployments,/scripts. -
Nhóm tài liệu & công cụ:
/docs,/tools,/test.
-
-
Phần 3: Nguyên tắc đặt tên package trong Go
-
Tên ngắn gọn, viết thường, không gạch dưới, không camelCase.
-
Tránh các tên vô nghĩa:
common,util,helpers,base.
-
3. Cấu trúc thư mục minh họa
Plaintext
my-service/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ └── user/
│ └── user.go
├── go.mod
└── README.md
4. Ví dụ code minh họa
Minh họa sự gọn nhẹ ở điểm khởi đầu: main.go chỉ đảm nhiệm vai trò kích hoạt, logic thực sự nằm ở lớp internal.
internal/user/user.go:
Go
package user
import "fmt"
type Service struct{}
func NewService() *Service {
return &Service{}
}
func (s *Service) Hello(name string) string {
return fmt.Sprintf("Xin chào, %s!", name)
}
cmd/api/main.go:
Go
package main
import (
"fmt"
"my-service/internal/user"
)
func main() {
userService := user.NewService()
fmt.Println(userService.Hello("Gopher"))
}
Bài 02: Trái tim dự án: Phân biệt vai trò giữa /cmd và /internal
1. Mục tiêu bài học
-
Làm chủ quy tắc biên dịch nhiều binary từ cùng một codebase thông qua
/cmd. -
Hiểu rõ cơ chế bảo vệ mã nguồn đặc biệt của trình biên dịch Go đối với từ khóa
internal. -
Thiết kế ranh giới đóng gói (encapsulation) chuẩn xác cho ứng dụng backend.
2. Nội dung chi tiết
-
Phần 1: Bản chất của thư mục
/cmd-
Mỗi thư mục con trong
/cmdtương ứng với một binary output. -
Trách nhiệm duy nhất của
/cmd/<app>: Khởi tạo dependency (wiring/bootstrap), đọc cấu hình, và gọi hàm chạy. -
Tuyệt đối không viết business logic hoặc truy vấn cơ sở dữ liệu trực tiếp trong
/cmd.
-
-
Phần 2: Cơ chế Compiler Enforcement của
/internal-
Quy tắc Go compiler: Bất kỳ package nào nằm trong hoặc dưới cây thư mục có tên
internalchỉ có thể được import bởi các package cùng cấp hoặc là con của thư mục cha chứainternal. -
Ngăn chặn việc các dự án bên ngoài (hoặc các module khác) import các package nội bộ không ổn định.
-
-
Phần 3: Mô hình đa ứng dụng (Multi-binary)
- Tổ chức chạy song song REST API server, background worker, và migration tool trong cùng một repository.
3. Cấu trúc thư mục minh họa
Plaintext
shop-engine/
├── cmd/
│ ├── server/ # Binary 1: Chạy REST/gRPC Server
│ │ └── main.go
│ └── worker/ # Binary 2: Chạy Queue Consumer
│ └── main.go
├── internal/
│ ├── order/ # Logic xử lý đơn hàng (private)
│ │ ├── model.go
│ │ └── service.go
│ └── platform/ # Hạ tầng dùng nội bộ: DB, Redis
│ └── database.go
├── go.mod
└── go.sum
4. Ví dụ code minh họa
Minh họa việc chia sẻ logic nghiệp vụ từ /internal cho hai binary khác nhau trong /cmd.
internal/order/service.go:
Go
package order
import "log"
type Service struct{}
func NewService() *Service {
return &Service{}
}
// ProcessOrder được tái sử dụng bởi cả Server và Worker
func (s *Service) ProcessOrder(id string) error {
log.Printf("[Order Service] Đang xử lý đơn hàng #%s", id)
return nil
}
cmd/server/main.go (HTTP Server trigger):
Go
package main
import (
"log"
"net/http"
"shop-engine/internal/order"
)
func main() {
orderSvc := order.NewService()
http.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) {
_ = orderSvc.ProcessOrder("ORD-101")
w.Write([]byte(`{"status":"success"}`))
})
log.Println("Server HTTP đang chạy trên port :8080...")
_ = http.ListenAndServe(":8080", nil)
}
cmd/worker/main.go (Background Consumer):
Go
package main
import (
"log"
"time"
"shop-engine/internal/order"
)
func main() {
orderSvc := order.NewService()
log.Println("Worker hàng đợi đã khởi động...")
// Giả lập đọc tin nhắn từ RabbitMQ / Kafka
for {
log.Println("Worker nhận task từ queue...")
_ = orderSvc.ProcessOrder("ORD-ASYNC-202")
time.Sleep(5 * time.Second)
}
}
All rights reserved