25 tháng 12 năm 2020 Trần Hạo Bình luận 18 bình luận 53.040 người đọc
Vấn đề chính mà lập trình generic muốn giải quyết là sự kết hợp chặt chẽ giữa kiểu dữ liệu và thuật toán xử lý. Khi làm việc với ngôn ngữ có kiểu tĩnh, các thuật toán hoặc chương trình xử lý dữ liệu thường cần được sao chép cho mỗi kiểu dữ liệu khác nhau. Điều này dẫn đến sự phức tạp và khó bảo trì trong mã nguồn. Generic programming giúp giảm thiểu điều này bằng cách cho phép bạn viết mã mà không cần lo lắng về loại dữ liệu cụ thể đang được xử lý, thay vào đó chỉ tập trung vào logic xử lý.
- Mẫu thiết kế lập trình Go: Slices, Interface, Thời gian và Hiệu suất
- Mẫu thiết kế lập trình Go: Xử lý lỗi
- Mẫu thiết kế lập trình Go: Functional Options
- Mẫu thiết kế lập trình Go: Delegation và Inversion of Control
- Mẫu thiết kế lập trình Go: Map-Reduce
- Mẫu thiết kế lập trình Go: Code Generation
- Mẫu thiết kế lập trình Go: Decorator
- Mẫu thiết kế lập trình Go: Pipeline
- Mẫu thiết kế lập trình Go: Visitor Pattern trong Kubernetes
- Mẫu thiết kế lập trình Go: Generic Programming
« Bài viết trước | Bài viết sau »
MỤC LỤC
- So sánh thực tế
- Kiểm tra kiểu trong ngôn ngữ Go
- Type Assertion
- Reflection
- Học hỏi từ các ngôn ngữ khác
- Generator trong Go
- Mẫu hàm
- Kịch bản tạo mã
- Tạo mã nguồn
- Filter mới
- Công cụ bên thứ ba
So sánh thực tế
Hãy lấy ví dụ về tua vít để minh họa vấn đề. Một cây tua vít cơ bản thực hiện công việc vặn bu lông, nhưng vì có nhiều loại bu lông khác nhau (bình thường, chữ thập, lục giác…) và kích thước khác nhau, nên chúng ta cần nhiều loại tua vít khác nhau để đáp ứng nhu cầu.
Thực tế, thay vì tạo ra nhiều loại tua vít riêng biệt, chúng ta có thể thiết kế một công cụ thông minh hơn, có khả năng tự điều chỉnh để phù hợp với nhiều loại bu lông khác nhau. Đây chính là mục tiêu mà lập trình generic hướng tới: tập trung vào chức năng cốt lõi của công cụ thay vì phải quan tâm quá nhiều đến đặc điểm cụ thể của đối tượng cần xử lý.
Kiểm tra kiểu trong ngôn ngữ Go
Hiện tại, Go chưa hỗ trợ đầy đủ tính năng generic. Do đó, để khắc phục hạn chế này, chúng ta thường sử dụng interface{}
- một kiểu dữ liệu tổng quát tương tự như void*
trong C/C++. Tuy nhiên, việc này đòi hỏi phải kiểm tra kiểu dữ liệu trong quá trình thực thi. Trong Go, có hai kỹ thuật phổ biến để kiểm tra kiểu: Type Assertion và Reflection.
Type Assertion
Type Assertion là kỹ thuật dùng để chuyển đổi kiểu dữ liệu của một biến từ interface{}
sang kiểu cụ thể. Kết quả trả về bao gồm hai giá trị: giá trị đã được chuyển đổi và một boolean cho biết quá trình chuyển đổi có thành công hay không.
Dưới đây là một ví dụ về cách sử dụng Type Assertion:
|
|
Reflection
Reflection là kỹ thuật mạnh mẽ hơn, cho phép kiểm tra và thao tác với cấu trúc của đối tượng tại thời gian chạy. Dưới đây là một ví dụ về cách sử dụng reflection trong Go:
|
|
Trong đoạn mã trên:
- Hàm
NewContainer()
khởi tạo một slice dựa trên kiểu dữ liệu được chỉ định. - Hàm
Put()
kiểm tra xem giá trị cần thêm có đúng kiểu với slice hay không. - Hàm
Get()
yêu cầu truyền tham chiếu để lưu trữ giá trị được lấy ra, do không thể trả về trực tiếpreflect.Value
hoặcinterface{}
.
Sử dụng reflection
|
|
Reflection mặc dù mạnh mẽ nhưng lại khiến mã nguồn trở nên phức tạp hơn. Vậy có cách nào đơn giản hơn không?
Học hỏi từ các ngôn ngữ khác
Trong C++, vấn đề generic được giải quyết bằng cách sử dụng Template. Ví dụ:
|
|
Compiler của C++ sẽ tự động tạo ra các phiên bản hàm hoặc lớp theo từng kiểu dữ liệu khác nhau mà không cần bất kỳ kiểm tra kiểu nào tại thời gian chạy. Mã nguồn trở nên sạch sẽ và dễ bảo trì hơn.
Tương tự, chúng ta cũng có thể áp dụng kỹ thuật này trong Go bằng cách sử dụng code generation.
Generator trong Go
Để tạo mã nguồn trong Go, bạn cần ba bước chính:
- Một mẫu hàm chứa các placeholder.
- Một kịch bản tự động thay thế placeholder và tạo mã mới.
- Một dòng chú thích đặc biệt.
Mẫu hàm
Chúng ta có thể viết một mẫu hàm như sau và lưu nó dưới tên container.tmp.go
trong thư mục ./template/
:
|
|
Placeholder bao gồm:
PACKAGE_NAME
– Tên góiGENERIC_NAME
– Tên đại diệnGENERIC_TYPE
– Kiểu dữ liệu cụ thể
Kịch bản tạo mã
Tiếp theo, chúng ta tạo một script shbet love gọi là game nổ hũ đăng ký tặng code gen.sh
để tự động thay thế placeholder:
|
|
Script này nhận bốn tham số:
- File mẫu nguồn
- Tên gói
- Kiểu dữ liệu cụ thể
- Phần mở rộng tên file đích
Tạo mã nguồn
Cuối cùng, chúng ta chỉ cần thêm một dòng chú thích đặc biệt trong mã nguồn:
|
|
Sau đó, chạy lệnh go generate
trong thư mục dự án, bạn sẽ nhận được hai file mới:
File uint32_container.go
:
|
|
File string_container.go
:
|
|
Hai file này đảm bảo rằng mã nguồn hoàn toàn biên dịch được, với chi phí duy nhất là phải chạy thêm lệnh go generate
.
Filter mới
Bằng cách áp dụng kỹ thuật này, chúng ta có thể viết mã nguồn sạch hơn mà không cần sử dụng reflection phức tạp. Dưới đây là một ví dụ về filter:
|
|
Sử dụng filter
|
|
Công cụ bên thứ ba
Ngoài việc tự viết script, bạn có thể sử dụng các công cụ sẵn có từ cộng đồng:
- Genny
- Generic
- GenGen
- Gen