Đừng Phức Tạp Hóa Vấn Đề: Hãy Chỉ Dùng Go Thôi
Bài viết này lập luận mạnh mẽ rằng Go (Golang) là lựa chọn tối ưu cho phát triển backend nhờ sự đơn giản, thư viện chuẩn mạnh mẽ và khả năng triển khai dễ dàng. Tác giả chỉ trích xu hướng sử dụng các framework và kiến trúc phức tạp không cần thiết trong khi Go cung cấp mọi thứ cần thiết một cách hiệu quả.
Này, hãy tỉnh táo lại đi. Bạn có biết ngôn ngữ nào biên dịch trong hai giây, triển khai dưới dạng một tệp binary duy nhất, và không bị "đứt gãy" khi một dependency phụ thuộc bị xóa khỏi npm lúc 3 giờ sáng không? Đó chính là Go. Cũng giống như HTML đã luôn ở đó từ buổi bình minh của internet chờ đợi bạn ngừng làm phức tạp hóa frontend, Go đã chờ đợi bạn hơn một thập kỷ để ngừng làm phức tạp hóa backend.
Nhưng không. Bạn lại đi gắn kết mười lăm gói Node, ba công cụ build TypeScript, và một cụm Kubernetes chỉ để phục vụ một cái form đơn giản. Bạn thuê một Platform Team chỉ để trông chừng monolith Rails của mình. Bạn thuyết phục CTO rằng Rust là cần thiết cho một ứng dụng CRUD chỉ xử lý khoảng bốn mươi yêu cầu mỗi giây. Chúc mừng nhé, bạn đã tự làm khó chính mình.
Ngôn ngữ này nhàm chán có chủ đích
Bạn có biết tại sao Go cảm thấy nhàm chán không? Bởi vì nó đúng là như vậy, và đó chính là cả vấn đề. Không có decorators. Không có metaclasses. Không có macros. Không có traits, monads, hay bất kỳ sự trừu tượng nào bị nguyền rủa mà đám đông Haskell đang hít hà tuần này. Chỉ có structs, functions, interfaces, goroutines, và channels. Đó là tất cả. Bạn có thể đọc spec trong giờ nghỉ trưa và trở nên năng suất vào buổi chiều.
Nhàm chán nghĩa là nhân viên junior bạn thuê tháng trước có thể đọc code mà principal viết hai năm trước. Chỉ có một cách để định dạng nó và gofmt đã làm việc đó rồi. Đồng nghiệp "thông minh" của bạn không thể lén lút đưa một lớp trừu tượng mười bảy tầng vào codebase vì ngôn ngữ không cho phép. Đó chính là diện mạo của việc "ship" sản phẩm khi không ai đang chảy nước miếng vì sự thông minh của chính mình.
Thư viện chuẩn chính là framework
Đừng tìm kiếm framework nữa, được không?
Thư viện chuẩn chính là framework.
package main
import (
"embed"
"html/template"
"net/http"
)
//go:embed templates/*.html
var files embed.FS
var tmpl = template.Must(template.ParseFS(files, "templates/*.html"))
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl.ExecuteTemplate(w, "index.html", map[string]string{
"Name": "LeHuy",
})
})
http.ListenAndServe(":8080", nil)
}
Đó là một ứng dụng web hoạt động. Các mẫu HTML được biên dịch ngay vào binary. Không cần webpack. Không cần Vite. Không cần "dev server". Không có thư mục node_modules to như một chiếc xe hơi. Bạn chạy go build và ship một tệp tin duy nhất. Thả nó lên server. Xong.
Bạn muốn database? database/sql. JSON? encoding/json. Muốn nói chuyện với service khác? net/http cũng là một client. Muốn làm năm việc cùng lúc? Đặt từ khóa go vào trước nó. Test? go test. Benchmark? go test -bench. Profile? pprof đã ở đó chờ đợi và chế giễu những lần bạn debug bằng console.log.
Thư viện chuẩn thực sự rất sâu sắc
io.Reader và io.Writer là hai giao diện với một phương thức mỗi cái. Chúng cũng là lý do bạn có thể chuyển (pipe) nội dung phản hồi HTTP vào một gzip writer rồi vào một tệp trên đĩa chỉ trong ba dòng code mà không cần suy nghĩ nhiều. Mọi gói nghiêm túc trong hệ sinh thái đều sử dụng chúng. Một khi bạn hiểu điều này, một nửa "phép thuật" của Go hóa ra lại là cùng hai giao diện đó xuất hiện ở khắp mọi nơi.
context.Context là cách bạn hủy bỏ các tác vụ. Người dùng đóng tab trình duyệt, ngữ cảnh yêu cầu bị hủy, truy vấn database bị hủy, và cuộc gọi HTTP hạ nguồn cũng bị hủy. Tất cả theo chuỗi. Không có goroutines bị rò rỉ. Không có các truy vấn zombie "nhau" qua pool kết nối của bạn. Bạn truyền nó làm đối số đầu tiên và tôn trọng nó. Đó là toàn bộ API.
encoding/json, encoding/xml, encoding/csv, encoding/binary, tất cả đều trong thư viện chuẩn. Cùng một mẫu struct tag. Cùng một cách tiện lợi khi decode vào một con trỏ. Học một cái và bạn cơ bản biết hết tất cả.
Độ đồng thời (Concurrency) không khiến bạn phải khóc
Goroutines không phải là threads. Chúng có stack riêng, được multiplex lên các luồng OS bởi runtime, và chi phí khởi tạo khoảng 2KB. Bạn có thể spawn một trăm nghìn cái trên một chiếc laptop. Hãy thử làm điều đó với vòng lặp sự kiện (event loop) của Node và xem nó "sập" thế nào.
Channels là các đường ống có kiểu dữ liệu giữa các goroutines. Bạn gửi ở một đầu, nhận ở đầu kia, và runtime xử lý việc đồng bộ hóa. Nếu bạn cần trạng thái chia sẻ thay thế, sync.Mutex ngay ở đó, và race detector sẽ báo cho bạn biết khi bạn screwed up.
results := make(chan string, len(urls))
for _, url := range urls {
go func(u string) {
resp, _ := http.Get(u)
results <- resp.Status
}(url)
}
for range urls {
fmt.Println(<-results)
}
Đó là một trình fetcher HTTP song song. Không thư viện, không framework, không có nghi thức async/await. Ngôn ngữ làm được điều đó.
Một ví dụ thực tế, không phải Hello World
Đây là một route CRUD đọc từ Postgres và render HTML. Cả thứ đó.
//go:embed templates/*.html
var tmplFS embed.FS
var tmpl = template.Must(template.ParseFS(tmplFS, "templates/*.html"))
type Post struct {
ID int
Title string
Body string
}
func postsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.QueryContext(r.Context(),
"SELECT id, title, body FROM posts ORDER BY id DESC LIMIT 50")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var posts []Post
for rows.Next() {
var p Post
if err := rows.Scan(&p.ID, &p.Title, &p.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
posts = append(posts, p)
}
tmpl.ExecuteTemplate(w, "posts.html", posts)
}
}
Database, templates, và một HTTP handler trong một màn hình. Ngữ cảnh yêu cầu được dẫn dây qua đến truy vấn để một kết nối bị đóng sẽ hủy lệnh SQL. Không ORM, không container DI, không lớp service, không thư mục controllers/ với mười bảy lớp cơ sở trừu tượng. Bạn có thể đọc nó từ trên xuống dưới và biết chính xác nó làm gì.
Các dependency không phá hỏng cuối tuần của bạn
go mod init. Xong. Các dependency của bạn sống trong go.mod và go.sum. Tệp sum là một bản ghi mật mã của những gì bạn thực sự nhận được, vì vậy bạn có thể biết khi nào ai đó cố gắng "left-pad" bạn. Không có thư mục node_modules. Không có sự trôi dạt của lockfile giữa dev và CI. Không có peer dependencies, không có optional dependencies... Chỉ có một tệp liệt kê những gì bạn sử dụng, và một tệp chứng minh bạn nhận được những gì bạn mong đợi.
Bạn muốn build offline? go mod vendor thả mọi thứ vào thư mục vendor/ và toolchain sẽ tự động sử dụng nó. Cả dự án, bao gồm cả dependency, vừa vặn trong một tarball. Đội ngũ bảo mật của bạn sẽ rơi nước mắt vì biết ơn.
Bộ công cụ đi kèm với trình biên dịch
gofmt định dạng code của bạn. Không có tranh luận. Không có "thánh chiến" .prettierrc. Định dạng là định dạng và mọi người đều dùng nó. Các diff của bạn nhỏ gọn vì không ai sắp xếp lại khoảng trắng.
go vet bắt các lỗi rõ ràng. go test chạy test của bạn. go test -race chạy chúng với race detector và tìm các cuộc đua dữ liệu (data races) mà bạn nghĩ mình không có. go test -bench chạy benchmark. go test -cover cho bạn biết những gì bạn đã bỏ sót. go tool pprof cung cấp biểu đồ flame cho việc sử dụng CPU và bộ nhớ từ một dịch vụ sản phẩm đang chạy thông qua một endpoint HTTP mà bạn kết nối chỉ trong hai dòng.
Không có cái nào là bên thứ ba, không cái nào là plugin, và không cái nào yêu cầu tệp cấu hình bạn phải duy trì. Tất cả đều có sẵn trong hộp.
Triển khai là một lệnh copy
Đây là phần khiến đám người làm Rails và Node tức giận về mặt thể chất. Bạn build một binary Go. Bạn copy nó đến server. Bạn chạy nó.
GOOS=linux GOARCH=amd64 go build -o myapp ./cmd/myapp
scp myapp user@server:/usr/local/bin/
ssh user@server 'systemctl restart myapp'
Ba lệnh. Xong. Không Dockerfile. Không build đa giai đoạn. Không cảnh báo CVE về base image mỗi thứ Ba. Không manifest Kubernetes. Không Helm chart. Không ArgoCD. Không service mesh. Không sidecar.
Một binary liên kết tĩnh 12MB và một tệp unit systemd 20 dòng là một bản triển khai sản phẩm. Nó sẽ sống lâu hơn sự nghiệp của bạn. Lý do duy nhất để với tay lấy Docker là nếu đội ngũ ops của bạn bị bắt buộc phải dùng nó theo hợp đồng, và ngay cả khi đó, bạn có thể nhét binary vào một image FROM scratch và coi như xong việc.
"Nhưng Rails / Django / Express / Next thì sao?"
Chúng thì sao? Rails cần một nghi thức triển khai liên quan đến Capistrano, ba tệp cấu hình, và một con dê. Django muốn bạn học ORM của nó, admin của nó, hệ thống middleware của nó, và quan điểm của nó về mọi thứ. Express được giữ lại bởi các cảnh báo npm audit và những lời cầu nguyện. Next.js thay đổi quy ước định tuyến mỗi sáu tháng và gaslight bạn về điều đó.
Binary Go của bạn không quan tâm. Nó đã biên dịch và nó chạy, và nó sẽ vẫn chạy trong năm năm trên phần cứng chưa tồn tại. Framework của bạn? Sẽ bị lỗi thời vào Giáng sinh, và người bảo trì sẽ viết một bài đăng trên Medium về kiệt sức.
"Nhưng microservices!"
Không.
Hãy viết cái monolith đi. Một binary Go. Một Postgres. Một Redis nếu bạn thực sự bắt buộc. Phục vụ HTML trên cùng cổng phục vụ JSON API của bạn. Chạy nó trên một VPS duy nhất tốn ít hơn ngân sách sữa yến mạch hàng tháng của bạn. Mở rộng quy mô lên mười nghìn yêu cầu mỗi giây mà không toát mồ hôi vì Go được thiết kế cho việc này và goroutines rẻ như bèo.
Khi bạn thực sự cần chia nhỏ nó ra, và bạn sẽ không cần đâu, việc chia nhỏ một monolith Go chỉ là chuyển các gói vào repo riêng của chúng. Các giao diện đã ở đó rồi. Bạn đã thiết kế cho nó mà không cố gắng, vì ngôn ngữ đã buộc bạn làm thế.
"Nhưng generics! Nhưng xử lý lỗi! Nhưng không có exceptions!"
if err != nil là tính năng, không phải lỗi. Nó buộc bạn phải nhìn vào mọi nơi có thể sai sót và quyết định làm gì với nó. Địa ngục lồng nhau try/catch của bạn không làm lỗi biến mất, nó chỉ ẩn chúng cho đến khi ra sản phẩm lúc 2 giờ sáng.
Generics đã có mặt từ phiên bản 1.18. Chúng ổn thôi. Dùng khi bạn cần. Đừng than vãn nữa.
Hãy chỉ dùng Go thôi
Đừng giả vờ bạn cần một framework. Bạn cũng không cần microservices, hay việc viết lại bằng Rust, hay bất kỳ meta-framework JavaScript nào ra mắt thứ Ba tuần trước sẽ cứu bạn trong khi sáu cái trước không làm được.
Mở editor của bạn. Chạy go mod init, viết một main.go, nhúng các template của bạn, và biên dịch. Ship cái thứ đó đi.
Lựa chọn nhàm chán là lựa chọn đúng đắn. Nó luôn như vậy.
Bài viết liên quan
Phần mềm
Lo ngại về Bun: Liệu sự suy giảm của Claude Code có phải là điềm báo cho tương lai của runtime này?
04 tháng 5, 2026

Phần mềm
Google phát hành Chrome 148, vá 127 lỗ hổng bảo mật bao gồm các lỗi nghiêm trọng
07 tháng 5, 2026

Phần mềm
Tấn công chuỗi cung ứng WordPress: Kẻ tấn công mua 30 plugin trên Flippa và cài cửa sau
06 tháng 5, 2026
