|
@@ -0,0 +1,346 @@
|
|
|
|
|
+# Go Hands-on
|
|
|
|
|
+
|
|
|
|
|
+6 april 2017
|
|
|
|
|
+
|
|
|
|
|
+Harry de Boer, Quintor
|
|
|
|
|
+
|
|
|
|
|
+## Wat is go
|
|
|
|
|
+
|
|
|
|
|
+Volgens https://golang.org: _Go is an open source programming language that makes it easy to build simple, reliable,
|
|
|
|
|
+and efficient software._
|
|
|
|
|
+
|
|
|
|
|
+Eigenschappen:
|
|
|
|
|
+ * type safe
|
|
|
|
|
+ * compiled
|
|
|
|
|
+ * garbage collected
|
|
|
|
|
+ * staticly linked binaries
|
|
|
|
|
+
|
|
|
|
|
+## Tips
|
|
|
|
|
+* zoek online op 'golang' ipv 'go'
|
|
|
|
|
+
|
|
|
|
|
+## Opgaven
|
|
|
|
|
+
|
|
|
|
|
+In deze hands-on sessie beginnen we met een simpel programma.
|
|
|
|
|
+Deze breiden we steeds verder uit naar volledig werkende backend voor een webapplicatie.
|
|
|
|
|
+Opdrachten 1 t/m 5 zijn bedoeld als eerste kennismakinng.
|
|
|
|
|
+Voor opdrachten 6 t/m 10 is een simpele website aangeleverd waarvoor gedurende de opdrachten de backend geschreven
|
|
|
|
|
+wordt.
|
|
|
|
|
+
|
|
|
|
|
+### Opgave 1: een eerste programma
|
|
|
|
|
+
|
|
|
|
|
+In deze opgave maken we ons eerste werkende go programma.
|
|
|
|
|
+Een go programma begint altijd met een package declaratie.
|
|
|
|
|
+Voor een executable - wat we hier gaan maken - is altijd dit altijd
|
|
|
|
|
+~~~~
|
|
|
|
|
+package main
|
|
|
|
|
+~~~~
|
|
|
|
|
+Na de package declaratie komen de import statements:
|
|
|
|
|
+~~~~
|
|
|
|
|
+import (
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+)
|
|
|
|
|
+~~~~
|
|
|
|
|
+Functies uit een geimporteerd package kan je aanroepen met `packagenaam.Functie(...)`.
|
|
|
|
|
+Alleen funties/methoden/typen/velden die beginnen met een hoofdletter zijn _exported_ (public) en zichtbaar voor
|
|
|
|
|
+andere packges. Binnen een package is alles zichtbaar.
|
|
|
|
|
+Een package zit altijd in een directory van dezelfde naam, met uitzondering van `main`.
|
|
|
|
|
+
|
|
|
|
|
+Het entrypoint van een executable is de `main` functie.
|
|
|
|
|
+~~~~
|
|
|
|
|
+func main() {
|
|
|
|
|
+ statement
|
|
|
|
|
+}
|
|
|
|
|
+~~~~
|
|
|
|
|
+Het getoonde statement heeft geen `;` aan het einde, deze zijn optioneel in go.
|
|
|
|
|
+
|
|
|
|
|
+Schrijf nu een programma dat je favoriete paar woorden afdrukt.
|
|
|
|
|
+Importeer hier voor [package fmt](https://golang.org/pkg/fmt) en gebruik de `Println` functie.
|
|
|
|
|
+De naam van het bestand maakt niet uit, maar deze moet eindigen op `.go`
|
|
|
|
|
+
|
|
|
|
|
+Compileer het programma met
|
|
|
|
|
+~~~~
|
|
|
|
|
+go build <bestandsnaam>.go
|
|
|
|
|
+~~~~
|
|
|
|
|
+De resulterende executable zal dezelfde naam krijgen als de directory waar deze in staat.
|
|
|
|
|
+Waar wacht je op? Voer hem uit!
|
|
|
|
|
+
|
|
|
|
|
+### Opgave 2: functies
|
|
|
|
|
+
|
|
|
|
|
+Functies in go beginnen met het `func` keyword gevolgd door een lijst van parameters tussen haakjes
|
|
|
|
|
+~~~~
|
|
|
|
|
+func bla(naam string) string {
|
|
|
|
|
+ return naam + "bla"
|
|
|
|
|
+}
|
|
|
|
|
+~~~~
|
|
|
|
|
+In tegenstelling tot veel andere talen komen de types van de parameters na de naam.
|
|
|
|
|
+
|
|
|
|
|
+Voeg nu je eerste functie toe aan je programma.
|
|
|
|
|
+Geef als parameter een string mee, verwerk deze in een andere string met `fmt.Sprintf`, return het resultaat en druk dit
|
|
|
|
|
+af.
|
|
|
|
|
+
|
|
|
|
|
+### Opgave 3: template parsing
|
|
|
|
|
+
|
|
|
|
|
+`Sprintf` is prima voor kleine strings, maar voor grotere teksten is een template parser handig.
|
|
|
|
|
+Importeer het `text/template` package en maak een nieuw template:
|
|
|
|
|
+~~~~
|
|
|
|
|
+template.New("helloTemplate").Parse("Hello {{.}}\n")
|
|
|
|
|
+~~~~
|
|
|
|
|
+In de documentatie van `template.New` kan je zien dat deze twee return values heeft.
|
|
|
|
|
+De eerste is het template, noem deze `t`.
|
|
|
|
|
+De tweede is een error die je bijvoorbeeld kan printen, of je kan je programma laten crashen met
|
|
|
|
|
+~~~~
|
|
|
|
|
+if err != nil {
|
|
|
|
|
+ panic(err)
|
|
|
|
|
+}
|
|
|
|
|
+~~~~
|
|
|
|
|
+Het template verwerken gaat met de `Execute` methode die op `t` aangeroepen kan worden
|
|
|
|
|
+~~~~
|
|
|
|
|
+t.Execute(os.Stdout, "Quintor")
|
|
|
|
|
+~~~~
|
|
|
|
|
+De eerste parameter is een interface, `io.Writer`, die afdwingt dat het meegegeven argument een `Write([]byte)`
|
|
|
|
|
+methode heeft. In dit geval wordt naar de standaard uitvoer geschreven.
|
|
|
|
|
+De tweede parameter is een object waarmee de placeholders in het template vervangen worden.
|
|
|
|
|
+In dit voorbeeld is dit een simpele string
|
|
|
|
|
+
|
|
|
|
|
+Verwerk nu het bovenstaande in je programma.
|
|
|
|
|
+
|
|
|
|
|
+### Opgave 4: template parsing #2
|
|
|
|
|
+
|
|
|
|
|
+Templates kunnen ook uit een file geladen worden met
|
|
|
|
|
+~~~~
|
|
|
|
|
+template.ParseFiles("mijntemplate.tpl")
|
|
|
|
|
+~~~~
|
|
|
|
|
+
|
|
|
|
|
+Pas je programma nu zo aan dat het `ParseFiles` gebruikt ipv `New`/`Parse`.
|
|
|
|
|
+
|
|
|
|
|
+### Opgave 5: een webserver
|
|
|
|
|
+
|
|
|
|
|
+Het `io.Writer` interface kan veelzijdig gebruikt worden.
|
|
|
|
|
+Zoals in het voorbeeld hieronder te zien is voldoet `http.ResponseWriter` ook aan dit interface.
|
|
|
|
|
+
|
|
|
|
|
+Analyseer het onderstaande programma en bouw een webserver in je eigen programma in.
|
|
|
|
|
+Gebruik evt. de templates/resources in `opgave5.zip`
|
|
|
|
|
+
|
|
|
|
|
+Een paar hints:
|
|
|
|
|
+ * go kent geen classes
|
|
|
|
|
+ * datatypen worden gedefineerd met `type`, hieronder wordt het type `server`
|
|
|
|
|
+ gedefineerd als een `struct` met een veld `template`
|
|
|
|
|
+ * `main()` is een funcie
|
|
|
|
|
+ * `handleRoot()` is een methode op het type `server`
|
|
|
|
|
+ * `http.ListenAndServe` start een http server op
|
|
|
|
|
+ * `http.FileServer` serveert static files in een directory
|
|
|
|
|
+ * wees niet bang om de documentatie van een package er bij te pakken
|
|
|
|
|
+
|
|
|
|
|
+~~~~
|
|
|
|
|
+package main
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "log"
|
|
|
|
|
+ "net/http"
|
|
|
|
|
+ "text/template"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type server struct {
|
|
|
|
|
+ template *template.Template
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func main() {
|
|
|
|
|
+ t := template.Must(template.ParseFiles("templates/index.html"))
|
|
|
|
|
+ s := server{template: t}
|
|
|
|
|
+
|
|
|
|
|
+ http.HandleFunc("/", s.handleRoot)
|
|
|
|
|
+ http.Handle("/resources/", http.FileServer(http.Dir(".")))
|
|
|
|
|
+
|
|
|
|
|
+ err := http.ListenAndServe(":8080", nil)
|
|
|
|
|
+ log.Fatal(err)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (s *server) handleRoot(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
+ w.Header().Set("Content-Type", "text/html; charset=utf8")
|
|
|
|
|
+
|
|
|
|
|
+ err := s.template.Execute(w, "Vrolijk pasen!")
|
|
|
|
|
+
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ log.Println(err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+~~~~
|
|
|
|
|
+
|
|
|
|
|
+### Opgave 6: paaseieren zoeken
|
|
|
|
|
+
|
|
|
|
|
+`opgave6.zip` bevat een html/javascript webapplicatie.
|
|
|
|
|
+Serveer de bestanden in `/resources` met `http.FileServer` en zoek de eieren door in het scherm te klikken.
|
|
|
|
|
+
|
|
|
|
|
+Je zult merken dat je geen mooie paaseieren krijgt, hiervoor moet de backend geimplementeerd.
|
|
|
|
|
+
|
|
|
|
|
+Maak een server type aan met een veld naam.
|
|
|
|
|
+~~~~
|
|
|
|
|
+type server struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+}
|
|
|
|
|
+~~~~
|
|
|
|
|
+Initialiseer een variabele van dit type:
|
|
|
|
|
+~~~~
|
|
|
|
|
+s := server{name: "Golang Hands-on"}
|
|
|
|
|
+~~~~
|
|
|
|
|
+Maak een endpoint onder `/api/greet`
|
|
|
|
|
+~~~~
|
|
|
|
|
+http.HandleFunc("/api/greet/", s.titleHandler)
|
|
|
|
|
+~~~~
|
|
|
|
|
+En de bijbehorende handler
|
|
|
|
|
+~~~~
|
|
|
|
|
+func (s server) titleHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
+ var err error
|
|
|
|
|
+
|
|
|
|
|
+ w.Header().Set("Content-Type", "text/plain; charset=utf8")
|
|
|
|
|
+ _, err = fmt.Fprint(w, s.name)
|
|
|
|
|
+
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ log.Println(err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+~~~~
|
|
|
|
|
+Als het goed is kan je nu een paasei vinden!
|
|
|
|
|
+
|
|
|
|
|
+### Opgave 7: marshalling json
|
|
|
|
|
+
|
|
|
|
|
+Om het tweede ei te vinden moet het endpoint om kunnen gaan met de `Accept: application/json`
|
|
|
|
|
+http header. Voor het eerste ei werd om `text/plain` gevraagd, voor het tweede ei
|
|
|
|
|
+wordt json gevraagd _op hetzelfde endpoint_. Er wordt een json object in het volgende formaat verwacht:
|
|
|
|
|
+~~~~
|
|
|
|
|
+{title: "title", subtitle: "subtitle"}
|
|
|
|
|
+~~~~
|
|
|
|
|
+
|
|
|
|
|
+Je kan de header ophalen met
|
|
|
|
|
+~~~~
|
|
|
|
|
+r.Header.Get("Accept")
|
|
|
|
|
+~~~~
|
|
|
|
|
+Je kan het `strings` package gebruiken om strings te manipuleren.
|
|
|
|
|
+
|
|
|
|
|
+Om json te encoden heb je een geschikt type nodig om te encoden, bijvoorbeeld
|
|
|
|
|
+~~~~
|
|
|
|
|
+type greet struct {
|
|
|
|
|
+ Title string `json:"title"`
|
|
|
|
|
+ Subtitle string `json:"subtitle"`
|
|
|
|
|
+}
|
|
|
|
|
+~~~~
|
|
|
|
|
+Deze kan je encoden met het `json` package.
|
|
|
|
|
+`json.NewEncoder` verwacht wederom een `io.Writer`.
|
|
|
|
|
+~~~~
|
|
|
|
|
+ e := json.NewEncoder(w)
|
|
|
|
|
+ err = e.Encode(greet{Title: s.name, Subtitle: "Hallo Quintor!"})
|
|
|
|
|
+~~~~
|
|
|
|
|
+
|
|
|
|
|
+Stuur ook het juiste Content-Type mee:
|
|
|
|
|
+~~~~
|
|
|
|
|
+w.Header().Set("Content-Type", "application/json; charset=utf8")
|
|
|
|
|
+~~~~
|
|
|
|
|
+
|
|
|
|
|
+Als je dit goed doet heb je een ei erbij!
|
|
|
|
|
+
|
|
|
|
|
+### Opgave 8: unmarshalling json
|
|
|
|
|
+
|
|
|
|
|
+Maak een nieuwe Handler voor het endpoint `/api/sum/`.
|
|
|
|
|
+In het request wordt een json string meegestuurd.
|
|
|
|
|
+Om deze te kunnen decoden hebben we een struct nodig met de juiste structuur:
|
|
|
|
|
+~~~~
|
|
|
|
|
+type sumRequest struct {
|
|
|
|
|
+ Start int `json:"start"`
|
|
|
|
|
+ End int `json:"end"`
|
|
|
|
|
+ Numbers []int `json:"numbers"`
|
|
|
|
|
+}
|
|
|
|
|
+~~~~
|
|
|
|
|
+En decode met:
|
|
|
|
|
+~~~~
|
|
|
|
|
+var sreq sumRequest
|
|
|
|
|
+d := json.NewDecoder(r.Body)
|
|
|
|
|
+err := d.Decode(&sreq)
|
|
|
|
|
+~~~~
|
|
|
|
|
+Als response verwacht de client een json object van de vorm
|
|
|
|
|
+~~~~
|
|
|
|
|
+{answer: 13, contributers: 3}
|
|
|
|
|
+~~~~
|
|
|
|
|
+Het veld `answer` moet de som van de elementen tussen `start` (inclusief) en `end` (exclusief)
|
|
|
|
|
+in de numbers slice bevatten. `contributers` moet gelijk zijn aan `end - start`.
|
|
|
|
|
+Maak hier zelf een type voor aan.
|
|
|
|
|
+
|
|
|
|
|
+### Opgave 9: data opslaan in een database
|
|
|
|
|
+
|
|
|
|
|
+Maak een nieuw endpoint `/api/store/` waar een json request in de vorm
|
|
|
|
|
+~~~~
|
|
|
|
|
+{name: "name", message: "message"}
|
|
|
|
|
+~~~~
|
|
|
|
|
+verwacht en een response van de vorm terug stuurt.
|
|
|
|
|
+~~~~
|
|
|
|
|
+{id: 0}
|
|
|
|
|
+~~~~
|
|
|
|
|
+
|
|
|
|
|
+Om data op te slaan gaan we gebruik maken maken van [gorm](github.com/jinzhu/gorm).
|
|
|
|
|
+Voeg de volgende imports toe:
|
|
|
|
|
+~~~~
|
|
|
|
|
+"github.com/jinzhu/gorm"
|
|
|
|
|
+_ "github.com/jinzhu/gorm/dialects/sqlite"
|
|
|
|
|
+~~~~
|
|
|
|
|
+
|
|
|
|
|
+Pas de server struct aan en voeg een type `Message` toe, dit wordt onze database entiteit.
|
|
|
|
|
+~~~~
|
|
|
|
|
+type server struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ db *gorm.DB
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type Message struct {
|
|
|
|
|
+ ID uint
|
|
|
|
|
+ Name string
|
|
|
|
|
+ Content string
|
|
|
|
|
+}
|
|
|
|
|
+~~~~
|
|
|
|
|
+
|
|
|
|
|
+Voeg de volgende regels toe bovenaan je main()
|
|
|
|
|
+
|
|
|
|
|
+~~~~
|
|
|
|
|
+db, err := gorm.Open("sqlite3", "sqlite.db")
|
|
|
|
|
+if err != nil {
|
|
|
|
|
+ log.Fatal("Could not open database", err)
|
|
|
|
|
+}
|
|
|
|
|
+defer db.Close()
|
|
|
|
|
+
|
|
|
|
|
+db.DropTableIfExists(&Message{})
|
|
|
|
|
+db.AutoMigrate(&Message{})
|
|
|
|
|
+
|
|
|
|
|
+s := server{name: "Golang Hands-on", db: db}
|
|
|
|
|
+~~~~
|
|
|
|
|
+In je handler kan je nu een message als volgt opslaan.
|
|
|
|
|
+Zorg ervoor dat je de message vult met de waarden uit het json request.
|
|
|
|
|
+~~~~
|
|
|
|
|
+m := Message{Name: "naam", Content: "inhoud"}
|
|
|
|
|
+db.Create(&m)
|
|
|
|
|
+log.Println("message id", m.ID)
|
|
|
|
|
+~~~~
|
|
|
|
|
+Na de create kan je het ID opvragen waarmee het message is opgeslagen.
|
|
|
|
|
+Deze stuur je terug naar de client.
|
|
|
|
|
+
|
|
|
|
|
+### Opgave 10: data ophalen uit database
|
|
|
|
|
+
|
|
|
|
|
+Voor het laatste paasei gaan we data weer ophalen uit de database aan de hand van een id.
|
|
|
|
|
+De client zal een request doen naar `/api/store/<id>` en verwacht json object in hen volgende
|
|
|
|
|
+formaat als response:
|
|
|
|
|
+~~~~
|
|
|
|
|
+{name: "name", message: "message"}
|
|
|
|
|
+~~~~
|
|
|
|
|
+Gebruik voor het ophalen `First`, zie http://jinzhu.me/gorm/crud.html#query
|
|
|
|
|
+
|
|
|
|
|
+## Bonusopgaven
|
|
|
|
|
+
|
|
|
|
|
+* Implementeer https mbv https://golang.org/pkg/net/http/#ListenAndServeTLS
|
|
|
|
|
+* Implementeer Server Push https://blog.golang.org/h2push
|
|
|
|
|
+* Gebruik een locale MySQL of Postgres database ipv SQLite
|
|
|
|
|
+* Volg de Go Tour: https://tour.golang.org/
|
|
|
|
|
+
|
|
|
|
|
+## Resources
|
|
|
|
|
+* [golang.org/doc](https://golang.org/doc/) - alle go documentatie
|
|
|
|
|
+* [golang.org/pkg](https://golang.org/pkg/) - documentatie standard library
|
|
|
|
|
+* [godoc.org](https://godoc.org/) - documentatie externe packages
|
|
|
|
|
+* [jinzhu.me/gorm](http://jinzhu.me/gorm/) - gorm database mapper
|
|
|
|
|
+* [github.com/julienschmidt/httprouter](https://github.com/julienschmidt/httprouter) - meer flexibele url router
|
|
|
|
|
+
|