Rumah > pembangunan bahagian belakang > Golang > Go Generics: A Deep Dive

Go Generics: A Deep Dive

Mary-Kate Olsen
Lepaskan: 2025-01-01 01:51:09
asal
968 orang telah melayarinya

Go Generics: A Deep Dive

1. Pergi Tanpa Generik

Sebelum pengenalan generik, terdapat beberapa pendekatan untuk melaksanakan fungsi generik yang menyokong jenis data yang berbeza:

Pendekatan 1: Laksanakan fungsi untuk setiap jenis data
Pendekatan ini membawa kepada kod yang sangat berlebihan dan kos penyelenggaraan yang tinggi. Sebarang pengubahsuaian memerlukan operasi yang sama dilakukan pada semua fungsi. Selain itu, memandangkan bahasa Go tidak menyokong lebihan fungsi dengan nama yang sama, ia juga menyusahkan untuk mendedahkan fungsi ini untuk panggilan modul luaran.

Pendekatan 2: Gunakan jenis data dengan julat terbesar
Untuk mengelakkan lebihan kod, kaedah lain ialah menggunakan jenis data dengan julat terbesar, iaitu, Pendekatan 2. Contoh biasa ialah matematik.Max, yang mengembalikan dua nombor yang lebih besar. Untuk dapat membandingkan data pelbagai jenis data, math.Max ​​menggunakan float64, jenis data dengan julat terbesar antara jenis angka dalam Go, sebagai parameter input dan output, sekali gus mengelakkan kehilangan ketepatan. Walaupun ini menyelesaikan masalah redundansi kod sedikit sebanyak, sebarang jenis data perlu ditukar kepada jenis float64 terlebih dahulu. Contohnya, apabila membandingkan int dengan int, pemutus jenis masih diperlukan, yang bukan sahaja merendahkan prestasi tetapi juga kelihatan tidak wajar.

Pendekatan 3: Gunakan antara muka{} jenis
Menggunakan jenis antara muka{} menyelesaikan masalah di atas dengan berkesan. Walau bagaimanapun, jenis antara muka{} memperkenalkan overhed masa jalan tertentu kerana ia memerlukan penegasan jenis atau pertimbangan jenis semasa masa jalan, yang mungkin membawa kepada kemerosotan prestasi. Selain itu, apabila menggunakan jenis antara muka{}, pengkompil tidak boleh melakukan semakan jenis statik, jadi sesetengah ralat jenis hanya boleh ditemui semasa masa jalan.

2. Kelebihan Generik

Go 1.18 memperkenalkan sokongan untuk generik, yang merupakan perubahan ketara sejak sumber terbuka bahasa Go.
Generik ialah ciri bahasa pengaturcaraan. Ia membolehkan pengaturcara menggunakan jenis generik dan bukannya jenis sebenar dalam pengaturcaraan. Kemudian, melalui hantaran eksplisit atau potongan automatik semasa panggilan sebenar, jenis generik diganti, mencapai tujuan penggunaan semula kod. Dalam proses menggunakan generik, jenis data yang akan dikendalikan ditentukan sebagai parameter. Jenis parameter sedemikian dipanggil kelas generik, antara muka generik dan kaedah generik dalam kelas, antara muka dan kaedah masing-masing.
Kelebihan utama generik ialah meningkatkan kebolehgunaan semula kod dan keselamatan jenis. Berbanding dengan parameter formal tradisional, generik menjadikan penulisan kod universal lebih ringkas dan fleksibel, memberikan keupayaan untuk mengendalikan pelbagai jenis data dan meningkatkan lagi ekspresif dan kebolehgunaan semula bahasa Go. Pada masa yang sama, memandangkan jenis generik khusus ditentukan pada masa penyusunan, semakan jenis boleh disediakan, mengelakkan ralat penukaran jenis.

3. Perbezaan Antara Generik dan antara muka{}

Dalam bahasa Go, kedua-dua antara muka{} dan generik ialah alat untuk mengendalikan berbilang jenis data. Untuk membincangkan perbezaan mereka, mari kita lihat pada prinsip pelaksanaan antara muka{} dan generik dahulu.

