Di Go ada berbagai cara untuk mengembalikan struct
nilai atau potongannya. Untuk individu yang pernah saya lihat:
type MyStruct struct {
Val int
}
func myfunc() MyStruct {
return MyStruct{Val: 1}
}
func myfunc() *MyStruct {
return &MyStruct{}
}
func myfunc(s *MyStruct) {
s.Val = 1
}
Saya mengerti perbedaan di antara ini. Yang pertama mengembalikan salinan struct, yang kedua pointer ke nilai struct yang dibuat dalam fungsi, yang ketiga mengharapkan struct yang ada untuk diteruskan dan menimpa nilainya.
Saya telah melihat semua pola ini digunakan dalam berbagai konteks, saya bertanya-tanya apa praktik terbaik mengenai ini. Kapan Anda akan menggunakan yang mana? Misalnya, yang pertama bisa ok untuk struct kecil (karena overhead minimal), yang kedua untuk yang lebih besar. Dan yang ketiga jika Anda ingin menjadi sangat efisien dalam memori, karena Anda dapat dengan mudah menggunakan kembali instance inst tunggal di antara panggilan. Apakah ada praktik terbaik untuk kapan menggunakannya?
Demikian pula, pertanyaan yang sama tentang irisan:
func myfunc() []MyStruct {
return []MyStruct{ MyStruct{Val: 1} }
}
func myfunc() []*MyStruct {
return []MyStruct{ &MyStruct{Val: 1} }
}
func myfunc(s *[]MyStruct) {
*s = []MyStruct{ MyStruct{Val: 1} }
}
func myfunc(s *[]*MyStruct) {
*s = []MyStruct{ &MyStruct{Val: 1} }
}
Sekali lagi: apa praktik terbaik di sini. Saya tahu irisan selalu pointer, jadi mengembalikan pointer ke irisan tidak berguna. Namun, haruskah saya mengembalikan sepotong nilai struct, sepotong pointer ke struct, haruskah saya meneruskan sebuah pointer ke sebuah slice sebagai argumen (pola yang digunakan dalam Go App Engine API )?
new(MyStruct)
:) Tapi tidak ada perbedaan antara metode yang berbeda untuk mengalokasikan pointer dan mengembalikannya.Jawaban:
tl; dr :
Satu kasus di mana Anda harus sering menggunakan pointer:
Beberapa situasi di mana Anda tidak perlu petunjuk:
Pedoman tinjauan kode menyarankan untuk meneruskan struct kecil seperti
type Point struct { latitude, longitude float64 }
, dan mungkin bahkan hal-hal yang sedikit lebih besar, sebagai nilai, kecuali fungsi yang Anda panggil harus dapat memodifikasinya.bytes.Replace
dibutuhkan args senilai 10 kata (tiga potong dan satuint
).Untuk irisan , Anda tidak perlu meneruskan pointer untuk mengubah elemen array.
io.Reader.Read(p []byte)
mengubah bytep
, misalnya. Ini bisa dibilang kasus khusus "perlakukan struct kecil seperti nilai," karena secara internal Anda melewati struktur kecil yang disebut header slice (lihat penjelasan Russ Cox (rsc) ). Demikian pula, Anda tidak perlu pointer untuk mengubah peta atau berkomunikasi di saluran .Untuk irisan, Anda akan memilih ulang (mengubah start / panjang / kapasitas), fungsi bawaan seperti
append
menerima nilai irisan dan mengembalikan yang baru. Saya akan meniru itu; itu menghindari alias, mengembalikan sepotong baru membantu menarik perhatian pada fakta bahwa array baru mungkin dialokasikan, dan itu akrab bagi penelepon.interface{}
parameter.Peta, saluran, string, dan nilai fungsi dan antarmuka , seperti irisan, sudah merupakan referensi atau struktur internal yang sudah berisi referensi, jadi jika Anda hanya berusaha menghindari penyalinan data yang mendasarinya, Anda tidak perlu memberikan petunjuk kepada mereka . (rsc menulis posting terpisah tentang bagaimana nilai antarmuka disimpan ).
flag.StringVar
ambil*string
karena alasan itu, misalnya.Di mana Anda menggunakan pointer:
Pertimbangkan apakah fungsi Anda harus menjadi metode pada struct mana pun yang Anda perlukan pointer. Orang-orang berharap banyak metode
x
untuk memodifikasix
, sehingga membuat struct yang dimodifikasi penerima dapat membantu meminimalkan kejutan. Ada pedoman kapan penerima harus menjadi petunjuk.Fungsi-fungsi yang memiliki efek pada params non-penerima mereka harus memperjelas di godoc, atau lebih baik lagi, godoc dan namanya (seperti
reader.WriteTo(writer)
).Anda menyebutkan menerima pointer untuk menghindari alokasi dengan mengizinkan penggunaan kembali; mengubah API demi penggunaan kembali memori adalah pengoptimalan yang akan saya tunda hingga jelas bahwa alokasi memiliki biaya nontrivial, dan kemudian saya akan mencari cara yang tidak memaksa API yang lebih rumit pada semua pengguna:
bytes.Buffer
.Reset()
metode untuk mengembalikan objek ke kondisi kosong, seperti beberapa jenis stdlib. Pengguna yang tidak peduli atau tidak dapat menyimpan alokasi tidak harus menyebutnya.existingUser.LoadFromJSON(json []byte) error
bisa dibungkus denganNewUserFromJSON(json []byte) (*User, error)
. Sekali lagi, ini mendorong pilihan antara kemalasan dan menjepit alokasi untuk penelepon individu.sync.Pool
menangani beberapa detail. Jika alokasi tertentu menciptakan banyak tekanan memori, Anda yakin Anda tahu kapan alokasi tersebut tidak lagi digunakan, dan Anda tidak memiliki optimasi yang lebih baik,sync.Pool
dapat membantu. (CloudFlare menerbitkan posting blog bermanfaat (pra-sync.Pool
) tentang daur ulang.)Akhirnya, pada apakah irisan Anda harus dari pointer: irisan nilai dapat bermanfaat, dan menghemat alokasi dan cache Anda. Mungkin ada pemblokir:
NewFoo() *Foo
daripada membiarkan Go menginisialisasi dengan nilai nol .append
menyalin item ketika menumbuhkan array yang mendasarinya . Pointer yang Anda dapatkan sebelumappend
titik ke tempat yang salah setelah itu, penyalinan bisa lebih lambat untuk struct besar, dan untuk misalnyasync.Mutex
menyalin tidak diperbolehkan. Sisipkan / hapus di tengah dan mengurutkan item yang sama.Secara umum, nilai irisan dapat masuk akal jika Anda mendapatkan semua item di tempat di depan dan tidak memindahkannya (misalnya, tidak ada lagi
append
setelah pengaturan awal), atau jika Anda terus memindahkannya, tetapi Anda yakin itu OK (tidak / hati-hati menggunakan pointer ke item, item cukup kecil untuk menyalin secara efisien, dll.). Kadang-kadang Anda harus memikirkan atau mengukur spesifik situasi Anda, tetapi itu adalah panduan kasar.sumber
Replace(s, old, new []byte, n int) []byte
; s, lama, dan baru adalah tiga kata masing-masing ( header slice(ptr, len, cap)
) dann int
merupakan satu kata, jadi 10 kata, yang pada delapan byte / kata adalah 80 byte.Tiga alasan utama ketika Anda ingin menggunakan penerima metode sebagai petunjuk:
"Pertama, dan yang paling penting, apakah metode perlu memodifikasi penerima? Jika ya, penerima harus menjadi penunjuk."
"Kedua adalah pertimbangan efisiensi. Jika penerima besar, struct besar misalnya, akan jauh lebih murah menggunakan pointer pointer."
"Berikutnya adalah konsistensi. Jika beberapa metode dari jenis harus memiliki penerima pointer, sisanya harus juga, jadi set metode konsisten terlepas dari bagaimana jenis digunakan"
Referensi: https://golang.org/doc/faq#methods_on_values_or_pointers
Sunting: Hal penting lainnya adalah mengetahui "tipe" aktual yang Anda kirim berfungsi. Jenisnya dapat berupa 'tipe nilai' atau 'tipe referensi'.
Bahkan ketika irisan dan peta bertindak sebagai referensi, kita mungkin ingin meneruskannya sebagai petunjuk dalam skenario seperti mengubah panjang irisan dalam fungsi.
sumber
Sebuah kasus di mana Anda biasanya perlu mengembalikan pointer adalah ketika membangun sebuah instance dari beberapa sumber daya stateful atau dibagikan . Ini sering dilakukan oleh fungsi yang diawali dengan
New
.Karena mereka mewakili contoh spesifik dari sesuatu dan mereka mungkin perlu mengkoordinasikan beberapa kegiatan, itu tidak masuk akal untuk menghasilkan duplikat / menyalin struktur yang mewakili sumber daya yang sama - jadi penunjuk yang kembali bertindak sebagai pegangan untuk sumber daya itu sendiri .
Beberapa contoh:
func NewTLSServer(handler http.Handler) *Server
- instantiate server web untuk pengujianfunc Open(name string) (*File, error)
- mengembalikan pegangan akses fileDalam kasus lain, pointer dikembalikan hanya karena struktur mungkin terlalu besar untuk disalin secara default:
func NewRGBA(r Rectangle) *RGBA
- mengalokasikan gambar dalam memoriAtau, mengembalikan pointer secara langsung dapat dihindari dengan mengembalikan salinan struktur yang berisi pointer secara internal, tetapi mungkin ini tidak dianggap idiomatis:
sumber
Jika Anda bisa (mis. Sumber daya yang tidak dibagi-pakai yang tidak perlu diteruskan sebagai referensi), gunakan nilai. Dengan alasan berikut:
Alasan 1 : Anda akan mengalokasikan lebih sedikit item dalam tumpukan. Mengalokasikan / membatalkan alokasi dari tumpukan adalah langsung, tetapi mengalokasikan / membatalkan alokasi pada Heap mungkin sangat mahal (waktu alokasi + pengumpulan sampah). Anda dapat melihat beberapa angka dasar di sini: http://www.macias.info/entry/201802102230_go_values_vs_references.md
Alasan 2 : terutama jika Anda menyimpan nilai yang dikembalikan dalam irisan, objek memori Anda akan lebih dipadatkan dalam memori: perulangan irisan di mana semua item bersebelahan jauh lebih cepat daripada mengulangi irisan di mana semua item mengarah ke bagian lain dari memori . Bukan untuk langkah tipuan tetapi untuk peningkatan cache yang meleset.
Mitos breaker : garis cache x86 khas adalah 64 byte. Kebanyakan struct lebih kecil dari itu. Waktu menyalin garis cache di memori mirip dengan menyalin pointer.
Hanya jika bagian penting dari kode Anda lambat, saya akan mencoba beberapa optimasi mikro dan memeriksa apakah menggunakan pointer meningkatkan kecepatan, dengan biaya lebih sedikit keterbacaan dan mantainabilitas.
sumber