Rumah > Artikel > pembangunan bahagian belakang > Pergi: Petunjuk & Pengurusan Memori
TL;DR: Terokai pengendalian memori Go dengan penunjuk, peruntukan tindanan dan timbunan, analisis melarikan diri dan pengumpulan sampah dengan contoh
Apabila saya mula belajar Go, saya tertarik dengan pendekatannya terhadap pengurusan ingatan, terutamanya apabila ia berkaitan dengan petunjuk. Go mengendalikan memori dengan cara yang cekap dan selamat, tetapi ia boleh menjadi sedikit kotak hitam jika anda tidak mengintip di bawah tudung. Saya ingin berkongsi beberapa cerapan tentang cara Go mengurus memori dengan penunjuk, tindanan dan timbunan serta konsep seperti analisis melarikan diri dan pengumpulan sampah. Sepanjang perjalanan, kita akan melihat contoh kod yang menggambarkan idea ini dalam amalan.
Sebelum menyelami petunjuk dalam Go, adalah berguna untuk memahami cara tindanan dan timbunan berfungsi. Ini adalah dua kawasan ingatan di mana pembolehubah boleh disimpan, masing-masing mempunyai ciri tersendiri.
Dalam Go, pengkompil memutuskan sama ada untuk memperuntukkan pembolehubah pada tindanan atau timbunan berdasarkan cara ia digunakan. Proses membuat keputusan ini dipanggil analisis melarikan diri, yang akan kami terokai dengan lebih terperinci kemudian.
Dalam Go, apabila anda menghantar pembolehubah seperti integer, rentetan atau boolean kepada fungsi, ia secara semula jadi dihantar oleh nilai. Ini bermakna salinan pembolehubah dibuat, dan fungsi berfungsi dengan salinan itu. Ini bermakna, sebarang perubahan yang dibuat kepada pembolehubah di dalam fungsi tidak akan menjejaskan pembolehubah di luar skopnya.
Berikut ialah contoh mudah:
package main import "fmt" func increment(num int) { num++ fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num) } func main() { n := 21 fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n) increment(n) fmt.Printf("After increment(): n = %d, address = %p \n", n, &n) }
Output:
Before increment(): n = 21, address = 0xc000012070 Inside increment(): num = 22, address = 0xc000012078 After increment(): n = 21, address = 0xc000012070
Dalam kod ini:
Bawa pulang: Menerusi nilai adalah selamat dan mudah, tetapi untuk struktur data yang besar, penyalinan mungkin menjadi tidak cekap.
Untuk mengubah suai pembolehubah asal di dalam fungsi, anda boleh menghantar penuding kepadanya. Penunjuk memegang alamat memori pembolehubah, membenarkan fungsi mengakses dan mengubah suai data asal.
Begini cara anda boleh menggunakan penunjuk:
package main import "fmt" func incrementPointer(num *int) { (*num)++ fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num) } func main() { n := 42 fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n) incrementPointer(&n) fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n) }
Output:
Before incrementPointer(): n = 42, address = 0xc00009a040 Inside incrementPointer(): num = 43, address = 0xc00009a040 After incrementPointer(): n = 43, address = 0xc00009a040
Dalam contoh ini:
Takeaway: Menggunakan penunjuk membenarkan fungsi mengubah suai pembolehubah asal, tetapi ia memperkenalkan pertimbangan tentang peruntukan memori.
Apabila anda mencipta penuding kepada pembolehubah, Go perlu memastikan pembolehubah itu hidup selagi penuding itu berfungsi. Ini selalunya bermakna memperuntukkan pembolehubah pada timbunan dan bukannya tindanan.
Pertimbangkan fungsi ini:
package main import "fmt" func increment(num int) { num++ fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num) } func main() { n := 21 fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n) increment(n) fmt.Printf("After increment(): n = %d, address = %p \n", n, &n) }
Di sini, num ialah pembolehubah setempat dalam createPointer(). Jika num disimpan pada timbunan, ia akan dibersihkan sebaik sahaja fungsi kembali, meninggalkan penuding berjuntai. Untuk mengelakkan ini, Go memperuntukkan num pada timbunan supaya ia kekal sah selepas createPointer() keluar.
Penunjuk Berjuntai
penunjuk berjuntai berlaku apabila penunjuk merujuk kepada ingatan yang telah dibebaskan.
Go menghalang penunjuk berjuntai dengan pemungut sampahnya, memastikan memori tidak dibebaskan semasa ia masih dirujuk. Walau bagaimanapun, memegang penunjuk lebih lama daripada yang diperlukan boleh menyebabkan penggunaan memori meningkat atau kebocoran memori dalam senario tertentu.
Analisis melarikan diri menentukan sama ada pembolehubah perlu hidup di luar skop fungsinya. Jika pembolehubah dikembalikan, disimpan dalam penuding, atau ditangkap oleh goroutine, ia terlepas dan diperuntukkan pada timbunan. Walau bagaimanapun, walaupun pembolehubah tidak terlepas, pengkompil mungkin memperuntukkannya pada timbunan atas sebab lain, seperti keputusan pengoptimuman atau pengehadan saiz tindanan.
Contoh Pembolehubah Melarikan diri:
Before increment(): n = 21, address = 0xc000012070 Inside increment(): num = 22, address = 0xc000012078 After increment(): n = 21, address = 0xc000012070
Dalam kod ini:
Memahami Analisis Melarikan Diri dengan go build -gcflags '-m'
Anda boleh melihat perkara yang diputuskan oleh pengkompil Go dengan menggunakan pilihan -gcflags '-m':
package main import "fmt" func incrementPointer(num *int) { (*num)++ fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num) } func main() { n := 42 fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n) incrementPointer(&n) fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n) }
Ini akan mengeluarkan mesej yang menunjukkan sama ada pembolehubah melarikan diri ke timbunan.
Go menggunakan pemungut sampah untuk mengurus peruntukan memori dan deallocation pada timbunan. Ia membebaskan memori yang tidak lagi dirujuk secara automatik, membantu mengelakkan kebocoran memori.
Contoh:
Before incrementPointer(): n = 42, address = 0xc00009a040 Inside incrementPointer(): num = 43, address = 0xc00009a040 After incrementPointer(): n = 43, address = 0xc00009a040
Dalam kod ini:
Bawa pulang: Pengumpul sampah Go memudahkan pengurusan ingatan tetapi boleh memperkenalkan overhed.
Walaupun penunjuk kuat, ia boleh membawa kepada isu jika tidak digunakan dengan berhati-hati.
Walaupun pemungut sampah Go membantu mengelakkan penunjuk berjuntai, anda masih boleh menghadapi masalah jika anda memegang penunjuk lebih lama daripada yang diperlukan.
Contoh:
package main import "fmt" func increment(num int) { num++ fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num) } func main() { n := 21 fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n) increment(n) fmt.Printf("After increment(): n = %d, address = %p \n", n, &n) }
Dalam kod ini:
Berikut ialah contoh di mana penunjuk terlibat secara langsung:
Before increment(): n = 21, address = 0xc000012070 Inside increment(): num = 22, address = 0xc000012078 After increment(): n = 21, address = 0xc000012070
Mengapa Kod Ini Gagal:
Membetulkan Perlumbaan Data:
Kami boleh membetulkannya dengan menambahkan penyegerakan dengan mutex:
package main import "fmt" func incrementPointer(num *int) { (*num)++ fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num) } func main() { n := 42 fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n) incrementPointer(&n) fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n) }
Cara Pembetulan Ini Berfungsi:
Perlu diambil perhatian bahawa Spesifikasi bahasa Go tidak secara langsung menentukan sama ada pembolehubah diperuntukkan pada tindanan atau timbunan. Ini ialah butiran pelaksanaan masa jalan dan pengkompil, membenarkan kefleksibelan dan pengoptimuman yang boleh berbeza-beza merentas versi atau pelaksanaan Go.
Ini bermakna:
Contoh:
Walaupun anda menjangkakan pembolehubah akan diperuntukkan pada tindanan, pengkompil mungkin memutuskan untuk mengalihkannya ke timbunan berdasarkan analisisnya.
package main import "fmt" func increment(num int) { num++ fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num) } func main() { n := 21 fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n) increment(n) fmt.Printf("After increment(): n = %d, address = %p \n", n, &n) }
Takeaway: Memandangkan butiran peruntukan memori adalah pelaksanaan yang agak dalaman dan bukan sebahagian daripada Spesifikasi Bahasa Go, maklumat ini hanyalah garis panduan umum dan bukan peraturan tetap yang mungkin berubah pada masa akan datang.
Apabila membuat keputusan antara lulus dengan nilai atau dengan penunjuk, kita mesti mempertimbangkan saiz data dan implikasi prestasi.
Meluluskan Struktur Besar mengikut Nilai:
Before increment(): n = 21, address = 0xc000012070 Inside increment(): num = 22, address = 0xc000012078 After increment(): n = 21, address = 0xc000012070
Melalui Struktur Besar dengan Penunjuk:
package main import "fmt" func incrementPointer(num *int) { (*num)++ fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num) } func main() { n := 42 fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n) incrementPointer(&n) fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n) }
Pertimbangan:
Dalam kerjaya awal, mengimbau kembali masa ketika saya mengoptimumkan aplikasi Go yang memproses set data yang besar. Pada mulanya, saya lulus struct besar mengikut nilai, dengan mengandaikan ia akan memudahkan penaakulan tentang kod tersebut. Walau bagaimanapun, saya secara kebetulan mendapati penggunaan memori yang tinggi dan kekerapan pengumpulan sampah dijeda.
Selepas memprofilkan aplikasi menggunakan alat pprof Go dalam pengaturcaraan pasangan dengan senior saya, kami mendapati bahawa menyalin struct besar adalah satu halangan. Kami memfaktorkan semula kod untuk menghantar penunjuk dan bukannya nilai. Ini mengurangkan penggunaan memori dan meningkatkan prestasi dengan ketara.
Tetapi perubahan itu bukan tanpa cabaran. Kami perlu memastikan bahawa kod kami selamat untuk benang kerana berbilang goroutine kini mengakses data kongsi. Kami melaksanakan penyegerakan menggunakan mutex dan menyemak kod dengan teliti untuk kemungkinan keadaan perlumbaan.
Pengajaran yang Diperoleh: Pemahaman awal bagaimana Go mengendalikan peruntukan memori boleh membantu anda menulis kod yang lebih cekap, kerana ia penting untuk mengimbangi peningkatan prestasi dengan keselamatan dan kebolehselenggaraan kod.
Pendekatan Go terhadap pengurusan ingatan (seperti yang berlaku di tempat lain) menyeimbangkan antara prestasi dan kesederhanaan. Dengan mengabstraksi banyak butiran peringkat rendah, ia membolehkan pembangun menumpukan pada membina aplikasi yang mantap tanpa terperangkap dalam pengurusan memori manual.
Perkara penting yang perlu diingat:
Dengan mengingati konsep ini dan menggunakan alatan Go untuk memprofil dan menganalisis kod anda, anda boleh menulis aplikasi yang cekap dan selamat.
Saya harap penerokaan pengurusan ingatan Go dengan petunjuk ini akan membantu. Sama ada anda baru bermula dengan Go atau ingin mendalami pemahaman anda, bereksperimen dengan kod dan memerhati bagaimana pengkompil dan masa jalan berkelakuan adalah cara yang bagus untuk belajar.
Jangan ragu untuk berkongsi pengalaman anda atau sebarang soalan yang mungkin anda ada — Saya sentiasa berminat untuk berbincang, mempelajari dan menulis lebih lanjut tentang Go!
Anda tahu? Penunjuk boleh dibuat terus untuk jenis data tertentu dan tidak boleh, untuk sesetengahnya. Meja pendek ini meliputi mereka.
Type | Supports Direct Pointer Creation? | Example |
---|---|---|
Structs | ✅ Yes | p := &Person{Name: "Alice", Age: 30} |
Arrays | ✅ Yes | arrPtr := &[3]int{1, 2, 3} |
Slices | ❌ No (indirect via variable) | slice := []int{1, 2, 3}; slicePtr := &slice |
Maps | ❌ No (indirect via variable) | m := map[string]int{}; mPtr := &m |
Channels | ❌ No (indirect via variable) | ch := make(chan int); chPtr := &ch |
Basic Types | ❌ No (requires a variable) | val := 42; p := &val |
time.Time (Struct) | ✅ Yes | t := &time.Time{} |
Custom Structs | ✅ Yes | point := &Point{X: 1, Y: 2} |
Interface Types | ✅ Yes (but rarely needed) | var iface interface{} = "hello"; ifacePtr := &iface |
time.Duration (Alias of int64) | ❌ No | duration := time.Duration(5); p := &duration |
Sila beritahu saya dalam komen jika anda suka ini; Saya akan cuba menambah kandungan bonus sedemikian pada artikel saya pada masa hadapan.
Terima kasih kerana membaca! Untuk lebih banyak kandungan, sila pertimbangkan untuk mengikuti.
Semoga kod itu bersama anda :)
Pautan Sosial Saya: LinkedIn | GitHub | ? (dahulunya Twitter) | Substack | Dev.to | Hashnode
Atas ialah kandungan terperinci Pergi: Petunjuk & Pengurusan Memori. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!