Thu gom rác trong Go
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 23 trên 107.
Không giống như các ngôn ngữ như C hay C++, Go tự động quản lý bộ nhớ thông qua thu gom rác (GC). Bạn không cần tự giải phóng bộ nhớ khi dùng xong. Go runtime theo dõi vùng nhớ nào vẫn đang được sử dụng và thu hồi phần còn lại.
Khi cấp phát bộ nhớ bằng new(), make() hoặc tạo biến, bộ thu gom rác của Go sẽ theo dõi các vùng bộ nhớ này. Khi chương trình không còn phần nào có thể truy cập đến một giá trị, GC sẽ tự động giải phóng vùng bộ nhớ đó.
func createPerson() *Person {
p := &Person{Name: "Alice", Age: 30}
return p // p thoát ra vùng nhớ heap, GC sẽ quản lý vùng nhớ đó
}
func main() {
person := createPerson()
fmt.Println(person.Name)
// Khi không còn dùng person, GC thu hồi bộ nhớ
}Trong ví dụ này, struct Person được cấp phát trên heap vì nó được trả về từ hàm. Trình biên dịch Go thực hiện phân tích thoát (escape analysis) để xác định biến có thể nằm trên stack hay phải chuyển sang heap. Bộ thu gom rác chỉ quản lý bộ nhớ heap.
Việc quản lý bộ nhớ tự động này giúp bạn tập trung xây dựng ứng dụng mà không phải lo về rò rỉ bộ nhớ do quên giải phóng bộ nhớ. Tuy nhiên, hiểu rằng GC tồn tại sẽ giúp bạn viết code hiệu quả hơn bằng cách giảm thiểu việc cấp phát không cần thiết khi hiệu năng quan trọng.
Thử thách
DễHãy xây dựng một trình quản lý phiên để minh họa cách bộ thu gom rác (garbage collector) của Go xử lý bộ nhớ cho các đối tượng thoát ra heap. Bạn sẽ tạo các phiên được cấp phát động và trả về từ các hàm, để GC quản lý vòng đời của chúng.
Bạn sẽ sắp xếp code trong hai tệp:
session.go: Định nghĩa một cấu trúcSessionvới các trườngID(chuỗi),Username(chuỗi) vàData(một lát chuỗi). Tạo hàmNewSessionnhận ID và tên người dùng, cấp phát một Session mới trên heap (bằng cách trả về một con trỏ) và khởi tạo lát Data rỗng. Thêm cả phương thứcAddDatavới receiver là con trỏ để nối một chuỗi vào lát Data của phiên, và phương thứcSummarytrả về một chuỗi đã định dạng để hiển thị thông tin chi tiết của phiên.main.go: Đọc thông tin phiên từ đầu vào, tạo một phiên bằng hàm khởi tạo, thêm một vài mục dữ liệu vào phiên rồi hiển thị phần tóm tắt. Vì phiên được tạo bên trong một hàm và được trả về dưới dạng con trỏ, nó thoát ra heap, nơi bộ thu gom rác sẽ quản lý phiên.
Đầu vào sẽ gồm:
- Dòng 1: ID phiên
- Dòng 2: Tên người dùng
- Dòng 3: Mục dữ liệu đầu tiên cần thêm
- Dòng 4: Mục dữ liệu thứ hai cần thêm
Phương thức Summary của bạn sẽ trả về một chuỗi theo định dạng sau:
Session [ID] for user [Username]
Data entries: [count]
- [entry1]
- [entry2]Ví dụ, với sess-001, alice, login và view_dashboard, đầu ra sẽ là:
Session sess-001 for user alice
Data entries: 2
- login
- view_dashboardĐiểm mấu chốt ở đây là hàm NewSession tạo một Session rồi trả về con trỏ đến nó. Điều này khiến Session "thoát" ra heap thay vì nằm trên ngăn xếp, nhờ đó nó có thể được thu gom rác khi không còn tham chiếu nào đến nó. Code của bạn không cần giải phóng bộ nhớ này theo cách thủ công: Go tự xử lý.
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Đọc ID phiên
sessionID, _ := reader.ReadString('\n')
sessionID = sessionID[:len(sessionID)-1]
// Đọc tên người dùng
username, _ := reader.ReadString('\n')
username = username[:len(username)-1]
// Đọc mục dữ liệu đầu tiên
data1, _ := reader.ReadString('\n')
data1 = data1[:len(data1)-1]
// Đọc mục dữ liệu thứ hai
data2, _ := reader.ReadString('\n')
data2 = data2[:len(data2)-1]
// TODO: Tạo phiên mới bằng hàm (function) NewSession
// Phiên sẽ chuyển sang vùng nhớ heap vì ta trả về một con trỏ
// TODO: Thêm các mục dữ liệu vào phiên bằng phương thức (method) AddData
// TODO: In ra bản tóm tắt phiên bằng phương thức Summary
fmt.Println("")
}
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 poolTự luyện tập: Trình biên dịch Go online