3.1 antara muka{} Prinsip Pelaksanaan

antara muka{} ialah antara muka kosong tanpa kaedah dalam jenis antara muka. Memandangkan semua jenis melaksanakan antara muka{}, ia boleh digunakan untuk mencipta fungsi, kaedah atau struktur data yang boleh menerima sebarang jenis. Struktur asas antara muka{} pada masa jalan diwakili sebagai eface, yang strukturnya ditunjukkan di bawah, terutamanya mengandungi dua medan, _type dan data.

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

_type ialah penunjuk kepada struktur _type, yang mengandungi maklumat seperti saiz, jenis, fungsi cincang dan perwakilan rentetan bagi nilai sebenar. data adalah penunjuk kepada data sebenar. Jika saiz data sebenar kurang daripada atau sama dengan saiz penunjuk, data akan disimpan terus dalam medan data; jika tidak, medan data akan menyimpan penunjuk kepada data sebenar.
Apabila objek jenis tertentu diperuntukkan kepada pembolehubah jenis antara muka{}, bahasa Go secara tersirat melaksanakan operasi tinju eface, menetapkan medan _type kepada jenis nilai dan medan data kepada data nilai . Contohnya, apabila antara muka penyataan var i{} = 123 dilaksanakan, Go akan mencipta struktur eface, dengan medan _type mewakili jenis int dan medan data mewakili nilai 123.
Apabila mendapatkan semula nilai yang disimpan daripada antara muka{}, proses penyahkotak berlaku, iaitu, taip penegasan atau pertimbangan jenis. Proses ini memerlukan secara eksplisit menyatakan jenis yang dijangkakan. Jika jenis nilai yang disimpan dalam antara muka{} sepadan dengan jenis yang dijangkakan, penegasan jenis akan berjaya dan nilai boleh diambil semula. Jika tidak, penegasan jenis akan gagal dan pengendalian tambahan diperlukan untuk situasi ini.

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Ia boleh dilihat bahawa antara muka{} menyokong operasi pada berbilang jenis data melalui operasi tinju dan nyah kotak pada masa jalan.

3.2 Prinsip Pelaksanaan Generik

Pasukan teras Go sangat berhati-hati semasa menilai skim pelaksanaan Go generik. Sebanyak tiga skim pelaksanaan telah dikemukakan:

  • Skim stensilan
  • Skema kamus
  • Skim Stensil Bentuk GC

Skim Stensil juga merupakan skim pelaksanaan yang diterima pakai oleh bahasa seperti C dan Rust untuk melaksanakan generik. Prinsip pelaksanaannya ialah semasa tempoh penyusunan, mengikut parameter jenis tertentu apabila fungsi generik dipanggil atau elemen jenis dalam kekangan, pelaksanaan berasingan fungsi generik dijana untuk setiap hujah jenis untuk memastikan keselamatan jenis dan prestasi optimum . Walau bagaimanapun, kaedah ini akan melambatkan pengkompil. Kerana apabila terdapat banyak jenis data yang dipanggil, fungsi generik perlu menjana fungsi bebas untuk setiap jenis data, yang mungkin menghasilkan fail tersusun yang sangat besar. Pada masa yang sama, disebabkan isu seperti kesilapan cache CPU dan ramalan cawangan arahan, kod yang dijana mungkin tidak berjalan dengan cekap.

Skim Kamus hanya menjana satu logik fungsi untuk fungsi generik tetapi menambah dict parameter sebagai parameter pertama pada fungsi. Parameter dict menyimpan maklumat berkaitan jenis bagi argumen jenis apabila fungsi generik dipanggil dan menghantar maklumat kamus menggunakan daftar AX (AMD) semasa panggilan fungsi. Kelebihan skim ini ialah ia mengurangkan overhed fasa kompilasi dan tidak meningkatkan saiz fail binari. Walau bagaimanapun, ia meningkatkan overhed masa jalan, tidak dapat melaksanakan pengoptimuman fungsi pada peringkat penyusunan dan mempunyai masalah seperti rekursi kamus.

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Go akhirnya menyepadukan dua skim di atas dan mencadangkan skim Stensil Bentuk GC untuk pelaksanaan generik. Ia menjana kod fungsi dalam unit Bentuk GC sesuatu jenis. Jenis dengan Bentuk GC yang sama menggunakan semula kod yang sama (Bentuk GC bagi sesuatu jenis merujuk kepada perwakilannya dalam pengagih memori Go/pengumpul sampah). Semua jenis penunjuk menggunakan semula jenis *uint8. Untuk jenis dengan Bentuk GC yang sama, kod fungsi instantiated dikongsi digunakan. Skim ini juga secara automatik menambahkan parameter dict pada setiap kod fungsi yang di instantiated untuk membezakan jenis yang berbeza dengan Bentuk GC yang sama.

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

