Go is een door Google ontwikkelde programmeertaal gericht op expressiviteit, schaalbaarheid en productiviteit. Go bestaat zo’n 7 jaar, is volledig open source, snel, statisch getypeerd, ontworpen voor concurrency en voorzien van garbage-collection. Mede hierdoor neemt de adoptie van Go snel toe. Grote open source projecten en services zoals Docker, Prometheus and Kubernetes en Soundcloud zijn geschreven in Go en organisaties passen Go steeds vaker toe binnen hun eigen projecten. In dit artikel laten we je kennismaken met Go.
Waarom Go?
Go is ontworpen om een aantal problemen op te lossen waarmee Google geconfronteerd werd tijdens het ontwikkelen van hun eigen software. Dit is direct terug te zien in de taal:
- Zo bevat Go een groot aantal standaard bibliotheken voor veel voorkomende zaken, zoals networking, http-clients en -servers en het marshallen van JSON en XML.
- Concurrency is onderdeel van de taal zelf door de introductie van goroutines en channels.
- De taal is minimalistisch opgezet en dwingt standaarden voor formatteren en naamgeving af wat leidt tot beter onderhoudbare code
- De meegeleverde Go command line tooling is compleet, snel, eenvoudig te gebruiken en een uitstekend basis om je build proces te automatiseren
Simpele REST service
Om je de kracht van Go te laten ervaren, gaan we een eenvoudige REST service ontwikkelen die de huidige datum en tijd in een bepaalde tijdzone teruggeeft.
- package main
Listing 1
Een package heeft een naam en kan onder meer functies en types bevatten. Als de package main heet, geef je daarmee aan dat hij een main functie bevat en uitgevoerd kan worden.
- import (
- "fmt"
- "github.com/gin-gonic/gin"
- "net/http"
- "time"
- )
Listing 2
Daarna volgen de import statements. Deze verwijzen naar interne packages zoals “fmt” of externe packages zoals "github.com/gin-gonic/gin”. Externe packages worden door Go automatisch opgehaald, waarover later meer.
- func main() {
- router := gin.Default()
- router.GET("/time", currentTimeHandler)
- router.Run(":8888")
- }
Listing 3
In de main functie maken we een router. De toekenningsoperator := zorgt ervoor dat de variabele router het type overneemt van datgene dat aan hem toegekend wordt. Daarna koppelen we een handler aan het pad /time en publiceren we deze API op alle netwerk interfaces op poort 8888.
De functie currentTimeHandler is een functie die als argument aan de router.GET functie wordt meegegeven om aan te roepen als /time bezocht wordt. Functies zijn first-class-citizens, je kunt ze dus meegeven als parameters, retourneren, toewijzen aan variabelen en inline definiëren (zie Listing 4).
- func currentTimeHandler(c *gin.Context) {
- zoneQuery := c.DefaultQuery("zone", "UTC")
- if zone, err := time.LoadLocation(zoneQuery); err != nil {
- c.String(http.StatusBadRequest, fmt.Sprintf("zone %v not found", zoneQuery))
- } else {
- c.JSON(http.StatusOK, gin.H{"time": formatTimeInZone(time.Now(), zone)})
- }
- }
Listing 4
De functie currentTimeHandler berekent de datum en tijd in een bepaalde tijdzone en stuurt deze terug naar de client als string. De functie accepteert een parameter c van het type *gin.Context. Met een * wordt aangegeven dat het om een pointer gaat. De functie ontvangt een pointer naar c in plaats van een kopie van de waarde.
De functie currentTimeHandler haalt vervolgens de query parameter “zone” uit het request en als deze niet wordt gevonden, is de zone default “UTC”. Het if statement dat daarop volgt, probeert zone om te zetten in een tijdzone door time.LoadLocation() aan te roepen.
Er vallen een aantal dingen op:
- De functie-aanroep is geprefixed met de naam van de package waarin het zit. Dat is verplicht in Go. Dit dwingt je om goed na te denken over naamgeving. Bijvoorbeeld, een java.nio.ByteBuffer zou in Go als bytes.Buffer terug te vinden zijn. Het laatste pad element van een import statement is de package naam die je gebruikt in de source.
- De functie een zone en een err terug. Als zoneQuery een geldige tijdzone is, zal err gelijk zijn aan Nil en zone gevuld zijn met een geldige datastructuur. Anders bevat err de foutmelding.
- Het is toegestaan om een if te beginnen met één statement gevolgd door een boolean expressie: if <statement>; <boolean expression> { … }. zone en err zijn alleen beschikbaar binnen de body van de if/else. Buiten de if zijn ze niet meer benaderbaar, dat houdt de scope schoon.
- func formatTimeInZone(t time.Time, zone *time.Location) string {
- return t.In(zone).Format("2006-01-02T15:04:05.99MST")
- }
Listing 5
De functie formatCurrentTimeInZone stelt de tijdzone in en formatteert deze eenvoudig op basis van een voorbeeld lay-out. Dit in plaats van een complex patroon zoals in Java.
Documentatie
Met het volgende commando kun je op de commandline documentatie opvragen. Bijvoorbeeld van time.Format:
go doc time.Format
Listing 6
De documentatie van publieke projecten wordt automatisch geïndexeerd en kan worden ingezien en doorzocht op https://godoc.org. Zie bijvoorbeeld: https://godoc.org/github.com/toefel18/go-patan/metrics.
Dependencies, Bouwen en Uitvoeren
Als Go geïnstalleerd is op je systeem, heb je automatisch de beschikking over de Go command line tool. Deze tool gebruik je onder andere voor het bouwen, testen, downloaden en uitvoeren van programma’s. Alle sources, libraries en binaries worden geplaatst in de GOPATH folder. GOPATH bevat 3 subfolders: bin, pkg, src. De exacte locatie van je GOPATH folder kun je opvragen door het volgende commando uit te voeren:
go env
Listing 7
Laten we de code van onze Rest service bouwen en uitvoeren om te zien hoe hier gebruik van gemaakt wordt:
go get github.com/toefel18/go-have-fun/timeservice
Listing 8
Dit commando doet het volgende:
- Het cloned de github repo toefel18/go-have-fun repository in je GOPATH folder onder /src/github.com/toefel18/go-have-fun.
- De import paths in de source file (github.com/gin-gonic/gin) worden ook gecloned in je GOPATH als ze daar nog niet aanwezig zijn. Dit gebeurt recursief voor alle transitief binnengehaalde dependencies. Je hebt in één klap al je dependencies binnengehaald en netjes georganiseerd.
- Als laatste stap compileert hij de package timeservice en plaatst hij het resultaat met alle dependencies ingebakken in GOPATH/bin.
Enkele handige commando’s zijn bijvoorbeeld:
go run source.go
Een source file met main functie direct uitvoeren
go install github.com/toefel18/go-have-fun/timeservice
Sources compileren en installeren in GOPATH/bin. Als je in de working folder van het project staat, kun je ook direct go install uitvoeren.
go build -o program github.com/toefel18/go-have-fun/timeservice
Sources compileren en de executable in de huidige map plaatsen met de naam "program".
Listing 9
Standaard wordt voor dependencies de default branch uit binnengehaald. Om op een specifieke release versie te dependen, kun je een dependency manager gebruiken zoals gopkg.in, Glide of godep.
Unit testen
Unit testen worden ondersteund door Go. Ze bevinden zich in dezelfde folder als de source bestanden, alleen eindigt de naam op ‘_test.go’. Alle tests in dit bestand moeten met het woord ‘Test’ beginnen en een parameter van type *testing.T hebben, daarmee rapporteer je errors. Hieronder vind je de unit test voor de functie formatTimeZone.
- import (
- "time"
- "testing"
- )
-
- const ExpectedTime = "2016-12-24T18:14:54.22CST"
-
- func TestFormatTimeInZone(t *testing.T) {
- zoneShanghai, _ := time.LoadLocation("Asia/Shanghai")
- christmasUtc := time.Date(2016, time.December, 24,
10, 11, 12, 222222222222, time.UTC)
-
- shanghaiTime := formatTimeInZone(christmasUtc, zoneShanghai)
-
- if shanghaiTime != ExpectedTime {
- t.Errorf("time was %v, expected %v", shanghaiTime, ExpectedTime)
- }
- }
Listing 10
Deze test kun je uitvoeren met het command:
go test github.com/toefel18/go-have-fun/timeservice
ok github.com/toefel18/go-have-fun/timeservice 0.004s
Listing 11
Ook test coverage is out-of-the-box beschikbaar:
go test -cover github.com/toefel18/go-have-fun/timeservice
ok github.com/toefel18/go-have-fun/timeservice 0.004s
coverage: 12.5% of statements
Listing 12
Structs and Interfaces
Als je in C/C++ ontwikkeld hebt, herken je structs vast nog. Dit wordt gebruikt om een type met velden te definiëren.
- type message struct {
- Text string
- Author string
- }
Listing 13
Bovenstaande code beschrijft het type message met daarin de velden Text en Author.
- func (msg message) ToJson() string {
- jsonMsg, _ := json.Marshal(msg)
- return string(jsonMsg)
- }
Listing 14
Vervolgens definiëren we een functie ToJson. Doordat we de functie definitie vooraf laten gaan aan (msg message) maken we het mogelijk om de functie aan te roepen op variabelen van het type message, waarbij de betreffende message vanuit de functie te benaderen is via de parameter msg (zie Listing 15).
- func main() {
- fromChris := message{Text: "Hi Gopher", Author: "Christophe"}
- fromRob := message{"Hi Gopher", "Rob"}
- fmt.Println(fromChris.ToJson(), fromRob.ToJson())
- }
-
Listing 15
Binnen de main functie worden twee message structs aangemaakt en geprint. Dit kan op twee manieren, door accolades achter de naam te zetten en de velden in volgorde op te geven of door named parameters te gebruiken. Go kent geen constructors zoals Java.
- type Jsonizer interface {
- ToJson() string
- }
-
- func printJson(something Jsonizer) {
- fmt.Println(something.ToJson())
- }
-
- func main() {
- printJson(message{Text: "Hi Gopher", Author: "Duke"})
- }
Listing 16
Hierboven definiëren we een Jsonizer interface en een functie die een Jsonizer ontvangt. In de main roepen we deze functie aan met een message. Het opvallende hier is dat message niet expliciet vermeldt dat hij de Jsonizer interface implementeert. In Go implementeren types een interface automatisch als ze alle methoden hebben die de interface definieert.
Access modifiers
Als een identifier begint met een hoofdletter is het veld of de functie publiek in alle andere gevallen package-private. Private en protected kent Go niet omdat ook geen type hiërarchieën bestaan. Wel kun je types embedden in andere types. Kort door de bocht kun je stellen dat de taal compositie kiest over inheritance.
Collecties
Go kent geen uitgebreide collections bibliotheek zoals in Java. Je beschikt enkel over arrays, slices, linked lists en maps.
Arrays en Slices
Array en slice types beginnen altijd met de brackets gevolgd door het type, bijvoorbeeld []int. Bij het uitlezen komen de brackets achteraan, array[1] haalt het 2e element uit de array. Zo is er nooit ambigu tussen het definiëren en uitlezen van arrays.
- func main() {
- numbers := [5]int{1,2,3,4,5}
- print("numbers ", numbers[:])
- print("from 2 ", numbers[2:])
- print("until 2 ", numbers[:2])
- middle := numbers[2:3]
- print("middle ", middle)
- middle = append(middle, 6)
- print("middle ", middle)
- print("numbers ", numbers[:])
- }
-
Output:
numbers : 1 2 3 4 5
from 2 : 3 4 5
until 2 : 1 2
middle : 3
middle : 3 6
numbers : 1 2 3 6 5
Listing 17
In de bovenstaande code wordt een array van 5 elementen gemaakt, deze kan niet meer veranderen van grootte. Meerdere keren worden daar slices van genomen en geprint. [:] maakt een slice van een array.
Een slice is een dynamische view op de onderliggende array (een soort van subList() in Java), en mutaties veranderen de elementen van de originele array. Als je een slice neemt, kun je het begin of einde weglaten en alleen de upper of lower bound definiëren.
Maps
Maps zijn built-ins van de taal. Met make() wordt een map gealloceerd. In het gebruik lijkt een map erg op een array. Zo kun je de index operator gebruiken om te lezen en schrijven en de range operator om door de map te itereren.
- func main() {
- ages := make(map[string]int)
- ages["Christophe"] = 28
- ages["Rob"] = 38
-
- for name, age := range ages {
- fmt.Printf("%v has age %v\n", name, age)
- }
- }
Listing 18
Goroutines en Channels
Goroutines zijn lichtgewicht threads gemanaged door de Go runtime. Je kunt een goroutine starten door een functie aanroep te prefixen met go.
Channels bieden goroutines een manier om data met elkaar uit te wisselen. Schrijven en lezen naar een channel gebeurt door middel van de <- channel operator. Zo is x:= <- channel een lees actie en channel <- “hoi” een schrijfactie (zie Listing 19).
- func main() {
- payments := make(chan int)
- texts := make(chan string)
- quit := make(chan bool)
- go consume(payments, texts, quit)
- go producePayments(payments)
- go produceTexts(texts)
- time.Sleep(5 * time.Second)
- quit <- true
- time.Sleep(1 * time.Second)
- }
Listing 19
In de main functie definiëren we drie channels die worden gebruikt voor de communicatie tussen de producers en de consumer. Vervolgens starten we een goroutine voor de consumer en twee goroutines die payments en texts produceren. Tenslotte laten we de main goroutine 5 seconden slapen en sturen we het bericht true naar het quit channel om de applicatie af te sluiten (zie Listing 19).
- func produceTexts(strings chan string) {
- for i := 1; i < 7; i++ {
- strings <- "message " + strconv.Itoa(i)
- time.Sleep(800 * time.Millisecond)
- }
- }
Listing 20
De produceTexts functie verstuurd zeven keer een bericht naar het channel met de naam strings (zie Lising 20).
- func producePayments(ints chan int) {
- for i := 1; i < 9; i++ {
- ints <- i * 13
- time.Sleep(500 * time.Millisecond)
- }
- }
Listing 21
De producePayments functie verstuurd negen keer een bericht naar het channel met de naam ints (zie Listing 21).
- func consume(payments chan int, texts chan string, quit chan bool) {
- for {
- select {
- case p := <-payments:
- fmt.Println("got a payment of ", p, "euro")
- case t := <-texts:
- fmt.Println("got a text: ", t)
- case <-quit:
- fmt.Println("quitting, bye")
- return
- }
- }
- }
Listing 22
De consume functie, uitgevoerd als goroutine, wacht met een select statement op data uit de verschillende channels. De case van het channel dat als eerste data ontvangt wordt uitgevoerd. Als meerdere channels data hebben, wordt random een case gekozen (zie Listing 22).
Tot slot
We gebruiken Go nu in een project en het levert ons relatief snel resultaat. Omdat je niet elk type meerdere keren moet opschrijven (velden, constructors, getters, setters, type van vars) voelt de taal dynamischer en schrijf je code toch wat sneller. De frameworks om bijvoorbeeld een REST API te maken, zijn simpeler en een stuk duidelijker dan bijvoorbeeld JAX-RS (dit is natuurlijk wel een persoonlijke mening). De router uit Listing 1 geeft bijvoorbeeld direct aan welke paden ondersteund worden. Omdat je met foutcodes werkt in plaats van excepties kost foutafhandeling soms iets meer werk, maar dat maakt ook weer expliciet wat er fout kan gaan als je de code leest. Daarnaast compileert de code in slechts seconden en zijn de resulterende binaries snel. Kortom, een fijne taal om een keer een kans te geven.
Ga Go ontdekken
We hopen dat je eerste kennismaking met Go is bevallen en je helpt bij het vormen van een mening over deze taal. Het artikel geeft natuurlijk niet alle mogelijkheden van Go weer. Er is nog genoeg te ontdekken, zoals:
- Reflectie;
- Panics en recover();
- Type embedding i.p.v. Inheritance;
- Multiline strings tussen ` `, zoals Scala’s “”” “””;
- Dependency Management via Godep Glide en/of Gopkg.in;
- Gofmt, golint, govet, go imports;
- Profiling;
- Benchmarks;
- Race detector;
- Buffered Channels;
- Database frameworks;
- IDE ondersteuning in Atom, VSCode, Intellij en Gogland.
De volledige code gebruikt in dit artikel is te vinden op: https://github.com/toefel18/go-have-fun/.
Om zelf aan de slag te gaan, kun je het beste beginnen bij de Go Tour (zie referenties).
Referenties
https://golang.org/doc/articles/race_detector.html
https://golang.org/doc/code.html
https://github.com/toefel18/go-have-fun
https://tour.golang.org/welcome/1