Go chạy một loạt thứ trước khi main() bắt đầu. Bài này in ra đúng thứ tự đó.

Thứ tự thật

Tôi đặt một package kho được main import, mỗi nơi có biến khởi tạo bằng hàm và có init():

  biến kho.A
  init() của package kho
  biến main.X
  init() thứ nhất của main
  init() thứ hai của main
  main() chạy

Quy tắc rút ra:

Package được import chạy xong hoàn toàn trước. Cả biến lẫn init() của kho đều xong trước khi main bắt đầu khởi tạo bất cứ thứ gì.

Trong một package: biến trước, init() sau.

Một package có nhiều init() được — kể cả nhiều init() trong cùng một tệp. Chúng chạy theo thứ tự xuất hiện, và giữa các tệp thì theo thứ tự tên tệp mà trình biên dịch nhận được.

Chi tiết cuối là lý do đầu tiên để tránh init(): thứ tự giữa các tệp phụ thuộc vào tên tệp, và đó là thứ không ai nên dựa vào.

Biến package khởi tạo theo phụ thuộc

var X = khoiTao("main.X")
var Y = X + 1

Go không khởi tạo theo thứ tự khai báo — nó phân tích phụ thuộc và chạy theo thứ tự đúng. Y phụ thuộc X nên X chạy trước, dù bạn có viết Y lên trên.

Phụ thuộc vòng giữa các biến package là lỗi biên dịch, không phải lỗi lúc chạy. Đây là điểm Go làm tốt hơn Java, nơi trường tĩnh phụ thuộc vòng cho ra giá trị null im lặng.

Import chỉ để chạy init()

import _ "github.com/lib/pq"

Dấu _ nghĩa là "import package này nhưng không dùng tên của nó". Mục đích duy nhất: chạy init() của nó.

Đây là mẫu chuẩn để đăng ký driver:

import _ "github.com/lib/pq"        // driver PostgreSQL tự đăng ký
db, _ := sql.Open("postgres", dsn)  // rồi tìm được bằng tên

Cùng cơ chế với image/png, image/jpeg — import để thêm khả năng giải mã.

Nhược điểm rất thật: phụ thuộc trở nên vô hình. Xoá dòng import vì thấy "không dùng" là chương trình chết lúc chạy với unknown driver. Luôn thêm comment giải thích.

Vì sao nên tránh init()

Bốn lý do, theo thứ tự tôi thấy quan trọng:

Không trả về lỗi được. init() không có giá trị trả về. Hỏng thì chỉ còn cách panic, và panic lúc khởi động cho ra thông báo khó lần.

Không test được. Nó đã chạy trước khi test của bạn bắt đầu. Không tắt được, không giả lập được.

Thứ tự khó đoán giữa các tệp và các package.

Che giấu tác dụng phụ. Người đọc main() không thấy gì đang xảy ra.

Cách thay thế gần như luôn tốt hơn: một hàm New() hoặc Setup() trả về (T, error), gọi tường minh từ main(). Bạn kiểm soát thứ tự, xử lý được lỗi, và test được.

Khi nào init() chính đáng

Đăng ký vào một sổ đăng ký toàn cục — driver, codec, plugin. Đây là lý do nó tồn tại.

Biên dịch sẵn thứ tốn kém và không thể hỏng — biểu thức chính quy hằng:

var reEmail = regexp.MustCompile(`^[^@]+@[^@]+$`)

Ở đây không cần init() — biến package là đủ. MustCompile panic nếu regex sai, và điều đó đúng: regex hằng sai là lỗi lập trình, phải chết ngay lúc khởi động chứ không phải lúc phục vụ request.

Quy ước Must... trong Go nghĩa là "panic thay vì trả lỗi", và nó chỉ dành cho dữ liệu hằng do lập trình viên viết.

Thử ba mươi giây

Thêm vào một package bất kỳ trong dự án của bạn:

func init() { fmt.Println("init:", "tên package") }

rồi chạy chương trình. Thứ tự in ra cho bạn thấy đồ thị phụ thuộc thật của dự án — thường sâu hơn bạn nghĩ, và đôi khi lộ ra những package bạn không biết mình đang kéo vào.

Ngày mai: tổ chức dự án Go — và vì sao đừng bắt chước bố cục của Kubernetes.