3.3 Perbezaan

Daripada prinsip pelaksanaan asas antara muka{} dan generik, kita boleh mendapati bahawa perbezaan utama antara keduanya ialah antara muka{} menyokong pengendalian jenis data yang berbeza semasa masa jalan, manakala generik menyokong pengendalian jenis data yang berbeza secara statik pada peringkat penyusunan. Terdapat terutamanya perbezaan berikut dalam penggunaan praktikal:
(1) Perbezaan prestasi: Operasi tinju dan nyahbox yang dilakukan apabila jenis data yang berbeza ditetapkan atau diambil daripada antara muka{} adalah mahal dan memperkenalkan overhed tambahan. Sebaliknya, generik tidak memerlukan operasi tinju dan nyahbox, dan kod yang dijana oleh generik dioptimumkan untuk jenis tertentu, mengelakkan overhed prestasi masa jalan.
(2) Keselamatan jenis: Apabila menggunakan jenis antara muka{}, pengkompil tidak boleh melakukan semakan jenis statik dan hanya boleh melakukan penegasan jenis pada masa jalan. Oleh itu, beberapa jenis ralat hanya boleh ditemui pada masa jalan. Sebaliknya, kod generik Go dijana pada masa penyusunan, jadi kod generik boleh mendapatkan maklumat jenis pada masa penyusunan, memastikan keselamatan jenis.

4. Senario untuk Generik

4.1 Senario Berkenaan

  • Apabila melaksanakan struktur data umum: Dengan menggunakan generik, anda boleh menulis kod sekali dan menggunakannya semula pada jenis data yang berbeza. Ini mengurangkan pertindihan kod dan meningkatkan kebolehselenggaraan dan kebolehlanjutan kod.
  • Apabila beroperasi pada jenis bekas asli dalam Go: Jika fungsi menggunakan parameter jenis bekas terbina dalam Go seperti kepingan, peta atau saluran dan kod fungsi tidak membuat sebarang andaian khusus tentang jenis elemen dalam bekas , menggunakan generik boleh mengasingkan sepenuhnya algoritma bekas daripada jenis elemen dalam bekas. Sebelum sintaks generik tersedia, refleksi biasanya digunakan untuk pelaksanaan, tetapi refleksi menjadikan kod kurang boleh dibaca, tidak dapat melakukan semakan jenis statik dan meningkatkan overhed masa jalan program dengan ketara.
  • Apabila logik kaedah yang dilaksanakan untuk jenis data berbeza adalah sama: Apabila kaedah jenis data berbeza mempunyai logik fungsi yang sama dan satu-satunya perbezaan ialah jenis data parameter input, generik boleh digunakan untuk mengurangkan lebihan kod.

4.2 Senario Tidak Berkenaan

  • Jangan gantikan jenis antara muka dengan parameter jenis: Antara muka menyokong rasa pengaturcaraan generik tertentu. Jika operasi pada pembolehubah jenis tertentu hanya memanggil kaedah jenis itu, hanya gunakan jenis antara muka secara langsung tanpa menggunakan generik. Contohnya, io.Reader menggunakan antara muka untuk membaca pelbagai jenis data daripada fail dan penjana nombor rawak. io.Reader mudah dibaca dari perspektif kod, sangat cekap dan hampir tiada perbezaan dalam kecekapan pelaksanaan fungsi, jadi tidak perlu menggunakan parameter jenis.
  • Apabila butiran pelaksanaan kaedah untuk jenis data berbeza adalah berbeza: Jika pelaksanaan kaedah untuk setiap jenis berbeza, jenis antara muka harus digunakan dan bukannya generik.
  • Dalam senario dengan dinamik masa jalan yang kuat: Contohnya, dalam senario di mana pertimbangan jenis dilakukan menggunakan suis, secara langsung menggunakan antara muka{} akan mendapat hasil yang lebih baik.

