Tổng quan về tạo code
Thuộc phần Lập trình hướng đối tượng trong hành trình GO của Coddy. Bài học 84 trên 107.
Tạo code là một giải pháp thay thế cho reflection, tạo mã nguồn Go trong quá trình biên dịch thay vì kiểm tra kiểu dữ liệu khi chạy. Cách tiếp cận này giúp bạn đảm bảo an toàn kiểu dữ liệu và cải thiện hiệu suất, đồng thời giảm bớt code mẫu lặp lại.
Go cung cấp lệnh go generate để tự động hóa việc tạo code. Bạn đặt một chỉ thị chú thích đặc biệt trong tệp nguồn, rồi chạy go generate để thực thi công cụ được chỉ định:
//go:generate stringer -type=Status
type Status int
const (
Pending Status = iota
Active
Completed
)Chạy go generate ./... sẽ gọi công cụ stringer, công cụ này tạo một tệp mới chứa phương thức String() cho kiểu Status. Code được tạo là code Go thông thường và sẽ được biên dịch cùng với chương trình của bạn.
Các công cụ tạo code phổ biến trong hệ sinh thái Go gồm stringer để tạo phương thức chuỗi cho enum, mockgen để tạo mock kiểm thử từ interface và protoc để tạo code từ định nghĩa Protocol Buffer. Nhiều ORM và framework API cũng dùng cách tạo code để tạo truy vấn cơ sở dữ liệu có kiểm tra kiểu hoặc code phía client.
Ưu điểm chính so với reflection là trình biên dịch sẽ kiểm tra code được tạo. Lỗi được phát hiện khi build, không phải lúc chạy. Đổi lại, bạn cần thêm một bước build và quản lý các tệp được tạo trong repository. Với các mẫu lập trình hướng đối tượng cần triển khai lặp đi lặp lại cho nhiều kiểu dữ liệu, tạo code thường mang lại sự cân bằng tốt nhất giữa độ an toàn và tiện lợi.
Thử thách
DễCùng xây dựng một trình tạo code để tạo các phương thức String() cho các kiểu giống enum nhé! Dù không thể chạy go generate thật trong môi trường này, bạn sẽ mô phỏng cách các công cụ tạo code hoạt động: lập trình để tạo mã nguồn Go dựa trên định nghĩa kiểu.
Bạn sẽ sắp xếp code trong hai tệp:
generator.go: Tạo logic sinh code.Xây dựng một struct
EnumTypeđại diện cho định nghĩa kiểu giống enum, vớiName(chuỗi) vàValues(slice chứa các chuỗi đại diện cho tên hằng số).Tạo hàm
GenerateStringMethod(e EnumType) stringđể tạo mã nguồn Go cho phương thứcString(). Code được tạo cần theo mẫu sau:func (e TypeName) String() string { switch e { case Value1: return "Value1" case Value2: return "Value2" default: return "Unknown" } }Đồng thời tạo hàm
GenerateConstants(e EnumType) stringđể tạo các khai báo hằng số bằngiota:const ( Value1 TypeName = iota Value2 Value3 )Cuối cùng, tạo
GenerateFullCode(e EnumType) stringđể kết hợp cả hai đầu ra cùng một chú thích đầu tệp cho biết đây là code được tạo:// Code generated by enum generator. DO NOT EDIT. [constants] [string method]main.go: Dùng trình tạo để tạo code cho các kiểu enum khác nhau.Đọc tên kiểu enum, sau đó đọc số lượng giá trị và lần lượt đọc tên của từng giá trị. Truyền thông tin này cho trình tạo rồi in toàn bộ code đã tạo.
Chương trình sẽ cung cấp đầu vào như sau:
- Dòng 1: Tên kiểu enum
- Dòng 2: Số lượng giá trị
- Các dòng tiếp theo: Tên của từng giá trị
Ví dụ, với đầu vào:
Status
3
Pending
Active
CompletedĐầu ra của bạn sẽ là:
// Code generated by enum generator. DO NOT EDIT.
const (
Pending Status = iota
Active
Completed
)
func (e Status) String() string {
switch e {
case Pending:
return "Pending"
case Active:
return "Active"
case Completed:
return "Completed"
default:
return "Unknown"
}
}Và với đầu vào:
Priority
2
Low
HighĐầu ra của bạn sẽ là:
// Code generated by enum generator. DO NOT EDIT.
const (
Low Priority = iota
High
)
func (e Priority) String() string {
switch e {
case Low:
return "Low"
case High:
return "High"
default:
return "Unknown"
}
}Bài tập này minh họa khái niệm cốt lõi đằng sau các công cụ như stringer: tạo code lặp lại nhưng an toàn về kiểu dữ liệu trong lúc biên dịch, thay vì dùng reflection khi chạy. Trình tạo của bạn tạo ra code Go hợp lệ, có thể lưu vào tệp rồi biên dịch cùng phần còn lại của dự án.
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Đọc tên kiểu enum
typeName, _ := reader.ReadString('\n')
typeName = strings.TrimSpace(typeName)
// Đọc số lượng giá trị
countStr, _ := reader.ReadString('\n')
count, _ := strconv.Atoi(strings.TrimSpace(countStr))
// Đọc tên từng giá trị
values := make([]string, count)
for i := 0; i < count; i++ {
value, _ := reader.ReadString('\n')
values[i] = strings.TrimSpace(value)
}
// TODO: Tạo EnumType từ dữ liệu đã đọc
// TODO: Tạo và in ra toàn bộ code bằng GenerateFullCode
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Nền tảng OOP trong Go
Tệp bên ngoàiWorkspace và module trong GoPackage và importTên được export và không được exportGiới thiệu về OOP trong GoStruct như lớpĐịnh nghĩa phương thức cho structReceiver kiểu con trỏ và kiểu giá trịKhởi tạo structHàm khởi tạoÔn tập: máy tính đơn giản4Interface
Giới thiệu về interfaceTriển khai ngầm địnhInterface như một hợp đồngInterface rỗng (any)Khẳng định kiểuswitch kiểuKết hợp interfaceInterface Stringer và ErrorÔn tập: Máy tính hình học7Tính đóng gói
Trường được xuất và không được xuấtĐóng gói ở cấp packagePhương thức getter và setterẨn thông tin trong GoÔn tập: hồ sơ học sinh10Generics (Go 1.18 trở lên)
Giới thiệu về GenericsTham số kiểuRàng buộc kiểuStruct tổng quátGiải pháp thay thế cho phương thức tổng quátÔn tập: tập hợp tổng quát13Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonMẫu FactoryMẫu Abstract FactoryMẫu ObserverMẫu Strategy2Tìm hiểu chuyên sâu về kiểu dữ liệu và struct
Kiểu cơ bản và kiểu tổng hợpĐịnh nghĩa kiểu dữ liệu tùy chỉnhThẻ structStruct ẩn danhStruct lồng nhauGiá trị zero và giá trị mặc địnhÔn tập: sổ liên lạc5Ưu tiên kết hợp hơn kế thừa
Vì sao Go không có kế thừaNhập môn nhúng structTự động nâng phương thứcNhúng nhiều structNhúng và kết tậpChe khuất phương thức được nhúngÔn tập: hệ phân cấp nhân viên8Xử lý lỗi và lập trình hướng đối tượng
Giao diện errorCác kiểu lỗi tùy chỉnhBọc lỗi với fmt.ErrorfLỗi sentinelerrors.Is() và errors.As()Panic, defer và recoverÔn tập: trình phân tích tệp3Con trỏ và bộ nhớ
Cơ bản về con trỏ trong GoCon trỏ đến structTruyền theo giá trị và tham chiếuHàm new()Thu gom rác trong GoÔn tập: xây dựng danh sách liên kết6Đa hình trong Go
Đa hình qua interfaceĐịnh kiểu vịt trong GoQuy tắc thỏa mãn interfaceCác tập hợp đa hìnhTiêm phụ thuộcÔn tập: bộ xử lý thanh toán9Lập trình đồng thời và lập trình hướng đối tượng
Cơ bản về goroutineChannel và giao tiếpChannel có bộ đệm và không có bộ đệmCâu lệnh selectsync.Mutex và sync.RWMutexsync.WaitGroupThiết kế struct an toàn luồngÔn tập: worker pool12Các khái niệm OOP nâng cao
Mẫu tùy chọn hàmMẫu Builder trong GoChuỗi phương thứcBí danh kiểu dữ liệu và định nghĩa kiểuCơ bản về reflectionTổng quan về tạo codeTự luyện tập: Trình biên dịch Go online