2014-05-08 39 views
158

In Go'da, struct değerini veya bunun bir bölümünü döndürmenin çeşitli yolları vardır. Gördüğüm bireysel olanlar için: bunlar arasındaki farkları anlıyorum. İlk yapının bir kopyasını döndürür, ikincisi işlev içinde oluşturulmuş yapı değerine bir işaretçi, üçüncüsü varolan bir yapının geçirilip değeri geçersiz kıldığını bekler.Parametre ve dönüş değerlerinde göstergeler ve değerler

Bu kalıpların çeşitli bağlamlarda kullanıldığını gördüm, bunlarla ilgili en iyi uygulamaların neler olduğunu merak ediyorum. Ne zaman kullanırsın? Örneğin, ilk yapı küçük yapılar için iyi olabilir (çünkü havai minimumdur), ikincisi büyük olanlar için. Ve üçüncüsü son derece bellek verimli olmak istiyorsanız, çünkü aramalar arasında tek bir yapı örneğini kolayca yeniden kullanabilirsiniz. Hangi zaman kullanacağınız için en iyi yöntemler var mı?

Benzer şekilde, dilimler ilişkin aynı soru:

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} } 
} 

Tekrar: Buradaki en iyi uygulamalar nelerdir. Dilimlerin her zaman işaretçi olduğunu biliyorum, bu yüzden bir dilim için bir işaretçi döndürmek yararlı değildir. Bununla birlikte, bir dilim yapı değeri döndürmeli, yapılara işaretçi dilimi, argüman olarak bir işaretçiye (Go App Engine API'da kullanılan bir desen) bir işaretçiyi göstermeli miyim?

+0

Söylediğiniz gibi, gerçekten kullanım durumuna bağlıdır. Bütün bunlar duruma bağlı olarak geçerlidir - bu mutable bir nesne midir? bir kopya mı işaret mi istiyoruz? vb BTW kullanarak '' yeni (MyStruct) '' demeden bahsetmediniz :) Ancak, gerçekten farklı şekillerde gönderme ve geri gönderme yöntemleri arasında bir fark yoktur. –

+9

Bu, kelimenin tam anlamıyla mühendislik üzerindedir. Yapılar oldukça büyük olmalı, bir işaretçi döndürmek programınızı daha hızlı hale getirir. Sadece rahatsız etmeyin, kod, profil, işe yararsa düzeltin. – Volker

+0

Bir değer veya işaretçi döndürmenin tek bir yolu vardır; bu bir değer veya bir işaretçi döndürmektir. Onları nasıl ayırdığınız ayrı bir konudur. Durumunuz için neyin işe yaradığını kullanın ve endişelenmeden önce bazı kodları yazın. – JimB

cevap

225

tl; dr: alıcı işaretçiler kullanılarak

  • yöntemler yaygındır; the rule of thumb for receivers is, "Şüpheniz varsa, bir işaretçi kullanın."
  • Dilimler, haritalar, kanallar, dizeler, işlev değerleri ve arabirim değerleri dahili olarak işaretçilerle uygulanır ve bunlara bir işaretçi genellikle gereksizdir.
  • Başka bir yerde, büyük bir yapının veya yapıların işaretlerini kullanmanız gerekecek, aksi halde pass values, çünkü bir işaretçi aracılığıyla bir şeyleri şaşırtmakla ilgili şeyler kafa karıştırıcı oluyor. Sık sık bir işaretçi kullanmalısınız

Bir olgu:

  • Alıcıları işaretçileri daha sık dışındaki bağımsız değişkendir. Çağırılan şeyi veya çok büyük yapılar olması için adlandırılmış türleri değiştirmek için yöntemlerin alışılmadık bir şey değildir, bu nedenle nadir durumlar dışında işaretçilere varsayılan olarak the guidance is.
      Jeff Hodges ' copyfighter aracı otomatik olarak küçük boyutlu alıcılar tarafından geçirilen değeri arar. Eğer işaretçileri gerekmez

