0

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:

  1. Lộ dữ liệu nhạy cảm: Struct User chứa PasswordHash hoặc thẻ tín dụng. Nếu lập trình viên lỡ tay dùng fmt.Println(user), dữ liệu mật sẽ phơi bày trên Terminal. (Dù bạn đã viết Custom Handler cho slog ở 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).

  2. 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Ó, fmt sẽ tôn trọng hoàn toàn ý muốn của bạn: gọi hàm String() đó và chỉ in ra những gì hàm đó trả về.

  • Nếu KHÔNG, fmt mớ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 %+v và %#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

Viblo
Let's register a Viblo Account to get more interesting posts.