Menu

Tham số dòng lệnh Golang: os.Args, flag và biến môi trường

Cách một chương trình Go đọc dòng lệnh: os.Args, package flag cho các tùy chọn có kiểu, lệnh con với FlagSet, biến môi trường với os.Getenv và os.LookupEnv, và mã thoát với os.Exit.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

os.Args

os.Args là một slice chuỗi. os.Args[0] là tên chương trình, phần còn lại là các đối số (argument) đúng như shell đã truyền vào.

Trong khung Args của trình chỉnh sửa, mỗi ô là một đối số, được truyền nguyên dạng. Thử nhập hello, two words và -v vào ba ô: chương trình thấy ba đối số, và two words vẫn là một đối số có dấu cách ở giữa. Trong terminal, shell sẽ tách các đối số, nên điều tương tự được viết là go run . hello "two words" -v.

Luôn kiểm tra len(os.Args) trước khi truy cập theo chỉ số. os.Args[1] khi không có đối số sẽ panic với index out of range [1] with length 1.

Đối số là chuỗi. Hãy chuyển số bằng strconv.Atoi hoặc strconv.ParseFloat và xử lý lỗi, vì người dùng có thể gõ bất cứ thứ gì.

Package flag

Với các tùy chọn như -port 8080 -verbose, hãy dùng flag. Package này phân tích, chuyển kiểu, báo lỗi và tạo thông báo trợ giúp.

Khi không có đối số, chương trình in hello, world một lần. Trong khung Args, thử nhập -name và Gopher vào hai ô, rồi -count=3, -loud và extra vào ba ô nữa. Chương trình in HELLO, GOPHER! ba lần và remaining args: [extra].

Cách flag hoạt động:

  • Mỗi hàm định nghĩa (flag.String, flag.Int, flag.Bool, flag.Float64, flag.Duration, flag.Uint64...) nhận một tên, một giá trị mặc định và một chuỗi hướng dẫn, rồi trả về một con trỏ. Đọc giá trị bằng *name sau flag.Parse().
  • Các dạng Var gắn vào một biến bạn đã có: flag.IntVar(&cfg.Port, "port", 8080, "port"). Cách này gọn hơn khi dùng một struct cấu hình.
  • Người dùng có thể viết -name value, -name=value, --name value hoặc --name=value. Go không phân biệt một gạch hay hai gạch.
  • Boolean cần = để nhận giá trị. -loud đặt thành true, -loud=false đặt thành false, nhưng -loud false đặt thành true và để false lại làm một đối số vị trí.
  • Việc phân tích dừng ở đối số đầu tiên không phải flag (hoặc ở --). prog file.txt -v coi -v là một đối số vị trí. Hãy đặt flag lên trước.
  • flag.Args() trả về các đối số vị trí còn lại, flag.NArg() trả về số lượng của chúng, và flag.Arg(i) trả về một đối số trong đó.

Một flag không xác định hoặc một giá trị sai sẽ in ra lỗi kèm hướng dẫn sử dụng, rồi thoát với mã trạng thái 2. -h hoặc -help in ra hướng dẫn sử dụng và thoát với mã trạng thái 0 (từ Go 1.15). Nội dung hướng dẫn được tạo từ các định nghĩa của bạn:

Usage of greet:
  -count int
    	how many times (default 1)
  -delay duration
    	pause between greetings, e.g. 10ms
  -loud
    	shout the greeting
  -name string
    	who to greet (default "world")

Gán một hàm cho flag.Usage để in phần đầu riêng của bạn trước khi gọi flag.PrintDefaults().

Lệnh con với FlagSet

Các công cụ như git commit -m msg có lệnh con với flag riêng. Hãy tạo một flag.FlagSet cho mỗi lệnh con và chọn một cái bằng switch trên đối số đầu tiên:

flag.ContinueOnError khiến Parse trả về lỗi thay vì thoát, nhờ đó hàm vẫn kiểm thử được. run nhận các đối số qua tham số thay vì đọc os.Args, nên một bài test có thể gọi thẳng run([]string{"list", "-all"}). Thử nhập list và -all vào khung Args, hoặc delete để xem nhánh lỗi.

Với các CLI lớn có lệnh lồng nhau, tự hoàn thành trong shell và tài liệu tự sinh, phần lớn dự án dùng thư viện bên thứ ba github.com/spf13/cobra. Package flag chuẩn đáp ứng tốt các công cụ nhỏ.

