Go Zero to Hero - Bài 46: Hàm init() trong quản lý Package
Trong ngôn ngữ lập trình Golang, init() là một hàm đặc biệt đóng vai trò chuẩn bị môi trường trước khi chương trình chính thức vận hành. Tuy nhiên, việc sử dụng nó luôn đi kèm với những tranh cãi nhất định trong cộng đồng kiến trúc phần mềm.
1. Bản chất của hàm init()
init() là một hàm đặc biệt được tích hợp sẵn (built-in) trong Go. Nó có những đặc quyền và quy luật riêng biệt mà không một hàm nào khác có được:
- Không có tham số, không có giá trị trả về: Cú pháp duy nhất của nó là
func init() { ... }. - Chạy tự động: Bạn KHÔNG THỂ và KHÔNG ĐƯỢC PHÉP gọi hàm
init()một cách thủ công trong code (ví dụ gọiinit()trongmainsẽ bị trình biên dịch báo lỗi ngay). Trình biên dịch tự động gọi nó. - Chạy một lần duy nhất: Dù một package được import vào 10 file khác nhau trong dự án, hàm
init()của package đó cũng chỉ được chạy đúng 1 lần duy nhất khi ứng dụng khởi động. - Đa phân thân: Bạn có thể khai báo nhiều hàm
init()trong cùng một file, hoặc trong nhiều file khác nhau thuộc cùng một package!
2. Thứ tự thực thi: Cuộc đua lúc khởi động
Đây là câu hỏi kinh điển trong các buổi phỏng vấn kỹ sư Backend Go. Khi bạn gõ go run main.go, điều gì thực sự xảy ra trước khi dòng chữ đầu tiên trong main() được in ra?
Go tuân thủ một trật tự khởi động cực kỳ nghiêm ngặt: Import -> Khai báo biến toàn cục -> init() -> main().
Nếu file main của bạn có import package A, và package A import package B, quy trình sẽ đi từ dưới lên:
- Khởi tạo biến toàn cục của package B -> Chạy
init()của package B. - Khởi tạo biến toàn cục của package A -> Chạy
init()của package A. - Khởi tạo biến toàn cục của package
main. - Chạy
init()của packagemain. - Cuối cùng mới chạy vào hàm
main().
Hãy xem ví dụ nội bộ trong cùng một file:
package main
import "fmt"
// 1. Biến toàn cục được khởi tạo ĐẦU TIÊN
var globalVar = initializeVar()
func initializeVar() string {
fmt.Println("1. Khởi tạo biến toàn cục")
return "Xong!"
}
// 2. Hàm init() chạy THỨ HAI
func init() {
fmt.Println("2. Hàm init() thứ nhất đang chạy")
}
// Có thể khai báo nhiều init() trong cùng một file!
func init() {
fmt.Println("3. Hàm init() thứ hai đang chạy (theo thứ tự từ trên xuống)")
}
// 3. Hàm main chạy CUỐI CÙNG
func main() {
fmt.Println("4. Hàm main() bắt đầu làm việc")
}
3. Ứng dụng thực chiến của init()
Dù mang nhiều tai tiếng, init() vẫn là công cụ không thể thiếu trong một số kịch bản cụ thể của lập trình Backend:
A. Đăng ký Driver (Registry Pattern)
Đây là cách gói database/sql hoạt động. Khi bạn import ẩn _ "github.com/lib/pq", hàm init() của gói này sẽ báo cáo với hệ thống Go: "Này, tôi có khả năng xử lý giao thức 'postgres', hãy ghi danh tôi vào danh sách hỗ trợ nhé!"
B. Pre-compile (Biên dịch trước) các biểu thức chính quy (Regex)
Biên dịch Regex là một tác vụ cực kỳ tốn CPU. Nếu bạn để nó trong luồng xử lý API, API của bạn sẽ bị chậm. Chúng ta dùng init() để biên dịch nó sẵn ngay từ lúc Server mới bật lên.
package validator
import "regexp"
var emailRegex *regexp.Regexp
func init() {
// Compile regex 1 lần duy nhất lúc khởi động server
// MustCompile sẽ gây ra panic ngay lập tức nếu chuỗi regex viết sai cú pháp
emailRegex = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
}
// Hàm này được gọi hàng ngàn lần ở tầng API mà không bị tốn CPU compile lại regex
func IsValidEmail(email string) bool {
return emailRegex.MatchString(email)
}
4. Mặt tối của init(): Tại sao Clean Architecture lại "kỳ thị" nó?
Khi xây dựng các hệ thống lớn, việc lạm dụng init() sẽ sinh ra những "cơn ác mộng" bảo trì:
- Global State (Trạng thái toàn cục): Hàm
init()thường được dùng để gán giá trị cho các biến toàn cục (nhưDBConn,Logger,Config). Trong Clean Architecture, biến toàn cục là kẻ thù số 1 vì nó phá vỡ tính cách ly. - Khó kiểm thử (Unit Test): Khi bạn chạy test cho một package, hàm
init()sẽ tự động nổ. Nếuinit()của bạn chứa lỗi kết nối đến Redis, toàn bộ các Unit Test của bạn sẽ báo lỗi (Crash) nếu lúc đó không có server Redis nào đang chạy. - Che giấu logic (Implicit behavior): Khi một đồng nghiệp mới đọc code, họ nhìn hàm
main()và không hiểu tại sao một module tự nhiên hoạt động được dù chưa ai gọi hàm cài đặt nó. Sự "ma thuật" này làm giảm khả năng đọc hiểu luồng đi của dữ liệu.
💡 Lời khuyên thực chiến: Hãy ưu tiên Explicit Initialization (Khởi tạo tường minh). Thay vì dùng
init()để mở kết nối Database, hãy viết một hàmNewPostgresConnection(), sau đó chủ động gọi nó ở filemain.govà truyền đối tượng DB đó vào các tầng Service. Chỉ dùnginit()cho những tác vụ thiết lập nội bộ của package không dính dáng đến I/O (như compile Regex, thiết lập bảng băm mặc định).
Tổng kết
Bạn chỉ cần nhớ 3 nguyên tắc sinh tử về hàm init():
- Luôn chạy trước
main(): Và chạy sau khi các biến toàn cục đã khởi tạo xong. - Đa phân thân: Một package có thể có nhiều
init(), chúng sẽ chạy theo thứ tự xuất hiện trong file từ trên xuống dưới, và theo thứ tự tên file theo bảng chữ cái. - Hạn chế sử dụng: Để mã nguồn dễ dự đoán và dễ viết Unit Test, hãy khởi tạo tài nguyên một cách tường minh tại
main()thay vì giấu chúng vào bóng tối củainit().
All rights reserved