Cách triển khai của reflect trong Go
interface
là một công cụ mạnh mẽ trong Go để thực hiện trừu tượng hóa. Khi gán một biến giao diện với một kiểu thực thể, giao diện sẽ lưu trữ thông tin về kiểu của thực thể đó. Reflection được thực hiện thông qua thông tin kiểu của giao diện và được xây dựng dựa trên kiểu.
Trong Go, gói reflect
định nghĩa các loại và các hàm Reflection khác nhau, cho phép kiểm tra thông tin về kiểu và thay đổi giá trị của kiểu trong quá trình chạy.
Kiểu và giao diện
Trong Go, mỗi biến có một kiểu tĩnh được xác định trong quá trình biên dịch, ví dụ: int, float64, []int
, v.v. Lưu ý rằng đây là kiểu khai báo, không phải kiểu dữ liệu cơ bản.
Go blog đã đưa ra ví dụ như sau:
Mặc dù kiểu cơ bản của i
và j
đều là int
, nhưng chúng có các kiểu tĩnh khác nhau và không thể xuất hiện cùng nhau ở cả hai phía của dấu bằng, trừ khi có chuyển đổi kiểu. Kiểu tĩnh của j
là MyInt
.
Reflection chủ yếu liên quan đến kiểu interface{}
. Về cấu trúc cơ bản của giao diện, bạn có thể tham khảo nội dung về giao diện trong các phần trước, đây chỉ là một bài tóm tắt.
Trong đó, itab
bao gồm kiểu cụ thể _type
và interfacetype
. _type
đại diện cho kiểu cụ thể, trong khi interfacetype
đại diện cho kiểu giao diện mà kiểu cụ thể triển khai.
Thực tế, iface
mô tả giao diện không rỗng, nó bao gồm các phương thức; trong khi eface
mô tả giao diện rỗng, không chứa bất kỳ phương thức nào. Trong Go, tất cả các kiểu đều “triển khai” giao diện rỗng.
So với iface
, eface
đơn giản hơn. Nó chỉ duy trì một trường _type
, đại diện cho kiểu cụ thể mà giao diện rỗng mang. data
mô tả giá trị cụ thể.
Tiếp tục với ví dụ từ blog chính thức của Go về Reflection, tôi sẽ giải thích chi tiết bằng hình ảnh. Kết hợp cả hai sẽ giúp hiểu rõ hơn. Nhân tiện, đừng sợ văn bản tiếng Anh, đọc tài liệu gốc tiếng Anh là một bước cần thiết để trở thành chuyên gia kỹ thuật.
Hãy làm rõ một điều: biến giao diện có thể lưu trữ bất kỳ biến nào triển khai tất cả các phương thức được định nghĩa trong giao diện.
Trong Go, hai giao diện phổ biến nhất là Reader
và Writer
:
Tiếp theo, là các phép chuyển đổi và gán giữa các giao diện:
Đầu tiên, khai báo kiểu của r
là io.Reader
. Lưu ý rằng đây là kiểu tĩnh của r
, lúc này kiểu động của nó là nil
, và giá trị động của nó cũng là nil
.
Sau đó, câu lệnh r = tty
sẽ thay đổi kiểu động của r
thành *os.File
, và giá trị động của nó trở thành một đối tượng tệp đã mở. Lúc này, r
có thể được biểu diễn bằng cặp <giá trị, kiểu>
: <tty, *os.File>
.
Lưu ý trong hình ảnh trên, mặc dù hàm fun
chỉ trỏ đến một hàm Read
, thực tế *os.File
cũng triển khai giao diện io.Writer
. Do đó, câu lệnh kiểm tra kiểu sau có thể được thực hiện:
Lý do sử dụng kiểm tra kiểu là vì kiểu tĩnh của r
là io.Reader
, không triển khai giao diện io.Writer
. Khả năng kiểm tra thành công phụ thuộc vào kiểu động của r
có phù hợp hay không.
Sau đó, w
cũng có thể được biểu diễn bằng cặp <giá trị, kiểu>
: <tty, *os.File>
. Mặc dù nó giống với r
, nhưng w
chỉ có thể gọi các hàm dựa trên kiểu tĩnh của nó là io.Writer
, nghĩa là chỉ có thể gọi w.Write()
. Dạng bộ nhớ của w
được biểu diễn như sau:
So sánh với r
, chỉ có hàm fun
thay đổi từ Read
thành Write
.
Cuối cùng, một phép gán khác:
Vì empty
là một giao diện rỗng, nên tất cả các kiểu đều triển khai nó, w
có thể được gán trực tiếp cho nó mà không cần kiểm tra kiểu.
Từ ba hình ảnh trên, có thể thấy rằng giao diện bao gồm ba phần thông tin: _type
là thông tin kiểu, *data
trỏ đến giá trị thực tế của kiểu cụ thể, itab
chứa thông tin về kiểu cụ thể, bao gồm kích thước, đường dẫn gói và các phương thức được gắn liền với kiểu (không được vẽ trong hình). Bổ sung thêm về cấu trúc os.File
:
Cuối cùng, chúng ta sẽ thể hiện một mẹo:
Trước tiên, tham khảo mã nguồn, định nghĩa hai cấu trúc iface
và eface
giả mạo.
Tiếp theo, chúng ta sẽ ép buộc giải thích nội dung bộ nhớ của biến giao diện thành các kiểu được định nghĩa ở trên, sau đó in ra:
Kết quả chạy:
Động cơ và giá trị động của r, w, empty
đều giống nhau. Không cần giải thích chi tiết nữa, kết hợp với các hình ảnh trước đó, chúng ta có thể nhìn thấy rõ ràng.
Các hàm reflect cơ bản
Gói reflect
định nghĩa một giao diện và một cấu trúc, lần lượt là reflect.Type
và reflect.Value
, chúng cung cấp nhiều hàm để truy xuất thông tin về kiểu được lưu trữ trong một giao diện.
reflect.Type
chủ yếu cung cấp thông tin về kiểu, do đó nó liên quan chặt chẽ với _type
. Trong khi đó, reflect.Value
kết hợp cả _type
và data
, cho phép người lập trình truy xuất và thậm chí thay đổi giá trị của một kiểu.
Gói reflect
cung cấp hai hàm cơ bản để thực hiện phản chiếu và lấy thông tin về giao diện và cấu trúc đã đề cập:
Hàm TypeOf
được sử dụng để trích xuất thông tin kiểu của giá trị được lưu trữ trong một giao diện. Vì tham số đầu vào của nó là một interface{}
trống, khi gọi hàm này, đối số thực tế được chuyển đổi thành kiểu interface{}
trước. Điều này cho phép thông tin kiểu, tập hợp phương thức và thông tin giá trị của đối số thực tế được lưu trữ trong biến interface{}
.
Hãy xem mã nguồn:
Ở đây, emptyInterface
tương đương với eface
đã được đề cập (với một số khác biệt nhỏ trong tên trường) và nằm trong gói nguồn khác: trước là gói reflect
, sau là gói runtime
. eface.typ
đại diện cho kiểu động.
Còn về hàm toType
, nó chỉ thực hiện một phép chuyển đổi kiểu đơn giản:
Lưu ý rằng giá trị trả về Type
thực tế là một giao diện định nghĩa nhiều phương thức để lấy thông tin liên quan đến kiểu, trong khi *rtype
thực hiện giao diện Type
.
Giao diện Type
định nghĩa một loạt các phương thức để truy xuất thông tin về kiểu. Qua các phương thức này, ta có thể lấy được nhiều thông tin khác nhau về kiểu. Để hiểu rõ khả năng của giao diện Type
, cần đọc qua tất cả các phương thức đã được đề cập ở trên.
Lưu ý rằng phương thức thứ hai từ cuối trong tập hợp phương thức của giao diện Type
, common
, trả về một kiểu rtype
. Đây là cùng một kiểu như _type
đã được đề cập trong bài viết trước, và mã nguồn cũng có chú thích rằng hai phải được duy trì đồng bộ:
Tất cả các kiểu đều bao gồm trường rtype
, đại diện cho thông tin chung của các kiểu khác nhau. Ngoài ra, các kiểu khác nhau còn bao gồm các phần riêng biệt của chúng.
Ví dụ, arrayType
và chanType
đều bao gồm rtype
, nhưng arrayType
còn bao gồm thông tin liên quan đến mảng như slice và độ dài, trong khi chanType
bao gồm trường dir
để chỉ định hướng của kênh.
Lưu ý rằng giao diện Type
triển khai phương thức String()
, đáp ứng giao diện fmt.Stringer
. Do đó, khi sử dụng fmt.Println
, kết quả sẽ là kết quả của String()
. Ngoài ra, nếu %T
được sử dụng làm tham số định dạng trong fmt.Printf()
, kết quả sẽ là kết quả của reflect.TypeOf
, đại diện cho kiểu động. Ví dụ:
Sau khi đã trình bày về hàm TypeOf
, chúng ta sẽ tiếp tục xem xét hàm ValueOf
. Giá trị trả về reflect.Value
đại diện cho biến thực tế được lưu trữ trong interface{}
, nó cung cấp thông tin về biến thực tế đó. Các phương thức liên quan thường cần kết hợp thông tin về kiểu và giá trị. Ví dụ, để trích xuất thông tin về trường của một cấu trúc, ta cần sử dụng thông tin về trường và thông tin về vị trí (offset) được giữ bởi kiểu _type
(cụ thể là structType
), cùng với nội dung mà *data
trỏ tới - giá trị thực tế của cấu trúc.
Mã nguồn như sau:
Từ mã nguồn, ta có thể thấy rằng quá trình khá đơn giản: trước tiên, chúng ta chuyển đổi i
thành kiểu *emptyInterface
, sau đó chúng ta sử dụng trường typ
và word
cùng với một trường cờ để tạo thành một cấu trúc Value
. Đây chính là giá trị trả về của hàm ValueOf
, nó bao gồm con trỏ đến cấu trúc kiểu, địa chỉ dữ liệu thực tế và các cờ.
Cấu trúc Value
định nghĩa nhiều phương thức, cho phép trực tiếp thao tác với dữ liệu thực tế mà trường ptr
của Value
trỏ tới:
Cấu trúc Value
còn nhiều phương thức khác. Ví dụ:
Không liệt kê tất cả, nhưng có rất nhiều. Bạn có thể xem mã nguồn trong src/reflect/value.go
, tìm kiếm func (v Value)
để xem thêm.
Ngoài ra, thông qua các phương thức Type()
và Interface()
, ta có thể kết nối interface
, Type
và Value
. Phương thức Type()
cũng có thể trả về thông tin kiểu của biến, tương đương với hàm reflect.TypeOf()
. Phương thức Interface()
có thể chuyển đổi Value
trở lại thành interface
ban đầu.
Tóm tắt: Hàm TypeOf()
trả về một giao diện, giao diện này định nghĩa một loạt các phương thức để lấy thông tin về kiểu. Hàm ValueOf()
trả về một biến cấu trúc, bao gồm thông tin về kiểu và giá trị thực tế.
Hãy xem một hình ảnh để tóm tắt:
Trong hình ảnh trên, rtype
triển khai giao diện Type
và đại diện cho phần chung của tất cả các kiểu. Cấu trúc emptyInterface
và eface
thực chất là cùng một thứ, và rtype
thực chất là _type
, chỉ có một số trường có tên khác nhau, ví dụ như trường word
trong emptyInterface
và trường
Ba luật của reflection
Theo blog chính thức của Go về phản chiếu, có ba luật của phản chiếu:
- Reflection goes from interface value to reflection object.
- Reflection goes from reflection object to interface value.
- To modify a reflection object, the value must be settable.
Luật đầu tiên là cơ bản nhất: phản chiếu là cơ chế để xác định kiểu và giá trị được lưu trữ trong giao diện. Điều này có thể được thực hiện bằng cách sử dụng hàm TypeOf
và ValueOf
.
Luật thứ hai thực tế là cơ chế ngược lại của luật thứ nhất. Nó chuyển đổi giá trị trả về từ ValueOf
thành biến giao diện bằng cách sử dụng hàm Interface()
.
Hai luật đầu tiên đề cập đến việc chuyển đổi giữa biến giao diện
và đối tượng phản chiếu
. Đối tượng phản chiếu thực tế là reflect.Type
và reflect.Value
như đã đề cập ở trên.
Luật thứ ba không dễ hiểu: nếu muốn thao tác một biến phản chiếu, nó phải có thể được thiết lập. Khả năng thiết lập của biến phản chiếu là do nó lưu trữ chính giá trị ban đầu của biến, điều này có nghĩa là các thao tác trên biến phản chiếu sẽ phản ánh vào biến gốc. Nếu biến phản chiếu không thể đại diện cho biến gốc, việc thao tác trên biến phản chiếu sẽ không ảnh hưởng đến biến gốc, điều này có thể gây nhầm lẫn cho người sử dụng. Vì vậy, trường hợp thứ hai không được phép ở mức ngôn ngữ.
Dưới đây là một ví dụ kinh điển:
Việc thực thi mã trên sẽ gây ra panic, nguyên nhân là biến phản chiếu v
không thể đại diện cho x
chính xác. Tại sao? Vì khi gọi reflect.ValueOf(x)
, tham số được truyền vào trong hàm chỉ là một bản sao, là truyền giá trị, do đó v
chỉ đại diện cho một bản sao của x
, vì vậy việc thao tác trên v
bị cấm.
Khả năng thiết lập là một thuộc tính của biến phản chiếu Value
, nhưng không phải tất cả các Value
đều có thể được thiết lập.
Tương tự như trong các hàm thông thường, khi muốn thay đổi biến được truyền vào, ta sử dụng con trỏ để giải quyết.
Kết quả là:
p
vẫn không đại diện cho x
, chỉ khi gọi p.Elem()
thì mới thực sự đại diện cho x
, từ đó ta có thể thao tác trực tiếp trên x
:
Về luật thứ ba, hãy nhớ một câu: Để thao tác trên biến gốc, biến phản chiếu Value
phải giữ địa chỉ của biến gốc.