Bài 6: Custom Stringer (interface fmt.Stringer) trong package fmt
Trong các bài trước, chúng ta thấy rằng khi gọi fmt.Println(obj) hoặc dùng Verb %v, Go sẽ tự động dùng Reflection quét qua toàn bộ cấu trúc của object và in ra mọi thứ nó thấy.
Tuy nhiên, đôi khi cấu trúc mặc định này lại gây rắc rối:
-
Lộ dữ liệu nhạy cảm: Struct
UserchứaPasswordHashhoặc thẻ tín dụng. Nếu lập trình viên lỡ tay dùngfmt.Println(user), dữ liệu mật sẽ phơi bày trên Terminal. (Dù bạn đã viết Custom Handler choslogở series trước, việc ngăn chặn ngay từ gốc ở mọi hàm in ấn vẫn an toàn hơn). -
Khó đọc: Một Struct chứa các đối tượng thời gian (time.Time) lồng nhau sẽ in ra một chuỗi khổng lồ gồm cả múi giờ, memory address, rất rườm rà.
1. "Phép thuật" của interface fmt.Stringer
Để giải quyết bài toán trên, package fmt thiết kế một interface cực kỳ đơn giản mang tên Stringer:
Go
type Stringer interface {
String() string
}
Nguyên lý hoạt động: Bất cứ khi nào bạn yêu cầu package fmt in một đối tượng dưới dạng chuỗi (dùng Println, Sprint, hay Verb %s, %v), nó sẽ kiểm tra xem đối tượng đó có cài đặt (implement) hàm String() string hay không.
-
Nếu CÓ,
fmtsẽ tôn trọng hoàn toàn ý muốn của bạn: gọi hàmString()đó và chỉ in ra những gì hàm đó trả về. -
Nếu KHÔNG,
fmtmới dùng cơ chế quét Reflection mặc định (in ra toàn bộ Struct).
2. Thực hành: Masking dữ liệu nhạy cảm tại gốc
Chúng ta sẽ cài đặt hàm String() cho Struct User để đảm bảo dù ai có in nó ra bằng cách nào, thẻ tín dụng cũng sẽ bị che mờ.
Go
package main
import "fmt"
// Khai báo Struct User
type User struct {
ID int
Username string
CreditCard string
PinCode int
}
// CÀI ĐẶT INTERFACE fmt.Stringer (Buộc vào con trỏ *User)
func (u *User) String() string {
// Che đi những chữ số đầu của thẻ tín dụng
maskedCard := "****-****-****-"
if len(u.CreditCard) >= 4 {
maskedCard += u.CreditCard[len(u.CreditCard)-4:]
} else {
maskedCard += "N/A"
}
// Trả về một chuỗi tuỳ chỉnh, cố tình bỏ qua trường PinCode
return fmt.Sprintf("User[ID: %d | Tên: %s | Thẻ: %s]", u.ID, u.Username, maskedCard)
}
func main() {
customer := &User{
ID: 1024,
Username: "hieu.backend",
CreditCard: "4111222233334444",
PinCode: 8899,
}
// 1. Dùng Println
fmt.Println("Thông tin (Println):", customer)
// 2. Dùng Printf với %v
fmt.Printf("Thông tin (%%v): %v\n", customer)
// 3. Dùng Printf với %s (Ép về chuỗi)
fmt.Printf("Thông tin (%%s): %s\n", customer)
}
Kết quả Terminal:
Plaintext
Thông tin (Println): User[ID: 1024 | Tên: hieu.backend | Thẻ: ****-****-****-4444]
Thông tin (%v): User[ID: 1024 | Tên: hieu.backend | Thẻ: ****-****-****-4444]
Thông tin (%s): User[ID: 1024 | Tên: hieu.backend | Thẻ: ****-****-****-4444]
(Bạn thấy đấy, dù dùng kiểu in nào, chuỗi trả về luôn an toàn tuyệt đối, PinCode và số thẻ thật đã biến mất hoàn toàn).
3. Lưu ý sống còn: Vòng lặp vô hạn (Infinite Loop)
Khi bạn tự viết hàm String(), lỗi phổ biến nhất và nguy hiểm nhất (gây tràn RAM, sập chương trình ngay lập tức) là việc bạn gọi lại chính cái Struct đó bên trong hàm String().
Go
// VÍ DỤ LỖI (Đừng viết thế này)
func (u *User) String() string {
// LỖI: Việc truyền 'u' vào %v sẽ khiến Sprintf kiểm tra xem 'u' có hàm String() không.
// Có! Thế là nó lại gọi u.String().
// u.String() lại gọi %v. Tạo thành vòng lặp vô hạn (Stack Overflow).
return fmt.Sprintf("Data: %v", u)
}
Cách khắc phục: Nếu bên trong hàm String(), bạn lười format từng trường mà vẫn muốn tận dụng sức mạnh in Struct của Go (như %+v đã học ở Bài 4), bạn phải ép kiểu đối tượng về một type ẩn danh mới để phá vỡ interface Stringer.
Go
func (u *User) String() string {
// 1. Tạo một Type mới hệt như User nhưng KHÔNG mang theo hàm String()
type aliasUser User
// 2. Ép kiểu 'u' (đang là con trỏ) thành biến tĩnh của type mới
cloned := aliasUser(*u)
// 3. Lúc này bạn có thể thao tác ẩn dữ liệu trước
cloned.CreditCard = "***"
cloned.PinCode = 0
// 4. Giờ thì dùng %+v thoải mái, nó sẽ không gọi ngược lại hàm String() này nữa
return fmt.Sprintf("%+v", cloned)
}
4. Tổng kết Series
Tuyệt vời! bạn đã hoàn thành xuất sắc series khám phá package fmt trong Go.
Qua 6 bài học, bạn không chỉ dừng lại ở mức "in HelloWorld ra màn hình", mà đã nắm giữ trọn vẹn sức mạnh định dạng dữ liệu:
-
Phân rõ nhiệm vụ: in ra màn hình (
Print), ráp thành chuỗi (Sprint), hay đẩy qua mạng/ghi file (Fprint). -
Bóc tách Struct phức tạp để debug với
%+vvà%#v. -
Giữ nguyên mạch lỗi hệ thống trong các dự án Microservices với
%w. -
Tự chủ hoàn toàn cách hiển thị và bảo vệ dữ liệu với
fmt.Stringer.
Những kiến thức này kết hợp cùng series net/http và log/slog trước đó đã giúp bạn xây dựng một kỹ năng Backend bằng Go cực kỳ bài bản và chuyên nghiệp. Nếu bạn định viết một series bài đăng trên Viblo, 3 chủ đề này chắc chắn sẽ rất được cộng đồng kỹ sư đón nhận!
All Rights Reserved