Rumah  >  Artikel  >  pembangunan bahagian belakang  >  Pergi: Petunjuk & Pengurusan Memori

Pergi: Petunjuk & Pengurusan Memori

Patricia Arquette
Patricia Arquetteasal
2024-11-22 01:51:14444semak imbas

Go: Pointers & Memory Management

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.

Memahami Timbunan dan Ingatan Timbunan

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.

  • Timbunan: Ini ialah kawasan memori yang beroperasi dengan cara masuk terakhir, keluar dahulu. Ia pantas dan cekap, digunakan untuk menyimpan pembolehubah dengan skop jangka pendek, seperti pembolehubah tempatan dalam fungsi.
  • Timbunan: Ini ialah kumpulan memori yang lebih besar yang digunakan untuk pembolehubah yang perlu hidup di luar skop fungsi, seperti data yang dikembalikan daripada fungsi dan digunakan di tempat lain.

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.

Melewati Nilai: Gelagat Lalai

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:

  • Fungsi increment() menerima salinan n.
  • Alamat n dalam main() dan num dalam kenaikan() adalah berbeza.
  • Mengubah suai num dalam kenaikan() tidak menjejaskan n dalam main().

Bawa pulang: Menerusi nilai adalah selamat dan mudah, tetapi untuk struktur data yang besar, penyalinan mungkin menjadi tidak cekap.

Memperkenalkan Petunjuk: Melewati Rujukan

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:

  • Kami menghantar alamat n kepada incrementPointer().
  • Kedua-dua main() dan incrementPointer() merujuk kepada alamat memori yang sama.
  • Mengubah suai num dalam incrementPointer() mempengaruhi n dalam main().

Takeaway: Menggunakan penunjuk membenarkan fungsi mengubah suai pembolehubah asal, tetapi ia memperkenalkan pertimbangan tentang peruntukan memori.

Peruntukan Memori dengan Penunjuk

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: Memutuskan Peruntukan Tindanan lwn. Timbunan

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:

  • Data hirisan dalam createSlice() terlepas kerana ia dikembalikan dan digunakan dalam main().
  • Susun atur dasar hirisan diperuntukkan pada timbunan.

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.

Pengumpulan Sampah dalam Go

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:

  • Kami membuat senarai terpaut dengan 1,000,000 nod.
  • Setiap Nod diperuntukkan pada timbunan kerana ia terlepas daripada skop createLinkedList().
  • Pengumpul sampah membebaskan ingatan apabila senarai tidak diperlukan lagi.

Bawa pulang: Pengumpul sampah Go memudahkan pengurusan ingatan tetapi boleh memperkenalkan overhed.

Potensi Perangkap dengan Penunjuk

Walaupun penunjuk kuat, ia boleh membawa kepada isu jika tidak digunakan dengan berhati-hati.

Penunjuk Berjuntai (Bersambung)

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:

  • data ialah kepingan besar yang diperuntukkan pada timbunan.
  • Dengan menyimpan rujukan kepadanya ([]int), kami menghalang pemungut sampah daripada membebaskan ingatan.
  • Ini boleh menyebabkan penggunaan memori meningkat jika tidak diurus dengan betul.

Isu Concurrency - Perlumbaan Data dengan Penunjuk

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:

  • Penyahrujukan gorout berbilang dan tambahkan pembilang penunjukPtr tanpa sebarang penyegerakan.
  • Ini membawa kepada perlumbaan data kerana berbilang goroutine mengakses dan mengubah suai lokasi memori yang sama secara serentak tanpa penyegerakan. Operasi *counterPtr melibatkan berbilang langkah (baca, kenaikan, tulis) dan tidak selamat untuk benang.

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:

  • Mu.Lock() dan mu.Unlock() memastikan bahawa hanya satu goroutine mengakses dan mengubah suai penunjuk pada satu masa.
  • Ini menghalang keadaan perlumbaan dan memastikan nilai akhir pembilang adalah betul.

Apakah yang dinyatakan oleh Spesifikasi Bahasa Go?

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:

  • Cara memori diurus boleh berubah antara versi Go yang berbeza.
  • Anda tidak seharusnya bergantung pada pembolehubah yang diperuntukkan dalam kawasan memori tertentu.
  • Fokus pada menulis kod yang jelas dan betul daripada cuba mengawal peruntukan memori.

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.

Mengimbangi Prestasi dan Penggunaan Memori

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:

  • Melalui nilai adalah selamat dan mudah tetapi boleh menjadi tidak cekap untuk struktur data yang besar.
  • Melalui penuding mengelakkan penyalinan tetapi memerlukan pengendalian yang teliti untuk mengelakkan isu konkurensi.

Dari pengalaman lapangan:

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.

Fikiran Akhir

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:

  • Melalui nilai adalah mudah tetapi boleh menjadi tidak cekap untuk struktur data yang besar.
  • Menggunakan penunjuk boleh meningkatkan prestasi tetapi memerlukan pengendalian yang teliti untuk mengelakkan isu seperti perlumbaan data.
  • Analisis melarikan diri menentukan sama ada pembolehubah diperuntukkan pada tindanan atau timbunan, tetapi ini adalah butiran dalaman.
  • Pengumpulan sampah membantu mengelakkan kebocoran memori tetapi mungkin menyebabkan overhed.
  • Konkurensi memerlukan penyegerakan apabila data kongsi terlibat.

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!

Kandungan Bonus - Sokongan Penunjuk Langsung

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
Taip Menyokong Penciptaan Penunjuk Terus? Contoh Struktur ✅ Ya p := &Orang{Nama: "Alice", Umur: 30} Susun atur ✅ Ya arrPtr := &[3]int{1, 2, 3} Hirisan ❌ Tidak (tidak langsung melalui pembolehubah) slice := []int{1, 2, 3}; slicePtr := &hiris Peta ❌ Tidak (tidak langsung melalui pembolehubah) m := peta[rentetan]int{}; mPtr := &m Saluran ❌ Tidak (tidak langsung melalui pembolehubah) ch := buat(chan int); chPtr := &ch Jenis Asas ❌ Tidak (memerlukan pembolehubah) val := 42; p := &val masa.Masa (Struktur) ✅ Ya t := &masa.Masa{} Struktur Tersuai ✅ Ya titik := &Titik{X: 1, Y: 2} Jenis Antara Muka ✅ Ya (tetapi jarang diperlukan) var iface antara muka{} = "hello"; ifacePtr := &iface masa. Tempoh (Alias ​​int64) ❌ Tidak tempoh := masa.Duration(5); p := &tempoh

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!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn