Rumah > Artikel > pembangunan bahagian belakang > Timbunan dan penunjuk mekanisme bahasa Go
Siri ini mengandungi empat artikel, terutamanya menerangkan mekanisme dan konsep reka bentuk di sebalik petunjuk bahasa Go, tindanan, timbunan, analisis melarikan diri dan semantik nilai/penunjuk. Ini ialah artikel pertama dalam siri ini, terutamanya menerangkan tindanan dan petunjuk.
Saya tidak akan mengatakan perkara yang baik tentang petunjuk, ia sangat sukar untuk difahami. Jika digunakan secara tidak betul, ia boleh membawa kepada pepijat yang menjengkelkan dan juga masalah prestasi. Ini benar terutamanya apabila menulis perisian serentak atau berbilang benang. Tidak hairanlah banyak bahasa pengaturcaraan cuba mengelak daripada menggunakan penunjuk untuk pengaturcara. Walau bagaimanapun, jika anda menggunakan bahasa pengaturcaraan Go, penunjuk tidak dapat dielakkan. Hanya dengan memahami petunjuk yang mendalam anda boleh menulis kod yang bersih, ringkas dan cekap.
Sempadan bingkai menyediakan ruang memori yang berasingan untuk setiap fungsi, dan fungsi itu dilaksanakan dalam sempadan bingkai. Sempadan bingkai membolehkan fungsi berjalan dalam konteks mereka sendiri dan juga menyediakan kawalan aliran. Fungsi boleh terus mengakses memori dalam bingkai melalui penuding bingkai, manakala mengakses memori di luar bingkai hanya boleh dilakukan secara tidak langsung. Untuk setiap fungsi, jika anda ingin boleh mengakses memori di luar bingkai, memori ini mesti dikongsi dengan fungsi tersebut. Untuk memahami pelaksanaan yang dikongsi, kita perlu terlebih dahulu mempelajari dan memahami mekanisme dan kekangan untuk mewujudkan sempadan bingkai.
Apabila fungsi dipanggil, suis konteks berlaku di antara dua sempadan bingkai. Daripada fungsi panggilan ke fungsi yang dipanggil, jika parameter perlu diluluskan apabila fungsi dipanggil, parameter ini juga mesti diluluskan dalam sempadan bingkai fungsi yang dipanggil. Dalam bahasa Go, data dipindahkan antara dua bingkai mengikut nilai.
Kelebihan menghantar data mengikut nilai ialah kebolehbacaan yang baik. Apabila fungsi dipanggil, nilai yang anda lihat ialah nilai yang telah disalin dan diterima antara pemanggil fungsi dan penerima. Itulah sebabnya saya mengaitkan "pass by value" dengan WYSIWYG, kerana apa yang anda lihat ialah apa yang anda dapat.
Mari kita lihat sekeping kod yang menghantar data integer mengikut nilai:
Penyenaraian 1
package main func main() { // Declare variable of type int with a value of 10. count := 10 // Display the "value of" and "address of" count. println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]") // Pass the "value of" the count. increment(count) println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]") } //go:noinline func increment(inc int) { // Increment the "value of" inc. inc++ println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]") }
Apabila anda memulakan program Go, masa jalan akan mencipta coroutine utama untuk melaksanakan semua kod permulaan termasuk kod dalam fungsi main(). Goroutine ialah laluan pelaksanaan yang diletakkan pada utas sistem pengendalian, dan akhirnya dilaksanakan pada teras tertentu. Bermula dari Go 1.8, setiap goroutine akan memperuntukkan blok memori bersebelahan 2048-bait sebagai ruang tindanannya. Saiz ruang tindanan awal telah berubah selama bertahun-tahun dan mungkin berubah lagi pada masa hadapan.
Timbunan sangat penting kerana ia menyediakan ruang memori fizikal untuk sempadan bingkai setiap fungsi individu. Menurut Penyenaraian 1, apabila coroutine utama melaksanakan fungsi main(), ruang tindanan diedarkan seperti berikut:
Rajah 1
Anda boleh lihat dalam Rajah 1 bahagian timbunan fungsi utama telah dirangka keluar. Bahagian ini dipanggil "bingkai tindanan", dan bingkai ini mewakili sempadan fungsi utama pada tindanan. Bingkai dicipta apabila fungsi yang dipanggil dilaksanakan Anda juga boleh melihat bahawa kiraan pembolehubah diletakkan dalam bingkai fungsi utama() pada alamat memori 0x10429fa4.
Rajah 1 juga menggambarkan satu lagi titik menarik Semua memori tindanan di bawah bingkai aktif tidak tersedia. Sempadan antara ruang tindanan yang tersedia dan ruang tindanan yang tidak tersedia perlu dijelaskan.
Tujuan pembolehubah adalah untuk memberikan nama kepada alamat memori tertentu, menjadikan kod lebih mudah dibaca dan membantu anda menganalisis data yang anda sedang memproses. Jika anda mempunyai pembolehubah dan anda boleh mendapatkan nilainya disimpan dalam memori, mesti ada alamat dalam alamat memori yang menyimpan nilai ini. Dalam baris 9 kod, fungsi main() memanggil fungsi terbina dalam println() untuk memaparkan nilai dan alamat kiraan pembolehubah. Penyenaraian 2 Jika kod anda dijalankan pada komputer 32-bit, seperti go playground, outputnya akan serupa dengan yang berikut:
清单3
count: Value Of[ 10 ] Addr Of[ 0x10429fa4 ]
接下来的第 12 行代码,main() 函数调用 increment() 函数。
清单4
increment(count)
调用函数意味着协程需要在栈上构建出一块新的栈帧。但是,事情有点复杂。要想成功地调用函数,在发生上下文切换时,数据需要跨越帧边界传递到新的帧范围内。具体一点来说,函数调用的时候,整型值会被复制和传递。通过第 18 行代码、increment() 函数的声明,你就可以知道。
清单5
func increment(inc int) {
如果你回过头来再次看第 12 行代码函数 increment() 的调用,你会发现 count 变量是传值的。这个值会被拷贝、传递,最后存储在 increment() 函数的栈中。记住,increment() 函数只能在自己的栈内读写内存,因此,它需要 inc 变量来接收、存储和访问传递的 count 变量的副本。
就在 increment() 函数内部代码开始执行之前,协程的栈(站在一个非常高的角度)应该是像下图这样的:
图 2
你可以看到栈上现在有两个帧,一个属于 main() 函数,另一个属于 increment() 函数。在 increment() 函数的帧里面,你可以看到 inc 变量,它的值 10,是函数调用时拷贝、传递进来的。变量 inc 的地址是 0x10429f98,因为栈帧是从上至下使用栈空间的,所以它的内存地址较小,这只是具体的实现细节,并没任何意义。重要的是,协程从 main() 的栈帧里获取变量 count 的值,并使用 inc 变量将该值的副本放置在 increment() 函数的栈帧里。
increment() 函数的剩余代码显示 inc 变量的值和地址。
清单6
inc++ println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")
第 22 行代码输出类似下面这样:
清单7
inc: Value Of[ 11 ] Addr Of[ 0x10429f98 ]
执行这些代码之后,栈就会像下面这样:
图 3
第 21、22 行代码执行之后,increment() 函数返回并且 CPU 控制权交还给 main() 函数。第 14 行代码,main() 函数会再次显示 count 变量的值和地址。
清单8
println("count:\tValue Of[",count, "]\tAddr Of[", &count, "]")
上面例子完整的输出会像下面这样:
count: Value Of[ 10 ] Addr Of[ 0x10429fa4 ] inc: Value Of[ 11 ] Addr Of[ 0x10429f98 ] count: Value Of[ 10 ] Addr Of[ 0x10429fa4 ]
main() 函数栈帧里,变量 count 的值在调用 increment() 函数前后是相同的。
当函数返回并且控制权交还给调用函数时,栈上的内存实际上会发生什么?回答是:不会发生任何事情。当 increment() 函数返回时,栈上的空间看起来像下面这样:
Rajah 4
Kecuali bingkai tindanan yang dicipta untuk fungsi increment() menjadi tidak tersedia, pengagihan tindanan pada asasnya adalah sama seperti Rajah 3. Ini kerana bingkai fungsi main() menjadi bingkai aktif. Tiada pemprosesan dilakukan pada bingkai tindanan fungsi increment().
Apabila fungsi kembali, membersihkan bingkai fungsi akan membuang masa, kerana anda tidak tahu sama ada memori ini akan digunakan semula. Jadi ingatan ini tidak akan melakukan sebarang pemprosesan. Setiap kali fungsi dipanggil, bingkai yang diperuntukkan pada tindanan akan dikosongkan apabila bingkai diperlukan. Ini dilakukan apabila memulakan pembolehubah yang disimpan dalam bingkai. Oleh kerana semua nilai dimulakan kepada nilai sifar yang sepadan, timbunan membersihkan dirinya dengan betul setiap kali fungsi dipanggil.
Bagaimana jika sangat penting bahawa fungsi increment() mengendalikan terus pembolehubah kiraan yang disimpan dalam bingkai fungsi utama()? Ini memerlukan penggunaan penunjuk! Tujuan penunjuk adalah untuk berkongsi nilai antara fungsi Walaupun nilai itu tidak berada dalam bingkai fungsinya sendiri, fungsi itu boleh membaca dan menulisnya.
Jika anda tidak mempunyai konsep perkongsian dalam fikiran anda, anda mungkin tidak akan menggunakan petunjuk. Apabila mempelajari petunjuk, adalah penting untuk menggunakan perbendaharaan kata yang jelas dan bukannya hanya menghafal operator atau sintaks. Jadi, ingat bahawa petunjuk bertujuan untuk dikongsi dan apabila membaca kod, apabila anda memikirkan "berkongsi", anda harus memikirkan & operator.
Sama ada ia disesuaikan oleh anda atau disertakan dengan bahasa Go, untuk setiap jenis yang diisytiharkan, jenis penunjuk yang sepadan boleh diperoleh berdasarkan jenis ini untuk perkongsian. Sebagai contoh, int jenis terbina dalam, jenis penunjuk yang sepadan ialah *int. Jika anda mengisytiharkan jenis Pengguna sendiri, jenis penunjuk yang sepadan ialah *Pengguna.
Semua jenis penunjuk mempunyai ciri yang sama. Pertama, mereka bermula dengan simbol * kedua, mereka menduduki ruang memori yang sama dan kedua-duanya mewakili alamat, menggunakan 4 atau 8 bait panjang untuk mewakili alamat. Pada mesin 32-bit (seperti taman permainan), penunjuk memerlukan 4 bait memori; pada mesin 64-bit (seperti komputer anda), ia memerlukan 8 bait memori.
规范里有说明,指针类型可以看成是类型字面量,这意味着它们是有现有类型组成的未命名类型。
让我们来看一段代码,这段代码展示了函数调用时按值传递地址。main() 和 increment() 函数的栈帧会共享 count 变量:
清单10
package main func main() { // Declare variable of type int with a value of 10. count := 10 // Display the "value of" and "address of" count. println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]") // Pass the "address of" count. increment(&count) println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]") } //go:noinline func increment(inc *int) { // Increment the "value of" count that the "pointer points to". (dereferencing) *inc++ println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]\tValue Points To[", *inc, "]") }
基于原来的代码有三处改动的地方,第 12 行是第一处改动:
清单11
increment(&count)
现在,第 12 行代码拷贝、传递的并非 count 变量的值,而是变量的地址。可以认为,main() 函数与 increment() 函数是共享 count 变量的。这是 & 操作符起的作用。
重点理解,现在依旧是传值,唯一不同的是现在传递的是地址而不是一个整型数据。地址也是一个值,是函数调用时会跨帧边界发生拷贝和传递的内容。
因为地址会发生拷贝和传递,在 increment() 函数里面需要一个变量接收和存储该地址值。所以在第 18 行声明了整型的指针变量。
清单12
func increment(inc *int) {
如果你传递的是 User 类型值的地址,变量就应该声明成 *User。尽管指针变量存储的是地址,也不能传递任何类型的地址,只能传递与指针类型相一致的地址。关键在于,共享值的原因是因为接收函数能够对值进行读写操作。只有知道值的类型信息才能够进行读写操作。编译器会保证只有与指针类型相一致的值才能够实现函数间共享。
调用 increment() 函数时候,栈空间就像下面这样:
图 5
当一个地址作为值执行按值传递之后,你可以从图 5 看出栈是如何分布的。现在,increment() 函数帧空间里面的指针变量指向 count 变量,该变量在 main() 函数的帧空间里。
通过使用指针变量,increment() 函数可以间接对 count 变量执行读写操作。
清单 13
*inc++
这一次,字符 * 充当操作符,与指针变量搭配使用。使用 * 操作符是“获取指针指向的值”的意思。指针变量允许在帧外对函数帧内的内存进行间接访问。有时候,间接的读写操作也称为解引用。increment() 函数必须有指针变量,才能够对其他函数帧空间执行间接访问。
执行第 21 行代码之后,栈空间分布如图 6 所示。
图 6
程序最后输出:
清单 14
count: Value Of[ 10 ] Addr Of[ 0x10429fa4 ] inc: Value Of[ 0x10429fa4 ] Addr Of[ 0x10429f98 ] Value Points To[ 11 ] count: Value Of[ 11 ] Addr Of[ 0x10429fa4 ]
你可以看到,指针变量 inc 的值和 count 变量的地址是相同的。这将建立起共享关系,允许在帧外执行内存的间接访问。在 increment() 函数里,一旦通过指针执行了写操作,改变也会体现在 main() 函数里。
指针变量并不特别,它们和其他变量一样也是变量,有内存地址和值。正巧的是,无论指针变量指向的值的类型如何,所有的指针变量都有同样的大小和表现形式。唯一困惑的是使用 * 字符充当操作符,用来声明指针类型。如果你能分清指针类型声明和指针操作,你就没有那么困惑了。
这篇文章描述了设计指针背后的目的和 Go 语言中栈和指针的工作机制。这是理解 Go 语言机制、设计哲学的第一步,也对编写一致性且可读性的代码提供一些指导作用。
总结一下,通过这篇文章你能学习到的知识:
1.Sempadan bingkai menyediakan ruang memori yang berasingan untuk setiap fungsi, dan fungsi dilaksanakan dalam julat bingkai 2.Apabila fungsi dipanggil, konteks akan bertukar antara dua bingkai; 3. Keuntungan lulus dengan nilai adalah pembacaan yang baik; bingkai aktif tidak tersedia, hanya bingkai aktif dan memori tindanan di atasnya berguna 6.Panggil fungsi bermakna coroutine akan membuka bingkai tindanan baharu pada memori tindanan 7.Setiap kali fungsi dipanggil, apabila bingkai digunakan, ingatan tindanan yang sepadan akan dimulakan 8.Tujuan mereka bentuk penunjuk adalah untuk merealisasikan perkongsian nilai antara fungsi, walaupun nilai itu bukan dalam fungsi itu sendiri Ia boleh juga dibaca dan ditulis dalam bingkai tindanan; 9.Untuk setiap jenis, sama ada ia ditakrifkan sendiri atau terbina dalam bahasa Go, terdapat jenis penunjuk yang sepadan 10.Lulus Menggunakan pembolehubah penunjuk membenarkan capaian memori tidak langsung di luar bingkai fungsi;
Atas ialah kandungan terperinci Timbunan dan penunjuk mekanisme bahasa Go. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!