Từ KISS, SOLID đến Idempotency: Giải mã các thuật ngữ kinh điển dưới lăng kính Go Backend

Từ KISS, SOLID đến Idempotency: Giải mã các thuật ngữ kinh điển dưới lăng kính Go Backend

Mục lục

Nếu từng tham gia một buổi phỏng vấn backend hoặc ngồi thảo luận technical design cho một service mới, bạn sẽ nhận ra một hiện tượng quen thuộc: căn phòng tràn ngập các từ viết tắt. Từ KISS, DRY, YAGNI cho đến 5 chữ cái SOLID kinh điển, rồi nhảy sang Idempotency, Backpressure và Circuit Breaker.

Những thuật ngữ này xuất hiện dày đặc đến mức người ta bắt đầu dùng chúng như những đòn bẩy quyền lực trong tranh luận. Một kỹ sư muốn bảo vệ đoạn code 10 tầng trừu tượng của mình sẽ lôi Liskov Substitution ra làm lá chắn. Một người khác muốn phản đối việc tạo interface sẽ trích dẫn KISS hoặc YAGNI.

Nhưng có một khoảng cách thực tế rất lớn giữa việc đọc thuộc lòng định nghĩa trên Wikipedia và việc viết ra những dòng code chạy bền bỉ lúc 2 giờ sáng trên production. Hầu hết các nguyên lý thiết kế kinh điển được đúc kết từ thời kỳ hoàng kim của OOP thuần túy (C++, Java, C#) vào những năm 1990 - 2000. Trong khi đó, Golang ra đời tại Google chính từ sự mệt mỏi với sự cồng kềnh, phân cấp phức tạp và thời gian biên dịch vô tận của chính những hệ thống OOP đó.

Khi mang nguyên xi tư duy hướng đối tượng truyền thống áp đặt vào Go, bạn không có được một hệ thống “sạch” (Clean Architecture). Thứ bạn nhận được là một codebase dị dạng: Java được viết bằng cú pháp Go.

Bài viết này mổ xẻ lại những thuật ngữ quen thuộc nhất mà bất kỳ backend engineer nào cũng sẽ va phải, dưới góc nhìn trần trụi và thực dụng của Golang.


1. Bộ ba vỡ lòng: KISS, DRY và YAGNI

Ba chữ này thường được coi là bài học nhập môn của nghề lập trình. Nhưng trớ trêu thay, chúng cũng là ba khái niệm bị lạm dụng và hiểu sai nhiều nhất.

KISS (Keep It Simple, Stupid) và cái bẫy “Clever Code”

Rob Pike từng có một câu châm ngôn kinh điển về Go: “Clear is better than clever” (Rõ ràng tốt hơn thông minh).

KISS trong thế giới Go không có nghĩa là viết code cẩu thả, bỏ qua xử lý lỗi hay nhét toàn bộ logic 2.000 dòng vào một hàm main. Đơn giản ở đây là tính tường minh (explicitness). Một đoạn code đơn giản là đoạn code mà một kỹ sư trực on-call lúc nửa đêm, sau khi bị đánh thức bởi alert PagerDuty, có thể đọc từ trên xuống dưới và hiểu ngay luồng thực thi mà không cần phải nhảy qua 8 file interface khác nhau trong IDE.

Cái bẫy lớn nhất của các kỹ sư mới chuyển sang Go là cố gắng viết “clever code”. Trước khi Go 1.18 có generic, nhiều người cố gắng giả lập generic bằng cách dùng interface{} (nay là any) kết hợp với package reflect.

Warning

Mỗi lần bạn import package reflect chỉ để né việc viết lặp lại vài dòng code kiểm tra struct, bạn đang đánh đổi hai thứ quý giá nhất của Go: kiểm tra kiểu tĩnh ở compile-time và hiệu năng thực thi.

Hãy nhìn vào hai cách tiếp cận khi trích xuất danh sách ID của merchant từ một slice đơn hàng:

  • Viết 'thông minh' bằng Reflection
  • KISS kiểu Go (Tường minh & Đơn giản)
// Tỏ ra thông minh: Cố viết một hàm Pluck generic bằng reflection
// Nhược điểm: Runtime panic nếu sai kiểu, tốn allocations, chậm hơn 20-50 lần
func PluckIDs(slice any, fieldName string) ([]int64, error) {
    val := reflect.ValueOf(slice)
    if val.Kind() != reflect.Slice {
        return nil, errors.New("input must be a slice")
    }
    
    var ids []int64
    for i := 0; i < val.Len(); i++ {
        item := val.Index(i)
        field := item.FieldByName(fieldName)
        if !field.IsValid() || field.Kind() != reflect.Int64 {
            return nil, fmt.Errorf("invalid field %s", fieldName)
        }
        ids = append(ids, field.Int())
    }
    return ids, nil
}
// Tường minh, an toàn, zero-allocation ngoài slice kết quả
func ExtractMerchantIDs(orders []Order) []int64 {
    if len(orders) == 0 {
        return nil
    }
    ids := make([]int64, 0, len(orders))
    for _, o := range orders {
        ids = append(ids, o.MerchantID)
    }
    return ids
}

Cách thứ nhất nhìn có vẻ “tái sử dụng được cho mọi struct”. Nhưng nó tiềm ẩn lỗi runtime nếu ai đó đổi tên trường từ MerchantID thành PartnerID, và compiler hoàn toàn bất lực trong việc cảnh báo. Cách thứ hai dài hơn một chút nếu phải viết cho từng struct, nhưng nó an toàn 100%, chạy nhanh như chớp và bất kỳ ai đọc vào cũng hiểu ngay.

DRY (Don’t Repeat Yourself) vs “A Little Copying”

DRY là nguyên lý bị biến thành thứ tôn giáo độc hại nhất trong ngành phần mềm. Rất nhiều lập trình viên mắc chứng “ám ảnh cưỡng chế với code trùng lặp”: hễ thấy 3 dòng code giống nhau ở hai chỗ là ngay lập tức tạo hàm dùng chung, tạo package common hay pkg/utils.

Hậu quả là gì? Một package utils biến thành cái bãi rác chứa từ format tiền tệ, parse JWT, hàm tính khoảng cách tọa độ GPS cho tới hàm retry HTTP call. Khi một package dùng chung bị sửa, 10 service khác kéo theo nguy cơ vỡ logic.

Trong Go Proverbs, có một câu nói trực tiếp tát vào mặt tư duy DRY cực đoan:

“A little copying is better than a little dependency.” (Sao chép một chút còn tốt hơn phụ thuộc một chút.)

Trong thiết kế hệ thống, cần phân biệt rạch ròi giữa hai khái niệm:

  1. Essential Duplication (Trùng lặp bản chất): Hai đoạn code trùng nhau vì chúng cùng phản ánh một quy luật toán học hoặc một định luật vật lý (ví dụ: thuật toán hash SHA-256, công thức tính thuế VAT). Đây là thứ nên gom lại.
  2. Accidental Duplication (Trùng lặp ngẫu nhiên): Hai đoạn code hôm nay trông giống hệt nhau, nhưng thuộc về hai domain nghiệp vụ khác nhau và sẽ tiến hóa theo hai hướng hoàn toàn độc lập trong tương lai.

Ví dụ kinh điển trong backend: DTO request của API tạo cửa hàng (CreateMerchantRequest) và API cập nhật thông tin cửa hàng (UpdateMerchantRequest). Ban đầu, chúng đều có Name, Address, Phone. Nếu bạn gộp chúng làm một struct chung chỉ để “thỏa mãn DRY”, bạn sẽ sớm nếm mùi đau khổ khi nghiệp vụ cập nhật cho phép sửa từng trường (partial update / patch), trong khi tạo mới bắt buộc nhập đủ. Struct dùng chung lúc này sẽ mọc thêm hàng tá tag validation phức tạp và hàng loạt câu lệnh if-else dị hợm.

YAGNI (You Aren’t Gonna Need It)

YAGNI là liều thuốc giải độc cho căn bệnh over-engineering. Rất nhiều kỹ sư dành cả tuần để xây dựng một kiến trúc hỗ trợ:

  • Đổi từ MySQL sang MongoDB chỉ bằng một dòng config.
  • Đổi message broker từ RabbitMQ sang Kafka hoặc NATS thông qua một abstract adapter layer.
  • Xây dựng hệ thống dynamic plugin cho một tính năng mà sản phẩm chưa có nổi 100 người dùng tích cực.

Sự thật phũ phàng là: 99% trường hợp bạn sẽ không bao giờ đổi cơ sở dữ liệu. Khi bạn thực sự cần đổi từ MySQL sang MongoDB sau 5 năm nữa, toàn bộ data access pattern và business domain của bạn đã thay đổi đến mức cái abstract layer bạn viết từ ngày đầu sẽ trở thành chướng ngại vật thay vì cứu tinh.

Nguyên tắc thực tế: Hãy áp dụng Rule of Three. Lần đầu tiên viết code, cứ viết thẳng. Lần thứ hai tương tự, cứ copy hoặc viết lại tương tự. Đến lần thứ ba xuất hiện cùng một pattern, đó mới là lúc thích hợp để dừng lại và trừu tượng hóa.


2. Mổ xẻ SOLID dưới góc nhìn Go: Khi không có Class và Extends

SOLID do Robert C. Martin (Uncle Bob) tổng hợp. Nhưng hãy nhớ: các ví dụ kinh điển của SOLID hầu như đều xoay quanh phân cấp thừa kế (class Shape, class Circle extends Shape).

Go không có từ khóa class. Go không có từ khóa extends. Go không có nạp chồng hàm (method overloading). Vậy SOLID trong Go trông như thế nào?

graph TD
    subgraph "OOP Truyền thống (Java / C#)"
        ClassA[Class Cha] -->|Kế thừa extends| ClassB[Class Con]
        InterfaceA[Fat Interface 20 methods] -.->|implements| ClassB
    end

    subgraph "Go Idiomatic (Composition & Duck Typing)"
        StructA[Struct Nhỏ] -->|Embedding| StructB[Struct Lớn]
        StructB -.->|Thỏa mãn ngầm định| SmallInterface["Small Interface (1-2 methods)"]
    end

S: Single Responsibility Principle (SRP)

Ở tầng code, SRP khuyên một hàm hoặc một struct chỉ nên làm một việc. Nhưng trong Go, đơn vị ranh giới quan trọng nhất là Package.

Một lỗi phổ biến của dev từ Java sang Go là tạo package theo tầng kỹ thuật:

  • controllers/
  • services/
  • models/
  • repositories/

Cấu trúc này dẫn đến việc package models chứa tất cả các struct trên đời, biến thành điểm phụ thuộc vòng (circular dependency) kinh hoàng.

Trong Go idiomatic, ta tổ chức theo Package theo Domain / Module:

  • pkg/merchant/: Chứa struct Merchant, interface lưu trữ, và logic nghiệp vụ của merchant.
  • pkg/order/: Chứa struct Order và logic đơn hàng.

Mỗi package chịu trách nhiệm cho một domain nhất định. Khi nhìn vào file của package order, bạn không phải cuộn qua các struct không liên quan của merchant hay payment.

O: Open/Closed Principle (OCP)

Nguyên lý: “Mở cho mở rộng, đóng cho sửa đổi”. Trong Java, người ta dùng kế thừa đa hình (Polymorphism via Inheritance). Trong Go, ta dùng Composition over Inheritance và Functional Options Pattern.

Hãy xem bài toán cấu hình một gRPC Client kết nối tới microservice Merchant:

type Client struct {
    endpoint string
    timeout  time.Duration
    retries  int
    logger   *slog.Logger
}

// Option đại diện cho một hàm cấu hình
type Option func(*Client)

func WithTimeout(d time.Duration) Option {
    return func(c *Client) {
        c.timeout = d
    }
}

func WithRetries(n int) Option {
    return func(c *Client) {
        c.retries = n
    }
}

func NewClient(endpoint string, opts ...Option) *Client {
    // Giá trị mặc định hợp lý (KISS)
    client := &Client{
        endpoint: endpoint,
        timeout:  5 * time.Second,
        retries:  3,
        logger:   slog.Default(),
    }
    for _, opt := range opts {
        opt(client)
    }
    return client
}

Khi bạn cần thêm tính năng mới (ví dụ WithMetrics), bạn không cần sửa hàm NewClient, không làm gãy code của 50 caller hiện tại, và không cần tạo ra một constructor cồng kềnh NewClientWithTimeoutAndRetriesAndMetrics. Đó chính là OCP thuần khiết theo phong cách Go.

L: Liskov Substitution Principle (LSP)

LSP phát biểu rằng các đối tượng con phải có thể thay thế đối tượng cha mà không làm thay đổi tính đúng đắn của chương trình.

Trong Go, vì không có quan hệ cha - con, LSP được thể hiện qua Structural Typing (Duck Typing). Một kiểu dữ liệu thỏa mãn một interface một cách ngầm định (implicitly) ngay khi nó triển khai đủ các method của interface đó, không cần khai báo implements.

Tuy nhiên, cạm bẫy LSP trong Go thường nằm ở hành vi ngầm (behavioral invariants). Nếu một struct triển khai interface io.Reader:

type Reader interface {
    Read(p []byte) (n int, err error)
}

Nhưng khi gặp cuối stream, thay vì trả về io.EOF, implementation của bạn lại trả về nil, nil hoặc panic, thì dù code vẫn compile được, bạn đã vi phạm trắng trợn LSP. Bất kỳ hàm nào nhận io.Reader chuẩn đều sẽ rơi vào vòng lặp vô tận (infinite loop) vì vi phạm contract hành vi.

I: Interface Segregation Principle (ISP)

Đây là nơi Golang tỏa sáng rực rỡ nhất trong toàn bộ thế giới lập trình.

Châm ngôn Go khẳng định:

“The bigger the interface, the weaker the abstraction.” (Interface càng lớn, tính trừu tượng càng yếu.)

Hãy nhìn vào thư viện chuẩn của Go. Interface thành công nhất lịch sử ngành phần mềm chỉ có đúng một method:

  • io.Reader: Read(p []byte) (n int, err error)
  • io.Writer: Write(p []byte) (n int, err error)
  • http.Handler: ServeHTTP(ResponseWriter, *Request)

So sánh điều này với anti-pattern kinh điển của các dự án Go “bị Java hóa”:

// ANTI-PATTERN: Interface phình to như voi
type MerchantRepository interface {
    GetByID(ctx context.Context, id int64) (*Merchant, error)
    GetByCode(ctx context.Context, code string) (*Merchant, error)
    List(ctx context.Context, filter Filter) ([]*Merchant, error)
    Create(ctx context.Context, m *Merchant) error
    Update(ctx context.Context, m *Merchant) error
    Delete(ctx context.Context, id int64) error
    BatchUpdateStatus(ctx context.Context, ids []int64, status int) error
    // ... thêm 15 methods khác
}

Hậu quả: Khi bạn viết một hàm nhỏ chỉ làm nhiệm vụ kiểm tra xem cửa hàng có đang mở cửa không (CheckMerchantOpen), và hàm đó nhận MerchantRepository, bạn vô tình kéo theo sự phụ thuộc vào cả 20 method kia. Đến khi viết unit test, bạn phải mock toàn bộ 20 method dù chỉ dùng đúng 1 cái!

Quy tắc vàng của Go: Hãy định nghĩa interface ở nơi tiêu thụ (consumer), không phải ở nơi triển khai (producer).

// Tốt: Service Order chỉ cần lấy thông tin cơ bản của Merchant, 
// định nghĩa interface nhỏ gọn ngay tại package order
type MerchantFinder interface {
    GetByID(ctx context.Context, id int64) (*MerchantInfo, error)
}

type OrderService struct {
    merchants MerchantFinder
}

D: Dependency Inversion Principle (DIP)

DIP yêu cầu các module cấp cao không nên phụ thuộc vào module cấp thấp; cả hai nên phụ thuộc vào abstractions.

Trong Go, câu thần chú kinh điển của các lập trình viên kỳ cựu là:

“Accept interfaces, return structs.” (Nhận vào interface, trả về struct.)

// Trả về struct cụ thể
func NewPostgresMerchantRepo(db *sql.DB) *PostgresMerchantRepo {
    return &PostgresMerchantRepo{db: db}
}

// Nhận vào interface tối thiểu cần thiết
func NewOrderService(merchants MerchantFinder, repo OrderRepository) *OrderService {
    return &OrderService{
        merchants: merchants,
        repo:      repo,
    }
}

Tại sao Go không chuộng các thư viện Dependency Injection (DI) phức tạp dựa trên reflection (như Spring Bean hay Guice)? Vì trong Go, việc khởi tạo phụ thuộc bằng code thuần (Manual Dependency Injection) trong hàm main() hoặc file wire.go rõ ràng hơn hàng trăm lần:

func main() {
    db := initDB()
    merchantRepo := repository.NewPostgresMerchantRepo(db)
    orderRepo := repository.NewPostgresOrderRepo(db)
    
    orderSvc := service.NewOrderService(merchantRepo, orderRepo)
    handler := http.NewOrderHandler(orderSvc)
    
    // Rõ ràng, dễ đọc, trace stack thẳng tuột!
}

3. Nhóm thuật ngữ Vận hành & Hệ thống phân tán

Nếu các thuật ngữ ở trên giúp code của bạn dễ đọc và dễ bảo trì, thì nhóm thuật ngữ dưới đây quyết định xem hệ thống của bạn có sống sót qua đợt flash sale hay không.

Idempotency (Tính lũy đẳng)

Trong thế giới mạng phân tán, giao tiếp qua HTTP hay Message Broker đều chịu sự chi phối của định lý: Mạng không bao giờ đáng tin cậy.

Khi một mobile app gửi request tạo đơn hàng hoặc thanh toán lên server, kết nối 4G có thể bị đứt đúng vào mili-giây mà server vừa ghi database xong nhưng chưa kịp trả response về cho client. Ứng dụng client sẽ tưởng là lỗi timeout và tự động gửi lại request lần thứ hai (retry).

Nếu backend của bạn không có Tính lũy đẳng (Idempotency), khách hàng sẽ bị trừ tiền hai lần cho cùng một món ăn.

sequenceDiagram
    autonumber
    actor Client
    participant Gateway as API Gateway
    participant Redis as Redis (Deduplication)
    participant OrderSvc as Order Service
    participant DB as PostgreSQL

    Client->>Gateway: POST /orders (Header: Idempotency-Key: abc-123)
    Gateway->>Redis: SET order:idempotency:abc-123 LOCKED NX EX 60
    alt Key đã tồn tại (Request trùng lặp)
        Redis-->>Gateway: FALSE (Key already exists)
        Gateway-->>Client: 409 Conflict hoặc Trả kết quả đã lưu
    else Key mới (First attempt)
        Redis-->>Gateway: OK
        Gateway->>OrderSvc: CreateOrder()
        OrderSvc->>DB: INSERT INTO orders ...
        DB-->>OrderSvc: Order #999 created
        OrderSvc->>Redis: SET order:idempotency:abc-123 RESULT EX 86400
        OrderSvc-->>Client: 201 Created (Order #999)
    end

Tip

Công thức thực chiến cho Idempotency:

  1. Yêu cầu client sinh một UUID định danh cho thao tác (Idempotency-Key).
  2. Sử dụng Redis SET key value NX EX timeout để tạo một distributed lock ngắn hạn chống race-condition khi hai request bắn tới cùng một tích tắc.
  3. Sử dụng Unique Constraint trong Database (order_ref_id UNIQUE) làm chốt chặn cuối cùng.

Backpressure và Concurrency Control

Một trong những lý do khiến Go trở nên phổ biến là Goroutine. Khởi tạo một Goroutine chỉ tốn khoảng 2KB stack ban đầu, so với 1MB của một thread trong Java hay C++.

Chính vì Goroutine quá rẻ, dev Go rất hay mắc bẫy: cứ có request tới là dùng từ khóa go func() để đẩy việc ra chạy nền.

// ANTI-PATTERN: Tự sát bằng Goroutine không kiểm soát
func HandleWebhook(w http.ResponseWriter, r *http.Request) {
    payload := parse(r)
    // Nguy hiểm: Nếu traffic spike lên 50.000 req/s, 
    // 50.000 goroutine đồng thời đâm vào database => sập pool!
    go processPayload(payload)
    w.WriteHeader(http.StatusOK)
}

Khi có sự cố nghẽn mạng ở hạ tầng hạ nguồn (database bị lock hoặc dịch vụ bên thứ ba phản hồi chậm), 50.000 goroutine sẽ bị kẹt lại, tiêu tốn RAM, tạo áp lực khủng khiếp lên Garbage Collector và cuối cùng dẫn đến crash toàn bộ tiến trình (OOM).

Để khắc phục, bạn cần cơ chế Backpressure (Áp lực ngược) và Worker Pool. Hệ thống phải có khả năng từ chối hoặc xếp hàng tác vụ khi năng lực xử lý đạt giới hạn:

type JobQueue struct {
    jobs chan Job
}

func NewJobQueue(workers int, queueSize int) *JobQueue {
    q := &JobQueue{
        jobs: make(chan Job, queueSize),
    }
    for i := 0; i < workers; i++ {
        go q.worker()
    }
    return q
}

func (q *JobQueue) Submit(job Job) bool {
    select {
    case q.jobs <- job:
        return true // Đưa vào hàng đợi thành công
    default:
        return false // Hàng đợi đã đầy => Báo lỗi 429 hoặc 503 (Backpressure)
    }
}

Circuit Breaker & Fail-Fast

Nguyên lý Fail-Fast yêu cầu: Nếu một request chắc chắn sẽ thất bại, hãy làm cho nó thất bại ngay lập tức ở cửa ngõ, đừng lãng phí tài nguyên CPU, thread và connection để xử lý tiếp. Kiểm tra token JWT hết hạn chưa? Validate request body có thiếu trường không? Nếu lỗi, trả về HTTP 400/401 ngay tại middleware.

Nhưng khi một microservice bên thứ ba (như cổng thanh toán VNPAY/Momo) bị tê liệt, các request từ service của bạn gọi sang sẽ bị treo hàng loạt cho đến khi chạm timeout (ví dụ 10 giây). Hàng ngàn request bị treo đồng nghĩa với việc connection pool cạn kiệt, kéo theo toàn bộ hệ thống của bạn sụp đổ dây chuyền (Cascading Failure).

Circuit Breaker (Cầu dao ngắt mạch) là giải pháp phòng vệ:

  • Closed: Trạng thái bình thường, request đi qua.
  • Open: Khi tỷ lệ lỗi vượt ngưỡng (ví dụ 50% request lỗi trong 10 giây), cầu dao “bật mở”. Mọi request tiếp theo sẽ bị trả lỗi ngay lập tức (Fail-Fast) mà không thèm gọi sang third-party.
  • Half-Open: Sau một khoảng thời gian chờ (cooldown), cho một vài request thử nghiệm đi qua. Nếu thành công, đóng cầu dao lại; nếu vẫn lỗi, tiếp tục mở.

Trong Go, bạn có thể tích hợp thư viện nhẹ nhàng như sony/gobreaker thay vì phải dựng cả một cụm Hystrix hay Envoy cồng kềnh.

Graceful Shutdown (Tắt máy êm ái)

Trong kỷ nguyên Kubernetes, các pod bị terminate và scale up/down liên tục mỗi ngày. Khi Kubernetes gửi tín hiệu SIGTERM tới ứng dụng, nếu container chết ngay lập tức, toàn bộ các request đang xử lý dở dang (in-flight requests) sẽ bị ngắt kết nối đột ngột, sinh ra mã lỗi 502 Bad Gateway trên reverse proxy.

Mọi ứng dụng Go backend nghiêm túc đều bắt buộc phải có cơ chế Graceful Shutdown:

func main() {
    server := &http.Server{Addr: ":8080", Handler: router}

    go func() {
        if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            slog.Error("server error", "error", err)
        }
    }()

    // Lắng nghe tín hiệu dừng từ OS (Ctrl+C hoặc Kubernetes SIGTERM)
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    slog.Info("Shutting down server gracefully...")

    // Cho phép tối đa 10 giây để xử lý nốt các request đang dang dở
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        slog.Error("Server forced to shutdown:", "error", err)
    }

    // Đóng tiếp Database connection pool, flush Redis log...
    db.Close()
    slog.Info("Server exited cleanly.")
}