5. Perangkap dalam Generik

5.1 nol Perbandingan

Dalam bahasa Go, parameter jenis tidak dibenarkan dibandingkan secara langsung dengan sifar kerana parameter jenis disemak jenis pada masa penyusunan, manakala sifar ialah nilai khas pada masa jalan. Oleh kerana jenis asas parameter jenis tidak diketahui pada masa penyusunan, pengkompil tidak dapat menentukan sama ada jenis asas parameter jenis menyokong perbandingan dengan nol. Oleh itu, untuk mengekalkan keselamatan jenis dan mengelakkan kemungkinan ralat masa jalan, bahasa Go tidak membenarkan perbandingan langsung antara parameter jenis dan sifar.

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

5.2 Elemen Dasar Tidak Sah

Jenis T elemen asas mestilah jenis asas dan tidak boleh menjadi jenis antara muka.

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
type type struct {
    Size uintptr
    PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
    Hash uint32 // hash of type; avoids computation in hash tables
    TFlag TFlag // extra type information flags
    Align_ uint8 // alignment of variable with this type
    FieldAlign_ uint8 // alignment of struct field with this type
    Kind_ uint8 // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    Equal func(unsafe.Pointer, unsafe.Pointer) bool
    // GCData stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, GCData is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    GCData *byte
    Str NameOff // string form
    PtrToThis TypeOff // type for pointer to this type, may be zero
}
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

5.3 Elemen Jenis Kesatuan Tidak Sah

Elemen jenis kesatuan tidak boleh menjadi parameter jenis dan elemen bukan antara muka mesti dipisahkan secara berpasangan. Jika terdapat lebih daripada satu elemen, ia tidak boleh mengandungi jenis antara muka dengan kaedah tidak kosong, dan ia juga tidak boleh dibandingkan atau membenamkan setanding.

var i interface{} = "hello"
s, ok := i.(string)
if ok {
    fmt.Println(s) // Output "hello"
} else {
    fmt.Println("not a string")
}
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

5.4 Jenis Antara Muka Tidak Boleh Dibenamkan Secara Rekursif

type Op interface{
       int|float 
}
func Add[T Op](m, n T) T { 
       return m + n
} 
// After generation =>
const dict = map[type] typeInfo{
       int : intInfo{
             newFunc,
             lessFucn,
             //......
        },
        float : floatInfo
} 
func Add(dict[T], m, n T) T{}
Salin selepas log masuk

6. Amalan Terbaik

Untuk menggunakan generik dengan baik, perkara berikut harus diperhatikan semasa penggunaan:

  1. Elakkan terlalu generalisasi. Generik tidak sesuai untuk semua senario, dan adalah perlu untuk mempertimbangkan dengan teliti dalam senario yang sesuai. Refleksi boleh digunakan apabila sesuai: Go mempunyai refleksi masa jalan. Mekanisme refleksi menyokong rasa tertentu pengaturcaraan generik. Jika operasi tertentu perlu menyokong senario berikut, refleksi boleh dipertimbangkan: (1) Beroperasi pada jenis tanpa kaedah, di mana jenis antara muka tidak berkenaan. (2) Apabila logik operasi untuk setiap jenis adalah berbeza, generik tidak boleh digunakan. Contohnya ialah pelaksanaan pakej pengekodan/json. Oleh kerana tidak diingini bahawa setiap jenis yang akan dikodkan melaksanakan kaedah MarshalJson, jenis antara muka tidak boleh digunakan. Dan kerana logik pengekodan untuk jenis yang berbeza adalah berbeza, generik tidak boleh digunakan.
  2. Gunakan *T, []T dan peta[T1]T2 dengan jelas dan bukannya membiarkan T mewakili jenis penunjuk, kepingan atau peta. Berbeza daripada fakta bahawa parameter jenis dalam C ialah ruang letak dan akan digantikan dengan jenis sebenar, jenis parameter jenis T dalam Go ialah parameter jenis itu sendiri. Oleh itu, mewakilinya sebagai penunjuk, kepingan, peta dan jenis data lain akan membawa kepada banyak situasi yang tidak dijangka semasa penggunaan, seperti yang ditunjukkan di bawah:
