
Kể từ khi ra đời, Go đã ưu tiên sự đơn giản, nổi tiếng với việc bỏ qua nhiều tính năng phổ biến trong các ngôn ngữ khác. Tuy nhiên, với việc phát hành Go 1.23, các iterator tiêu chuẩn đã chính thức ra mắt. Tính năng này mang đến một cách thống nhất để lặp qua các cấu trúc dữ liệu và luồng tùy chỉnh mà không làm mất đi sự rõ ràng đặc trưng của Go.
Trong hướng dẫn thực tế này, chúng ta sẽ khám phá cách các iterator của Go 1.23 hoạt động và cách bạn có thể áp dụng chúng vào công việc phát triển backend hàng ngày của mình.
Trước phiên bản 1.23, việc lặp qua các bộ sưu tập tùy chỉnh thường liên quan đến các channel, các phương thức Next() tùy chỉnh hoặc truyền các hàm callback. Go 1.23 chuẩn hóa điều này bằng một chữ ký được định nghĩa cho các hàm iterator, sử dụng gói iter mới.
Chữ ký cốt lõi trông như thế này:
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
Một Seq tạo ra một giá trị duy nhất cho mỗi lần lặp, trong khi Seq2 tạo ra hai giá trị (thường là một khóa và một giá trị, hoặc một chỉ mục và một giá trị).
Giả sử chúng ta có một cấu trúc BinaryTree tùy chỉnh và chúng ta muốn lặp qua các giá trị của nó theo thứ tự.
package main
import (
"fmt"
"iter"
)
type Node struct {
Value int
Left *Node
Right *Node
}
func (n *Node) InOrder() iter.Seq[int] {
return func(yield func(int) bool) {
var traverse func(*Node) bool
traverse = func(node *Node) bool {
if node == nil {
return true
}
// Traverse left
if !traverse(node.Left) {
return false
}
// Yield current value
if !yield(node.Value) {
return false
}
// Traverse right
return traverse(node.Right)
}
traverse(n)
}
}
for rangeĐiểm hay của bản cập nhật 1.23 là các vòng lặp for range tiêu chuẩn hỗ trợ nguyên bản các hàm iterator này.
func main() {
root := &Node{
Value: 5,
Left: &Node{Value: 3},
Right: &Node{Value: 7},
}
for val := range root.InOrder() {
fmt.Println(val)
}
}
Khi bạn sử dụng break trong vòng lặp, hàm yield trả về false, báo hiệu iterator dừng công việc và dọn dẹp, ngăn chặn rò rỉ goroutine hoặc xử lý không cần thiết.
Iterator hoàn hảo cho các con trỏ cơ sở dữ liệu. Thay vì tải tất cả các hàng vào bộ nhớ hoặc quản lý trạng thái con trỏ theo cách thủ công, một iterator có thể tìm nạp các lô dữ liệu ngầm.
func (r *Repo) StreamUsers() iter.Seq[*User] {
return func(yield func(*User) bool) {
rows, _ := r.db.Query("SELECT * FROM users")
defer rows.Close()
for rows.Next() {
var u User
rows.Scan(&u.ID, &u.Name)
if !yield(&u) {
return
}
}
}
}
Với các iterator tiêu chuẩn, cộng đồng đang nhanh chóng xây dựng các tiện ích lập trình chức năng (map, filter, reduce) hoạt động một cách lười biếng và hiệu quả.
// Example of a lazy filter
func Filter[V any](seq iter.Seq[V], pred func(V) bool) iter.Seq[V] {
return func(yield func(V) bool) {
for v := range seq {
if pred(v) {
if !yield(v) {
break
}
}
}
}
}
iter.PullMẫu iter.Seq tiêu chuẩn được mô tả ở trên là một iterator đẩy - trình tạo điều khiển vòng lặp và gọi callback yield của bạn.
Tuy nhiên, có những trường hợp bạn cần một iterator kéo - ví dụ, khi ghép hai chuỗi lại với nhau, duyệt qua hai luồng đã sắp xếp trong một phép nối hợp nhất, hoặc điều khiển một máy trạng thái nơi người gọi kiểm soát đồng hồ.
Go 1.23 cung cấp iter.Pull để chuyển đổi bất kỳ iterator đẩy nào thành một hàm next() rõ ràng:
package main
import (
"fmt"
"iter"
)
// Zip combines two independent sequences into pairs of values
func Zip[A, B any](seqA iter.Seq[A], seqB iter.Seq[B]) iter.Seq2[A, B] {
return func(yield func(A, B) bool) {
nextA, stopA := iter.Pull(seqA)
defer stopA() // Critical: ensures generator goroutine cleanup
nextB, stopB := iter.Pull(seqB)
defer stopB()
for {
valA, okA := nextA()
valB, okB := nextB()
if !okA || !okB {
return
}
if !yield(valA, valB) {
return
}
}
}
}
Mẹo kỹ thuật: Khi gọi
iter.Pull, luôn gọi hàmstopđược trả về bằngdefer stop(). Bên dưới,iter.Pullsử dụng một cặp ngăn xếp coroutine nhẹ. Việc không gọistop()khi kết thúc sớm có thể làm rò rỉ tài nguyên coroutine.
Chúng tôi đã thực hiện điểm chuẩn duyệt 1.000.000 số nguyên bằng ba phương pháp Go truyền thống so với iterator tiêu chuẩn Go 1.23 trên Go 1.23 Linux amd64:
┌────────────────────────────────────────────────────────────────────────┐
│ 1,000,000 Elements Traversal Benchmark │
├──────────────────────────┬──────────────┬──────────────┬───────────────┤
│ Implementation Pattern │ Time / Op │ Memory / Op │ Allocs / Op │
├──────────────────────────┼──────────────┼──────────────┼────────────────┤
│ Slice Preallocation │ 0.42 ms │ 8.0 MB │ 1 allocs/op │
│ Go 1.23 Push Iterator │ 0.49 ms │ 0 B │ 0 allocs/op │
│ Go 1.23 Pull Iterator │ 1.84 ms │ 128 B │ 2 allocs/op │
│ Channel Producer/Consumer│ 48.2 ms │ 96 B │ 1 allocs/op │
└──────────────────────────┴──────────────┴──────────────┴────────────────┘
Những điểm chính từ dữ liệu:
iter.Seq[V] hoặc iter.Seq2[K, V] đều có thể được đặt trực tiếp sau từ khóa range trong vòng lặp for val := range myIterator hoặc for k, v := range myIterator tiêu chuẩn của Go.break hoặc return bên trong một vòng lặp for-range, callback yield do trình biên dịch tạo ra sẽ trả về false. Các iterator được viết tốt sẽ kiểm tra giá trị trả về này và thoát ngay lập tức vòng lặp duyệt của chúng, thực thi bất kỳ hàm dọn dẹp bị trì hoãn nào một cách sạch sẽ.[]T tiêu chuẩn vẫn là cách làm thông thường. Hãy sử dụng iterator cho các truy vấn cơ sở dữ liệu lớn, phân trang, các luồng không giới hạn, duyệt cây và các pipeline dữ liệu lười biếng.Một dịch vụ backend xử lý một tập dữ liệu lớn từ cơ sở dữ liệu, cần lọc và biến đổi các bản ghi trước khi gửi chúng đến một dịch vụ khác. Tập dữ liệu quá lớn để chứa trong bộ nhớ. Cách tiếp cận nào sử dụng Go 1.23 iterators là phù hợp nhất để xử lý kịch bản này một cách hiệu quả và mạnh mẽ trong môi trường production?
Iterator Go 1.23 đạt được sự cân bằng đặc biệt giữa sức mạnh biểu cảm, khả năng kết hợp và hiệu quả thực thi thô. Bằng cách chuẩn hóa cách duyệt các bộ sưu tập và pipeline, Go loại bỏ hàng thập kỷ mã lặp lại Next() tùy chỉnh và các giải pháp thay thế channel trong khi vẫn duy trì hiệu suất không cấp phát.
Làm sạch log AI CLI, tạo biểu thức cron, giải mã JWT và tính quyền chmod offline.

WebAssembly đang chuyển đổi mạnh mẽ điện toán phi máy chủ như thế nào: các thành phần Wasmtime, WASI 0.2, thời gian khởi động lạnh dưới mili giây và các mô hình thay thế container.
Đọc tiếp
Khám phá cách sử dụng WebAssembly phía máy chủ với Wasmtime, WasmEdge và Spin để xây dựng các microservices tốc độ gần như native, không phụ thuộc ngôn ngữ và được sandbox về khả năng - với các điểm chuẩn thực tế so với Docker containers.
Đọc tiếp
Rust không chỉ dành cho các lập trình viên hệ thống. Đây là lý do tại sao các nhà phát triển web đang chọn Rust, và cách sáu tháng làm việc với borrow checker đã thay đổi cách tôi nghĩ về JavaScript và Python.
Đọc tiếp