4. Bảng đối chiếu: Tư duy Cũ vs Cách hiểu chuẩn Go

Để dễ dàng tham chiếu trong công việc hàng ngày, bảng dưới đây tóm tắt sự khác biệt giữa tư duy lý thuyết OOP kinh điển và cách áp dụng thực dụng trong Go:

Thuật ngữTư duy cũ (OOP / Java)Cách hiểu chuẩn thực dụng trong Go
KISSViết code không cần suy nghĩ về kiến trúc.Tường minh (Explicit). Thà viết thêm vài dòng code dễ đọc còn hơn dùng reflection làm trò “ảo thuật”.
DRYHễ thấy code giống nhau là gom thành hàm/package dùng chung.Chấp nhận trùng lặp ngẫu nhiên. “A little copying is better than a little dependency.”
YAGNIViết sẵn tầng abstract database phòng trường hợp sau này đổi DB.Chỉ trừu tượng hóa khi xuất hiện use-case lặp lại thứ 3 (Rule of Three).
SRPMột class chỉ làm một việc. Tạo ra hàng chục class nhỏ li ti.Package Cohesion. Package gom nhóm logic theo business domain, không chia theo tầng kỹ thuật.
OCPMở rộng tính năng bằng cách tạo class con kế thừa class cha.Dùng Composition, Struct Embedding và Functional Options Pattern.
ISPTạo interface to rồi chia nhỏ thành các interface con kế thừa nhau.Interface siêu nhỏ (1-2 methods). Interface do Consumer định nghĩa, không phải Producer.
DIPDùng các container DI ma thuật quét annotation và tiêm dependency.“Accept interfaces, return structs”. Khởi tạo tường minh bằng constructor thuần.
IdempotencyTrách nhiệm của client, client không được bấm nút 2 lần.Trách nhiệm của backend. Mọi request làm thay đổi trạng thái nhạy cảm đều phải có Idempotency-Key.
Fail-FastBắt mọi exception bằng try-catch ở tầng ngoài cùng.Kiểm tra lỗi ngay tại cửa ngõ. Dùng Circuit Breaker để cô lập lỗi từ dịch vụ ngoài.

Lời kết: Đừng dùng búa tạ để đập một hạt dẻ

Thuật ngữ và nguyên lý sinh ra để giúp các kỹ sư có cùng một từ vựng chung (shared vocabulary) khi thảo luận về giải pháp kỹ thuật, chứ không phải để biến chúng thành những thước đo tôn giáo nhằm phán xét code của đồng nghiệp.

Điểm hấp dẫn nhất của Golang chính là tính thực dụng (pragmatism). Go không sinh ra để giành giải nhất trong cuộc thi “Ngôn ngữ biểu cảm nhất” hay “Ngôn ngữ trừu tượng tinh vi nhất”. Go được thiết kế để giải quyết bài toán kỹ thuật phần mềm ở quy mô lớn: hàng triệu dòng code, hàng ngàn kỹ sư cùng đóng góp, và hệ thống phải biên dịch trong vài giây rồi chạy ổn định suốt nhiều năm mà không cần khởi động lại.

Trước khi bạn chuẩn bị tách thêm một tầng interface hay tạo một pattern trừu tượng mới, hãy dừng lại 10 giây và tự hỏi bản thân: Liệu đoạn code này có đang giúp hệ thống giải quyết vấn đề tốt hơn, hay nó chỉ đang thỏa mãn cái tôi của người viết?