type V interface{
        int|float|*int|*float
} 
func F[T V](m, n T) {}
// 1. Generate templates for regular types int/float
func F[go.shape.int_0](m, n int){} 
func F[go.shape.float_0](m, n int){}
// 2. Pointer types reuse the same template
func F[go.shape.*uint8_0](m, n int){}
// 3. Add dictionary passing during the call
const dict = map[type] typeInfo{
        int : intInfo{},
        float : floatInfo{}
} 
func F[go.shape.int_0](dict[int],m, n int){}
Salin selepas log masuk

Kod di atas akan melaporkan ralat: operasi tidak sah: penunjuk ptr (pembolehubah jenis T dikekang oleh *int | *uint) mesti mempunyai jenis asas yang sama. Sebab ralat ini ialah T ialah parameter jenis, dan parameter jenis bukan penunjuk dan tidak menyokong operasi penyahrujukan. Ini boleh diselesaikan dengan menukar definisi kepada yang berikut:

// Wrong example
func ZeroValue0[T any](v T) bool {
    return v == nil  
}
// Correct example 1
func Zero1[T any]() T {
    return *new(T) 
}
// Correct example 2
func Zero2[T any]() T {
    var t T
    return t 
}
// Correct example 3
func Zero3[T any]() (t T) {
    return 
}
Salin selepas log masuk

Ringkasan

Secara keseluruhan, faedah generik boleh diringkaskan dalam tiga aspek:

  1. Jenis ditentukan semasa tempoh penyusunan, memastikan keselamatan jenis. Apa yang dimasukkan itulah yang dikeluarkan.
  2. Kebolehbacaan dipertingkatkan. Jenis data sebenar diketahui secara jelas dari peringkat pengekodan.
  3. Generik menggabungkan kod pemprosesan untuk jenis yang sama, meningkatkan kadar penggunaan semula kod dan meningkatkan fleksibiliti umum program. Walau bagaimanapun, generik bukanlah satu keperluan untuk jenis data umum. Ia masih perlu untuk mempertimbangkan dengan teliti sama ada untuk menggunakan generik mengikut situasi penggunaan sebenar.

Leapcell: Platform Terperinci untuk Go Web Hosting, Async Tasks dan Redis

Go Generics: A Deep Dive

Akhir sekali, izinkan saya memperkenalkan Leapcell, platform yang paling sesuai untuk menggunakan perkhidmatan Go.

1. Sokongan Pelbagai Bahasa

  • Bangun dengan JavaScript, Python, Go atau Rust.

2. Sebarkan projek tanpa had secara percuma

  • bayar hanya untuk penggunaan — tiada permintaan, tiada caj.

3. Kecekapan Kos yang tiada tandingan

  • Bayar semasa anda pergi tanpa caj terbiar.
  • Contoh: $25 menyokong 6.94 juta permintaan pada purata masa tindak balas 60ms.

4. Pengalaman Pembangun Diperkemas

  • UI intuitif untuk persediaan yang mudah.
  • Saluran paip CI/CD automatik sepenuhnya dan penyepaduan GitOps.
  • Metrik masa nyata dan pengelogan untuk mendapatkan cerapan yang boleh diambil tindakan.

5. Kebolehskalaan dan Prestasi Tinggi yang Mudah

  • Penskalaan automatik untuk mengendalikan konkurensi tinggi dengan mudah.
  • Sifar operasi overhed — hanya fokus pada pembinaan.

Teroka lebih lanjut dalam dokumentasi!

Twitter Leapcell: https://x.com/LeapcellHQ

Atas ialah kandungan terperinci Go Generics: A Deep Dive. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

sumber:dev.to
Kenyataan Laman Web ini
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
Artikel terbaru oleh pengarang
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan