Go (a.k.a. Golang) je jeden z jazykov, ktoré ľudí zaujímajú najviac. K aprílu 2018 stojí na 19. mieste v indexe TIOBE . Čoraz viac ľudí prechádza z PHP, Node.js a iných jazykov na Go a používa to vo výrobe. Mnoho skvelého softvéru (napríklad Kubernetes, Docker a Heroku CLI) sa píše pomocou Go.
Aký je teda kľúč spoločnosti Go k úspechu? Vo vnútri jazyka je veľa vecí, ktoré ho robia skutočne skvelým. Ale jednou z hlavných vecí, vďaka ktorým bol Go tak populárny, je jeho jednoduchosť, ako poukázal jeden z jeho tvorcov Rob Pike .
Jednoduchosť je super: nemusíte sa učiť veľa kľúčových slov. Učenie jazyka je veľmi ľahké a rýchle. Na druhej strane však niekedy vývojárom chýbajú niektoré funkcie, ktoré majú v iných jazykoch, a preto z dlhodobého hľadiska potrebujú kódové riešenia alebo napísať viac kódu. Go bohužiaľ nemá veľa funkcií z hľadiska dizajnu, a niekedy je to skutočne nepríjemné.
Cieľom Golangu bolo urýchliť vývoj, ale v mnohých situáciách píšete viac kódu, ako by ste napísali pomocou iných programovacích jazykov. Niektoré z týchto prípadov popíšem v mojej jazykovej kritike Go.
Tu uverejním príklad skutočného kódu. Keď som pracoval na väzbe Golang’s Selenium, potreboval som napísať funkciu, ktorá má tri parametre. Dva z nich boli voliteľné. Takto to vyzerá po implementácii:
func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { // the actual implementation was here } func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } func (wd *remoteWD) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) }
Musel som implementovať tri rôzne funkcie, pretože som nemohol len tak preťažiť funkciu alebo odovzdať predvolené hodnoty - Go to neposkytuje zámerne. Predstavte si, čo by sa stalo, keby som omylom zavolal nesprávneho? Tu je príklad:
Musím uznať, že preťaženie funkcií môže niekedy viesť k chaotickému kódu. Na druhej strane kvôli tomu musia programátori napísať viac kódu.
Rovnaký (takmer rovnaký) príklad v JavaScripte:
function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }
Ako vidíte, vyzerá to oveľa zreteľnejšie.
Páči sa mi na tom aj prístup Elixír. Takto by to vyzeralo v Elixire (viem, že by som mohol použiť predvolené hodnoty, ako v príklade vyššie - iba to ukazujem, ako sa to dá urobiť):
defmodule Waiter do @default_interval 1 @default_timeout 10 def wait(condition, timeout, interval) do // implementation here end def wait(condition, timeout), do: wait(condition, timeout, @default_interval) def wait(condition), do: wait(condition, @default_timeout, @default_interval) end Waiter.wait('condition', 2, 20) Waiter.wait('condition', 2) Waiter.wait('condition')
Toto je pravdepodobne funkcia, ktorú používatelia Go najviac požadujú.
Predstavte si, že chcete napísať mapovú funkciu, kde odovzdávate pole celých čísel a funkciu, ktorá sa použije na všetky jej prvky. Znie to jednoducho, však?
Urobme to pre celé čísla:
package main import 'fmt' func mapArray(arr []int, callback func (int) (int)) []int { newArray := make([]int, len(arr)) for index, value := range arr { newArray[index] = callback(value) } return newArray; } func main() { square := func(x int) int { return x * x } fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25] }
Vyzerá dobre, však?
Predstavte si, že to musíte urobiť aj pre struny. Budete musieť napísať ďalšiu implementáciu, ktorá je okrem podpisu úplne rovnaká. Táto funkcia bude vyžadovať iný názov, pretože Golang nepodporuje preťaženie funkcií. Vo výsledku budete mať kopu podobných funkcií s rôznymi názvami a bude vyzerať asi takto:
func mapArrayOfInts(arr []int, callback func (int) (int)) []int { // implementation } func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 { // implementation } func mapArrayOfStrings(arr []string, callback func (string) (string)) []string { // implementation }
To rozhodne ide proti princípu DRY (Don’t Repeat Yourself), ktorý hovorí, že musíte napísať čo najmenej kódu na kopírovanie / vkladanie a namiesto toho ho presunúť do funkcií a znova ich použiť.
Ďalším prístupom by bolo použitie jednotlivých implementácií s interface{}
ako parameter, ale môže to mať za následok runtime chybu, pretože runtime kontrola typu je náchylnejšia na chyby. A tiež to bude pomalšie, takže neexistuje jednoduchý spôsob, ako implementovať tieto funkcie ako jeden celok.
Existuje veľa dobrých jazykov, ktoré zahŕňajú všeobecnú podporu. Napríklad tu je rovnaký kód v Ruste (na zjednodušenie som použil vec
namiesto array
):
fn map(vec:Vec, callback:fn(T) -> T) -> Vec { let mut new_vec = vec![]; for value in vec { new_vec.push(callback(value)); } return new_vec; } fn square (val:i32) -> i32 { return val * val; } fn underscorify(val:String) -> String { return format!('_{}_', val); } fn main() { let int_vec = vec![1, 2, 3, 4, 5]; println!('{:?}', map::(int_vec, square)); // prints [1, 4, 9, 16, 25] let string_vec = vec![ 'hello'.to_string(), 'this'.to_string(), 'is'.to_string(), 'a'.to_string(), 'vec'.to_string() ]; println!('{:?}', map::(string_vec, underscorify)); // prints ['_hello_', '_this_', '_is_', '_a_', '_vec_'] }
Upozorňujeme, že map
existuje jediná implementácia a dá sa použiť pre všetky typy, ktoré potrebujete, dokonca aj pre tie vlastné.
Každý, kto má skúsenosti s Go, môže povedať, že riadenie závislostí je naozaj ťažké. Nástroje Go umožňujú používateľom inštalovať rôzne knižnice spustením go get
. Problémom je správa verzií. Ak správca knižnice urobí nejaké spätne nekompatibilné zmeny a nahrá ich na GitHub, každému, kto sa potom pokúsi použiť váš program, sa zobrazí chyba, pretože go get
nerobí nič iné ako git clone
uložte do priečinka knižnice. Pokiaľ knižnica nie je nainštalovaná, program sa kvôli tomu nebude kompilovať.
Môžete to urobiť o niečo lepšie, keď použijete Dep na správu závislostí ( https://github.com/golang/dep ), ale problém je v tom, že buď ukladáte všetky svoje závislosti do svojho úložiska (čo nie je dobré, pretože vaše úložisko bude obsahovať nielen váš kód, ale tisíce a tisíce riadkov kódu závislosti), alebo iba uložíte zoznam balíkov ( ale znova, ak správca závislosti urobí spätne nekompatibilnú zmenu, všetko sa zrúti).
Myslím, že dokonalým príkladom je tu Node.js (a všeobecne predpokladám JavaScript) a NPM. NPM je úložisko balíkov. Ukladá rôzne verzie balíkov, takže ak potrebujete konkrétnu verziu balíka, žiadny problém - odtiaľ ju získate. Jednou z vecí v každej aplikácii Node.js / JavaScript je tiež package.json
spis. Tu sú uvedené všetky závislosti a ich verzie, takže ich môžete nainštalovať všetky (a získať verzie, ktoré s vaším kódom určite pracujú) pomocou npm install
.
Skvelými príkladmi správy balíkov sú RubyGems / Bundler (pre balíčky Ruby) a Crates.io/Cargo (pre knižnice Rust).
Riešenie chýb v Go je úplne jednoduché. V aplikácii Go môžete v zásade z funkcií vrátiť viac hodnôt a funkcia môže vrátiť chybu. Niečo také:
err, value := someFunction(); if err != nil { // handle it somehow }
Teraz si predstavte, že musíte napísať funkciu, ktorá vykoná tri akcie, ktoré vrátia chybu. Bude to vyzerať asi takto:
func doSomething() (err, int) { err, value1 := someFunction(); if err != nil { return err, nil } err, value2 := someFunction2(value1); if err != nil { return err, nil } err, value3 := someFunction3(value2); if err != nil { return err, nil } return value3; }
Je tu veľa opakovateľných kódov, čo nie je dobré. A s veľkými funkciami môže to byť ešte horšie ! Pravdepodobne na to budete potrebovať klávesnicu:
Páči sa mi k tomu prístup JavaScriptu. Funkcia môže vyhodiť chybu a vy ju môžete chytiť. Zvážte príklad:
function doStuff() { const value1 = someFunction(); const value2 = someFunction2(value1); const value3 = someFunction3(value2); return value3; } try { const value = doStuff(); // do something with it } catch (err) { // handle the error }
Je to jasnejšie a neobsahuje opakovateľný kód na spracovanie chýb.
Aj keď má Go veľa nedokonalostí už od začiatku, má tiež skutočne vynikajúce vlastnosti.
Asynchronné programovanie bolo v aplikácii Go skutočne jednoduché. Zatiaľ čo programovanie viacerých vlákien je zvyčajne ťažké v iných jazykoch, vytvorenie nového vlákna a spustenie funkcie v ňom, aby neblokovalo súčasné vlákno, je naozaj jednoduché:
func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }
Zatiaľ čo v iných programovacích jazykoch musíte inštalovať rôzne knižnice / nástroje pre rôzne úlohy (ako je testovanie, formátovanie statického kódu atď.), Existuje veľa skvelých nástrojov, ktoré sú už v predvolenom nastavení zahrnuté v Go, napríklad:
gofmt
- Nástroj na statickú analýzu kódu. V porovnaní s jazykom JavaScript, kde je potrebné nainštalovať ďalšiu závislosť, napríklad eslint
alebo jshint
, tu je predvolene zahrnutý. A program sa ani nezkompiluje, ak nepíšete kód v štýle Go (nepoužívanie deklarovaných premenných, import nepoužitých balíkov atď.).go test
- Testovací rámec. Opäť v porovnaní s JavaScriptom musíte nainštalovať ďalšie závislosti na testovanie (Jest, Mocha, AVA atď.). Tu je predvolene zahrnutý. A umožňuje vám predvolene robiť veľa skvelých vecí, ako je testovanie, konverzia kódu v dokumentácii na testy atď.godoc
- Dokumentačný nástroj. Je pekné mať to zahrnuté v predvolených nástrojoch.Myslím si, že je to jedna z najkrajších funkcií v jazyku. Predstavte si, že musíte napísať funkciu, ktorá otvorí tri súbory. A ak niečo zlyhá, budete musieť zavrieť existujúce otvorené súbory. Ak je veľa takýchto stavieb, bude to vyzerať ako neporiadok. Zvážte tento príklad pseudokódu:
function openManyFiles() { let file1, file2, file3; try { file1 = open(‘path-to-file1’); } catch (err) { return; } try { file2 = open(‘path-to-file2’); } catch (err) { // we need to close first file, remember? close(file1); return; } try { file3 = open(‘path-to-file3’); } catch (err) { // and now we need to close both first and second file close(file1); close(file2); return; } // do some stuff with files // closing files after successfully processing them close(file1); close(file2); close(file3); return; }
Vyzerá komplikovane. To je miesto, kde je Go defer
prichádza na miesto:
package main import ( 'fmt' ) func openFiles() { // Pretending we’re opening files fmt.Printf('Opening file 1
'); defer fmt.Printf('Closing file 1
'); fmt.Printf('Opening file 2
'); defer fmt.Printf('Closing file 2
'); fmt.Printf('Opening file 3
'); // Pretend we've got an error on file opening // In real products, an error will be returned here. return; } func main() { openFiles() /* Prints: Opening file 1 Opening file 2 Opening file 3 Closing file 2 Closing file 1 */ }
Ako vidíte, ak sa nám pri otvorení súboru číslo tri zobrazí chyba, ostatné súbory sa automaticky zatvoria, pretože defer
príkazy sú vykonávané pred návratom v opačnom poradí. Tiež je pekné mať súbor otvárajúci a zatvárajúci sa na rovnakom mieste namiesto rôznych častí funkcie.
Nehovoril som o všetkých dobrých a zlých veciach v Go, iba o tých, ktoré považujem za najlepšie a najhoršie.
Go je skutočne jedným zo zaujímavých programovacích jazykov, ktoré sa v súčasnosti používajú, a má skutočne potenciál. Poskytuje nám skutočne skvelé nástroje a funkcie. Existuje však veľa vecí, ktoré sa dajú vylepšiť.
Keby sme, ako Choďte vývojári , implementuje tieto zmeny, bude mať veľký prínos pre našu komunitu, pretože uľahčí programovanie v prostredí Go.
Ak sa medzitým snažíte vylepšiť svoje testy pomocou Go, skúste to Testovanie aplikácie Go: Začnite správnym spôsobom kolega ApeeScapeer Gabriel Aszalos.
Súvisiace: Dobre štruktúrovaná logika: Výučba Golang OOPMedzi definíciou skriptu a programu je tenká čiara, ale povedal by som, že to nie je skriptovací jazyk, pretože programy Go sa nespúšťajú za behu - sú kompilované a spúšťané ako spustiteľné súbory.
Nie, program Go je skvelý a vylepšuje skúsenosti vývojára, ale nie je dokonalý, ako popisujem v tomto článku. Možno to nikdy nebude dokonalé, ale verím, že to dokážeme priblížiť.