Khám phá khái niệm Reflection trong ngôn ngữ lập trình Go, đi sâu vào các khả năng mạnh mẽ của nó trong việc chỉnh sửa và phân tích code động.
Ngôn ngữ lập trình Go nổi tiếng bởi khả năng biểu đạt của nó. Nó là một ngôn ngữ tạo kiểu mạnh mẽ nhưng vẫn cho ứng dụng khả năng chỉnh sửa linh hoạt và kiểm tra các đối tượng bao gồm biến, hàm và các kiểu tại thời gian chạy.
Reflection là cơ chế Go dùng để thực hiện chức năng này. Vậy Reflection trong Go là gì? Làm thế nào có thể đưa reflection vào ứng dụng Go.
Reflection là gì?
Reflection là tính năng của một chương trình kiểm tra các biến và cấu trúc của nó, đồng thời thao tác chúng tại thời gian chạy.
Reflection trong Go là cơ chế ngôn ngữ cung cấp cho thao tác đối tượng và kiểu động. Bạn có thể cần kiểm tra các đối tượng, update chúng, gọi phương thức, thậm chí triển khai hoạt động gốc với kiểu của chúng mà không cần phải biết kiểu tương ứng ở thời gian biên dịch. Reflection khiến cho tất cả điều trên được triển khai.
Các gói đa dạng trong Go bao gồm encoding, cho phép bạn làm việc với JSON và fmt, chủ yếu dựa vào reflection bên trong để triển khai nhiệm vụ của chúng.
Những điều cần biết về package reflect trong Go
Tìm hiểu về Golang có thể là thử thách bởi ngữ nghĩa và thư viện mạnh mẽ bao gồm các gói và phương thức tạo điều kiện thuận lợi cho phát triển phần mềm hiệu quả.
Package reflect là một trong số những gói trên. Nó chứa tất cả phương thức bạn cần để triển khai reflection trong ứng dụng Go.
Để bắt đầu với package reflect, bạn chỉ cần nhập nó như thế này:
import "reflect"
Package xác định hai kiểu chính, đặt nền tảng cho reflection trong Go: reflect.Type và reflect.Value.
Một Type đơn giản là một kiểu Go. reflect.Type là giao diện bao gồm nhiều phương thức khác nhau để xác định các kiểu khác nhau và kiểm tra thành phần của chúng.
Hàm này kiểm tra kiểu của đối tượng bất kỳ trong Go, reflect.TypeOf, chấp nhận bất kỳ giá trị (một interface{}) là đối số duy nhất và trả về giá trị reflect.Type đại diện cho kiểu năng động của đối tượng đó.
Code bên dưới minh họa cách dùng reflect.TypeOf:
x := "3.142"
y := 3.142
z := 3
typeOfX := reflect.TypeOf(x)
typeOfY := reflect.TypeOf(y)
typeOfZ := reflect.TypeOf(z)
fmt.Println(typeOfX, typeOfY, typeOfZ) // string float64 int
Kiểu thứ hai trong package reflect, reflect.Value có thể chứa một giá trị của kiểu bất kỳ. Hàm reflect.ValueOf chấp nhận bất kỳ interface{} và trả về giá trị động của interface.
Đây là ví dụ về cách dùng reflect.ValueOf để kiểm tra những giá trị trên:
valueOfX := reflect.ValueOf(x)
valueOfY := reflect.ValueOf(y)
valueOfZ := reflect.ValueOf(z)
fmt.Println(valueOfX, valueOfY, valueOfZ) // 3.142 3.142 3
Để kiểm tra các loại và kiểu của giá trị, bạn có thể dùng phương thức Kind và Type như thế này:
typeOfX2 := valueOfX.Type()
kindOfX := valueOfX.Kind()
fmt.Println(typeOfX2, kindOfX) // string string
Dù kết quả của cả hai phương thức gọi hàm đều giống nhau, chúng hoàn toàn khác nhau. typeOfX2 về cơ bản là giống typeOfX vì cả hai đều là các giá trị reflect.Type, nhưng kindOfX là một hằng số, có giá trị là kiểu cụ thể của x, string.
Đây là lí do tại sao có một số lượng hữu hạn các loại như int, string, float, array…, nhưng lại có vô số loại khác nhau do người dùng xác định.
interface{} và reflect.Value hoạt động gần như giống nhau, chúng có thể chứa các giá trị của kiểu bất kỳ.
Sự khác biệt giữa chúng nằm ở cách một interface{} trống không bao giờ hiển thị các hoạt động và phương thức của giá trị mà chúng nắm giữ. Vì thế, phần lớn thời gian bạn cần biết về kiểu động của giá trị và dùng xác nhận loại để truy cập nó trước khi có thể triển khai các hoạt động cùng với nó.
Ngược lại, reflect.Value có các phương thức mà bạn có thể dùng để kiểm tra nội dung và thuộc tính của nó mà không cần phải quan tâm tới kiểu. Phần tiếp theo kiểm tra một cách thực tế hai kiểu này và cho thấy lợi ích của chúng trong các chương trình.
Triển khai Reflection trong chương trình Go
Reflection rất rộng và có thể được sử dụng trong một chương trình vào mọi thời điểm. Bên dưới là một số ví dụ thực tế, minh họa cách dùng reflection trong chương trình:
- Kiểm tra kỹ độ cân bằng: Package reflect cung cấp hàm DeepEqual để kiểm tra giá trị của hai đối tượng trong chiều sâu của độ cân bằng. Ví dụ, hai struct hoàn toàn bằng nhau nếu tất cả các trường tương ứng của chúng đều có cùng kiểu và giá trị. Code mẫu:
// deep equality của hai mảng
arr1 := [...]int{1, 2, 3}
arr2 := [...]int{1, 2, 3}
fmt.Println(reflect.DeepEqual(arr1, arr2)) // true
- Sao chép slice và array: Bạn cũng có thể dùng API reflection của Go để sao chép nội dung của một slice hay một mảng vào mảng khác. Đây là cách thực hiện:
slice1 := []int{1, 2, 3}
slice2 := []int{4, 5, 6}
reflect.Copy(reflect.ValueOf(slice1), reflect.ValueOf(slice2))
fmt.Println(slice1) // [4 5 6]
- Xác định chức năng chung: Những ngôn ngữ như TypeScript cung cấp một kiểu chung, any, mà bạn có thể dùng để chứa các biến của kiểu bất kỳ. Còn Go không có sẵn kiểu chung nhưng bạn có thể dùng reflection để xác định các chức năng chung. Ví dụ:
// In kiểu của giá trị bất kỳ
func printType(x reflect.Value) {
fmt.Println("Value type:", x.Type())
}
- Truy cập tag struct: Tag được dùng để thêm metadata vào các trường struct Go, và nhiều thư viện dùng chúng để xác định và thao tác hành vi của từng trường. Bạn chỉ có thể truy cập tag struct cùng reflection. Code mẫu sau minh họa điều này:
type User struct {
Name string `json:"name" required:"true"`
}
user := User{"John"}
field, ok := reflect.TypeOf(user).Elem().FieldByName("Name")
if !ok {
fmt.Println("Field not found")
}
// in toàn bộ tag và giá trị của "required"
fmt.Println(field.Tag, field.Tag.Get("required"))
// json:"name" required:"true" true
Khi nào nên dùng Reflection và ứng dụng được khuyến khích
- Bạn chỉ nên dùng reflection khi không thể quyết định trước kiểu của một đối tượng trong chương trình.
- Reflection có thể giảm hiệu suất hoạt động của ứng dụng, vì thế, bạn nên tránh dùng nó cho các hoạt động quan trọng về hiệu suất.
- Reflection có thể ảnh hưởng tới khả năng đọc code, vì thế, bạn nên tránh lạm dụng nó.
- Với reflection, lỗi không được ghi lại ở thời gian biên dịch, vì thế, bạn có thể khiến ứng dụng gặp nhiều lỗi ở thời gian chạy hơn.
Reflection hiện có sẵn ở nhiều ngôn ngữ bao gồm C# và JavaScript. Go triển khai AI rất tốt. Ưu điểm chính của reflection trong Go là bạn có thể giải quyết vấn đề với ít code hơn khi khai thác khả năng của thư viện này.