Chia sẻ :

Bài viết liên quan

System Design cho Advanced Beginners: Giải phẫu hạ tầng từ monolith đơn giản đến hệ thống quy mô lớn

System Design cho Advanced Beginners: Giải phẫu hạ tầng từ monolith đơn giản đến hệ thống quy mô lớn

Bạn đã từng viết vài cái website nhỏ. Bạn biết dựng một CRUD app bằng Django, Laravel hay Gin trong một buổi chiều. Nhưng rồi bạn nhìn vào kiến trúc của một nền tảng thật như Stripe, GitHub hay một sàn thương mại điện tử, và tự hỏi: rốt cuộc phía sau có gì mà chúng nó chạy được cho hàng chục triệu người dùng?

Đọc thêm
Golang tắt Nagle's Algorithm mặc định: Cú lừa hiệu năng trên mạng chập chờn

Golang tắt Nagle's Algorithm mặc định: Cú lừa hiệu năng trên mạng chập chờn

Chào các bạn, hôm nay mình muốn chia sẻ với các bạn một câu chuyện dở khóc dở cười mà mình và nhiều anh em làm hệ thống đã từng nếm trải.

Đọc thêm
API Filtering: gọi dữ liệu như dân sành cà phê

API Filtering: gọi dữ liệu như dân sành cà phê

Năm đầu đi làm, mình từng viết một endpoint GET /api/menus trả về… toàn bộ menu. 200 items mỗi lần gọi. JSON nặng 1.2MB. Frontend chỉ cần tên + giá của 10 món đang active. Mình nhớ câu đầu tiên anh lead nói: “Mày đang gửi cả cái kho hàng cho người ta chỉ cần xem menu đấy à?”

Đọc thêm