0

Bài 4: Định dạng Số và Cấu trúc dữ liệu nâng cao trong package fmt

Ở Bài 2, chúng ta đã lướt qua các Verbs cơ bản như %s, %d, %f. Tuy nhiên, sức mạnh thực sự của package fmt nằm ở khả năng phân tách các cấu trúc dữ liệu phức tạp (Structs, Slices, Maps) và định dạng các hệ cơ số khác nhau (Nhị phân, Hex).

Đây là bài học "cứu cánh" cho mọi lập trình viên Go khi phải debug một API trả về những cục JSON lồng nhau chằng chịt, hoặc khi làm việc với các bit/byte trong lập trình hệ thống.

1. Phép thuật in Struct: %v vs %+v vs %#v

Khi bạn có một Struct lớn mang dữ liệu trả về từ Database, việc dùng %v thường không đủ để bạn hiểu rõ cấu trúc của nó, đặc biệt là khi các trường (fields) chứa giá trị rỗng hoặc zero-value.

Hãy xem sự khác biệt "một trời một vực" giữa 3 Verbs này:

Go

package main

import "fmt"

type Config struct {
	Timeout   int
	IsActive  bool
	Endpoints []string
}

func main() {
	cfg := Config{
		Timeout:   30,
		IsActive:  false, // zero-value của bool
		Endpoints: []string{"/api/v1/users", "/api/v1/orders"},
	}

	// 1. Dùng %v: Chỉ in ra Value
	fmt.Printf("Mức %%v  : %v\n", cfg)
	// Khó nhìn, bạn không biết số 30 hay false thuộc về trường nào.

	// 2. Dùng %+v: In Value KÈM THEO TÊN TRƯỜNG (Field Name)
	fmt.Printf("Mức %%+v : %+v\n", cfg)
	// Cực kỳ hữu ích khi debug. Bạn biết rõ Timeout:30, IsActive:false.

	// 3. Dùng %#v: In cú pháp Go thuần túy (Go-syntax representation)
	fmt.Printf("Mức %%#v : %#v\n", cfg)
	// In ra đầy đủ tên Package, tên Struct, kiểu dữ liệu của Slice.
	// Bạn có thể copy y nguyên dòng log này dán vào code để tạo test-case.
}

Kết quả Terminal:

Plaintext

Mức %v  : {30 false [/api/v1/users /api/v1/orders]}
Mức %+v : {Timeout:30 IsActive:false Endpoints:[/api/v1/users /api/v1/orders]}
Mức %#v : main.Config{Timeout:30, IsActive:false, Endpoints:[]string{"/api/v1/users", "/api/v1/orders"}}

Thực tiễn (Best Practice): Khi ghi log lỗi vào hệ thống (ví dụ bằng slog ở series trước), nếu cần đính kèm một object, luôn luôn sử dụng %+v.

2. Định dạng Số nâng cao: Nhị phân (Binary) và Hex

Khi bạn làm việc với mảng byte, mã hóa (hashing), xử lý màu sắc, hoặc quyền hạn (permissions / bitmask), việc nhìn số thập phân (%d) là vô nghĩa. Bạn cần ép Go hiển thị chúng dưới dạng Nhị phân hoặc Thập lục phân.

  • %b (Binary): In dạng cơ số 2 (chỉ gồm 0 và 1).

  • %x hoặc %X (Hexadecimal): In dạng cơ số 16 (hữu ích với byte, hash, màu sắc).

  • %o hoặc %O (Octal): In dạng cơ số 8 (thường dùng cho quyền file trên Linux như 0755).

Mẹo nhỏ: Ký tự # khi kết hợp với các Verb này sẽ tự động thêm tiền tố toán học chuẩn của Go (như 0b cho nhị phân, 0x cho hex) vào trước kết quả.

Go

package main

import "fmt"

func main() {
	number := 255

	// Định dạng Hex (hữu ích với mã màu)
	fmt.Printf("Màu trắng - Dec: %d | Hex: %x | Có tiền tố: %#x\n", number, number, number)
	// Kết quả: Màu trắng - Dec: 255 | Hex: ff | Có tiền tố: 0xff

	// Định dạng Nhị phân
	// Dùng %08b để luôn in đủ 8 bit (thêm số 0 vào trước nếu thiếu)
	fmt.Printf("Nhị phân thuần: %b | Nhị phân 8-bit chuẩn: %08b\n", 5, 5)
	// Kết quả: Nhị phân thuần: 101 | Nhị phân 8-bit chuẩn: 00000101
}

3. Ký tự %q: An toàn cho Chuỗi (Quoted String)

Khi in một chuỗi bằng %s, nếu chuỗi đó rỗng (""), Terminal sẽ chỉ in ra một khoảng trắng vô hình. Nếu chuỗi chứa các ký tự ẩn như Tab (\t) hoặc xuống dòng (\n), Terminal sẽ thực thi chúng, làm hỏng định dạng log của bạn.

Verb %q (Quoted) giải quyết triệt để vấn đề này. Nó tự động:

  1. Bọc chuỗi trong dấu ngoặc kép " ".

  2. Chuyển các ký tự ẩn (như \n, \t) thành text hiển thị thuần túy (Escape).

Go

package main

import "fmt"

func main() {
	userInput := "Hello\nWorld" // Chứa ký tự xuống dòng
	emptyInput := ""

	// Dùng %s (Bị vỡ form)
	fmt.Printf("Input1: %s\nInput2: %s\n", userInput, emptyInput)
	/* Kết quả bị vỡ:
	Input1: Hello
	World
	Input2: 
	*/

	// Dùng %q (An toàn tuyệt đối)
	fmt.Printf("Input1: %q\nInput2: %q\n", userInput, emptyInput)
	/* Kết quả an toàn:
	Input1: "Hello\nWorld"
	Input2: ""
	*/
}

4. Định dạng Ký tự (Rune/Char): %c

Khác với C++, Go không có kiểu char độc lập. Một ký tự trong Go thực chất là kiểu rune (bí danh của int32 - đại diện cho một mã Unicode). Nếu bạn in một rune bằng %v hoặc %d, nó sẽ in ra mã số. Để in ra ký tự mà mắt người đọc được, phải dùng %c.

Go

package main

import "fmt"

func main() {
	// Dấu nháy đơn định nghĩa kiểu rune (int32)
	char := 'A' 
	emoji := '🔥'

	fmt.Printf("Ký tự: %c (Mã ASCII: %d)\n", char, char)
	// Kết quả: Ký tự: A (Mã ASCII: 65)

	fmt.Printf("Ký tự: %c (Mã Unicode: %U)\n", emoji, emoji)
	// Kết quả: Ký tự: 🔥 (Mã Unicode: U+1F525)
}

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí