Giải thích từng phần một cách chuyên sâu với tư cách Senior Golang Engineer, tập trung vào Clean Code, Structuring Project (theo DDD và Clean Architecture), và cách viết Unit Test & Benchmark với package testing. Mỗi phần sẽ kèm ví dụ thực tế và best practices.
1. Viết Code Sạch (Clean Code) trong Golang
Nguyên tắc Clean Code:
Đơn giản hóa: Code dễ đọc, dễ hiểu, tránh phức tạp không cần thiết.
Tên biến/hàm rõ ràng: Thể hiện ý định (intent), không cần comment quá nhiều.
Hàm nhỏ, làm một việc: Mỗi hàm chỉ nên có một trách nhiệm.
Xử lý lỗi rõ ràng: Không ẩn lỗi, trả về lỗi như giá trị.
Không lặp code (DRY): Tái sử dụng thay vì sao chép.
Ví dụ Clean Code:
Trước (Bad Code):
package main
import "fmt"
func p(n string, a int) {
if a < 0 {
fmt.Println("Error")
} else {
fmt.Println(n, a)
}
}
func main() {
p("John", 25)
p("Doe", -1)
}
Tên hàm/variables không rõ ràng (p, n, a).
Xử lý lỗi không chuẩn (in ra "Error" thay vì return error).
Hàm làm nhiều việc (validate + print).
Sau (Clean Code):
package main
import (
"errors"
"fmt"
)
func printPerson(name string, age int) error {
if age < 0 {
return errors.New("age cannot be negative")
}
fmt.Printf("Name: %s, Age: %d\n", name, age)
return nil
}
func main() {
err := printPerson("John", 25)
if err != nil {
fmt.Println("Error:", err)
}
err = printPerson("Doe", -1)
if err != nil {
fmt.Println("Error:", err) // Error: age cannot be negative
}
}
Tên hàm (printPerson) và biến (name, age) rõ ý nghĩa.
Xử lý lỗi chuẩn bằng error.
Hàm chỉ làm một việc: in thông tin hoặc trả lỗi.
Best Practices:
Dùng Interface: Tăng tính linh hoạt, dễ test.
Tránh Magic Numbers: Dùng hằng số (const).
Comment khi cần: Chỉ giải thích "tại sao", không phải "làm gì".
Giới hạn độ dài hàm: < 20 dòng nếu có thể.
2. Structuring Project theo Best Practices (DDD & Clean Architecture)
Domain-Driven Design (DDD) và Clean Architecture
DDD: Tập trung vào domain (nghiệp vụ), chia project thành các tầng (layer) như Domain, Application, Infrastructure.
Clean Architecture: Tách biệt logic nghiệp vụ (domain) khỏi framework/công cụ (HTTP, DB), đảm bảo độc lập và dễ bảo trì.
Cấu trúc thư mục đề xuất:
project/
├── cmd/ # Điểm khởi chạy (main)
│ └── server/
│ └── main.go
├── internal/ # Code private, không cho bên ngoài import
│ ├── domain/ # Logic nghiệp vụ cốt lõi
│ │ ├── user.go # Định nghĩa entity/model và interface
│ │ └── errors.go # Custom errors
│ ├── application/ # Use case, điều phối giữa domain và infra
│ │ └── user_service.go
│ └── infrastructure/ # Implement cụ thể (DB, HTTP)
│ ├── repository/
│ │ └── user_repository.go
│ └── handler/
│ └── user_handler.go
├── pkg/ # Code chia sẻ (nếu cần)
└── go.mod
Ví dụ thực tế:
domain/user.go:
package domain
import "errors"
type User struct {
ID int
Name string
Email string
}
var ErrUserNotFound = errors.New("user not found")
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
Domain: Chứa logic nghiệp vụ cốt lõi (User, UserRepository).
Application: Điều phối giữa domain và infra (UserService).
Infrastructure: Implement cụ thể (repo, handler).
Dependency Injection: Truyền repo vào service, service vào handler, đảm bảo tính độc lập.
Best Practices:
Giữ domain thuần túy, không phụ thuộc vào framework (HTTP, DB).
Dùng interface để tách biệt tầng (dễ thay đổi implement).
Đặt code private trong internal/ để tránh lạm dụng.
3. Viết Unit Test & Benchmark với testing Package
Unit Test
Dùng package testing, đặt file test trong cùng thư mục với code, tên file kết thúc bằng _test.go.
Hàm test bắt đầu bằng Test.
Ví dụ Unit Test:
application/user_service_test.go:
package application
import (
"project/internal/domain"
"testing"
)
// MockUserRepository để test
type MockUserRepo struct{}
func (r *MockUserRepo) FindByID(id int) (*domain.User, error) {
if id == 1 {
return &domain.User{ID: 1, Name: "John", Email: "john@example.com"}, nil
}
return nil, domain.ErrUserNotFound
}
func (r *MockUserRepo) Save(user *domain.User) error {
return nil
}
func TestUserService_GetUser(t *testing.T) {
repo := &MockUserRepo{}
service := NewUserService(repo)
tests := []struct {
name string
id int
wantErr bool
}{
{"User exists", 1, false},
{"User not found", 2, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user, err := service.GetUser(tt.id)
if (err != nil) != tt.wantErr {
t.Errorf("GetUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && user.Name != "John" {
t.Errorf("GetUser() got = %v, want John", user.Name)
}
})
}
}
Benchmark
Hàm benchmark bắt đầu bằng Benchmark, dùng vòng lặp với b.N.
Ví dụ Benchmark:
application/user_service_benchmark_test.go:
package application
import "testing"
func BenchmarkUserService_GetUser(b *testing.B) {
repo := &MockUserRepo{}
service := NewUserService(repo)
b.ResetTimer() // Reset thời gian trước khi đo
for i := 0; i < b.N; i++ {
_, _ = service.GetUser(1)
}
}
Chạy Test & Benchmark:
Test: go test ./internal/application -v
Benchmark: go test ./internal/application -bench=.
Coverage: go test ./internal/application -cover
Best Practices:
Table-Driven Tests: Dùng slice struct để test nhiều trường hợp.
Mocking: Tạo mock implement interface để test độc lập.
Benchmark thực tế: Đo hiệu suất với dữ liệu gần giống production.
Coverage: Đặt mục tiêu >80% code coverage.
Tổng kết
Clean Code: Tên rõ ràng, hàm nhỏ, xử lý lỗi chuẩn.
Structuring: Dùng DDD/Clean Architecture với internal/ để tách biệt tầng.
Testing: Viết unit test với mock, đo hiệu suất bằng benchmark.