Optimize Code and Best Practice


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
}

application/user_service.go:

package application

import "project/internal/domain"

type UserService struct {
    repo domain.UserRepository
}

func NewUserService(repo domain.UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) GetUser(id int) (*domain.User, error) {
    user, err := s.repo.FindByID(id)
    if err != nil {
        return nil, err
    }
    return user, nil
}

func (s *UserService) CreateUser(name, email string) (*domain.User, error) {
    user := &domain.User{Name: name, Email: email}
    if err := s.repo.Save(user); err != nil {
        return nil, err
    }
    return user, nil
}

infrastructure/repository/user_repository.go:

package repository

import "project/internal/domain"

type InMemoryUserRepo struct {
    users map[int]*domain.User
}

func NewInMemoryUserRepo() *InMemoryUserRepo {
    return &InMemoryUserRepo{users: make(map[int]*domain.User)}
}

func (r *InMemoryUserRepo) FindByID(id int) (*domain.User, error) {
    user, exists := r.users[id]
    if !exists {
        return nil, domain.ErrUserNotFound
    }
    return user, nil
}

func (r *InMemoryUserRepo) Save(user *domain.User) error {
    user.ID = len(r.users) + 1 // Giả lập ID
    r.users[user.ID] = user
    return nil
}

infrastructure/handler/user_handler.go:

package handler

import (
    "net/http"
    "project/internal/application"
    "strconv"
)

type UserHandler struct {
    service *application.UserService
}

func NewUserHandler(service *application.UserService) *UserHandler {
    return &UserHandler{service: service}
}

func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
    idStr := r.URL.Query().Get("id")
    id, _ := strconv.Atoi(idStr)
    user, err := h.service.GetUser(id)
    if err != nil {
        http.Error(w, err.Error(), http.StatusNotFound)
        return
    }
    w.Write([]byte(user.Name + " - " + user.Email))
}

cmd/server/main.go:

package main

import (
    "net/http"
    "project/internal/application"
    "project/internal/infrastructure/handler"
    "project/internal/infrastructure/repository"
)

func main() {
    repo := repository.NewInMemoryUserRepo()
    service := application.NewUserService(repo)
    handler := handler.NewUserHandler(service)

    http.HandleFunc("/user", handler.GetUser)
    http.ListenAndServe(":8080", nil)
}

Phân tích:

  • 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.

Last updated