Go Programlama Dili: Giriş niteliğinde bir Golang Eğitimi
Yayınlanan: 2022-03-11Go Programlama Dili nedir?
Nispeten yeni Go programlama dili, manzaranın ortasında düzgün bir şekilde oturur, birçok iyi özellik sunar ve kasıtlı olarak birçok kötü özelliği atlar. Hızlı derler, hızlı çalışır, çalışma zamanı ve çöp toplama içerir, basit bir statik tip sisteme ve dinamik arayüzlere ve mükemmel bir standart kitaplığa sahiptir. Bu nedenle birçok geliştirici Go programlamayı öğrenmeye heveslidir.
OOP, Go'nun kasten ihmal ettiği özelliklerden biridir. Alt sınıflandırması yoktur ve bu nedenle sizi tetikleyecek miras elmasları, süper çağrılar veya sanal yöntemler yoktur. Yine de, OOP'nin faydalı bölümlerinin çoğu başka şekillerde de mevcuttur.
*Mixins*, yapıları isimsiz olarak gömerek, yöntemlerinin doğrudan içeren yapı üzerinde çağrılmasına izin vererek kullanılabilir (bkz. gömme). Yöntemleri bu şekilde yükseltmeye *yönlendirme* denir ve alt sınıflama ile aynı şey değildir: yöntem yine de iç, gömülü yapıda çağrılacaktır.
Gömme ayrıca polimorfizm anlamına gelmez. "A" bir "B"ye sahip olsa da, bu onun bir "B" olduğu anlamına gelmez -- "B" alan işlevler bunun yerine "A" almaz. Bunun için daha sonra kısaca karşılaşacağımız arayüzlere ihtiyacımız var.
Bu arada Golang, kafa karışıklığına ve hatalara yol açabilecek özellikler konusunda güçlü bir pozisyon alıyor. Kompozisyon ve basit arayüzler lehine kalıtım ve polimorfizm gibi OOP deyimlerini atlar. Dönüş değerlerinde açık hatalar lehine istisna işlemeyi önemsizleştirir. gofmt
aracı tarafından uygulanan Go kodunu düzenlemenin tam olarak bir doğru yolu vardır. Ve bunun gibi.
Neden Golang Öğrenin?
Go aynı zamanda eşzamanlı programlar yazmak için de harika bir dildir: birbirinden bağımsız çalışan birçok parçaya sahip programlar. Açık bir örnek bir web sunucusudur: Her istek ayrı ayrı çalışır, ancak isteklerin genellikle oturumlar, önbellekler veya bildirim kuyrukları gibi kaynakları paylaşması gerekir. Bu, yetenekli Go programcılarının bu kaynaklara eşzamanlı erişimle ilgilenmesi gerektiği anlamına gelir.
Golang, eşzamanlılığı yönetmek için mükemmel bir düşük seviyeli özelliklere sahip olsa da, bunları doğrudan kullanmak karmaşık hale gelebilir. Çoğu durumda, bu düşük seviyeli mekanizmalar üzerindeki bir avuç yeniden kullanılabilir soyutlama, hayatı çok daha kolaylaştırır.
Bugünün Go programlama eğitiminde, böyle bir soyutlamaya bakacağız: Herhangi bir veri yapısını işlemsel bir hizmete dönüştürebilen bir sarmalayıcı. Örnek olarak bir Fund
türü kullanacağız – başlangıçımızın kalan fonu için bakiyeyi kontrol edip para çekebileceğimiz basit bir mağaza.
Bunu pratikte göstermek için, hizmeti küçük adımlarla oluşturacağız, yol boyunca ortalığı karıştıracağız ve sonra tekrar temizleyeceğiz. Go eğitimimizde ilerledikçe, aşağıdakiler de dahil olmak üzere birçok harika Go dili özelliğiyle karşılaşacağız:
- Yapı türleri ve yöntemleri
- Birim testleri ve karşılaştırmalar
- Goroutinler ve kanallar
- Arayüzler ve dinamik yazma
Basit Bir Fon Oluşturma
Girişimimizin finansmanını izlemek için bir kod yazalım. Fon belirli bir bakiye ile başlar ve para yalnızca geri çekilebilir (geliri daha sonra hesaplayacağız).
Go kasıtlı olarak nesne yönelimli bir dil değildir : Sınıflar, nesneler veya kalıtım yoktur. Bunun yerine, yeni fon yapıları oluşturmak için basit bir işleve ve iki genel yönteme sahip Fund
adlı bir yapı türü bildireceğiz.
fon.go
package funding type Fund struct { // balance is unexported (private), because it's lowercase balance int } // A regular function returning a pointer to a fund func NewFund(initialBalance int) *Fund { // We can return a pointer to a new struct without worrying about // whether it's on the stack or heap: Go figures that out for us. return &Fund{ balance: initialBalance, } } // Methods start with a *receiver*, in this case a Fund pointer func (f *Fund) Balance() int { return f.balance } func (f *Fund) Withdraw(amount int) { f.balance -= amount }
Kıyaslamalarla Test Etme
Ardından, Fund
test etmek için bir yola ihtiyacımız var. Ayrı bir program yazmak yerine, Go'nun hem birim testleri hem de kıyaslamalar için bir çerçeve sağlayan test paketini kullanacağız. Fonumuzdaki basit mantık, birim testleri yazmaya değmez, ancak daha sonra Fund
eşzamanlı erişim hakkında çok konuşacağımız için, bir kıyaslama yazmak mantıklıdır.
Karşılaştırmalar, birim testleri gibidir, ancak aynı kodu birçok kez çalıştıran bir döngü içerir (bizim durumumuzda, fund.Withdraw(1)
). Bu, çerçevenin, disk aramalarından, önbellek kayıplarından, işlem zamanlamasından ve diğer öngörülemeyen faktörlerden geçici farklılıkların ortalamasını alarak her yinelemenin ne kadar süreceğini zamanlamasına olanak tanır.
Test çerçevesi, her bir kıyaslamanın en az 1 saniye (varsayılan olarak) çalışmasını ister. Bunu sağlamak için, çalışma en az bir saniye sürene kadar her seferinde artan bir "yineleme sayısı" değeri ( bN
alanı) ileterek kıyaslamayı birden çok kez çağırır.
Şimdilik, karşılaştırma ölçütümüz sadece bir miktar para yatıracak ve ardından her seferinde bir dolar çekecek.
fund_test.go
package funding import "testing" func BenchmarkFund(b *testing.B) { // Add as many dollars as we have iterations this run fund := NewFund(bN) // Burn through them one at a time until they are all gone for i := 0; i < bN; i++ { fund.Withdraw(1) } if fund.Balance() != 0 { b.Error("Balance wasn't zero:", fund.Balance()) } }
Şimdi çalıştıralım:
$ go test -bench . funding testing: warning: no tests to run PASS BenchmarkWithdrawals 2000000000 1.69 ns/op ok funding 3.576s
Bu iyi gitti. İki milyar (!) yineleme yaptık ve bakiyenin son kontrolü doğruydu. Yazmadığımız birim testlerine atıfta bulunan “çalıştırılacak test yok” uyarısını görmezden gelebiliriz (bu öğreticideki daha sonraki Go programlama örneklerinde uyarı kırpılır).
Go'da Eşzamanlı Erişim
Şimdi, aynı anda para çeken farklı kullanıcıları modellemek için kıyaslamayı eşzamanlı yapalım. Bunu yapmak için, on goroutin üreteceğiz ve her birinin paranın onda birini çekmesini sağlayacağız.
Goroutine'ler, Go dilinde eşzamanlılık için temel yapı taşıdır. Bunlar yeşil iş parçacıklarıdır - işletim sistemi tarafından değil, Go çalışma zamanı tarafından yönetilen hafif iş parçacıklarıdır. Bu, önemli bir ek yük olmadan binlerce (veya milyonlarca) çalıştırabileceğiniz anlamına gelir. Goroutine'ler go
anahtar sözcüğüyle oluşturulur ve her zaman bir işlevle (veya yöntem çağrısıyla) başlar:
// Returns immediately, without waiting for `DoSomething()` to complete go DoSomething()
Çoğu zaman, sadece birkaç satır kodla tek seferlik kısa bir işlev oluşturmak isteriz. Bu durumda bir fonksiyon adı yerine bir kapatma kullanabiliriz:
go func() { // ... do stuff ... }() // Must be a function *call*, so remember the ()
Tüm goroutinlerimiz oluşturulduktan sonra, bitirmelerini beklemek için bir yola ihtiyacımız var. Kanalları kullanarak kendimiz bir tane oluşturabiliriz, ancak bunlarla henüz karşılaşmadık, bu yüzden ileri atlamak olur.
Şimdilik, Go'nun standart kitaplığında, bu amaç için var olan WaitGroup
türünü kullanabiliriz. Bir tane oluşturacağız (“ wg
” olarak adlandırılır) ve kaç tane olduğunu takip etmek için her bir işçiyi yumurtlamadan önce wg.Add(1)
çağıracağız. Ardından işçiler wg.Done()
kullanarak rapor verecek. Bu arada ana goroutinde, wg.Wait()
her işçi işini bitirene kadar engellemek için söyleyebiliriz.
Bir sonraki örneğimizde işçi goroutinlerinin içinde, wg.Done()
çağırmak için defer
kullanacağız.
defer
bir işlev (veya yöntem) çağrısı alır ve diğer her şey yapıldıktan sonra, geçerli işlev geri dönmeden hemen önce onu çalıştırır. Bu temizlik için kullanışlıdır:
func() { resource.Lock() defer resource.Unlock() // Do stuff with resource }()
Bu şekilde, okunabilirlik için Unlock
Lock
ile kolayca eşleştirebiliriz. Daha da önemlisi, ana fonksiyonda bir panik olsa bile ertelenmiş bir fonksiyon çalışacaktır (diğer dillerde try-final ile halledebileceğimiz bir şey).
Son olarak, ertelenmiş işlevler çağrıldıkları sıranın tersinde yürütülür, yani iç içe temizlemeyi güzel bir şekilde yapabiliriz (iç içe goto
s ve label
s'nin C deyimine benzer, ancak çok daha düzgün):
func() { db.Connect() defer db.Disconnect() // If Begin panics, only db.Disconnect() will execute transaction.Begin() defer transaction.Close() // From here on, transaction.Close() will run first, // and then db.Disconnect() // ... }()
Tamam, tüm bunlarla birlikte, işte yeni sürüm:
fund_test.go
package funding import ( "sync" "testing" ) const WORKERS = 10 func BenchmarkWithdrawals(b *testing.B) { // Skip N = 1 if bN < WORKERS { return } // Add as many dollars as we have iterations this run fund := NewFund(bN) // Casually assume bN divides cleanly dollarsPerFounder := bN / WORKERS // WaitGroup structs don't need to be initialized // (their "zero value" is ready to use). // So, we just declare one and then use it. var wg sync.WaitGroup for i := 0; i < WORKERS; i++ { // Let the waitgroup know we're adding a goroutine wg.Add(1) // Spawn off a founder worker, as a closure go func() { // Mark this worker done when the function finishes defer wg.Done() for i := 0; i < dollarsPerFounder; i++ { fund.Withdraw(1) } }() // Remember to call the closure! } // Wait for all the workers to finish wg.Wait() if fund.Balance() != 0 { b.Error("Balance wasn't zero:", fund.Balance()) } }
Burada ne olacağını tahmin edebiliyoruz. İşçilerin tümü, Withdraw
üst üste yürütür. İçinde f.balance -= amount
bakiyeyi okuyacak, bir çıkaracak ve sonra geri yazacaktır. Ancak bazen iki veya daha fazla işçi aynı dengeyi okur ve aynı çıkarma işlemini yapar ve sonuçta yanlış toplam elde ederiz. Doğru?
$ go test -bench . funding BenchmarkWithdrawals 2000000000 2.01 ns/op ok funding 4.220s
Hayır, hala geçiyor. Burada ne oldu?
Goroutinlerin yeşil iplikler olduğunu unutmayın - işletim sistemi tarafından değil Go çalışma zamanı tarafından yönetilirler. Çalışma zamanı, sahip olduğu birçok işletim sistemi iş parçacığında goroutinleri programlar. Bu Go dili öğreticisini yazarken, Go kaç tane işletim sistemi dizisi kullanması gerektiğini tahmin etmeye çalışmıyor ve eğer birden fazla istiyorsak bunu söylemek zorundayız. Son olarak, mevcut çalışma zamanı goroutinleri engellemez – bir goroutine, ara vermeye hazır olduğunu gösteren bir şey yapana kadar (bir kanalla etkileşim kurmak gibi) çalışmaya devam eder.
Tüm bunlar, karşılaştırma ölçütümüzün şu anda eşzamanlı olmasına rağmen paralel olmadığı anlamına gelir. Bir seferde çalışanlarımızdan yalnızca biri koşacak ve bitene kadar çalışacak. Bunu, GOMAXPROCS
ortam değişkeni aracılığıyla daha fazla iş parçacığı kullanmasını söyleyerek değiştirebiliriz.
$ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 --- FAIL: BenchmarkWithdrawals-4 account_test.go:39: Balance wasn't zero: 4238 ok funding 0.007s
Bu daha iyi. Şimdi, beklediğimiz gibi, çekimlerimizin bir kısmını açıkça kaybediyoruz.
Sunucu yap
Bu noktada elimizde çeşitli seçenekler var. Fonun etrafına açık bir muteks veya okuma-yazma kilidi ekleyebiliriz. Bir sürüm numarasıyla karşılaştır ve değiştir özelliğini kullanabiliriz. Her şeyi göze alabilir ve bir CRDT şeması kullanabiliriz (belki de balance
alanını her müşteri için işlem listeleriyle değiştirerek ve bunlardan bakiyeyi hesaplayarak).
Ama bunların hiçbirini şimdi yapmayacağız çünkü bunlar dağınık, korkutucu ya da her ikisi. Bunun yerine, bir fonun sunucu olması gerektiğine karar vereceğiz. sunucu nedir? Bu konuştuğun bir şey. Go'da işler kanallar aracılığıyla konuşur.
Kanallar, goroutinler arasındaki temel iletişim mekanizmasıdır. Değerler kanala gönderilir ( channel <- value
ile) ve diğer taraftan alınabilir ( value = <- channel
ile). Kanallar “goroutine güvenlidir”, yani herhangi bir sayıda goroutine aynı anda gönderip alabilir.
Arabelleğe alınan iletişim kanalları, belirli durumlarda bir performans optimizasyonu olabilir, ancak çok dikkatli (ve kıyaslama!) kullanılmalıdır.
Ancak, arabelleğe alınmış kanalların doğrudan iletişimle ilgili olmayan kullanımları vardır.
Örneğin, yaygın bir kısma deyimi (örneğin) arabellek boyutu "10" olan bir kanal oluşturur ve ardından hemen ona on jeton gönderir. Daha sonra herhangi bir sayıda işçi goroutini oluşturulur ve her biri çalışmaya başlamadan önce kanaldan bir jeton alır ve daha sonra geri gönderir. O zaman, ne kadar çok işçi olursa olsun, aynı anda sadece on kişi çalışacak.
Varsayılan olarak, Go kanalları arabelleğe alınmamıştır . Bu, bir kanala değer göndermenin, başka bir goroutin onu hemen almaya hazır olana kadar engelleneceği anlamına gelir. Go ayrıca kanallar için sabit arabellek boyutlarını da destekler ( make(chan someType, bufferSize)
kullanarak). Ancak normal kullanım için bu genellikle kötü bir fikirdir .
Fonumuz için her talebin para çekme işlemi yaptığı bir web sunucusu hayal edin. İşler çok meşgul olduğunda, FundServer
ve komut kanalına gönderilmeye çalışan istekler bloke olmaya ve beklemeye başlar. Bu noktada, sunucuda maksimum istek sayısını zorunlu kılabiliriz ve bu sınırın üzerindeki istemcilere makul bir hata kodu ( 503 Service Unavailable
gibi) döndürebiliriz. Bu, sunucu aşırı yüklendiğinde mümkün olan en iyi davranıştır.

Kanallarımıza arabelleğe alma eklemek, bu davranışı daha az belirleyici hale getirir. İstemcinin çok daha önce gördüğü bilgilere (ve belki de o zamandan beri zaman aşımına uğrayan istekler için) dayalı olarak işlenmemiş uzun komut kuyruklarıyla kolayca sonuçlanabilirdik. Aynısı, alıcının göndericiye ayak uyduramadığı durumlarda TCP üzerinden geri basınç uygulamak gibi diğer birçok durumda da geçerlidir.
Her durumda, Go örneğimiz için varsayılan arabelleğe alınmamış davranışa bağlı kalacağız.
FundServer
komut göndermek için bir kanal kullanacağız. Her kıyaslama çalışanı kanala komutlar gönderir, ancak bunları yalnızca sunucu alır.
Fon türümüzü doğrudan bir sunucu uygulamasına dönüştürebiliriz, ancak bu dağınık olurdu - eşzamanlılık yönetimi ile iş mantığını karıştırıyor olurduk. Bunun yerine, Fon türünü olduğu gibi bırakacağız ve FundServer
onun etrafında ayrı bir sarmalayıcı yapacağız.
Herhangi bir sunucu gibi, sarmalayıcı da komutları beklediği ve sırayla her birine yanıt verdiği bir ana döngüye sahip olacaktır. Burada değinmemiz gereken bir ayrıntı daha var: Komutların türü.
Komutlar kanalımızın komutlara * işaretçiler * almasını sağlayabilirdik (`chan *TransactionCommand`). Neden yapmadık?
Goroutinler arasında işaretçileri geçmek risklidir, çünkü her iki goroutin de onu değiştirebilir. Diğer goroutine farklı bir CPU çekirdeğinde çalışıyor olabileceğinden (daha fazla önbellek geçersiz kılma anlamına gelir) genellikle daha az verimlidir.
Mümkün olduğunda, düz değerleri etrafa aktarmayı tercih edin.
Aşağıdaki bir sonraki bölümde, her biri kendi yapı tipine sahip birkaç farklı komut göndereceğiz. Sunucunun Komutlar kanalının bunlardan herhangi birini kabul etmesini istiyoruz. Bir OOP dilinde bunu polimorfizm yoluyla yapabiliriz: Kanalın, ayrı komut türlerinin alt sınıfları olduğu bir üst sınıf almasını sağlayın. Go'da bunun yerine arayüzler kullanıyoruz.
Bir arabirim, bir dizi yöntem imzasıdır. Tüm bu yöntemleri uygulayan herhangi bir tür, o arabirim olarak ele alınabilir (bunu yaptığı bildirilmeden). İlk çalıştırmamız için, komut yapılarımız aslında herhangi bir yöntem göstermeyecek, bu yüzden boş arayüzü kullanacağız, interface{}
. Herhangi bir gereksinimi olmadığı için, herhangi bir değer (tamsayılar gibi ilkel değerler dahil) boş arayüzü karşılar. Bu ideal değil – sadece komut yapılarını kabul etmek istiyoruz – ama buna daha sonra geri döneceğiz.
Şimdilik, Go sunucumuz için yapı iskelesine başlayalım:
server.go
package funding type FundServer struct { Commands chan interface{} fund Fund } func NewFundServer(initialBalance int) *FundServer { server := &FundServer{ // make() creates builtins like channels, maps, and slices Commands: make(chan interface{}), fund: NewFund(initialBalance), } // Spawn off the server's main loop immediately go server.loop() return server } func (s *FundServer) loop() { // The built-in "range" clause can iterate over channels, // amongst other things for command := range s.Commands { // Handle the command } }
Şimdi komutlar için birkaç Golang yapı türü ekleyelim:
type WithdrawCommand struct { Amount int } type BalanceCommand struct { Response chan int }
WithdrawCommand
yalnızca çekilecek tutarı içerir. Cevap yok. BalanceCommand
bir yanıtı vardır, bu nedenle göndermek için bir kanal içerir. Bu, fonumuz daha sonra sıra dışı yanıt vermeye karar verse bile yanıtların her zaman doğru yere gitmesini sağlar.
Artık sunucunun ana döngüsünü yazabiliriz:
func (s *FundServer) loop() { for command := range s.Commands { // command is just an interface{}, but we can check its real type switch command.(type) { case WithdrawCommand: // And then use a "type assertion" to convert it withdrawal := command.(WithdrawCommand) s.fund.Withdraw(withdrawal.Amount) case BalanceCommand: getBalance := command.(BalanceCommand) balance := s.fund.Balance() getBalance.Response <- balance default: panic(fmt.Sprintf("Unrecognized command: %v", command)) } } }
Hmm. Bu biraz çirkin. Komut türünü açıyoruz, tür iddialarını kullanıyoruz ve muhtemelen çöküyoruz. Yine de ilerleyelim ve sunucuyu kullanmak için kıyaslamayı güncelleyelim.
func BenchmarkWithdrawals(b *testing.B) { // ... server := NewFundServer(bN) // ... // Spawn off the workers for i := 0; i < WORKERS; i++ { wg.Add(1) go func() { defer wg.Done() for i := 0; i < dollarsPerFounder; i++ { server.Commands <- WithdrawCommand{ Amount: 1 } } }() } // ... balanceResponseChan := make(chan int) server.Commands <- BalanceCommand{ Response: balanceResponseChan } balance := <- balanceResponseChan if balance != 0 { b.Error("Balance wasn't zero:", balance) } }
Bu da biraz çirkindi, özellikle de dengeyi kontrol ettiğimizde. Boşver. Hadi deneyelim:
$ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 5000000 465 ns/op ok funding 2.822s
Çok daha iyi, artık para çekme işlemlerini kaybedmiyoruz. Ancak kodun okunması zorlaşıyor ve daha ciddi sorunlar var. Bir BalanceCommand
ve ardından yanıtı okumayı unutursak, fon sunucumuz onu göndermeye çalışmayı sonsuza kadar engeller. İşleri biraz temizleyelim.
Bunu bir Hizmet haline getirin
Sunucu, konuştuğunuz bir şeydir. Hizmet nedir? Hizmet, bir API ile konuştuğunuz bir şeydir. İstemci kodunun doğrudan komut kanalıyla çalışmasını sağlamak yerine, kanalı dışa aktarılmamış (özel) yapacağız ve mevcut komutları işlevlerde toplayacağız.
type FundServer struct { commands chan interface{} // Lowercase name, unexported // ... } func (s *FundServer) Balance() int { responseChan := make(chan int) s.commands <- BalanceCommand{ Response: responseChan } return <- responseChan } func (s *FundServer) Withdraw(amount int) { s.commands <- WithdrawCommand{ Amount: amount } }
Şimdi karşılaştırma ölçütümüz sadece server.Withdraw server.Withdraw(1)
ve balance := server.Balance()
ve yanlışlıkla geçersiz komutlar gönderme veya yanıtları okumayı unutma olasılığı daha düşüktür.
Komutlar için hala çok fazla ek bilgi var, ancak buna daha sonra geri döneceğiz.
işlemler
Sonunda, para her zaman biter. Fonumuz son on dolara düştüğünde geri çekilmeyi bırakacağımıza ve bu parayı kutlamak ya da taziye için ortak bir pizzaya harcayacağımıza karar verelim. Kriterimiz bunu yansıtacaktır:
// Spawn off the workers for i := 0; i < WORKERS; i++ { wg.Add(1) go func() { defer wg.Done() for i := 0; i < dollarsPerFounder; i++ { // Stop when we're down to pizza money if server.Balance() <= 10 { break } server.Withdraw(1) } }() } // ... balance := server.Balance() if balance != 10 { b.Error("Balance wasn't ten dollars:", balance) }
Bu sefer sonucu gerçekten tahmin edebiliriz.
$ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 --- FAIL: BenchmarkWithdrawals-4 fund_test.go:43: Balance wasn't ten dollars: 6 ok funding 0.009s
Başladığımız yere geri döndük - birkaç çalışan aynı anda bakiyeyi okuyabilir ve ardından tümü güncelleyebilir. Bununla başa çıkmak için fonun kendisine bir minimumBalance
özelliği gibi bir mantık ekleyebilir veya WithdrawIfOverXDollars
adında başka bir komut ekleyebiliriz. Bunların ikisi de korkunç fikirler. Anlaşmamız kendi aramızda, fonun malı değil. Uygulama mantığında tutmalıyız.
Gerçekten ihtiyacımız olan şey, veritabanı işlemleriyle aynı anlamda işlemlerdir. Hizmetimiz bir seferde yalnızca bir komut çalıştırdığından, bu çok kolaydır. Geri arama (kapatma) içeren bir Transact
komutu ekleyeceğiz. Sunucu, ham Fund
geçerek bu geri aramayı kendi goroutini içinde yürütür. Geri arama daha sonra Fund
ile istediğini güvenle yapabilir.
Bu sonraki örnekte iki küçük şeyi yanlış yapıyoruz.
İlk olarak, arama kodunun işlemi bittiğinde bilmesini sağlamak için semafor olarak bir "Bitti" kanalı kullanıyoruz. Sorun değil, ama neden kanal türü "bool"? "Tamamlandı" anlamına gelen "doğru"yu göndereceğiz ("yanlış" göndermek bile ne anlama gelir?). Gerçekten istediğimiz tek durumlu bir değerdir (değeri olmayan bir değer?). Go'da bunu boş yapı türünü kullanarak yapabiliriz: `struct{}`. Bu aynı zamanda daha az bellek kullanma avantajına da sahiptir. Örnekte çok korkutucu görünmemek için 'bool' ile kalacağız.
İkincisi, işlem geri aramamız hiçbir şey döndürmüyor. Birazdan göreceğimiz gibi, kapsam hilelerini kullanarak geri aramadan arama koduna değerler alabiliriz. Ancak, gerçek bir sistemdeki işlemler muhtemelen bazen başarısız olur, bu nedenle Go kuralı, işlemin bir "hata" döndürmesini sağlamak (ve ardından çağrı kodunda "nil" olup olmadığını kontrol etmek) olacaktır.
Oluşturacak herhangi bir hatamız olmadığı için bunu da şimdilik yapmıyoruz.
// Typedef the callback for readability type Transactor func(fund *Fund) // Add a new command type with a callback and a semaphore channel type TransactionCommand struct { Transactor Transactor Done chan bool } // ... // Wrap it up neatly in an API method, like the other commands func (s *FundServer) Transact(transactor Transactor) { command := TransactionCommand{ Transactor: transactor, Done: make(chan bool), } s.commands <- command <- command.Done } // ... func (s *FundServer) loop() { for command := range s.commands { switch command.(type) { // ... case TransactionCommand: transaction := command.(TransactionCommand) transaction.Transactor(s.fund) transaction.Done <- true // ... } } }
İşlem geri aramalarımız doğrudan hiçbir şey döndürmez, ancak Go dili, değerleri bir kapanıştan doğrudan çıkarmayı kolaylaştırır, bu nedenle bunu, para azaldığında pizzaTime
bayrağını ayarlamak için karşılaştırmalı değerlendirmede yapacağız:
pizzaTime := false for i := 0; i < dollarsPerFounder; i++ { server.Transact(func(fund *Fund) { if fund.Balance() <= 10 { // Set it in the outside scope pizzaTime = true return } fund.Withdraw(1) }) if pizzaTime { break } }
Ve çalışıp çalışmadığını kontrol edin:
$ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 5000000 775 ns/op ok funding 4.637s
İşlemler dışında hiçbir şey
Şimdi işleri biraz daha temizlemek için bir fırsat görmüş olabilirsiniz. Genel bir Transact
komutumuz olduğundan, artık WithdrawCommand
veya BalanceCommand
ihtiyacımız yok. Bunları işlemler açısından yeniden yazacağız:
func (s *FundServer) Balance() int { var balance int s.Transact(func(f *Fund) { balance = f.Balance() }) return balance } func (s *FundServer) Withdraw(amount int) { s.Transact(func (f *Fund) { f.Withdraw(amount) }) }
Artık sunucunun aldığı tek komut TransactionCommand
, bu nedenle uygulamasındaki tüm interface{}
karmaşasını kaldırabilir ve yalnızca işlem komutlarını kabul etmesini sağlayabiliriz:
type FundServer struct { commands chan TransactionCommand fund *Fund } func (s *FundServer) loop() { for transaction := range s.commands { // Now we don't need any type-switch mess transaction.Transactor(s.fund) transaction.Done <- true } }
Çok daha iyi.
Burada atabileceğimiz son bir adım var. Balance
ve Withdraw
için kolaylık fonksiyonlarının yanı sıra, hizmet uygulaması artık Fund
bağlı değildir. Bir Fund
yönetmek yerine, bir interface{}
yönetebilir ve herhangi bir şeyi sarmak için kullanılabilir. Ancak, her işlem geri araması daha sonra interface{}
gerçek bir değere geri dönüştürmek zorunda kalacaktır:
type Transactor func(interface{}) server.Transact(func(managedValue interface{}) { fund := managedValue.(*Fund) // Do stuff with fund ... })
Bu çirkin ve hataya açık. Gerçekten istediğimiz şey, derleme zamanı jenerikleridir, böylece belirli bir tür için bir sunucuyu "şablonlaştırabiliriz" ( *Fund
gibi).
Ne yazık ki Go, henüz jenerik ilaçları desteklemiyor. Birisi mantıklı bir sözdizimi ve anlambilimi bulduğunda, sonunda gelmesi bekleniyor. Bu arada, dikkatli arayüz tasarımı genellikle jeneriklere olan ihtiyacı ortadan kaldırır ve bunlar olmadığında tür iddialarıyla (çalışma zamanında kontrol edilir) geçinebiliriz.
Tamam mıyız?
Evet.
Pekala, tamam, hayır.
Örneğin:
Bir işlemdeki panik tüm hizmeti öldürür.
Zaman aşımı yok. Asla geri gelmeyen bir işlem, hizmeti sonsuza kadar engeller.
Fonumuz bazı yeni alanlar büyütürse ve bir işlem bunları güncellemenin yarısında çökerse, tutarsız bir duruma sahip olacağız.
İşlemler, yönetilen
Fund
nesnesini sızdırabilir, bu iyi değildir.Birden fazla fon arasında işlem yapmanın makul bir yolu yoktur (birinden para çekmek ve diğerine para yatırmak gibi). Kilitlenmelere izin vereceği için işlemlerimizi iç içe geçiremeyiz.
Bir işlemi eşzamansız olarak çalıştırmak artık yeni bir goroutine ve çok fazla karışıklık gerektiriyor. Buna bağlı olarak, uzun süredir devam eden bir işlem devam ederken muhtemelen en son
Fund
durumunu başka bir yerden okuyabilmek istiyoruz.
Bir sonraki Go programlama dili eğitimimizde, bu sorunları çözmenin bazı yollarına bakacağız.