Package fmt có ba nhóm hàm in, và mỗi nhóm có cùng ba biến thể:
| Hàm | Đầu ra đi tới | Định dạng |
|---|---|---|
Print, Println, Printf | đầu ra chuẩn | mặc định, mặc định kèm dấu cách và xuống dòng, chuỗi định dạng |
Sprint, Sprintln, Sprintf | một string được trả về | cùng ba kiểu trên |
Fprint, Fprintln, Fprintf | bất kỳ io.Writer nào (tệp, bộ đệm, HTTP response) | cùng ba kiểu trên |
Errorf | một error được trả về | chuỗi định dạng, thêm %w để wrap |
Println thêm dấu cách giữa các toán hạng và ký tự xuống dòng ở cuối. Printf không thêm gì: bạn tự viết \n. Print chỉ thêm dấu cách giữa hai toán hạng khi cả hai đều không phải chuỗi, một quy tắc đủ gây bất ngờ để hầu hết code dùng Println hoặc Printf.
Các verb định dạng
Tổng quát
| Verb | In ra | Ví dụ đầu ra |
|---|---|---|
%v | giá trị ở định dạng mặc định | {Ana 31 [admin]} |
%+v | struct kèm tên trường | {Name:Ana Age:31 Tags:[admin]} |
%#v | giá trị theo cú pháp Go | main.User{Name:"Ana", Age:31, Tags:[]string{"admin"}} |
%T | kiểu dữ liệu | main.User |
%% | một dấu phần trăm | % |
%+v là lựa chọn nên dùng khi gỡ lỗi. Một con trỏ tới struct được in ra dưới dạng &{...} thay vì một địa chỉ. Map được in với các khóa đã sắp xếp, nên đầu ra ổn định dù thứ tự duyệt map là ngẫu nhiên.
Số nguyên
| Verb | Ý nghĩa | fmt.Sprintf(verb, 255) |
|---|---|---|
%d | thập phân | 255 |
%b | nhị phân | 11111111 |
%o | bát phân | 377 |
%O | bát phân kèm tiền tố 0o | 0o377 |
%x / %X | thập lục phân, chữ thường hoặc chữ hoa | ff / FF |
%#x | thập lục phân kèm tiền tố 0x | 0xff |
%c | ký tự có code point đó | ÿ |
%q | literal ký tự trong dấu nháy | 'ÿ' |
%U | định dạng Unicode | U+00FF |
Số thực
| Verb | Ý nghĩa | fmt.Sprintf(verb, 1234.5678) |
|---|---|---|
%f | thập phân, mặc định 6 chữ số sau dấu chấm | 1234.567800 |
%.2f | thập phân, 2 chữ số sau dấu chấm | 1234.57 |
%e | ký hiệu khoa học | 1.234568e+03 |
%g | %e hoặc %f, tùy cách nào ngắn hơn, không có số 0 thừa ở cuối | 1234.5678 |
%v | giống %g | 1234.5678 |
%.2f làm tròn giá trị nhị phân chính xác của số thực, nên fmt.Sprintf("%.2f", 2.675) cho ra 2.67: số float64 gần 2.675 nhất nhỏ hơn nó một chút. Đừng bao giờ định dạng tiền từ một số thực; hãy lưu số xu trong một số nguyên.
Chuỗi và byte
| Verb | Ý nghĩa | fmt.Sprintf(verb, "go\n") |
|---|---|---|
%s | chuỗi nguyên bản | go và một ký tự xuống dòng |
%q | trong dấu nháy kép, hiện ký tự escape | "go\n" |
%x | thập lục phân của từng byte | 676f0a |
% x | thập lục phân có dấu cách | 67 6f 0a |
%s với một []byte in nó ra dưới dạng văn bản; %v in ra các con số ([104 105]).
Các kiểu khác
| Verb | Kiểu | In ra |
|---|---|---|
%t | bool | true hoặc false |
%p | con trỏ, slice, map, channel, func | địa chỉ, như 0xc000012345 |
%w | error (chỉ trong Errorf) | thông báo của lỗi, đồng thời wrap lỗi đó |
Độ rộng, độ chính xác và căn lề
Giữa % và verb, bạn có thể đặt flag, độ rộng và độ chính xác:
| Dạng | Tác dụng |
|---|---|
%5d | độ rộng 5, căn phải (thêm dấu cách bên trái) |
%-5d | độ rộng 5, căn trái |
%05d | thêm số 0 vào đầu |
%.2f | 2 chữ số sau dấu chấm thập phân |
%8.2f | độ rộng 8 và 2 chữ số thập phân |
%.3s | tối đa 3 ký tự của chuỗi |
%+d | luôn hiển thị dấu |
%*d | độ rộng lấy từ đối số tiếp theo |
Lưu ý %.0f của 2.5 in ra 2: ở đây Go làm tròn nửa về số chẵn. Với chuỗi, độ rộng đếm theo rune chứ không theo cột hiển thị, nên ký tự CJK và emoji vẫn có thể làm lệch bảng. Để căn cột cho văn bản có độ dài thay đổi, text/tabwriter sẽ đo giúp bạn.
Chỉ số đối số
%[n] chọn một đối số theo vị trí, cho phép bạn dùng lại nó:
fmt.Printf("%[2]s %[1]s\n", "world", "hello") // hello world
fmt.Printf("%d %[1]x %[1]b\n", 10) // 10 a 1010
Errorf và %w
fmt.Errorf tạo một error từ chuỗi định dạng. Với %w, nó còn wrap một lỗi khác, để bên gọi vẫn phát hiện được lỗi gốc:
Dùng %w khi bên gọi có thể cần kiểm tra nguyên nhân, và %v khi bạn cố ý che giấu nó. Từ Go 1.20, một lời gọi Errorf có thể chứa nhiều verb %w. Trang xử lý lỗi trình bày chi tiết về việc wrap lỗi.
Tùy chỉnh định dạng với String()
Bất kỳ kiểu nào có phương thức String() string đều tự quyết định cách %v, %s và Println hiển thị nó:
%d bỏ qua String() và in ra con số bên dưới. Với các kiểu lỗi, phương thức tương ứng là Error() string, và nó được ưu tiên hơn String().
Khi verb bị sai
fmt không bao giờ panic khi gặp định dạng sai. Nó in vấn đề ngay tại chỗ:
fmt.Printf("%d\n", "oops")
fmt.Printf("%d %d\n", 1)
fmt.Printf("%d\n", 1, 2)
%!d(string=oops)
1 %!d(MISSING)
1
%!(EXTRA int=2)
Kiểu đầu ra đó thường lọt được lên production vì nó không làm gì bị crash. go vet bắt được cả ba trường hợp lúc build:
./main.go:8:2: fmt.Printf format %d has arg "oops" of wrong type string
./main.go:9:2: fmt.Printf format %d reads arg #2, but call has 1 arg
./main.go:10:2: fmt.Printf call needs 1 arg but has 2 args
Lưu ý về hiệu năng
fmt nhận mọi đối số dưới dạng any và kiểm tra kiểu lúc chạy (dùng reflection với struct, slice và map), điều này ổn cho việc ghi log và in đầu ra nhưng tốn thời gian đáng kể trong các vòng lặp chặt. Để chuyển đổi một con số đơn lẻ, strconv.Itoa và strconv.FormatFloat nhanh hơn Sprintf. Để xây dựng một chuỗi dài trong vòng lặp, hãy ghi vào một strings.Builder bằng fmt.Fprintf(&b, ...) thay vì nối các kết quả của Sprintf.
Câu hỏi thường gặp
Println, Printf và Sprintf trong Go khác nhau thế nào?
fmt.Println in các đối số cách nhau bằng dấu cách và thêm ký tự xuống dòng ở cuối. fmt.Printf in theo một chuỗi định dạng và không thêm ký tự xuống dòng. fmt.Sprintf định dạng giống Printf nhưng trả kết quả về dưới dạng chuỗi thay vì in ra. fmt.Errorf cũng làm vậy và trả về một error.
Làm sao in struct kèm tên trường trong Go?
Dùng %+v: fmt.Printf("%+v\n", user) in ra {Name:Ana Age:31}. %v chỉ in các giá trị, {Ana 31}, còn %#v in theo cú pháp Go kèm cả kiểu, main.User{Name:"Ana", Age:31}.
Làm sao định dạng số thực với 2 chữ số thập phân trong Go?
Dùng %.2f: fmt.Sprintf("%.2f", 3.14159) trả về "3.14". Thêm độ rộng để căn cột, %8.2f, hoặc dấu trừ để căn trái, %-8.2f. strconv.FormatFloat(f, 'f', 2, 64) cho cùng kết quả mà không cần chuỗi định dạng.
%w trong fmt.Errorf có tác dụng gì?
%w định dạng một lỗi giống %v và đồng thời wrap nó, nên lỗi mới mang theo lỗi gốc. Khi đó errors.Is và errors.As có thể tìm thấy lỗi được wrap: err := fmt.Errorf("load config: %w", os.ErrNotExist) khiến errors.Is(err, os.ErrNotExist) trả về true. %w chỉ hoạt động trong fmt.Errorf.
Tại sao đầu ra của tôi hiện %!d(string=...)?
Verb không khớp với kiểu của đối số, ví dụ %d nhận một chuỗi. fmt in vấn đề ngay tại chỗ thay vì panic: %!d(string=oops). Thiếu đối số sẽ in %!d(MISSING) và thừa đối số sẽ in %!(EXTRA int=2). go vet bắt được cả ba trường hợp trước khi bạn chạy chương trình.