Limbajul de programare Go: un tutorial introductiv Golang
Publicat: 2022-03-11Ce este limbajul de programare Go?
Limbajul de programare Go, relativ nou, se află perfect în mijlocul peisajului, oferind o mulțime de caracteristici bune și omițând în mod deliberat multe pe cele proaste. Compilează rapid, rulează rapid, include o colecție de execuție și de gunoi, are un sistem simplu de tip static și interfețe dinamice și o bibliotecă standard excelentă. Acesta este motivul pentru care atât de mulți dezvoltatori sunt dornici să învețe programarea Go.
OOP este una dintre acele caracteristici pe care Go le omite în mod deliberat. Nu are nicio subclasare și, prin urmare, nu există diamante de moștenire sau super apeluri sau metode virtuale care să te împiedice. Cu toate acestea, multe dintre părțile utile ale OOP sunt disponibile în alte moduri.
*Mixinele* sunt disponibile prin încorporarea structurilor anonime, permițând apelarea metodelor lor direct pe structura care le conține (vezi încorporarea). Promovarea metodelor în acest fel se numește *forwarding* și nu este același lucru cu subclasarea: metoda va fi invocată în continuare pe structura internă, încorporată.
Încorporarea nu implică, de asemenea, polimorfism. Deși `A` poate avea un `B`, asta nu înseamnă că este un `B` -- funcțiile care iau un `B` nu vor lua un `A`. Pentru asta, avem nevoie de interfețe , pe care le vom întâlni pe scurt mai târziu.
Între timp, Golang ia o poziție puternică în ceea ce privește caracteristicile care pot duce la confuzie și erori. Omite idiomuri OOP precum moștenirea și polimorfismul, în favoarea compoziției și a interfețelor simple. Minimizează gestionarea excepțiilor în favoarea erorilor explicite în valorile returnate. Există exact o modalitate corectă de a aranja codul Go, impusă de instrumentul gofmt
. Și așa mai departe.
De ce să înveți Golang?
Go este, de asemenea, un limbaj excelent pentru scrierea de programe concurente : programe cu multe părți care rulează independent. Un exemplu evident este un server web: fiecare solicitare rulează separat, dar solicitările trebuie adesea să partajeze resurse precum sesiuni, cache sau cozi de notificări. Aceasta înseamnă că programatorii Go calificați trebuie să se ocupe de accesul simultan la acele resurse.
În timp ce Golang are un set excelent de caracteristici de nivel scăzut pentru gestionarea concurenței, utilizarea lor directă poate deveni complicată. În multe cazuri, o mână de abstracții reutilizabile peste acele mecanisme de nivel scăzut face viața mult mai ușoară.
În tutorialul de programare Go de astăzi, vom analiza o astfel de abstractizare: un wrapper care poate transforma orice structură de date într-un serviciu tranzacțional . Vom folosi un tip de Fund
ca exemplu – un magazin simplu pentru finanțarea rămasă a startup-ului nostru, unde putem verifica soldul și face retrageri.
Pentru a demonstra acest lucru în practică, vom construi serviciul în pași mici, făcând mizerie pe parcurs și apoi curățându-l din nou. Pe măsură ce progresăm prin tutorialul nostru Go, vom întâlni o mulțime de funcții interesante ale limbajului Go, inclusiv:
- Tipuri de structuri și metode
- Teste unitare și benchmark-uri
- Goroutine și canale
- Interfețe și tastare dinamică
Construirea unui fond simplu
Să scriem un cod pentru a urmări finanțarea startup-ului nostru. Fondul începe cu un anumit sold, iar banii pot fi doar retrași (ne vom da seama de venituri mai târziu).
Go nu este în mod deliberat un limbaj orientat pe obiecte: nu există clase, obiecte sau moștenire. În schimb, vom declara un tip de struct numit Fund
, cu o funcție simplă pentru a crea noi structuri de fond și două metode publice.
fund.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 }
Testarea cu benchmark-uri
Apoi avem nevoie de o modalitate de a testa Fund
. În loc să scriem un program separat, vom folosi pachetul de testare Go, care oferă un cadru atât pentru testele unitare, cât și pentru benchmark-uri. Logica simplă din Fund
nostru nu merită cu adevărat să scrieți teste unitare, dar din moment ce vom vorbi mult despre accesul concomitent la fond mai târziu, scrierea unui benchmark are sens.
Benchmark-urile sunt ca testele unitare, dar includ o buclă care rulează același cod de multe ori (în cazul nostru, fund.Withdraw(1)
). Acest lucru permite cadrului să cronometreze cât durează fiecare iterație, făcând o medie a diferențelor tranzitorii de la căutările de disc, erorile de cache, programarea procesului și alți factori imprevizibili.
Cadrul de testare dorește ca fiecare benchmark să ruleze timp de cel puțin 1 secundă (în mod implicit). Pentru a asigura acest lucru, va apela benchmark-ul de mai multe ori, transmițând de fiecare dată o valoare crescândă a „numărului de iterații” (câmpul bN
), până când rularea durează cel puțin o secundă.
Pentru moment, indicele nostru de referință va depune doar niște bani și apoi îi va retrage câte un dolar.
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()) } }
Acum hai să-l rulăm:
$ go test -bench . funding testing: warning: no tests to run PASS BenchmarkWithdrawals 2000000000 1.69 ns/op ok funding 3.576s
A decurs bine. Am rulat două miliarde (!) de iterații, iar verificarea finală a balanței a fost corectă. Putem ignora avertismentul „fără teste de rulat”, care se referă la testele unitare pe care nu le-am scris (în exemplele de programare Go ulterioare din acest tutorial, avertismentul este eliminat).
Acces simultan în Go
Acum să facem concomitent benchmark-ul, pentru a modela diferiți utilizatori care fac retrageri în același timp. Pentru a face asta, vom genera zece goroutine și vom cere fiecăruia să retragă o zecime din bani.
Goroutinele sunt blocul de bază pentru concurența în limbajul Go. Sunt fire verzi – fire ușoare gestionate de runtime Go, nu de sistemul de operare. Aceasta înseamnă că puteți rula mii (sau milioane) dintre ele fără nicio suprasolicitare semnificativă. Goroutinele sunt generate cu cuvântul cheie go
și încep întotdeauna cu o funcție (sau apel de metodă):
// Returns immediately, without waiting for `DoSomething()` to complete go DoSomething()
Adesea, dorim să generăm o funcție scurtă unică cu doar câteva linii de cod. În acest caz, putem folosi o închidere în locul unui nume de funcție:
go func() { // ... do stuff ... }() // Must be a function *call*, so remember the ()
Odată ce toate goroutinele noastre sunt generate, avem nevoie de o modalitate de a aștepta ca acestea să se termine. Am putea construi unul singur folosind canale , dar nu le-am întâlnit încă, așa că ar fi săriți înainte.
Deocamdată, putem folosi doar tipul WaitGroup
din biblioteca standard Go, care există chiar în acest scop. Vom crea unul (numit „ wg
”) și vom apela wg.Add(1)
înainte de a genera fiecare lucrător, pentru a ține evidența câte sunt. Apoi lucrătorii vor raporta folosind wg.Done()
. Între timp, în goroutine principală, putem spune doar wg.Wait()
pentru a bloca până când fiecare lucrător a terminat.
În interiorul rutinelor de lucru din următorul nostru exemplu, vom folosi defer
pentru a apela wg.Done()
.
defer
preia un apel de funcție (sau metodă) și îl rulează imediat înainte ca funcția curentă să revină, după ce toate celelalte sunt făcute. Acesta este util pentru curățare:
func() { resource.Lock() defer resource.Unlock() // Do stuff with resource }()
În acest fel, putem potrivi cu ușurință Unlock
cu Lock
sa, pentru lizibilitate. Mai important, o funcție amânată va rula chiar dacă există o panică în funcția principală (ceva pe care l-am putea gestiona prin try-finally în alte limbi).
În cele din urmă, funcțiile amânate se vor executa în ordinea inversă în care au fost apelate, ceea ce înseamnă că putem face curățarea imbricată frumos (similar cu idiomul C al goto
-urilor imbricate și al label
imbricate, dar mult mai ordonat):
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() // ... }()
OK, deci cu toate acestea spuse, iată noua versiune:
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()) } }
Putem prezice ce se va întâmpla aici. Muncitorii vor executa cu toții Withdraw
unul peste altul. În interiorul acestuia, f.balance -= amount
va citi soldul, va scădea unul și apoi îl va scrie înapoi. Dar uneori doi sau mai mulți lucrători vor citi amândoi același echilibru și vor face aceeași scădere și vom ajunge la un total greșit. Dreapta?
$ go test -bench . funding BenchmarkWithdrawals 2000000000 2.01 ns/op ok funding 4.220s
Nu, tot trece. Ce s-a intamplat aici?
Amintiți-vă că goroutinele sunt fire verzi – sunt gestionate de runtime Go, nu de sistemul de operare. Timpul de execuție programează goroutine în orice fire de operare pe care le are disponibile. La momentul scrierii acestui tutorial de limba Go, Go nu încearcă să ghicească câte fire de operare ar trebui să folosească, iar dacă vrem mai multe, trebuie să spunem asta. În cele din urmă, timpul de execuție curent nu anticipează goroutine - o goroutine va continua să ruleze până când face ceva care sugerează că este gata pentru o pauză (cum ar fi interacțiunea cu un canal).
Toate acestea înseamnă că, deși benchmark-ul nostru este acum simultan, nu este paralel . Doar unul dintre lucrătorii noștri va rula o dată și va funcționa până se va termina. Putem schimba acest lucru spunând lui Go să folosească mai multe fire, prin variabila de mediu GOMAXPROCS
.
$ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 --- FAIL: BenchmarkWithdrawals-4 account_test.go:39: Balance wasn't zero: 4238 ok funding 0.007s
Asa e mai bine. Acum, în mod evident, pierdem unele dintre retragerile noastre, așa cum ne așteptam.
Fă-l un server
În acest moment avem diverse opțiuni. Am putea adăuga un mutex explicit sau un blocare de citire-scriere în jurul fondului. Am putea folosi o comparație și un schimb cu un număr de versiune. Am putea face totul și să folosim o schemă CRDT (poate înlocuind câmpul de balance
cu liste de tranzacții pentru fiecare client și calculând soldul din acestea).
Dar nu vom face nimic din acele lucruri acum, pentru că sunt dezordonate sau înfricoșătoare sau ambele. În schimb, vom decide că un fond ar trebui să fie un server . Ce este un server? E ceva cu care vorbești. În Go, lucrurile vorbesc prin canale.
Canalele sunt mecanismul de comunicare de bază între goroutine. Valorile sunt trimise către canal (cu channel <- value
) și pot fi primite pe cealaltă parte (cu value = <- channel
). Canalele sunt „sigure pentru goroutine”, ceea ce înseamnă că orice număr de goroutine pot trimite și primi de la ele în același timp.
Buffering-ul canalelor de comunicare poate fi o optimizare a performanței în anumite circumstanțe, dar ar trebui utilizat cu mare grijă (și benchmarking!).
Cu toate acestea, există utilizări pentru canalele tamponate care nu se referă direct la comunicare.
De exemplu, un mod obișnuit de throttling creează un canal cu (de exemplu) dimensiunea tamponului „10” și apoi trimite imediat zece jetoane în el. Orice număr de goroutine ale lucrătorilor sunt apoi generate și fiecare primește un token de la canal înainte de a începe lucrul și îl trimite înapoi după aceea. Apoi, oricât de mulți lucrători ar fi, doar zece vor lucra în același timp.
În mod prestabilit, canalele Go sunt fără tampon . Aceasta înseamnă că trimiterea unei valori către un canal se va bloca până când o altă goroutine este gata să o primească imediat. Go acceptă, de asemenea, dimensiuni fixe de buffer pentru canale (folosind make(chan someType, bufferSize)
). Cu toate acestea, pentru utilizare normală, aceasta este de obicei o idee proastă .
Imaginați-vă un server web pentru fondul nostru, unde fiecare solicitare face o retragere. Când lucrurile sunt foarte ocupate, FundServer
nu va putea ține pasul, iar cererile care încearcă să fie trimise către canalul său de comandă vor începe să se blocheze și să aștepte. În acel moment, putem impune un număr maxim de cereri pe server și returnăm un cod de eroare sensibil (cum ar fi un 503 Service Unavailable
) clienților peste această limită. Acesta este cel mai bun comportament posibil atunci când serverul este supraîncărcat.

Adăugarea de buffering la canalele noastre ar face acest comportament mai puțin determinist. Am putea ajunge cu ușurință la cozi lungi de comenzi neprocesate bazate pe informațiile pe care clientul le-a văzut mult mai devreme (și poate pentru solicitări care de atunci expiraseră în amonte). Același lucru se aplică în multe alte situații, cum ar fi aplicarea contrapresiunii peste TCP atunci când receptorul nu poate ține pasul cu expeditorul.
În orice caz, pentru exemplul nostru Go, vom rămâne cu comportamentul implicit fără tampon.
Vom folosi un canal pentru a trimite comenzi către FundServer
. Fiecare lucrător de referință va trimite comenzi către canal, dar doar serverul le va primi.
Am putea transforma tipul nostru de fond într-o implementare de server direct, dar asta ar fi dezordonat – am amesteca gestionarea concurenței și logica de afaceri. În schimb, vom lăsa tipul de fond exact așa cum este și vom face FundServer
un înveliș separat în jurul acestuia.
Ca orice server, wrapper-ul va avea o buclă principală în care așteaptă comenzi și răspunde la fiecare pe rând. Mai este un detaliu pe care trebuie să-l abordăm aici: tipul comenzilor.
Am fi putut face ca canalul nostru de comenzi să preia *pointeri* către comenzi (`chan *TransactionCommand`). De ce nu am făcut-o?
Trecerea de indicatoare între goroutine este riscantă, deoarece oricare dintre goroutine l-ar putea modifica. De asemenea, este adesea mai puțin eficient, deoarece cealaltă goroutine ar putea rula pe un nucleu CPU diferit (adică mai multă invalidare a memoriei cache).
Ori de câte ori este posibil, preferați să transmiteți valori simple.
În următoarea secțiune de mai jos, vom trimite mai multe comenzi diferite, fiecare cu propriul tip de structură. Dorim ca canalul de comenzi al serverului să accepte oricare dintre ele. Într-un limbaj OOP, am putea face acest lucru prin polimorfism: canalul trebuie să ia o superclasă, dintre care tipurile individuale de comandă au fost subclase. În Go, folosim în schimb interfețe .
O interfață este un set de semnături de metodă. Orice tip care implementează toate aceste metode poate fi tratat ca acea interfață (fără a fi declarat că face acest lucru). Pentru prima noastră rulare, structurile noastre de comandă nu vor expune de fapt nicio metodă, așa că vom folosi interfața goală, interface{}
. Deoarece nu are cerințe, orice valoare (inclusiv valorile primitive precum numerele întregi) satisface interfața goală. Acest lucru nu este ideal – vrem doar să acceptăm structuri de comandă – dar vom reveni la el mai târziu.
Deocamdată, să începem cu schelele pentru serverul nostru Go:
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 } }
Acum să adăugăm câteva tipuri de structuri Golang pentru comenzi:
type WithdrawCommand struct { Amount int } type BalanceCommand struct { Response chan int }
Comanda de WithdrawCommand
conține doar suma de retras. Nu există niciun răspuns. BalanceCommand
are un răspuns, deci include un canal pentru a-l trimite. Acest lucru asigură că răspunsurile vor merge întotdeauna la locul potrivit, chiar dacă fondul nostru decide ulterior să răspundă în afara ordinului.
Acum putem scrie bucla principală a serverului:
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. E cam urat. Activam tipul de comandă, utilizăm afirmații de tip și, eventual, blocăm. Să mergem mai departe oricum și să actualizăm benchmark-ul pentru a folosi serverul.
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) } }
Și asta a fost cam urât, mai ales când am verificat echilibrul. Nu face nimic. Hai sa incercam:
$ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 5000000 465 ns/op ok funding 2.822s
Mult mai bine, nu mai pierdem retrageri. Dar codul devine greu de citit și există probleme mai grave. Dacă lansăm vreodată un BalanceCommand
și apoi uităm să citim răspunsul, serverul nostru de fonduri va bloca pentru totdeauna încercarea de a-l trimite. Hai să curățăm puțin lucrurile.
Fă-l un serviciu
Un server este ceva cu care vorbiți. Ce este un serviciu? Un serviciu este ceva cu care vorbiți cu un API . În loc să facem ca codul client să lucreze direct cu canalul de comandă, vom face canalul neexportat (privat) și vom include comenzile disponibile în funcții.
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 } }
Acum, benchmark-ul nostru poate spune doar server.Withdraw(1)
și balance := server.Balance()
și există mai puține șanse de a-i trimite accidental comenzi nevalide sau de a uita să citești răspunsurile.
Mai sunt încă o mulțime de elemente suplimentare pentru comenzi, dar vom reveni la asta mai târziu.
Tranzacții
Până la urmă, banii se epuizează mereu. Să fim de acord că vom înceta să ne retragem atunci când fondul nostru va ajunge la ultimii zece dolari și să cheltuim acești bani pe o pizza comună pentru a sărbători sau a ne plini de compasiune. Benchmark-ul nostru va reflecta acest lucru:
// 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) }
De data aceasta chiar putem prezice rezultatul.
$ 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
Ne-am întors de unde am început – mai mulți lucrători pot citi bilanţul deodată, apoi toţi îl pot actualiza. Pentru a face față acestui lucru, am putea adăuga o anumită logică în fondul însuși, cum ar fi o proprietate minimumBalance
, sau adăugați o altă comandă numită WithdrawIfOverXDollars
. Acestea sunt ambele idei groaznice. Acordul nostru este între noi, nu o proprietate a fondului. Ar trebui să-l păstrăm în logica aplicației.
Ceea ce ne trebuie cu adevărat sunt tranzacții , în același sens ca tranzacțiile cu bazele de date. Deoarece serviciul nostru execută o singură comandă la un moment dat, acest lucru este foarte ușor. Vom adăuga o comandă Transact
care conține un apel invers (o închidere). Serverul va executa acel callback în interiorul propriei sale goroutine, trecând în Fund
brut . Reapelarea poate face apoi în siguranță orice dorește cu Fund
.
În acest exemplu următor, facem greșit două lucruri mici.
În primul rând, folosim un canal „Terminat” ca semafor pentru a anunța codul de apel când tranzacția sa s-a încheiat. Este în regulă, dar de ce este tipul de canal `bool`? Vom trimite vreodată „adevărat” în el doar pentru a însemna „terminat” (ce ar însemna chiar trimiterea „fals”?). Ceea ce ne dorim cu adevărat este o valoare cu o singură stare (o valoare care nu are valoare?). În Go, putem face acest lucru folosind tipul de struct gol: `struct{}`. Acest lucru are și avantajul de a folosi mai puțină memorie. În exemplu, vom rămâne cu `bool` pentru a nu arăta prea înfricoșător.
În al doilea rând, apelarea noastră a tranzacției nu returnează nimic. După cum vom vedea într-un moment, putem obține valori din apel invers în codul de apel folosind trucuri de scope. Cu toate acestea, tranzacțiile într-un sistem real ar eșua uneori, așa că convenția Go ar fi ca tranzacția să returneze o „eroare” (și apoi să verificați dacă a fost „nul” în codul de apelare).
Nici deocamdată nu facem asta, deoarece nu avem erori de generat.
// 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 // ... } } }
Reapelurile noastre pentru tranzacții nu returnează nimic în mod direct, dar limbajul Go facilitează obținerea directă a valorilor dintr-o închidere, așa că vom face acest lucru în benchmark pentru a seta indicatorul pizzaTime
atunci când banii se scad:
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 } }
Și verificați dacă funcționează:
$ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 5000000 775 ns/op ok funding 4.637s
Nimic în afară de tranzacții
Este posibil să fi găsit o oportunitate de a mai curăța lucrurile acum. Deoarece avem o comandă generică Transact
, nu mai avem nevoie de WithdrawCommand
sau BalanceCommand
de echilibru. Le vom rescrie în termeni de tranzacții:
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) }) }
Acum, singura comandă pe care o primește serverul este TransactionCommand
, astfel încât să putem elimina întreaga interface{}
dezordine în implementarea sa și să acceptăm doar comenzile de tranzacție:
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 } }
Mult mai bine.
Există un ultim pas pe care l-am putea face aici. În afară de funcțiile sale comode pentru Balance
și Withdraw
, implementarea serviciului nu mai este legată de Fund
. În loc să gestioneze un Fund
, acesta ar putea gestiona o interface{}
și poate fi folosit pentru a încheia orice . Cu toate acestea, fiecare tranzacție de apel invers ar trebui apoi să convertească interface{}
înapoi la o valoare reală:
type Transactor func(interface{}) server.Transact(func(managedValue interface{}) { fund := managedValue.(*Fund) // Do stuff with fund ... })
Acest lucru este urât și predispus la erori. Ceea ce ne dorim cu adevărat sunt generice în timp de compilare, astfel încât să putem „șablona” un server pentru un anumit tip (cum ar fi *Fund
).
Din păcate, Go nu acceptă medicamente generice – încă. Se așteaptă să ajungă în cele din urmă, odată ce cineva își dă seama de sintaxă și semantică sensibilă pentru el. Între timp, proiectarea atentă a interfeței elimină adesea nevoia de generice, iar atunci când nu o fac, ne putem descurca cu aserțiuni de tip (care sunt verificate în timpul execuției).
Am terminat?
Da.
Ei bine, bine, nu.
De exemplu:
O panică într-o tranzacție va distruge întregul serviciu.
Nu există timeout-uri. O tranzacție care nu se întoarce niciodată va bloca serviciul pentru totdeauna.
Dacă Fondul nostru crește câteva câmpuri noi și o tranzacție se blochează la jumătatea actualizării lor, vom avea o stare inconsecventă.
Tranzacțiile pot scurge obiectul
Fund
gestionat, ceea ce nu este bun.Nu există o modalitate rezonabilă de a face tranzacții cu mai multe fonduri (cum ar fi retragerea dintr-unul și depunerea în altul). Nu ne putem imbrica tranzacțiile, deoarece ar permite blocaje.
Rularea unei tranzacții asincron necesită acum o nouă rutină și multă bătaie de cap. În mod similar, probabil dorim să putem citi cea mai recentă stare
Fund
din altă parte, în timp ce o tranzacție de lungă durată este în desfășurare.
În următorul nostru tutorial în limbajul de programare Go, vom analiza câteva modalități de a aborda aceste probleme.