[Go] ใช้ generic ครอบ method ที่รับ any เพื่อให้อ่านและใช้งานง่ายยิ่งขึ้น
เรามี method ที่รับค่าเป็น type any
ซึ่งจะส่งอะไรให้ก็ได้แต่จริงๆเราอยากจำกัดให้ส่งได้แค่บาง type วันนี้จะลองใช้ generic type ช่วยสร้าง type ใหม่ครอบการทำงานของ method นี้ทำให้คนใช้งานเห็นชัดเจนว่าใช้กับ type ไหนได้บ้าง แถม compiler ช่วยเช็คให้ตั้งแต่ตอน compile และ IDE/Editor ช่วย autocomplete ขึ้นมาให้อีกด้วย
สมมติว่าเรามี method ที่เขียน Audit Log ซึ่งมี method แบบนี้
// package auditlog
package auditlog
type auditLogger struct {
// ...
}
func (l *auditLogger) Log(ctx context.Context, actionKey string, actionInfo any) error {
// ...
return nil
}
func New() *auditLogger {
return &auditLogger{}
}
ตอนใช้งานเราก็จะส่งค่าอะไรให้กับ parameter actionInfo ก็ได้แบบนี้
type SendSMSAuditLogInfo struct {
CustomerName string
}
type ChangePasswordAuditLogInfo struct {
Email string
}
type Logger interface {
Log(ctx context.Context, actionKey string, actionInfo any) error
}
type Service struct {
auditLogger Logger
}
func (s *Service) SendSMS(ctx context.Context) error {
if err := s.auditLogger.Log(ctx, "send_sms", SendSMSAuditLogInfo{
CustomerName: "John",
}); err != nil {
return err
}
return nil
}
จะเห็นว่าก็ใช้งานง่ายดีแต่ว่าถ้าเราใช้ง่าย auditLogger.Log เราจะไม่รู้เลยว่า actionInfo เป็น type อะไรได้บ้าง ถ้าเราเปลี่ยนไปใช้ Generic แล้วสร้าง type constraint ขึ้นมาใหม่ให้มีแต่เซตของ type ที่เราต้องการ ก็จะทำให้ compiler ช่วยตรวจสอบได้แบบนี้
// 1) เริ่มจากสร้าง type constraint เพื่อระบุเซตของ AuditLogInfo type ที่เราอนุญาติให้ใช้เท่านั้น
type AuditLogAction interface {
SendSMSAuditLogInfo | ChangePasswordAuditLogInfo
}
// 2) เริ่มจากสร้าง type ขึ้นมาใหม่เพื่อครอบการทำงานของ auditLogger.Log method
// ที่รองรับ generic type parameter T ที่มี constraint เป็น AuditLogAction
// และมี field ที่เก็บ type ใดๆที่ implements Log method
type AuditLogger[T AuditLogAction] struct {
logger Logger
}
// 3) จากนั้นสร้าง wrapper method ชื่อ Log สำหรับ type AuditLogger
// ที่รองรับ generic type parameter T เพื่อเอาไปใช้เป็น type ของ actionInfo parameter
func (l *AuditLogger[T]) Log(ctx context.Context, actionKey string, actionInfo T) error {
// 4) เรียก l.logger.Log อีกทีโดยใช้ค่าที่รับมาผ่าน generic type
return l.logger.Log(ctx, actionKey, actionInfo)
}
// 5) สร้าง constructor function ให้กับ AuditLogger[T AuditLogAction]
func NewAuditLogger[T AuditLogAction](l Logger) *AuditLogger[T] {
return &AuditLogger[T]{
logger: l,
}
}
จากขั้นตอนที่ 3 และ 4 จะเห็นว่าเรา implement wrapper method โดยก็ไปเรียก method ที่รับเป็น type any อีกที
ตอนใช้งานแทนที่เราจะใช้ผ่าน Log ที่รับ type any เราก็ใช้ผ่าน AuditLogger แทนแบบนี้
func (s *Service) SendSMS(ctx context.Context) error {
auditLogger := NewAuditLogger[SendSMSAuditLogInfo](s.auditLogger)
if err := auditLogger.Log(ctx, "send_sms", SendSMSAuditLogInfo{
CustomerName: "John",
}); err != nil {
return err
}
return nil
}
ตอนใช้งานกับ IDE/Editor ที่รองรับ ก็จะมี autocomplete ที่ระบุ type ให้ชัดเจน ไม่ใส่ผิดพลาดแบบนี้