Merge | |
---|---|
Clasa de limba | multithreaded , imperativ , structurat , orientat pe obiecte [1] [2] |
Tipul de execuție | compilate |
Aparut in | 10 noiembrie 2009 |
Autor | Robert Grismer , Rob Pike și Ken Thompson |
Dezvoltator | Google , Rob Pike , Ken Thompson , The Go Authors [d] și Robert Grismer [d] |
Extensie de fișier | .go |
Eliberare |
|
Tip sistem | strict , static , cu inferență de tip |
A fost influențat | C [4] , Oberon-2 , Limbo , Oberon activ , Teoria interacțiunii proceselor secvențiale , Pascal [4] , Oberon [4] , Smalltalk [5] , Newsqueak [d] [6] , Modula-2 [6] , Alef [d] , APL [7] , BCPL , Modula și Occam |
Licență | BSD |
Site-ul web | go.dev _ |
OS | DragonFly BSD , FreeBSD , Linux , macOS , NetBSD , OpenBSD , Plan 9 , Solaris , Microsoft Windows , iOS , Android , AIX și Illumos |
Fișiere media la Wikimedia Commons |
Go (deseori și golang ) este un limbaj de programare multi- thread compilat dezvoltat intern de Google [8] . Dezvoltarea Go a început în septembrie 2007, cu Robert Grismer , Rob Pike și Ken Thompson [9] , care au lucrat anterior la proiectul de dezvoltare a sistemului de operare Inferno , direct implicați în proiectarea acestuia . Limba a fost introdusă oficial în noiembrie 2009 . În acest moment, suportul pentru compilatorul oficial dezvoltat de creatorii limbajului este oferit pentru sistemele de operare FreeBSD , OpenBSD , Linux , macOS , Windows , DragonFly BSD , Plan 9 , Solaris , Android , AIX . [10] . Go este suportat și de setul de compilatori gcc și există mai multe implementări independente. O a doua versiune a limbajului este în curs de dezvoltare.
Numele limbajului ales de Google este aproape același cu numele limbajului de programare Go! , creat de F. Gee. McCabe și C. L. Clark în 2003 [11] . Numele este discutat pe pagina Go [11] .
Pe pagina de pornire a limbii și, în general, în publicațiile de pe Internet, denumirea alternativă „golang” este adesea folosită.
Limbajul Go a fost dezvoltat ca un limbaj de programare pentru crearea de programe extrem de eficiente care rulează pe sisteme moderne distribuite și procesoare multi-core. Poate fi văzută ca o încercare de a crea un înlocuitor pentru limbajele C și C++ , ținând cont de tehnologiile informatice schimbate și de experiența acumulată în dezvoltarea de sisteme mari [12] . În cuvintele lui Rob Pike [12] , „Go a fost conceput pentru a rezolva problemele de dezvoltare software din viața reală la Google”. El enumeră următoarele ca principale probleme:
Principalele cerințe pentru limbă au fost [13] :
Go a fost creat cu așteptarea ca programele de pe acesta să fie traduse în cod obiect și executate direct fără a necesita o mașină virtuală , așa că unul dintre criteriile de alegere a soluțiilor arhitecturale a fost capacitatea de a asigura o compilare rapidă în cod obiect eficient și absența excesivă. cerințe pentru suport dinamic.
Rezultatul a fost un limbaj „care nu a reprezentat o descoperire, dar a fost totuși un instrument excelent pentru dezvoltarea unor proiecte software mari” [12] .
Deși este disponibil un interpret pentru Go , practic nu este mare nevoie de el, deoarece viteza de compilare este suficient de rapidă pentru a permite dezvoltarea interactivă.
Principalele caracteristici ale limbajului Go [9] :
Go nu include multe dintre caracteristicile sintactice populare disponibile în alte limbaje moderne de programare a aplicațiilor. În multe cazuri, acest lucru este cauzat de o decizie conștientă a dezvoltatorilor. Scurte justificări ale deciziilor de proiectare alese pot fi găsite în „Întrebările frecvente” [9] privind limbajul, mai detaliat – în articolele și discuțiile publicate pe site-ul limbajului, având în vedere diverse opțiuni de proiectare. În special:
Sintaxa limbajului Go este similară cu cea a limbajului C , cu elemente împrumutate din Oberon și limbaje de scripting .
Go este un limbaj sensibil la majuscule și minuscule, cu suport complet Unicode pentru șiruri și identificatori.
În mod tradițional, un identificator poate fi orice secvență nevidă de litere, numere și un caracter de subliniere care începe cu o literă și nu se potrivește cu niciunul dintre cuvintele cheie Go. „Litere” se referă la toate caracterele Unicode care se încadrează în categoriile „Lu” (litere mari), „Ll” (litere mici), „Lt” (litere majuscule), „Lm” (litere modificatoare) sau „Lo” ( alte litere), sub „numere” - toate caracterele din categoria „Nd” (cifre, cifre zecimale). Astfel, nimic nu împiedică utilizarea chirilicului în identificatori, de exemplu.
Identificatorii care diferă numai prin majuscule sunt distincte. Limba are o serie de convenții pentru utilizarea literelor mari și mici. În special, numai litere mici sunt folosite în numele pachetelor. Toate cuvintele cheie Go sunt scrise cu litere mici. Variabilele care încep cu litere mari sunt exportabile (publice), iar cele care încep cu litere mici sunt neexportabile (private).
Literale șiruri pot folosi toate caracterele Unicode fără restricții. Șirurile sunt reprezentate ca secvențe de caractere UTF-8 .
Any Go program include unul sau mai multe pachete. Pachetul căruia îi aparține un fișier cu cod sursă este dat de descrierea pachetului de la începutul fișierului. Numele pachetelor au aceleași restricții ca și identificatorii, dar pot conține doar litere mici. Sistemul de pachete goroutine are o structură arborescentă similară cu un arbore de directoare. Orice obiect global (variabile, tipuri, interfețe, funcții, metode, elemente de structuri și interfețe) sunt disponibile fără restricții în pachetul în care sunt declarate. Obiectele globale ale căror nume încep cu o literă majusculă sunt exportabile.
Pentru a utiliza obiecte exportate de un alt pachet într-un fișier de cod Go, pachetul trebuie importat folosind import.
pachet principal /* Import */ import ( "fmt" // Pachetul standard pentru ieșirea formatată "database/sql" // Import pachetul imbricat cu "os" // Import cu alias . "math" // Import fără calificare când se utilizează _ "gopkg.in/goracle.v2" // Pachetul nu are referințe explicite în cod ) func main () { for _ , arg := interval w . Args { // Accesarea matricei Args declarată în pachetul „os” prin aliasul fmt . Println ( arg ) // Apelarea funcției Println() declarată în pachetul „fmt” cu numele pachetului } var db * sql . db = sql . Open ( driver , dataSource ) // Numele din pachetul imbricat sunt calificate // numai prin numele pachetului în sine (sql) x := Sin ( 1.0 ) // apel math.Sin() - calificare prin numele pachetului math nu este necesar // deoarece a fost importat fără nume // Nu există nicio referință la pachetul „goracle.v2” în cod, dar va fi importat. }Listează căile către pachetele importate din directorul src din arborele sursă, a cărui poziție este dată de variabila de mediu GOPATH, în timp ce pentru pachetele standard, trebuie doar să specificați numele. Un șir care identifică un pachet poate fi precedat de un alias, caz în care va fi folosit în cod în locul numelui pachetului. Obiectele importate sunt disponibile în fișierul care le importă cu o calificare completă precum „ пакет.Объект”. Dacă un pachet este importat cu un punct în loc de un alias, atunci toate numele pe care le exportă vor fi disponibile fără calificare. Această caracteristică este utilizată de unele utilitare de sistem, dar utilizarea ei de către programator nu este recomandată, deoarece calificarea explicită oferă protecție împotriva coliziunilor de nume și a modificărilor „imperceptibile” ale comportamentului codului. Nu este posibil să importați fără calificare două pachete care exportă același nume.
Importul pachetelor în Go este strâns controlat: dacă un pachet este importat de un modul, atunci cel puțin un nume exportat de acel pachet trebuie utilizat în codul acelui modul. Compilatorul Go tratează importarea unui pachet neutilizat ca pe o eroare; o astfel de soluție obligă dezvoltatorul să țină în permanență la zi listele de import. Acest lucru nu creează dificultăți, deoarece instrumentele de asistență pentru programare Go (editore, IDE-uri) oferă de obicei verificarea și actualizarea automată a listelor de import.
Când un pachet conține cod care este folosit doar prin introspecție , există o problemă: importarea unui astfel de pachet este necesară pentru a-l include în program, dar nu va fi permisă de compilator, deoarece nu este accesat direct. _Importul anonim este furnizat pentru astfel de cazuri: „ ” (singura subliniere) este specificat ca un alias ; un pachet importat în acest mod va fi compilat și inclus în program dacă nu este menționat în mod explicit în cod. Cu toate acestea, un astfel de pachet nu poate fi utilizat în mod explicit; acest lucru împiedică controlul importului să fie ocolit prin importul tuturor pachetelor ca anonime.
Un program Go executabil trebuie să conțină un pachet numit main, care trebuie să conțină o funcție main()fără parametri și o valoare returnată. Funcția main.main()este „corpul programului” - codul său este rulat când programul pornește. Orice pachet poate conține o funcție init() - aceasta va fi rulată când programul este încărcat, înainte de a începe executarea, înainte de a apela orice funcție din acest pachet și în orice pachet care îl importă. Pachetul principal este întotdeauna inițializat ultimul și toate inițializările sunt făcute înainte ca funcția să înceapă execuția main.main().
Sistemul de ambalare Go a fost conceput cu presupunerea că întregul ecosistem de dezvoltare există ca un singur arbore de fișiere care conține versiuni actualizate ale tuturor pachetelor, iar atunci când apar versiuni noi, acesta este complet recompilat. Pentru programarea aplicațiilor folosind biblioteci terțe, aceasta este o limitare destul de puternică. În realitate, există adesea restricții asupra versiunilor de pachete utilizate de unul sau altul cod, precum și situații în care versiuni (ramuri) diferite ale unui proiect folosesc versiuni diferite de pachete de bibliotecă.
Începând cu versiunea 1.11, Go acceptă așa-numitele module . Un modul este un pachet descris special care conține informații despre versiunea sa. Când un modul este importat, versiunea care a fost folosită este fixată. Acest lucru permite sistemului de compilare să controleze dacă toate dependențele sunt satisfăcute, să actualizeze automat modulele importate atunci când autorul le face modificări compatibile și să blocheze actualizările versiunilor necompatibile cu versiunea anterioară. Modulele ar trebui să fie o soluție (sau o soluție mult mai ușoară) la problema managementului dependenței.
Go utilizează ambele tipuri de comentarii în stil C: comentarii inline (începând cu // ...) și comentarii bloc (/* ... */). Un comentariu de linie este tratat de compilator ca o linie nouă. Bloc, situat pe o linie - ca un spațiu, pe mai multe linii - ca o nouă linie.
Punctul și virgulă din Go este folosit ca separator obligatoriu în unele operațiuni (dacă, pentru, comută). Formal, ar trebui, de asemenea, să încheie fiecare comandă, dar în practică nu este nevoie să puneți un astfel de punct și virgulă la sfârșitul liniei, deoarece compilatorul însuși adaugă punct și virgulă la sfârșitul fiecărei linii, excluzând caracterele goale, la sfârșitul liniei. identificator, număr, un caracter literal, un șir, întrerupere, continuare, fallthrough, returnare cuvinte cheie, o comandă de creștere sau decrementare (++ sau --) sau o paranteză de închidere, pătrată sau acoladă (o excepție importantă este aceea că virgula nu este inclusă în lista de mai sus). De aici rezultă două lucruri:
Limbajul conține un set destul de standard de tipuri de date simple încorporate: numere întregi, numere în virgulă mobilă, caractere, șiruri de caractere, booleeni și câteva tipuri speciale.
ÎntregiExistă 11 tipuri de numere întregi:
Creatorii limbajului recomandă utilizarea doar a tipului standard pentru lucrul cu numere în interiorul programului int. Tipurile cu dimensiuni fixe sunt concepute pentru a funcționa cu date primite de la sau transmise către surse externe, atunci când este important pentru corectitudinea codului să se specifice o dimensiune specifică a tipului. Tipurile sunt sinonime byteși runesunt concepute pentru a funcționa cu date binare și, respectiv, simboluri. Tipul uintptreste necesar doar pentru interacțiunea cu codul extern, de exemplu, în C.
Numere în virgulă mobilăNumerele în virgulă mobilă sunt reprezentate de două tipuri float32și float64. Dimensiunea lor este de 32, respectiv 64 de biți, implementarea respectă standardul IEEE 754 . Gama de valori poate fi obținută din pachetul standard math.
Tipuri numerice cu precizie nelimitatăBiblioteca standard Go conține și pachetul big, care oferă trei tipuri cu precizie nelimitată: big.Int, big.Ratși big.Float, care reprezintă numere întregi, raționale și, respectiv, numere în virgulă mobilă; dimensiunea acestor numere poate fi orice și este limitată doar de cantitatea de memorie disponibilă. Deoarece operatorii din Go nu sunt supraîncărcați, operațiile de calcul pe numere cu precizie nelimitată sunt implementate ca metode obișnuite. Performanța calculelor cu numere mari, desigur, este semnificativ inferioară tipurilor numerice încorporate, dar atunci când se rezolvă anumite tipuri de probleme de calcul, utilizarea unui pachet bigpoate fi de preferată optimizării manuale a unui algoritm matematic.
Numere complexeLimbajul oferă, de asemenea, două tipuri încorporate pentru numere complexe complex64și complex128. Fiecare valoare a acestor tipuri conține o pereche de părți reale și imaginare având tipuri, respectiv float32, float64. Puteți crea o valoare de tip complex în cod într-unul din două moduri: fie utilizând o funcție încorporată, complex()fie utilizând un literal imaginar într-o expresie. Puteți obține părțile reale și imaginare ale unui număr complex folosind funcțiile real()și imag().
var x complex128 = complex ( 1 , 2 ) // 1 + 2i y := 3 + 4i // 3 + 4i , 4 fiind un număr urmat de un sufix i // este un fmt literal imaginar . Println ( x * y ) // afișează „(-5+10i)” fmt . Println ( real ( x * y )) // afișează „-5” fmt . Println ( imagine ( x * y )) // va tipări „10” Valori booleeneTipul boolean booleste destul de comun - include valorile predefinite trueși falsecare denotă, respectiv, adevărat și fals. Spre deosebire de C, valorile booleene din Go nu sunt numerice și nu pot fi convertite direct în numere.
ȘiruriValorile de tip șir stringsunt matrice imuabile de octeți care conțin UTF-8. Acest lucru determină o serie de caracteristici specifice ale șirurilor de caractere (de exemplu, în cazul general, lungimea unui șir nu este egală cu lungimea matricei care îl reprezintă, adică numărul de caractere conținute în acesta nu este egal cu numărul de octeți din matricea corespunzătoare). Pentru majoritatea aplicațiilor care procesează șiruri întregi, această specificitate nu este importantă, dar în cazurile în care programul trebuie să proceseze în mod direct anumite rune (caractere Unicode), unicode/utf8este necesar un pachet care să conțină instrumente auxiliare pentru lucrul cu șiruri Unicode.
Pentru orice tip de date, inclusiv cele încorporate, pot fi declarate noi tipuri analogice care repetă toate proprietățile originalelor, dar sunt incompatibile cu acestea. Aceste noi tipuri pot, de asemenea, să declare opțional metode. Tipurile de date definite de utilizator în Go sunt pointeri (declarate cu simbolul *), matrice (declarate cu paranteze drepte), structuri ( struct), funcții ( func), interfețe ( interface), mapări ( map) și canale ( chan). Declarațiile acestor tipuri specifică tipurile și eventual identificatorii elementelor lor. Noile tipuri sunt declarate cu cuvântul cheie type:
tip PostString șir // Tastați „șir”, similar cu cel încorporat tip StringArray [] șir // Tip matrice cu elemente de tip șir type Person struct { // Struct tip nume șir // câmp al tipului șirului standard post PostString // câmp al tipului șirului personalizat declarat anterior bdate time . Time // câmp de tip Time, importat din pachetul time edate time . Timp șef * Persoană // câmpul indicator deducere []( * Persoană ) // câmp matrice } type InOutString chan string // tip de canal pentru transmiterea șirurilor tip CompareFunc func ( a , b interfață {}) int // tipul funcției.Începând cu versiunea Go 1.9, este disponibilă și declarația de tip alias (alias-uri):
tip TitleString = șir // „TitleString” este un alias pentru tipul încorporat tip șir Integer = int64 // „Integer” este un alias pentru tipul încorporat de 64 de biți întregUn alias poate fi declarat fie pentru un tip de sistem, fie pentru orice tip definit de utilizator. Diferența fundamentală dintre aliasuri și declarațiile de tip obișnuit este că declarația creează un nou tip care nu este compatibil cu originalul, chiar dacă nu sunt adăugate modificări tipului original în declarație. Un alias este doar un alt nume pentru același tip, ceea ce înseamnă că aliasul și tipul original sunt complet interschimbabile.
Câmpurile de structură pot avea etichete în descriere - secvențe arbitrare de caractere cuprinse între ghilimele din spate:
// Structură cu etichete de câmp tip XMLInvoices struct { XMLName xml . Nume `xml:"INVOICES"` Versiune int `xml:"version,attr"` Factură [] * XMLInvoice `xml:"FACTURA"` }Etichetele sunt ignorate de compilator, dar informațiile despre ele sunt plasate în cod și pot fi citite folosind funcțiile pachetului reflectinclus în biblioteca standard. În mod obișnuit, etichetele sunt folosite pentru a oferi tip de marshaling pentru stocarea și restaurarea datelor pe medii externe sau pentru a interacționa cu sisteme externe care primesc sau transmit date în propriile formate. Exemplul de mai sus folosește etichete procesate de biblioteca standard pentru a citi și scrie date în format XML.
Sintaxa pentru declararea variabilelor este rezolvată în principal în spiritul lui Pascal: declarația începe cu cuvântul cheie var, urmat de numele variabilei prin separator, apoi, prin separator, tipul acesteia.
Merge | C++ |
---|---|
var v1 int const v2 șir var v3 [ 10 ] int var v4 [] int var v5 struct { f int } var v6 * int /* aritmetica pointerului nu este acceptată */ var v7 map [ șir ] int var v8 func ( a int ) int | int v1 ; const std :: stringv2 ; _ /* despre */ intv3 [ 10 ] ; int * v4 ; /* despre */ struct { int f ; } v5 ; int * v6 ; std :: unordered_map v7 ; /* despre */ int ( * v8 )( int a ); |
Declarația variabilei poate fi combinată cu inițializarea:
var v1 int = 100 var v2 șir = „Bună ziua!” var v3 [ 10 ] int = { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } var v4 [] int = { 1000 , 2000 , 12334 } var v5 struct { f int } = { 50 } var v6 * int = & v1 var v7 map [ șir ] int = { „unu” : 1 , „două” : 2 , „trei” : 3 } var v8 func ( a int ) int = func ( a int ) int { return a + 1 }Dacă o variabilă nu este inițializată în mod explicit atunci când o declară , atunci este inițializată automat la „valoare nulă” pentru tipul dat. Valoarea nulă pentru toate tipurile numerice este 0, pentru un tip string este șirul gol, pentru pointeri este nil. Structurile sunt inițializate implicit cu seturi de valori zero pentru fiecare dintre câmpurile incluse în ele, elementele matricei sunt inițializate cu valori zero de tipul specificat în definiția matricei.
Reclamele pot fi grupate:
var ( i int m float )Limbajul Go acceptă, de asemenea, deducerea tipului automat . Dacă o variabilă este inițializată atunci când este declarată, tipul acesteia poate fi omis - tipul expresiei care i se atribuie devine tipul variabilei. Pentru literale (numere, caractere, șiruri de caractere), standardul de limbaj definește tipuri specifice încorporate cărora le aparține fiecare astfel de valoare. Pentru a inițializa o variabilă de alt tip, trebuie aplicată literalului o conversie explicită de tip.
var p1 = 20 // p1 int - literalul întreg 20 este de tip int. var p2 = uint ( 20 ) // p2 uint - valoare turnată în mod explicit la uint. var v1 = & p1 // v1 *int este un pointer către p1, pentru care se deduce tipul int. var v2 = & p2 // v2 *uint este un pointer către p2, care este inițializat în mod explicit ca un întreg fără semn.Pentru variabilele locale, există o formă scurtă de declarație combinată cu inițializare folosind inferența de tip:
v1 := 100 v2 := „Bună ziua!” v3 := [ 10 ] int { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } v4 := [] int { 1000 , 2000 , 12334 } v5 := struct { f int }{ 50 } v6 := & v1Go folosește simbolul ca operator de atribuire =:
a = b // Setați variabila a la bDupă cum s-a menționat mai sus, există o formă de definire a unei variabile cu inferență de tip automat combinată cu inițializare, care seamănă în exterior cu atribuirea în Pascal :
v1 := v2 // similar cu var v1 = v2Compilatorul Go urmărește cu strictețe definițiile și atribuțiile și le distinge una de alta. Deoarece redefinirea unei variabile cu același nume este interzisă într-un singur domeniu, într-un singur bloc de cod, o variabilă poate apărea în stânga semnului :=o singură dată:
a := 10 // Declararea și inițializarea unei variabile întregi a. b := 20 // Declararea și inițializarea unei variabile întregi b. ... a := b // EROARE! Încercarea de a redefini a.Go permite executarea mai multor sarcini în paralel:
i , j = j , i // Schimbați valorile i și j.În acest caz, numărul de variabile din stânga semnului de atribuire trebuie să se potrivească exact cu numărul de expresii din dreapta semnului de atribuire.
Alocarea în paralel este posibilă și atunci când se utilizează :=. Particularitatea sa este că printre variabilele enumerate în stânga semnului :=, pot fi deja existente. În acest caz, vor fi create noi variabile, cele existente vor fi reutilizate. Această sintaxă este adesea folosită pentru tratarea erorilor:
x , err := SomeFunction () // Funcția returnează două valori (vezi mai jos), // două variabile sunt declarate și inițializate. if ( err != nil ) { return nil } y , err := SomeOtherFunction () // Aici este declarat doar y, err i se atribuie pur și simplu o valoare.În ultima linie a exemplului, prima valoare returnată de funcție este atribuită noii variabile y, a doua variabilei deja existente err, care este folosită în tot codul pentru a plasa ultima eroare returnată de funcțiile apelate. Dacă nu este pentru această caracteristică a operatorului :=, în al doilea caz ar trebui să declarați o nouă variabilă (de exemplu, err2) sau să declarați separat yși apoi să utilizați alocarea paralelă obișnuită.
Go implementează semantica „copy-on-assignment”, adică o atribuire are ca rezultat realizarea unei copii a valorii variabilei originale și plasarea acelei copii într-o altă variabilă, după care valorile variabilelor sunt diferite și schimbarea uneia dintre ele nu-l schimbă pe celălalt. Cu toate acestea, acest lucru este valabil numai pentru tipurile scalare, structurile și matricele încorporate cu o lungime dată (adică tipurile ale căror valori sunt alocate pe stivă). Matricele de lungime nedefinită și mapările sunt alocate pe heap , variabilele de aceste tipuri conțin de fapt referințe la obiecte, atunci când sunt alocate, este copiată doar referința, dar nu și obiectul în sine. Uneori, acest lucru poate duce la efecte neașteptate. Luați în considerare două exemple aproape identice:
vector de tip [ 2 ] float64 // Lungimea matricei este setată explicit v1 := vector { 10 , 15.5 } // Inițializare - v1 conține matricea în sine v2 := v1 // Matricea v1 este copiată în matricea v2 v2 [ 0 ] = 25.3 // S-a schimbat doar matricea fmt v2 . Println ( v1 ) // Imprimă „[10 15.5]” - matricea originală nu s-a schimbat. fmt . Println ( v2 ) // Imprimă „[25.3 15.5]”Aici tipul este vectordefinit ca o matrice de două numere. Atribuirea unor astfel de matrice se comportă în același mod ca și atribuirea de numere și structuri.
Și în exemplul următor, codul diferă cu exact un caracter: tipul vectoreste definit ca o matrice cu o dimensiune nedefinită. Dar acest cod se comportă complet diferit:
tip vector [] float64 // Matrice cu lungime nedefinită v1 := vector { 10 , 15.5 } // Inițializare - v1 conține referința matricei v2 := v1 // Referința matricei este copiată de la v1 la v2 v2 [ 0 ] = 25.3 / / Poate fi considerat ca schimbând numai matricea fmt v2 . Println ( v1 ) // Imprimă „[25.3 15.5]” - matricea originală s-a SCHIMBAT! fmt . Println ( v2 ) // Imprimă „[25.3 15.5]”În același mod ca în al doilea exemplu, mapările și interfețele se comportă. Mai mult decât atât, dacă structura are un câmp de tip referință sau interfață sau un câmp este o matrice adimensională sau o mapare, atunci când se atribuie o astfel de structură, se va copia și referința, adică vor începe câmpurile diferitelor structuri. pentru a indica aceleași obiecte din memorie.
Pentru a evita acest efect, trebuie să utilizați în mod explicit o funcție de sistem copy()care să garanteze crearea unei a doua instanțe a obiectului.
sunt declarate astfel:
func f ( i , j , k int , s , t șir ) șir { }Tipurile de astfel de valori sunt incluse între paranteze:
func f ( a , b int ) ( int , șir ) { return a + b , „adăugare” }Rezultatele funcției pot fi, de asemenea, denumite:
func incTwo ( a , b int ) ( c , d int ) { c = a + 1 d = b + 1 return }Rezultatele numite sunt considerate declarate imediat după antetul funcției cu valori inițiale zero. Instrucțiunea return într-o astfel de funcție poate fi utilizată fără parametri, caz în care, după revenirea din funcție, rezultatele vor avea valorile care le-au fost atribuite în timpul execuției acesteia. Deci, în exemplul de mai sus, funcția va returna o pereche de valori întregi, cu una mai mare decât parametrii săi.
Valorile multiple returnate de funcții sunt atribuite variabilelor prin enumerarea lor separate prin virgule, în timp ce numărul de variabile cărora le este atribuit rezultatul apelului de funcție trebuie să se potrivească exact cu numărul de valori returnate de funcție:
first , second := incTwo ( 1 , 2 ) // first = 2, second = 3 first := incTwo ( 1 , 2 ) // WRONG - nicio variabilă atribuită celui de-al doilea rezultatSpre deosebire de Pascal și C, unde declararea unei variabile locale fără a o folosi mai târziu, sau pierderea valorii unei variabile locale (atunci când valoarea atribuită variabilei nu este citită nicăieri) poate provoca doar un avertisment al compilatorului, în Go această situație este considerată o eroare de limbaj si duce la imposibilitatea compilarii programului. Aceasta înseamnă, în special, că programatorul nu poate ignora valoarea (sau una dintre valori) returnată de funcție, pur și simplu atribuind-o unei variabile și refuzând să o folosească în continuare. Dacă devine necesară ignorarea uneia dintre valorile returnate de un apel de funcție, se folosește o pseudovariabilă predefinită numită „_” (o liniuță de subliniere). Poate fi specificat oriunde unde ar trebui să existe o variabilă care ia o valoare. Valoarea corespunzătoare nu va fi atribuită nici unei variabile și pur și simplu va fi pierdută. Sensul unei astfel de decizii arhitecturale este de a identifica în etapa de compilare o posibilă pierdere a rezultatelor calculului: o omisiune accidentală a procesării valorii va fi detectată de către compilator, iar utilizarea pseudovariabilei „_” va indica că programatorul a ignorat în mod deliberat rezultatele. În exemplul următor, dacă este necesară doar una dintre cele două valori returnate de funcția incTwo, trebuie specificat „_” în locul celei de-a doua variabile:
first := incTwo ( 1 , 2 ) // INVALID primul , _ := incTwo ( 1 , 2 ) // TRUE, al doilea rezultat nu este folositVariabila „_” poate fi specificată în lista de atribuiri de orice număr de ori. Toate rezultatele funcției care se potrivesc cu „_” vor fi ignorate.
Apelul amânat înlocuiește mai multe caracteristici sintactice simultan, în special, tratatorii de excepții și blocurile de completare garantată. Un apel de funcție precedat de cuvântul cheie defer este parametrizat în punctul din program în care este plasat și este executat imediat înainte ca programul să iasă din domeniul în care a fost declarat, indiferent de cum și din ce motiv are loc această ieșire. Dacă o singură funcție conține mai multe declarații de amânare, apelurile corespunzătoare sunt executate secvențial după ce funcția se termină, în ordine inversă. Mai jos este un exemplu de utilizare a amânării ca bloc de finalizare garantat [15] :
// Funcție care copiază fișierul func CopyFile ( dstName , srcName șir ) ( scris int64 , eroare err ) { src , err := os . Open ( srcName ) // Deschideți fișierul sursă dacă err != nil { // Verificați return // Dacă eșuează, returnați cu o eroare } // Dacă ați ajuns aici, fișierul sursă a fost deschis cu succes defer src . Close () // Apel întârziat: src.Close() va fi apelat când CopyFile se finalizează dst , err := os . Creați ( dstName ) // Deschideți fișierul destinație dacă err != nil { // Verificați și returnați la returnarea erorii } defer dst . Close () // Apel întârziat: dst.Close() va fi apelat când CopyFile se finalizează intoarcere._ _ _ Copiați ( dst , src ) // Copiați datele și reveniți din funcție // După ce toate operațiunile vor fi apelate: mai întâi dst.Close(), apoi src.Close() }Spre deosebire de majoritatea limbilor cu sintaxă asemănătoare C, Go nu are paranteze pentru constructele condiționate for, if, switch:
if i >= 0 && i < len ( arr ) { println ( arr [ i ]) } ... for i := 0 ; i < 10 ; i ++ { } }Go folosește o construcție de bucle pentru a organiza toate tipurile de bucle for.
for i < 10 { // buclă cu precondiție, similar cu while în C } pentru i := 0 ; i < 10 ; i ++ { // buclă cu un numărător, similar cu for din C } for { // buclă infinită // Ieșirea din buclă trebuie gestionată manual, // de obicei făcută cu return sau break } for { // buclă cu postcondiție ... // corpul buclei dacă i >= 10 { // ieșire întreruperea condiției } } for i , v := range arr { // parcurge colecția (matrice, felie, afișare) arr // i - index (sau cheie) elementului curent // v - copie a valorii elementului de matrice curent } pentru i := range arr { // parcurge colecția, este folosit doar indexul } pentru _ , v := range arr { // treceți în buclă prin colecție, utilizați numai valorile elementului } for range arr { // Buclă prin colecție fără variabile (colecția este folosită // doar ca contor de iterații). } pentru v := interval c { // buclă prin canal: // v va citi valorile de pe canalul c, // până când canalul este închis de o rutină concomitentă // }Sintaxa operatorului cu variante multiple switchare o serie de caracteristici. În primul rând, spre deosebire de C, nu este necesară utilizarea operatorului break: după ce ramura selectată a fost procesată, execuția operatorului se încheie. Dacă, dimpotrivă, doriți ca următoarea ramură să continue procesarea după ramura selectată, trebuie să utilizați operatorul fallthrough:
comutare valoare { cazul 1 : fmt . Println ( "One" ) fallthrough // În continuare, ramura "case 0:" va fi executată case 0 : fmt . println ( "Zero" ) }Aici, când value==1vor fi afișate două linii, „Unul” și „Zero”.
Expresia de alegere și, în consecință, alternativele din instrucțiunea switch pot fi de orice tip, este posibil să enumerați mai multe opțiuni într-o singură ramură:
comutați caractere [ cod ]. categorie { case "Lu" , "Ll" , "Lt" , "Lm" , "Lo" : ... case "Nd" : ... implicit : ... }Este permisă absența unei expresii de alegere, caz în care condițiile logice trebuie scrise în alternative. Se execută prima ramură, a cărei condiție este adevărată:
comutare { case '0' <= c && c <= '9' : returnează c - '0' case 'a' <= c && c <= 'f' : return c - 'a' + 10 case 'A' <= c && c <= 'F' : returnează c - 'A' + 10 }Un detaliu important: dacă una dintre ramurile cu condiția se termină cu operatorul fallthrough, atunci după această ramură va fi procesată următoarea, indiferent dacă condiția sa este îndeplinită . Dacă doriți ca următoarea ramură să fie procesată numai dacă condiția sa este îndeplinită, trebuie să utilizați construcții secvențiale if.
Limbajul Go nu acceptă sintaxa structurată de gestionare a excepțiilor tipică majorității limbilor moderne , care implică aruncarea excepțiilor cu o comandă specială (de obicei throwsau raise) și gestionarea lor într-un bloc try-catch. În schimb, este recomandat să utilizați returnarea erorii ca unul dintre rezultatele funcției (ceea ce este la îndemână, deoarece în Go o funcție poate returna mai mult de o valoare):
Mulți critici ai limbii cred că această ideologie este mai rea decât gestionarea excepțiilor, deoarece numeroase verificări aglomera codul și nu permit ca toate gestionarea erorilor să fie concentrată în blocuri catch. Creatorii limbii nu consideră aceasta o problemă serioasă. Sunt descrise o serie de modele de tratare a erorilor în Go (vezi, de exemplu, articolul lui Rob Pike pe blogul oficial Go , traducere în limba rusă ), care pot reduce cantitatea de coduri de tratare a erorilor.
Când apar erori fatale care fac imposibilă executarea ulterioară a programului (de exemplu, împărțirea la zero sau accesarea limitelor matricei), apare o stare de panică , care în mod implicit duce la prăbușirea programului cu un mesaj de eroare și o urmărire a stivei de apeluri. Panicile pot fi prinse și gestionate folosind constructul de execuție amânată deferdescris mai sus. Apelul funcției specificat în deferse face înainte de a părăsi domeniul curent, inclusiv în caz de panică. În interiorul funcției numite în defer, puteți apela o funcție standard recover() - oprește procesarea sistemului de panică și returnează cauza acesteia sub forma unui obiect errorcare poate fi procesat ca o eroare normală. Dar programatorul poate relua și o panică prinsă anterior apelând standardul panic(err error).
// Programul efectuează o împărțire întreg // a primului său parametru cu al doilea // și tipărește rezultatul. func main () { defer func () { err := recover () if v , ok := err .( error ); ok { // Gestionarea unei panică corespunzătoare erorii de interfață fmt . Fprintf ( os . Stderr , "Eroare %v \"%s\"\n" , err , v . Eroare ()) } else if err != nil { panic ( err ) // Gestionarea erorilor neașteptate - ridicați din nou panică. } }() a , err := strconv . ParseInt ( os . Args [ 1 ], 10 , 64 ) if err != nil { panic ( err ) } b , err := strconv . ParseInt ( os . Args [ 2 ], 10 , 64 ) if err != nil { panic ( err ) } fmt . fprintf ( os . Stdout , "%d / %d = %d\n" , a , b , a / b ) }În exemplul de mai sus, pot apărea erori atunci când argumentele programului sunt convertite în numere întregi de către funcția strconv.ParseInt(). De asemenea, este posibil să intrați în panică la accesarea matricei os.Args cu un număr insuficient de argumente sau la împărțirea la zero dacă al doilea parametru este zero. Pentru orice situație de eroare, se generează o panică, care este procesată în apel defer:
> împărțiți 10 5 10 / 5 = 2 > împărțiți 10 0 Eroare runtime.errorString „eroare de rulare: împărțire întreg la zero” > împărțiți 10,5 2 Eroare *strconv.NumError „strconv.ParseInt: analizarea „10.5”: sintaxă nevalidă” > împarte 10 Eroare runtime.errorString „eroare de rulare: index în afara intervalului”O panică nu poate fi declanșată într-o rutină de execuție paralelă (vezi mai jos), ci gestionată în alta. De asemenea, nu este recomandat să „treceți” panica peste granița unui pachet.
Modelul de threading al lui Go a fost moștenit din limbajul Active Oberon , bazat pe CSP -ul lui Tony Hoare , folosind idei din limbajele Occam și Limbo [9] , dar sunt prezente și caracteristici precum pi-calculus și channeling.
Go vă permite să creați un nou fir de execuție a programului folosind cuvântul cheie go , care rulează o funcție anonimă sau numită într-o goroutine nou creată (termenul Go pentru corutine ). Toate goroutinele din cadrul aceluiași proces utilizează un spațiu de adresă comun, care se execută pe firele de execuție a sistemului de operare , dar fără a leagă greu la acestea din urmă, ceea ce permite unei goroutine care rulează să părăsească un fir cu o goroutine blocată (în așteptarea, de exemplu, să trimită sau să primească un mesaj de pe un canal) și continuați. Biblioteca runtime include un multiplexor pentru a partaja numărul disponibil de nuclee de sistem între goroutine. Este posibil să se limiteze numărul maxim de nuclee de procesor fizic pe care va fi executat programul. Auto-suportarea goroutinelor de către biblioteca Go runtime facilitează utilizarea unui număr mare de goroutine în programe, depășind cu mult limita numărului de fire de execuție acceptate de sistem.
func server ( i int ) { pentru { print ( i ) time . Sleep ( 10 ) } } go server ( 1 ) go server ( 2 )Închiderile pot fi folosite într-o expresie go .
var g int go func ( i int ) { s := 0 pentru j := 0 ; j < i ; j ++ { s += j } g = s }( 1000 )Pentru comunicarea între goroutine se folosesc canale (de tip încorporat chan ), prin care poate fi trecută orice valoare. Un canal este creat de funcția încorporată make(), căreia îi este transmis tipul și (opțional) volumul canalului. În mod implicit, volumul canalului este zero. Astfel de canale sunt nebufferizate . Puteți seta orice volum întreg pozitiv al canalului, apoi va fi creat un canal tamponat .
O țeavă fără tampon sincronizează strâns firele de citire și scriere care o folosesc. Când un thread de scriitor scrie ceva într-o conductă, se întrerupe și așteaptă până când valoarea a fost citită. Când un thread de citire încearcă să citească ceva dintr-o conductă în care a fost deja scris, citește valoarea și ambele fire pot continua să se execute. Dacă nicio valoare nu a fost încă scrisă pe canal, firul de citire se întrerupe și așteaptă ca cineva să scrie pe canal. Adică, conductele fără tampon din Go se comportă la fel ca și conductele din Occam sau mecanismul de întâlnire în limbajul Ada .
Un canal tamponat are un buffer de valoare a cărui dimensiune este egală cu dimensiunea canalului. Când scrieți într-o astfel de conductă, valoarea este plasată în buffer-ul conductei, iar firul de scriere continuă fără pauză, cu excepția cazului în care bufferul conductei este plin în momentul scrierii. Dacă tamponul este plin, atunci firul de scriere este suspendat până când cel puțin o valoare a fost citită de pe canal. Firul de citire citește și o valoare dintr-o țeavă tamponată fără pauză dacă există valori necitite în țeavă tampon; dacă buffer-ul canalului este gol, atunci firul se întrerupe și așteaptă până când un alt thread îi scrie o valoare.
Când ați terminat, canalul poate fi închis cu funcția încorporată close(). O încercare de a scrie pe un canal privat duce la o panică, citirea de pe un canal privat are loc întotdeauna fără întrerupere și citește valoarea implicită. Dacă canalul este în buffer și la momentul închiderii conține N valori scrise anterior în buffer, atunci primele N operații de citire vor fi efectuate ca și cum canalul ar fi încă deschis și vor citi valorile din buffer și numai după aceea citirea de pe canal va returna valorile implicite.
Operația este folosită pentru a transmite o valoare către și de la un canal <-. Când scrieți pe un canal, este folosit ca operator binar, când citiți - ca operator unar:
in := make ( chan string , 0 ) // Creați un canal nebuffered in out := make ( chan int , 10 ) // Creați un canal tamponat out ... in <- arg // Scrieți o valoare canalului în ... r1 := <- out // citire din canal out ... r2 , ok := <- out // citire cu verificarea daca canalul este inchis if ok { // if ok == true - canalul este deschis ... } else { // dacă canalul este închis, faceți altceva ... }Operația de citire dintr-un canal are două opțiuni: fără verificare și cu verificare pentru închiderea canalului. Prima opțiune (citirea r1 în exemplul de mai sus) citește pur și simplu următoarea valoare în variabilă; dacă canalul este închis, valoarea implicită va fi citită în r1. A doua opțiune (citirea r2) citește, în plus față de valoare, o valoare booleană - indicatorul de stare al canalului ok, care va fi adevărat dacă datele plasate acolo de orice flux au fost citite de pe canal și false dacă canalul este închis și tamponul său este gol. Cu această operație, firul de citire poate determina când canalul de intrare este închis.
Citirea dintr-o țeavă este, de asemenea, acceptată folosind construcția buclei for-range:
// Funcția începe citirea paralelă de pe canalul de intrare în numere întregi și scrierea // în canalul de ieșire numai a acelor întregi care sunt pozitive. // Returnează canalul de ieșire. func pozitive ( in < -chan int64 ) < -chan int64 { out := make ( chan int64 ) go func () { // Bucla va continua până când canalul in este închis pentru următorul := interval în { if next > 0 { ieși <- următorul } } închide ( ieși ) } () întoarce afară }În plus față de CSP sau împreună cu mecanismul de canalizare, Go vă permite, de asemenea, să utilizați modelul obișnuit de interacțiune sincronizată a firelor de execuție prin intermediul memoriei partajate, folosind instrumente tipice de sincronizare a accesului, cum ar fi mutexurile . În același timp, însă, specificația limbajului avertizează împotriva oricăror încercări de interacțiune nesincronizată a firelor paralele prin intermediul memoriei partajate, deoarece în absența sincronizării explicite, compilatorul optimizează codul de acces la date fără a ține cont de posibilitatea accesului simultan de la diferite fire, care pot duce la erori neașteptate. De exemplu, scrierea valorilor în variabilele globale într-un fir poate să nu fie vizibilă sau vizibilă în ordine greșită dintr-un fir paralel.
De exemplu, luați în considerare programul de mai jos. Codul funcției este main()scris pe presupunerea că funcția lansată în goroutine setup()va crea o structură de tip T, o va inițializa cu șirul „hello, world” și apoi va atribui o referință la structura inițializată variabilei globale g. B main()pornește o buclă goală, așteptând gsă apară o valoare diferită de zero. Imediat ce apare, main()tipărește un șir din structura indicată g, presupunând că structura a fost deja inițializată.
tip T struct { șir mesaj } varg * T _ func setup () { t : = new ( T ) t . msg = "bună ziua, lume" g = t } func main () { go setup () for g == nil { // NU FUNCTIONEAZA!!! } print ( g . msg ) }În realitate, una dintre cele două erori este posibilă.
Singura modalitate corectă de organizare a transferului de date prin memoria partajată este utilizarea instrumentelor de sincronizare a bibliotecii, care garantează că toate datele scrise de unul dintre fluxurile sincronizate înainte de punctul de sincronizare sunt garantate a fi disponibile într-un alt flux sincronizat după punctul de sincronizare.
O caracteristică a multithreading-ului în Go este că o goroutine nu este identificată în niciun fel și nu este un obiect limbaj la care se poate face referire atunci când se apelează funcții sau care poate fi plasat într-un container. În consecință, nu există mijloace care să vă permită să influențați direct execuția unei corutine din exteriorul acesteia, cum ar fi suspendarea și apoi pornirea acesteia, schimbarea priorității, așteptarea finalizării unei corutine în alta și întreruperea forțată a execuției. Orice acțiune asupra unui goroutine (altul decât terminarea programului principal, care termină automat toate goroutinele) poate fi făcută numai prin conducte sau alte mecanisme de sincronizare. Următorul este exemplu de cod care pornește mai multe goroutine și așteaptă ca acestea să se finalizeze folosind obiectul de sincronizare WaitGroup din pachetul de sistem de sincronizare. Acest obiect conține un contor, inițial zero, care poate crește și decrementa, și o metodă Wait(), care face ca firul de execuție curent să se întrerupă și să aștepte până când contorul este resetat la zero.
func main () { var wg sync . WaitGroup // Creați un grup de așteptare. Valoarea inițială a contorului este 0 logger := log . Nou ( os . Stdout , "" , 0 ) // log.Logger este un tip de ieșire thread-safe pentru _ , arg := range os . Args { // Buclă prin toate argumentele liniei de comandă wg . Adăugați ( 1 ) // Creșteți contorul grupului de așteptare cu unul // Rulați o rutină pentru a procesa parametrul arg go func ( șir de cuvinte ) { // Scăderea întârziată a contorului grupului de așteptare cu una. // Se întâmplă când funcția se termină. amână wg . Terminat () logger . Println ( prepareWord ( cuvânt )) // Efectuați procesarea și imprimați rezultatul }( arg ) } wg . Wait () // Așteptați până când contorul din waitgroup wg este zero. }Aici, înainte de crearea fiecărei noi gorutine, contorul obiectului wg este incrementat cu unu, iar după finalizarea gorutinei, acesta este decrementat cu unu. Ca urmare, în bucla care începe procesarea argumentelor, la contor vor fi adăugate atâtea unități câte goroutine sunt lansate. Când bucla se termină, apelarea wg.Wait() va face ca programul principal să se întrerupă. Pe măsură ce fiecare dintre goroutine se termină, decrește contorul wg cu una, astfel încât așteptarea programului principal se va încheia când se vor termina atâtea goroutine câte rula. Fără ultima linie, programul principal, după ce a rulat toate goroutinele, ar ieși imediat, întrerupând execuția celor care nu au avut timp să se execute.
În ciuda prezenței multithreadingului încorporat în limbaj, nu toate obiectele limbajului standard sunt sigure pentru fire. Deci, tipul de hartă standard (afișajul) nu este sigur pentru fire. Creatorii limbajului au explicat această decizie cu considerente de eficiență, deoarece asigurarea securității pentru toate astfel de obiecte ar duce la supraîncărcare suplimentară, ceea ce este departe de a fi întotdeauna necesar (aceleași operațiuni cu mapări pot face parte din operațiuni mai mari care sunt deja sincronizate de programator). , iar apoi sincronizarea suplimentară nu va face decât să complice și să încetinească programul). Începând cu versiunea 1.9, pachetul de bibliotecă de sincronizare, care conține suport de procesare paralelă, a adăugat tipul de sincronizare sigură pentru fire.Map, care poate fi folosit dacă este necesar. De asemenea, puteți acorda atenție tipului thread-safe utilizat în exemplu pentru a afișa rezultatele log.Logger; este folosit în locul pachetului fmt standard, ale cărui funcții (Printf, Println și așa mai departe) nu sunt sigure pentru fire și ar necesita sincronizare suplimentară.
Nu există un cuvânt cheie special pentru declararea unei clase în Go, dar metodele pot fi definite pentru orice tip numit, inclusiv structuri și tipuri de bază precum int , deci în sensul OOP toate astfel de tipuri sunt clase.
tastați newInt intSintaxa definiției metodei este împrumutată din limbajul Oberon-2 și diferă de definiția obișnuită a funcției prin aceea că după cuvântul cheie func, așa-numitul „receptor” ( în engleză receiver ) este declarat în paranteze , adică obiectul pentru care este numită metoda și tipul căruia îi aparține metoda. În timp ce în limbajele obiect tradiționale receptorul este implicit și are un nume standard (în C++ sau Java este „acest lucru”, în ObjectPascal este „self”, etc.), în Go este specificat în mod explicit și numele său poate fi orice identificator Go valid.
type myType struct { i int } // Aici p este receptorul în metodele de tip myType. func ( p * myType ) get () int { return p . i } func ( p * myType ) set ( i int ) { p . i = i }Nu există o moștenire formală a claselor (structurilor) în Go, dar există un mecanism de încorporare apropiat din punct de vedere tehnic . În descrierea structurii, puteți utiliza așa-numitul câmp anonim - un câmp pentru care nu este indicat un nume, ci doar un tip. Ca rezultat al unei astfel de descrieri, toate elementele structurii încorporate vor deveni elementele cu același nume ale structurii încorporate.
// Nou tip de struct tip myType2 struct { myType // Câmpul anonim oferă încorporarea tipului myType. // Acum myType2 conține câmpul i și metodele get() și set(int). k int }Spre deosebire de moștenirea clasică, integrarea nu implică un comportament polimorf (un obiect al unei clase de încorporare nu poate acționa ca obiect al unei clase încorporabile fără conversie explicită de tip).
Nu este posibil să declarați în mod explicit metode pentru un tip fără nume (sintaxa pur și simplu nu vă permite să specificați tipul receptorului în metodă), dar această limitare poate fi ocolită cu ușurință prin introducerea tipului numit cu metodele necesare.
Polimorfismul clasei este furnizat în Go prin mecanismul interfețelor (similar cu clasele complet abstracte în C++ ). O interfață este descrisă folosind cuvântul cheie interfață; în interiorul (spre deosebire de declarațiile de tip clasă) descrierile declară metodele furnizate de interfață.
tastați interfața myInterface { get () int set ( i int ) }În Go, nu este necesar să se precizeze în mod explicit că un tip implementează o anumită interfață. În schimb, regula este că fiecare tip care oferă metode definite într-o interfață poate fi folosit ca implementare a acelei interfețe. Tipul declarat mai sus myTypeimplementează interfața myInterface, deși acest lucru nu este menționat în mod explicit nicăieri, deoarece conține metode get()și set(), ale căror semnături se potrivesc cu cele descrise în myInterface.
La fel ca și clasele, interfețele pot fi aliniate:
tastați mySecondInterface interfață { myInterface // la fel ca și declarați explicit get() int; set(i int) schimbare ( i int ) int }Aici, interfața mySecondInterface moștenește interfața myInterface (adică declară că expune metodele incluse în myInterface) și în plus declară o metodă nativă change().
Deși este posibil, în principiu, să construiți un program Go într-o ierarhie de interfețe, așa cum se face în alte limbaje obiect, și chiar să simulați moștenirea, acest lucru este considerat o practică proastă. Limbajul dictează nu o abordare ierarhică, ci o compozițională a sistemului de clase și interfețe. Clasele de structură cu această abordare pot rămâne în general independente din punct de vedere formal, iar interfețele nu sunt combinate într-o singură ierarhie, ci sunt create pentru aplicații specifice, dacă este necesar, înglobând pe cele existente. Implementarea implicită a interfețelor în Go oferă acestor mecanisme o flexibilitate extremă și un minim de dificultăți tehnice în utilizarea lor.
Această abordare a moștenirii este în conformitate cu unele tendințe practice în programarea modernă. Deci, în celebra carte „gașca celor patru” ( Erich Gamma și alții) despre modelele de design , în special, este scris:
Dependența de implementare poate cauza probleme atunci când se încearcă reutilizarea unei subclase. Dacă chiar și un aspect al implementării moștenite este nepotrivit pentru noul domeniu, atunci clasa părinte trebuie rescrisă sau înlocuită cu ceva mai adecvat. Această dependență limitează flexibilitatea și reutilizarea. Problema poate fi depășită doar prin moștenirea din clase abstracte, deoarece acestea de obicei nu au implementare sau o implementare minimă.
Nu există un concept de funcție virtuală în Go . Polimorfismul este asigurat de interfețe. Dacă o variabilă de tip obișnuit este folosită pentru a apela o metodă, atunci un astfel de apel este legat static, adică metoda definită pentru acest tip particular este întotdeauna apelată. Dacă metoda este apelată pentru o variabilă de tip „interfață”, atunci un astfel de apel este legat dinamic, iar în momentul execuției, varianta de metodă care este definită pentru tipul obiectului efectiv alocat în momentul apelării acestuia. variabila este selectată pentru lansare.
Suportul dinamic pentru programarea orientată pe obiecte pentru Go este oferit de proiectul GOOP .
Capacitatea de a introspecta în timpul execuției, adică accesul și procesarea valorilor de orice tip și ajustarea dinamică a tipurilor de date procesate, este implementată în Go folosind pachetul de sistem reflect. Acest pachet vă permite să:
Pachetul reflectconține și multe instrumente auxiliare pentru efectuarea operațiunilor în funcție de starea dinamică a programului.
Facilități de acces la memorie de nivel scăzut sunt concentrate în pachetul de sistem unsafe. Particularitatea sa este că, deși arată ca un pachet Go obișnuit, este implementat de compilator însuși. Pachetul unsafeoferă acces la reprezentarea internă a datelor și la pointerii de memorie „reali”. Oferă caracteristici:
Pachetul oferă, de asemenea, un tip unsafe.Pointercare poate fi convertit la orice pointer și poate fi convertit într-un pointer de orice tip, precum și un tip standard uintptr , o valoare întreagă fără semn suficient de mare pentru a stoca o adresă completă pe platforma curentă. Prin conversia indicatorului în unsafe.Pointerși apoi în uintptr, puteți obține adresa ca un întreg, căruia îi puteți aplica operații aritmetice. Prin convertirea valorii înapoi la unsafe.Pointerși într-un pointer către orice tip anume, puteți accesa aproape oriunde în spațiul de adrese în acest fel.
Transformările descrise pot fi nesigure, așa că se recomandă să fie evitate dacă este posibil. În primul rând, există probleme evidente asociate cu accesul eronat la zona de memorie greșită. Un aspect mai subtil este că, în ciuda utilizării pachetului unsafe, obiectele Go continuă să fie gestionate de managerul de memorie și de colectorul de gunoi. Convertirea unui pointer într-un număr scapă de sub control acel pointer, iar programatorul nu se poate aștepta ca un astfel de pointer convertit să rămână relevant pe termen nelimitat. De exemplu, încercarea de a stoca un pointer către un obiect de tip nou, Тastfel:
pT := uintptr ( unsafe . Pointer ( new ( T ))) // GREȘIT!va face ca obiectul să fie creat, indicatorul către acesta convertit într-un număr (care va fi atribuit pT). Cu toate acestea, pTare un tip întreg și nu este considerat de către colectorul de gunoi ca fiind un pointer către un obiect creat, așa că după finalizarea operațiunii, sistemul de management al memoriei va considera acest obiect neutilizat. Adică, poate fi eliminat de către colectorul de gunoi, după care pointerul convertit pTva deveni invalid. Acest lucru se poate întâmpla în orice moment, atât imediat după finalizarea operațiunii, cât și după multe ore de funcționare a programului, astfel încât eroarea se va exprima în blocări aleatorii ale programului, a căror cauză va fi extrem de greu de identificat. Și atunci când utilizați un colector de gunoi în mișcare [* 1] , indicatorul convertit într-un număr poate deveni irelevant chiar și atunci când obiectul nu a fost încă eliminat din memorie.
Deoarece specificația Go nu oferă indicații precise cu privire la măsura în care un programator se poate aștepta să țină actualizat un pointer convertit într-un număr, există o recomandare: să mențineți astfel de conversii la minimum și să le organizați astfel încât conversia de pointerul original, modificările sale și conversia înapoi sunt într-o singură instrucțiune de limbă, iar atunci când apelați orice funcții de bibliotecă care returnează o adresă sub forma uintptr, convertiți imediat rezultatul lor în pentru unsafe.Pointera păstra garanția că pointerul nu va fi pierdut.
Pachetul unsafeeste rareori folosit direct în programarea aplicațiilor, dar este folosit în mod activ în pachetele reflect, os, syscall, contextși netaltele.
Există mai multe instrumente externe care oferă interfețe cu funcție străină (FFI) pentru programele Go. Utilitarul cgo poate fi folosit pentru a interacționa cu codul C extern (sau având o interfață compatibilă cu C) . Este apelat automat atunci când compilatorul procesează un modul Go scris corect și asigură crearea unui pachet temporar Go wrapper care conține declarații de toate tipurile și funcțiile necesare. În apelurile de funcții C, de multe ori trebuie să recurgeți la facilități de pachet , folosind în principal . Un instrument mai puternic este SWIG [16] , care oferă caracteristici mai avansate, cum ar fi integrarea cu clasele C++ . unsafeunsafe.Pointer
Biblioteca standard Go acceptă crearea de aplicații de consolă și aplicații de server bazate pe web , dar nu există instrumente standard pentru crearea interfețelor grafice în aplicațiile client. Acest decalaj este umplut de wrapper-uri terță parte pentru cadrele UI populare, cum ar fi GTK+ și Qt , sub Windows puteți utiliza instrumentele grafice WinAPI accesându-le prin pachet syscall, dar toate aceste metode sunt destul de greoaie. Există, de asemenea, mai multe dezvoltări ale cadrelor UI în Go însuși, dar niciunul dintre aceste proiecte nu a atins nivelul de aplicabilitate industrială. În 2015, la conferința GopherCon de la Denver , unul dintre creatorii limbajului, Robert Grismer, a răspuns la întrebări, a fost de acord că Go are nevoie de un pachet UI, dar a remarcat că un astfel de pachet ar trebui să fie universal, puternic și multi-platformă, ceea ce îi face dezvoltare lungă şi dificilă.proces. Problema implementării unui client GUI în Go este încă deschisă.
Datorită tinereții limbii, criticile sale se concentrează în principal în articole, recenzii și forumuri de pe Internet.
O mare parte din criticile la adresa limbii se concentrează pe lipsa anumitor caracteristici populare oferite de alte limbi. Printre ei [17] [18] [19] [20] :
După cum sa menționat mai sus, lipsa unui număr de funcții disponibile în alte limbi populare se datorează alegerii conștiente a dezvoltatorilor, care cred că astfel de caracteristici fie împiedică compilarea eficientă, fie provoacă programatorul să facă greșeli sau creează ineficiente sau „rău” în ceea ce privește întreținerea codului sau au alte reacții adverse nedorite.
Criticii subliniază că unele caracteristici ale Go sunt implementate în ceea ce privește cea mai simplă sau mai eficientă implementare, dar nu îndeplinesc „ principiul celei mai mici surprize ”: comportamentul lor diferă de ceea ce s-ar aștepta programatorul pe baza intuiției și experienței anterioare. Astfel de caracteristici necesită o atenție sporită a programatorului, fac dificilă învățarea și trecerea de la alte limbi.
Deseori criticat este mecanismul punctului și virgulă automat, din cauza căruia unele forme de scriere a declarațiilor, apelurilor de funcții și listelor devin incorecte. Comentând această decizie, autorii limbajului notează [9] că, împreună cu prezența unui formatator de cod în trusa oficială de instrumente, gofmta condus la fixarea unui standard destul de rigid de codare în Go. Cu greu este posibil să se creeze un standard de scriere a codului care să se potrivească tuturor; introducerea în limbaj a unei caracteristici care în sine stabilește un astfel de standard, unifică aspectul programelor și elimină conflictele neprincipiale din cauza formatării, care este un factor pozitiv pentru dezvoltarea și întreținerea de grup a software-ului.
Popularitatea lui Go a crescut în ultimii ani: din 2014 până în 2020, Go a urcat de pe locul 65 pe locul 11 în clasamentul TIOBE , valoarea ratingului pentru august 2020 fiind de 1,43%. Conform rezultatelor unui sondaj dou.ua [22] , limba Go în 2018 a devenit al nouălea în lista celor mai utilizate și al șaselea în lista limbilor cărora dezvoltatorii le acordă preferințe personale.
De la prima lansare publică în 2012, utilizarea limbii a crescut constant. Lista companiilor care folosesc limbajul în dezvoltarea industrială publicată pe site-ul proiectului Go include câteva zeci de nume. S-a acumulat o gamă largă de biblioteci pentru diverse scopuri. Lansarea versiunii 2.0 a fost planificată pentru 2019, dar lucrările au fost amânate și sunt încă în curs pentru a doua jumătate a anului 2022. Se așteaptă să apară o serie de caracteristici noi , inclusiv generice și sintaxă specială pentru a simplifica gestionarea erorilor, a căror absență este una dintre cele mai frecvente plângeri ale criticilor limbajului .
Serverul web RoadRunner (server de aplicații) a fost dezvoltat în Golang , ceea ce permite aplicațiilor web să atingă o viteză de solicitare-răspuns de 10-20 ms în loc de tradiționala 200 ms. Acest serviciu web este planificat să fie inclus în cadre populare, cum ar fi Yii .
Alături de C++ , Golang este folosit pentru a dezvolta microservicii, ceea ce vă permite să „încărcați” platformele cu mai multe procesoare cu lucru. Puteți interacționa cu un microserviciu folosind REST , iar limbajul PHP este excelent pentru asta.
Spiral Framework a fost dezvoltat cu ajutorul PHP și Golang. [23]
Există o singură versiune majoră a limbajului Go în sine, versiunea 1. Versiunile mediului de dezvoltare Go (compilator, instrumente și biblioteci standard) sunt numerotate fie din două cifre ("<versiune de limbă>.<versiune majoră>"), fie trei cifre ("<versiune lingvistică>.< lansare majoră>.<versiune minoră>") către sistem. Lansarea unei noi versiuni „din două cifre” încetează automat suportul pentru versiunea anterioară „din două cifre”. Versiunile „cu trei cifre” sunt lansate pentru a remedia erorile raportate și problemele de securitate; corecțiile de securitate din astfel de versiuni pot afecta ultimele două versiuni „cu două cifre” [24] .
Autorii au declarat [25] dorința de a păstra, pe cât posibil, retrocompatibilitatea în cadrul versiunii principale a limbajului. Aceasta înseamnă că înainte de lansarea Go 2, aproape orice program creat în mediul Go 1 se va compila corect în orice versiune ulterioară a Go 1.x și va rula fără erori. Excepții sunt posibile, dar sunt puține. Cu toate acestea, compatibilitatea binară între versiuni nu este garantată, așa că un program trebuie să fie complet recompilat atunci când se trece la o versiune ulterioară a Go.
Din martie 2012, când a fost introdus Go 1, au fost lansate următoarele versiuni majore:
Progres de dezvoltare Din 2017, au fost în curs de pregătire pentru lansarea următoarei versiuni de bază a limbajului, care are simbolul „Go 2.0” [26] . Culegere de comentarii la versiunea actuală și propuneri de transformări, acumulate pe site-ul wiki al proiectului [27] . Inițial, s-a presupus că procesul de pregătire va dura „aproximativ doi ani”, iar unele dintre noile elemente ale limbajului vor fi incluse în următoarele versiuni ale versiunii Go 1 (desigur, doar cele care nu încalcă compatibilitatea cu versiunea anterioară). ). [26] Din aprilie 2021, versiunea 2.0 nu este încă gata, unele dintre modificările planificate sunt în faza de proiectare și implementare. Conform planurilor prezentate în blogul proiectului [28] , lucrările privind implementarea modificărilor planificate vor continua cel puțin în 2021. Inovații sugerate Printre inovațiile fundamentale se numără valorile constante declarate în mod explicit, un nou mecanism de gestionare a erorilor și instrumente de programare generice. Proiectele de inovație sunt disponibile online. Pe 28 august 2018, pe blogul oficial pentru dezvoltatori a fost publicat un videoclip prezentat anterior la conferința Gophercon 2018 , care demonstrează versiunile preliminare ale noului design de gestionare a erorilor și mecanismul funcțiilor generice. Există, de asemenea, multe modificări mai puțin vizibile, dar foarte semnificative planificate, [29] cum ar fi extinderea regulilor de admisibilitate a caracterelor pentru identificatorii în alfabete non-latine, permițând operațiuni de schimbare pentru numere întregi cu semn, folosind liniuța de subliniere ca separator de grupuri de mii în numere, literale binare. Cele mai multe dintre ele sunt deja implementate și disponibile în cele mai recente versiuni ale Go 1. Eroare la procesare Au fost luate în considerare mai multe opțiuni pentru modificarea mecanismului de gestionare a erorilor, în special, un design cu un handler separat de erori („ Manipularea erorilor - Proiectare de proiect ”). Ultima variantă pentru iulie 2019 este descrisă în articolul „ Propunere: O funcție încorporată de verificare a erorilor Go, încercați ”. Această opțiune este cea mai minimalistă și implică adăugarea unei singure funcții încorporate try()care procesează rezultatul unui apel de funcție. Utilizarea sa este ilustrată de pseudocodul de mai jos. func f ( … )( r1 type_1 , … , rn type_n , err error ) { // Funcție testată // Returnează n+1 rezultate: r1... rn, err of type error. } func g ( … )( … , err error ) { // Apelați funcția f() cu verificarea erorilor: … x1 , x2 , … xn = try ( f ( … )) // Folosiți încorporat try: // if f( ) returnat non-nul în ultimul rezultat, apoi g() se va termina automat, // returnând aceeași valoare în ultimul rezultat ITS. … } func t ( … )( … , err error ) { // Asemănător cu g() fără a utiliza noua sintaxă: t1 , t2 , … tn , te := f ( … ) // Apelați f() cu rezultatele stocate în temporar variabile. if te != nil { // Verificați codul de returnare pentru egalitate nil err = te // Dacă codul de returnare nu este zero, atunci este scris în ultimul rezultat al lui t(), return // după care t() se termină imediat. } // Dacă nu a existat nicio eroare, x1 , x2 , … xn = t1 , t2 , … tn // … variabilele x1…xn își primesc valorile // și execuția t() continuă. … } Adică, try()furnizează pur și simplu o verificare a erorilor în apelul la funcția care este verificată și o întoarcere imediată de la funcția curentă cu aceeași eroare. Puteți utiliza mecanismul pentru a gestiona o eroare înainte de a reveni de la funcția curentă defer. Utilizarea try()necesită ca atât funcția care este verificată, cât și funcția în care este apelată să aibă ultima valoare returnată de tip error. Prin urmare, de exemplu, nu puteți main()utiliza try(); la nivelul superior, toate erorile trebuie tratate în mod explicit. Acest mecanism de gestionare a erorilor trebuia să fie inclus în Go 1.14 , dar acest lucru nu a fost făcut. Datele de implementare nu sunt specificate. Cod generic La sfârșitul anului 2018, a fost prezentat un proiect de implementare a tipurilor și funcțiilor generice în Go [30] . La 9 septembrie 2020, a fost publicat un proiect revizuit [31] în care funcțiile, tipurile și parametrii funcției pot fi parametrizați prin tipuri de parametri , care, la rândul lor, sunt controlați de constrângeri . // Stringer este o interfață de constrângere care necesită tipul pentru a implementa // o metodă String care returnează o valoare șir. tip Stringer interface { String () string } // Funcția primește ca intrare un tablou de valori de orice tip care implementează metoda String și returnează // tabloul corespunzătoare de șiruri de caractere obținute prin apelarea metodei String pentru fiecare element al tabloului de intrare. func Stringify [ T Stringer ] ( s [] T ) [] string { // parametrul de tip T, supus constrângerii Stringer, // este tipul de valoare al parametrului de matrice s. ret = make ([] string , len ( s )) for i , v := range s { ret [ i ] = v . String () } return ret } ... v := make ([] MyType ) ... // Pentru a apela o funcție generică, trebuie să specificați un anumit tip s := Stringify [ String ]( v ) Aici, funcția Stringifyconține un parametru de tip T, care este utilizat în descrierea unui parametru obișnuit s. Pentru a apela o astfel de funcție, așa cum se arată în exemplu, trebuie să specificați în apel tipul specific pentru care este apelată. Stringerîn această descriere, este o constrângere care necesită ca MyType să implementeze o Stringmetodă fără parametri care returnează o valoare șir. Acest lucru permite compilatorului să proceseze corect expresia " " v.String(). Implementarea codului generic este anunțată în versiunea 1.18, planificată pentru august 2021. [28]
În prezent, există două compilatoare principale Go:
Există și proiecte:
Mediul de dezvoltare Go conține mai multe instrumente de linie de comandă: utilitarul go, care asigură compilarea, testarea și gestionarea pachetelor și utilitarele auxiliare godoc și gofmt, concepute pentru a documenta programe și, respectiv, a format codul sursă conform regulilor standard. Pentru a afișa o listă completă de instrumente, trebuie să apelați utilitarul go fără a specifica argumente. Depanatorul gdb poate fi folosit pentru a depana programe. Dezvoltatorii independenți oferă un număr mare de instrumente și biblioteci concepute pentru a sprijini procesul de dezvoltare, în principal pentru a facilita analiza codului, testarea și depanarea.
În prezent, sunt disponibile două IDE-uri care sunt concentrate inițial pe limbajul Go - acesta este proprietarul GoLand [1] (dezvoltat de JetBrains pe platforma IntelliJ) și LiteIDE gratuit [2] (anterior proiectul se numea GoLangIDE). LiteIDE este un mic shell scris în C++ folosind Qt . Vă permite să compilați, să depanați, să formatați codul, să rulați instrumente. Editorul acceptă evidențierea sintaxei și completarea automată.
Go este susținut și de pluginuri în IDE-urile universale Eclipse, NetBeans, IntelliJ, Komodo, CodeBox IDE, Visual Studio, Zeus și altele. Evidențierea automată, completarea automată a codului Go și rularea utilităților de compilare și procesare a codului sunt implementate ca plugin-uri pentru mai mult de două duzini de editori de text obișnuiți pentru diverse platforme, inclusiv Emacs, Vim, Notepad++, jEdit.
Mai jos este un exemplu de „Hello, World!” în limba Go.
pachet principal import "fmt" func main () { fmt . println ( "Bună ziua, lume!" ) }Un exemplu de implementare a comenzii Unix echo :
pachet principal import ( "os" "flag" // parser linie de comandă ) var omitNewLine = steag . Bool ( "n" , false , "nu tipăriți noua linie" ) const ( Space = " " NewLine = "\n" ) func main () { steag . Parse () // Scanează lista de argumente și setează flags var s șir pentru i := 0 ; i < steag . Narg (); i ++ { dacă i > 0 { s += Spațiu } s += steag . Arg ( i ) } dacă ! * omitNewLine { s += NewLine } os . Stdout . WriteString ( e ) } ![]() | |
---|---|
Site-uri tematice | |
În cataloagele bibliografice |
Limbaje de programare | |
---|---|
|