Biến môi trường

  • os.Getenv trả về "" cả khi biến không tồn tại lẫn khi biến được đặt là chuỗi rỗng. os.LookupEnv phân biệt được hai trường hợp.
  • Giá trị luôn là chuỗi. Hãy chuyển kiểu và kiểm tra chúng ngay lúc khởi động, và báo lỗi bằng một thông điệp rõ ràng thay vì thất bại giữa chừng một request.
  • os.Setenv ảnh hưởng tới tiến trình hiện tại và các tiến trình con khởi chạy sau đó. Nó không thể thay đổi môi trường của shell đã chạy chương trình.
  • os.Environ() trả về mọi biến dưới dạng chuỗi "KEY=value".

Một cách bố trí cấu hình phổ biến: flag cho những gì người dùng gõ, biến môi trường cho thiết lập triển khai (các nền tảng container đặt chúng), trong đó flag ghi đè biến môi trường và biến môi trường ghi đè giá trị mặc định.

Mã thoát và os.Exit

Một chương trình Go thoát với mã trạng thái 0 khi main trả về. os.Exit(code) kết thúc tiến trình ngay lập tức với mã trạng thái đó. Theo quy ước, 0 là thành công, 1 là lỗi chung, và 2 là lỗi cách dùng (package flag dùng 2).

os.Exit không chạy các hàm defer. Tệp không được ghi hết và phần dọn dẹp bằng defer bị bỏ qua. log.Fatal gọi os.Exit(1) và có cùng tác dụng. Hãy để os.Exit ở một chỗ duy nhất, cuối main, và đặt chương trình thật vào một hàm run trả về lỗi, như ví dụ lệnh con ở trên:

func main() {
	if err := run(os.Args[1:]); err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
}

Ghi thông báo lỗi ra os.Stderr, không phải os.Stdout, để chúng vẫn hiện ra khi đầu ra được chuyển hướng vào tệp hoặc nối ống sang lệnh khác. Một panic không được recover sẽ thoát với mã trạng thái 2.

Lỗi thường gặp

  • Truy cập os.Args theo chỉ số mà không kiểm tra độ dài. Thiếu đối số sẽ gây panic.
  • Đọc flag trước flag.Parse(). Bạn chỉ nhận được giá trị mặc định.
  • Quên dấu *. fmt.Println(port) in ra một địa chỉ như 0xc000012345, không phải giá trị.
  • Đặt flag sau đối số vị trí. prog input.txt -v không phân tích -v.
  • -verbose false cho một flag boolean. Hãy viết -verbose=false.
  • Gọi os.Exit hoặc log.Fatal sâu bên trong chương trình. Phần dọn dẹp bằng defer không bao giờ chạy và code không kiểm thử được. Hãy trả lỗi ngược lên main.

Câu hỏi thường gặp

Lấy tham số dòng lệnh trong Go bằng cách nào?

os.Args là một []string chứa tên chương trình ở chỉ số 0 và các đối số theo sau. os.Args[1:] là các đối số người dùng đã gõ. Hãy kiểm tra len(os.Args) trước khi truy cập theo chỉ số, nếu không chương trình sẽ panic khi thiếu đối số.

Dùng package flag trong Go như thế nào?

Khai báo các flag, gọi flag.Parse(), rồi đọc chúng: port := flag.Int("port", 8080, "port to listen on"), flag.Parse(), fmt.Println(*port). Các hàm này trả về con trỏ. Người dùng viết -port=9000, -port 9000 hoặc --port 9000, và -h in ra hướng dẫn sử dụng được tạo tự động.

Đọc biến môi trường trong Go bằng cách nào?

os.Getenv("HOME") trả về giá trị, hoặc một chuỗi rỗng nếu biến chưa được đặt. Để phân biệt chưa đặt với đặt thành rỗng, dùng v, ok := os.LookupEnv("HOME"). os.Setenv thay đổi môi trường của tiến trình hiện tại và của các tiến trình con được khởi chạy sau đó.

os.Exit có chạy các hàm defer trong Go không?

Không. os.Exit kết thúc tiến trình ngay lập tức với mã trạng thái đã cho, và các lời gọi defer không chạy, nên đầu ra trong buffer có thể bị mất và tệp có thể chưa được ghi hết. Một mẫu hay dùng là func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) } }, với toàn bộ công việc thật và các defer nằm trong run.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu