Iterator praktis saat data mengalir melalui pipeline, tetapi pada akhirnya Anda mungkin perlu menyimpan nilai-nilai tersebut dalam sebuah slice. Jika sudah memiliki slice tujuan, mengumpulkan iterator secara terpisah lalu menambahkannya hanya menciptakan langkah perantara yang tidak perlu.
Go 1.23 menambahkan slices.AppendSeq tepat untuk batas ini. Fungsi tersebut mengonsumsi sebuah iter.Seq, menambahkan setiap nilai yang dihasilkan ke slice yang sudah ada, lalu mengembalikan slice hasilnya.
Apa yang dilakukan slices.AppendSeq
Fungsi ini memiliki signature yang ringkas:
func AppendSeq[Slice ~[]E, E any](s Slice, seq iter.Seq[E]) SliceArgumen pertama adalah slice yang akan diperluas. Argumen kedua adalah iterator yang menghasilkan elemen dengan tipe yang sama. AppendSeq melakukan range atas sequence tersebut dan menambahkan nilai yang dihasilkan sesuai urutan iterasi.
Berikut contoh lengkapnya:
package main
import (
"fmt"
"iter"
"slices"
)
func evens(limit int) iter.Seq[int] {
return func(yield func(int) bool) {
for n := 0; n < limit; n++ {
if n%2 == 0 && !yield(n) {
return
}
}
}
}
func main() {
numbers := []int{10, 20}
numbers = slices.AppendSeq(numbers, evens(7))
fmt.Println(numbers)
}Output-nya adalah:
[10 20 0 2 4 6]Nilai yang sudah ada tetap berada di depan, kemudian nilai dari sequence mengikutinya sesuai urutan iterasi.
Gunakan slices.AppendSeq ketika sudah memiliki tujuan
Alternatif yang umum adalah mematerialisasi iterator terlebih dahulu:
extra := slices.Collect(evens(7))
numbers = append(numbers, extra...)Cara itu bekerja, tetapi extra hanya ada untuk menjembatani iterator dan tujuan. slices.AppendSeq menyatakan operasi sebenarnya secara langsung:
numbers = slices.AppendSeq(numbers, evens(7))Perbedaan ini berguna pada kode yang membangun hasil secara bertahap. Misalnya, sebuah konfigurasi dimulai dengan nilai default lalu menerima nilai tambahan dari iterator:
func buildPorts(discovered iter.Seq[int]) []int {
ports := []int{80, 443}
return slices.AppendSeq(ports, discovered)
}Tidak ada alasan membuat slice kedua hanya untuk menggabungkannya dengan ports setelah itu.
Jika Anda belum memiliki tujuan dan hanya ingin membuat slice baru yang berisi semua nilai dari sequence, slices.Collect biasanya lebih jelas. Keduanya menyelesaikan masalah yang sedikit berbeda: Collect mematerialisasi sequence, sedangkan AppendSeq memperluas slice menggunakan sequence.
Slice yang dikembalikan itu penting
Perlakukan slices.AppendSeq seperti append: selalu gunakan slice yang dikembalikan.
numbers = slices.AppendSeq(numbers, seq)Penambahan dapat melampaui capacity numbers. Ketika itu terjadi, Go dapat mengalokasikan backing array baru dan slice yang dikembalikan akan merujuk ke storage baru tersebut. Mengabaikan return value berisiko membuat kode tetap memakai slice header lama yang tidak mencerminkan panjang hasil penambahan.
Aturan kepemilikan ini sama dengan append biasa:
numbers = append(numbers, 30)AppendSeq tidak mengubah semantik alokasi slice hanya karena input-nya datang melalui iterator.
Jika perencanaan capacity penting, Anda tetap dapat menyediakan ruang sebelum menambahkan ketika memiliki estimasi ukuran yang dapat diandalkan:
ports := make([]int, 0, expected)
ports = slices.AppendSeq(ports, discovered)Hal ini dapat mengurangi realokasi ketika estimasinya bermakna. Namun, jangan menetapkan capacity yang terlalu besar hanya karena sequence bersifat lazy. Untuk sequence yang ukurannya tidak diketahui dengan murah, membiarkan slice tumbuh secara normal sering kali lebih sederhana.
AppendSeq mengonsumsi iterator
Memanggil slices.AppendSeq adalah operasi yang mengonsumsi iterator. Fungsi meminta nilai dari sequence sampai sequence selesai.
Hal ini penting ketika iterator melakukan pekerjaan saat menghasilkan nilai. Bayangkan sequence yang didukung oleh parsing record, pembacaan nilai yang dihasilkan, atau traversal struktur data. AppendSeq tidak mempertahankan sifat lazy tersebut setelah pemanggilan selesai; slice hasil berisi semua nilai yang telah dihasilkan.
Untuk data terbatas yang perlu disimpan, itulah tujuannya. Namun, untuk iterator yang secara konseptual tidak terbatas, ini merupakan ketidakcocokan serius. Sequence berikut tidak pernah selesai:
func countForever() iter.Seq[int] {
return func(yield func(int) bool) {
for n := 0; ; n++ {
if !yield(n) {
return
}
}
}
}Memberikan countForever() langsung ke slices.AppendSeq tidak memberi consumer titik selesai alami. Pemanggilan terus meminta nilai dan slice terus membesar sampai sesuatu dari luar menghentikan proses atau resource habis.
Batasi sequence sebelum mematerialisasikannya, atau tetap proses secara lazy jika Anda sebenarnya tidak membutuhkan semua nilai berada di memori.
Sequence kosong mempertahankan tujuan
Sequence kosong tidak menambahkan apa pun. Secara khusus, standard library menetapkan bahwa sequence kosong mempertahankan status nil dari slice tujuan.
func empty() iter.Seq[int] {
return func(yield func(int) bool) {}
}
var numbers []int
numbers = slices.AppendSeq(numbers, empty())
fmt.Println(numbers == nil) // trueTujuan kosong yang non-nil juga tetap non-nil:
numbers := []int{}
numbers = slices.AppendSeq(numbers, empty())
fmt.Println(numbers == nil) // falseSebagian besar kode aplikasi tidak perlu terlalu bergantung pada perbedaan antara slice nil dan kosong, tetapi perbedaan tersebut dapat penting pada batas API atau pengujian yang sengaja mempertahankan representasi. AppendSeq tidak menghapus perbedaan itu ketika tidak ada yang ditambahkan.
Error iterator tetap memerlukan desain eksplisit
iter.Seq menghasilkan nilai, bukan pasangan (value, error) seperti bentuk iterator konvensional. Karena itu, slices.AppendSeq tidak memiliki hasil error sendiri.
Jika proses menghasilkan nilai dapat gagal, tentukan bagaimana kegagalan direpresentasikan sebelum menyerahkan sequence ke AppendSeq. Salah satu opsi adalah menghasilkan tipe result yang membawa nilai atau error, lalu memeriksa hasil tersebut. Opsi lain adalah menjaga I/O yang dapat gagal tetap di luar abstraksi sequence ketika itu membuat control flow lebih jelas.
Jangan menganggap AppendSeq dapat melaporkan kegagalan producer hanya karena ia bertindak sebagai consumer. Kontraknya hanya menambahkan nilai yang dihasilkan sequence.
Peringatan yang sama berlaku untuk panic. Jika iterator panic saat dikonsumsi, AppendSeq tidak mengubah panic tersebut menjadi error. Semantik panic normal tetap berlaku.
Jangan gunakan AppendSeq ketika tujuannya streaming
Mematerialisasi nilai berguna ketika operasi berikutnya memerlukan indexing, traversal berulang, sorting, atau API yang menerima slice. Langkah itu tidak diperlukan jika tahap berikutnya sudah dapat mengonsumsi iterator.
Sebagai contoh, kode berikut menjaga sequence tetap lazy:
for port := range discovered {
if err := check(port); err != nil {
return err
}
}Mengubah discovered menjadi slice terlebih dahulu akan menambah kebutuhan storage dan memaksa seluruh sequence selesai sebelum check melihat nilai pertama. Hal itu juga mencegah loop menghindari produksi nilai berikutnya setelah terjadi error lebih awal.
Gunakan slices.AppendSeq pada batas materialisasi yang nyata, bukan secara otomatis setiap kali iterator muncul.
Pilih consumer yang sesuai dengan operasi berikutnya
slices.AppendSeq paling berguna ketika sebuah slice sudah ada dan nilai yang dihasilkan iterator memang perlu ditambahkan di bagian akhirnya. Fungsi ini menghilangkan pola sementara Collect-lalu-append sambil mempertahankan perilaku pertumbuhan slice biasa.
Simpan slice yang dikembalikan, ingat bahwa iterator dikonsumsi sepenuhnya, dan hindari memberinya sequence yang tidak terbatas. Jika tidak ada tujuan yang sudah tersedia, gunakan slices.Collect; jika operasi berikutnya dapat mengonsumsi sequence secara langsung, pertahankan sifat lazy-nya. Pilihan yang tepat adalah yang sesuai dengan titik tempat program Anda benar-benar membutuhkan nilai berubah menjadi data yang tersimpan.