Bazı durumlar:

  • Kod incelemesi kurallar, değerler olarak, belki biraz daha büyük, hatta şeylertype Point struct { latitude, longitude float64 } gibi küçük yapılar geçen ve önermek Aradığınız işlevin yerini değiştirebilmesi gerekmediği sürece.

    • Değer semantikleri, burada bir ödevin burada bir değeri şaşkınlıkla değiştirdiği takas durumlarından kaçınır.
    • It Go-y değil biraz hız için temiz semantiğini feda ve cache misses veya öbek ayırmalarının önler çünkü bazen değeriyle küçük yapılar geçirerek, aslında daha verimlidir için.
    • Yani, Go Wiki'nin sayfası, yapılar küçük olduğunda ve bu şekilde kalması muhtemel olduğunda değeri iletmeyi önerir.
    • "Büyük" eşik belirsiz görünüyorsa; Muhtemelen birçok yapı, bir işaretçinin veya bir değerin Tamam olduğu bir aralıktadır. Alt sınır olarak, kod gözden geçirme yorumları, dilimlerin (üç makine kelimesi) değer alıcıları olarak kullanılmasının makul olduğunu ileri sürmektedir. Bir üst sınır yakın bir şey olarak, bytes.Replace, 10 sözcük değerinde args (üç dilim ve bir int) alır.
  • için dilimler, Dizinin öğelerini değiştirmek için bir işaretçi geçmesi gerekmez. io.Reader.Read(p []byte), örneğin, p baytlarını değiştirir. Muhtemelen "değerler gibi küçük yapıları tedavi etme" özel bir olgudur, çünkü dahili olarak dilim başlığı (bkz. Russ Cox (rsc)'s explanation) adı verilen küçük bir yapıdan geçiyorsunuz. Benzer şekilde, bir haritayı değiştirmek veya numaralı bir kanalda iletişim kurmak için işaretçisine ihtiyacınız yoktur.

  • dilimler için, (başlangıç ​​/ uzunluk/kapasite değiştirme) reslice edeceğiz append gibi yerleşik işlevler bir dilim değeri kabul ve yenisini döndürür. Bunu taklit ederim; Yeni bir dilim döndürmek, yeni bir dizinin ayrılabileceğine ve arayanlara tanıdık geldiğine dikkat çekmeye yardımcı olur.

    • Her zaman pratik o deseni takip değil. database interfaces veya serializers gibi bazı araçlar, derleme zamanında türü bilinmeyen bir dilime eklenmelidir. Bazen interface{} parametresindeki bir dilim için bir işaretçi kabul ederler.
  • Haritalar, kanallar, dizeleri ve işlev ve arayüz dilimleri gibi, içten referanslar ya da sadece temel veriler kopyalanan alma engellemeye çalışıyoruz eğer öyleyse, zaten referanslar içerebilir yapıları, sen, değerleri onlara işaretçiler iletmeye gerek yok. (rsc wrote a separate post on how interface values are stored).

    • Hala arayanın yapı değiştirmek istiyoruz nadir durumda işaretçileri geçmesi gerekebilir: flag.StringVar örneğin, bu nedenle bir *string sürer. Eğer işaretçileri kullanmak

:

  • sizin işlevi için bir işaretçi gereksinim hangisi yapı üzerinde bir yöntem olup olmaması gerektiği göz önünde bulundurun. İnsanlar, x'u değiştirmek için x'da çok fazla yöntem beklerler, bu yüzden değiştirilmiş yapıyı yapmak alıcıya sürprizliği en aza indirebilir. Alıcıların işaretçi olması gerektiğinde guidelines vardır.Alıcı olmayan paramiksler üzerinde etkileri olan fonksiyonlar, tanrıçada veya daha iyisi, tanrıda ve isimde (reader.WriteTo(writer) gibi) açıklığa kavuşturmalıdır. Yeniden kullanıma izin vererek ayırmalardan kaçınmak için bir işaretçi kabul etmekten bahsediyorsunuz; Bellek yeniden kullanımı için API'leri değiştirmek, tahsislerin belirgin olmayan bir maliyete sahip olduğunu anlayana kadar geciktirecek bir optimizasyon ve daha sonra, tüm kullanıcılar için Trickier API'sini zorlamayan bir yol arıyordum:

    1. Tahsislerden kaçınmak için, Go'nun escape analysis arkadaşınızdır. Bazen önemsiz bir kurucu, düz bir değişmez veya bytes.Buffer gibi kullanışlı bir sıfır değeriyle başlatılabilen türler oluşturarak yığın ayırmalarından kaçınmanıza yardımcı olabilirsiniz.
    2. Bazı stdlib türleri gibi bir nesneyi boş duruma getirmek için Reset() yöntemini düşünün. Bir tahsisatı umursamayan veya kaydedemeyen kullanıcılar aramak zorunda kalmaz.
    3. Kolaylık sağlamak için, yerinde değişiklik yapma yöntemleri ve eşleme çiftleri olarak sıfırdan oluştur işlevlerini yazmayı düşünün: existingUser.LoadFromJSON(json []byte) error, NewUserFromJSON(json []byte) (*User, error) tarafından sarılmış olabilir. Yine, tembellik ve ayrılıklar arasındaki seçimi bireysel arayana doğru iter.
    4. Belleği geri dönüştürmeyi arayan kişiler, sync.Pool'un bazı ayrıntılarla ilgilenmesine izin verebilir. Eğer özel bir tahsis çok fazla bellek baskısı yaratırsa, tahsis artık kullanılmadığında bildiğinizden eminsiniz ve daha iyi bir optimizasyonunuz yok demektir, sync.Pool yardımcı olabilir. (CloudFlare, geri dönüşüm hakkında a useful (pre-sync.Pool) blog post yayınladı.)
    5. Merakla, karmaşık kurucular için, new(Foo).Reset() bazen NewFoo() neden olmazsa bir ayırmadan kaçınabilir. Deyimsiz; Evde bunu denemeye dikkat et.

Son olarak, dilim işaretçiler olmalıdır üzerine olsun: değerlerin dilim yararlı olabilir ve size tahsisleri ve önbellek özlüyor kaydedebilirsiniz., sana mesela işaretçileri zorlayabilir öğeleri oluşturmak için

  • API: blokerler olabilir Go'nun zero value ile başlamasını sağlamak yerine NewFoo() *Foo numaralı telefonu aramanız gerekir.
  • öğelerinin istenilen ömürleri hepsi aynı olmayabilir. Bütün dilim bir kerede serbest bırakılır; öğelerin% 99'u artık kullanışlı değilse ancak diğer% 1'e işaretçiler varsa, dizinin tamamı ayrılmaya devam eder.
  • öğesinin taşınması, sorun çıkarmanıza neden olabilir. Özellikle, append, öğeleri grows the underlying array olduğunda kopyalar. append'dan önce aldığınız işaretçiler sonradan yanlış yere işaret eder, kopyalama büyük yapılar için daha yavaş olabilir ve ör. sync.Mutex kopyalama izin verilmiyor. Ortadaki ekleme/silme ve benzer şekilde öğeleri hareket ettirin. Eğer ön yerde tüm öğeleri almak ya onları (örneğin ilk kurulumdan sonra artık append ler) ya da bunu yaparsanız onları hareket etmeye devam hareket yoksa Geniş

, değer dilimleri mantıklı olabilir Etrafında ama emin olursunuz (öğelerin işaretçilerinin dikkatli kullanımı, öğelerin verimli kopyalanması için yeterince küçük, vb.). Bazen durumunuzun özelliklerini düşünmeniz veya ölçmeniz gerekir, ancak bu zor bir kılavuzdur.

+4

Büyük yapılar ne demektir? Büyük bir yapı ve küçük bir yapı örneği var mı? –

+0

Nasıl bytes.Replace amd64 üzerinde 80 baytlık değerinde args alırsınız? –

+0

İmza 'Değiştir (eski, yeni [] bayt, n int) [] byte'dır; s, eski ve yeni her biri üç kelimedir ([dilim başlıkları '(ptr, len, cap)'] (http://research.swtch.com/godata)) ve 'n int' tek kelimedir, yani 10 Sekiz bayt/kelime olan 80 bayt olan kelimeler. – twotwotwo

İlgili konular