Writing Web Applications — Deutsche Übersetzung
- Das Original:
-
http://golang.org/doc/articles/wiki/
October 14, 2013 - Diese Übersetzung:
-
http://www.bitloeffel.de/DOC/golang/writewebapp_de.html
Stand: 17.02.2014
© 2014 Hans-Werner Heinzen @ Bitloeffel.de
Die Nutzung dieses Dokuments ist unter den Bedingungen der "Creative Commons Namensnennung 3.0 Unported"-Lizenz erlaubt. Für den Quellkode gilt eine BSD-Lizenz.
Wie man einen Netzdienst schreibt
Überblick
Folgende Themen werden behandelt:
- Erzeugen einer Datenstruktur mit Methoden zum Laden und Speichern
-
Erstellen von Netzdiensten mit dem
net/http
-Paket -
Verarbeiten von HTML-Schablonen mit dem
html/template
-Paket -
Validieren von Benutzereingaben mit dem
regexp
-Paket - Schließungen benutzen
Es wird vorausgesetzt:
- Programmiererfahrung
- Verständnis grundlegender Internet-Techniken (HTTP, HTML)
- Kenntnis einiger UNIX/DOS-Kommandos
Erste Schritte
Um mit Go arbeiten zu können, benötigen Sie aktuell einen Rechner mit FreeBSD,
Linux, OS X oder Windows.
Als Eingabeaufforderung wollen wir hier das Symbol $
verwenden.
Installieren Sie Go. (siehe: Installationsanleitung (de))
Legen Sie in Ihrem GOPATH
-Ordner einen neuen Unterordner an,
und wechseln Sie dorthin.
$ mkdir gowiki $ cd gowiki
Legen sie eine Datei wiki.go
an, öffnen diese mit einem
Editor Ihrer Wahl und schreiben folgende Zeilen:
package main import ( "fmt" "io/ioutil" )
Wir importieren also die Pakete fmt
und ioutil
aus der Go-Standardbibliothek. Wenn wir später die Funktionalität erweitern,
werden wir der import
-Deklaration weitere Pakete hinzufügen.
Datenstrukturen
Anfangen wollen wir mit der Datenstruktur. Ein Wiki besteht aus mehreren
miteinander verlinkten Seiten, die jeweils aus einem Titel und einem
Rumpf (dem Seiteninhalt) bestehen. Wir definieren also Page
als Struktur mit zwei Feldern, die für Titel und Rumpf stehen:
type Page struct { Title string Body []byte }
Der Typ []byte
bedeutet Byte-Slice.
(Mehr über Slices erfahren Sie im Artikel:
"
Slices: usage and internals".)
Das Element Body
ist ein []byte
und
kein string
, weil genau dieser Typ von den
io
-Bibliotheken erwartet wird, die wir benutzen wollen.
Die Struktur Page
beschreibt, wie die Daten der Seite
im Hauptspeicher aussehen, aber wie sieht's mit persistenter
Speicherung aus? Nun, die kriegen wir in den Griff mit einer Methode
save
auf die Struktur Page
:
func (p *Page) save() error { filename := p.Title + ".txt" return ioutil.WriteFile(filename, p.Body, 0600) }
Die Signatur der Methode besagt: "Dies ist eine Methode
namens save
mit einem Empfänger p
vom Typ Zeiger auf Page
. Sie hat keine Parameter
und gibt einen Wert vom Typ error
zurück."
Diese Methode wird den Rumpf (Body
) der Seite
(Page
) in einer Textdatei speichern. Um's einfach zu halten,
benutzen wir den Titel auch für den Dateinamen.
Die Methode save
gibt einen error
-Wert zurück,
weil das auch der Ergebniswert von ioutil.WriteFile
ist, einer
Funktion aus der Standardbibliothek, die ein Byte-Slice in eine
Datei schreibt. Sie gibt diesen Wert zurück,
damit die Anwendung reagieren kann, falls beim Schreiben der Datei
ein Fehler auftritt. Wenn alles gut geht, gibt Page.save
den Wert nil
zurück; das ist der Nullwert für Zeiger und
Interfaces und einige andere Typen.
Die Oktalzahl 0600
, das dritte Argument beim Aufruf
von WriteFile
, gibt an, dass die Datei mit Lese- und Schreibrecht
nur für den aktuellen Benutzer angelegt wird. (Einzelheiten: siehe
Unix-Handbuchseite für open(2)
)
Neben dem Speichern von Seiten, wollen wir auch Seiten laden können:
func loadPage(title string) *Page { filename := title + ".txt" body, _ := ioutil.ReadFile(filename) return &Page{Title: title, Body: body} }
Die Funktion loadPage
konstruiert den Dateinamen aus dem
Parameter title
, liest den Dateiinhalt in eine neue
Variable body
und gibt einen Zeiger auf ein
Page
-Literal zurück, das mit den passenden Werten für
Titel und Rumpf erzeugt wurde.
Funktionen können mehrere Ergebniswerte haben. Die Funktion
ioutil.ReadFile
aus der Standardbibliothek gibt
[]byte
und error
zurück.
In loadPage
haben wir bis jetzt den Fehler nicht berücksichtigt.
Stattdessen haben wir ihn mithilfe des "Leeren Bezeichners" (_
) ignoriert;
der Wert wurde ans "Nichts" gebunden.
Was aber geschieht, wenn bei ReadFile
ein Fehler auftritt?
Wenn die Datei gar nicht existiert? So einen Fehler dürfen
wir nicht ignorieren! Ändern wir also die Funktion, so dass sie
*Page
und error
zurückgibt.
func loadPage(title string) (*Page, error) { filename := title + ".txt" body, err := ioutil.ReadFile(filename) if err != nil { return nil, err } return &Page{Title: title, Body: body}, nil }
Rufende Funktionen sind damit in der Lage, den zweiten Ergebniswert zu prüfen
Ist er nil
, so wurde die Seite erfolgreich geladen. Ist er's nicht,
kann der Rufer auf error
reagieren. (Einzelheiten: siehe
Sprachbeschreibung
(de).)
Somit haben wir jetzt eine einfache Datenstruktur und die Fähigkeit, sie in
einer Datei zu speichern und von dort auch wieder zu laden. Kodieren wir also noch
die Funktion main
, um zu testen, was wir erreicht haben:
func main() { p1 := &Page{Title: "Testseite", Body: []byte("Dies ist eine Beispielseite.")} p1.save() p2, _ := loadPage("Testseite") fmt.Println(string(p2.Body)) }
Durch Umwandeln und Ausführen des Kodes wird eine Datei Testseite.txt
erzeugt, die den Inhalt von p1
enthält. Anschließend wird diese Datei
in die Struktur p2
eingelesen, und dann deren Body
-Element am
Bildschirm ausgegeben.
Umwandeln und Ausführen des Programms geht so:
$ go build wiki.go $ ./wiki Dies ist eine Beispielseite.
(Unter Windows müssen Sie den Aufruf "wiki
" ohne "./
" schreiben.)
Ein Klick hier zeigt den Kode, den wir bisher geschrieben haben.
Zwischenspiel: Das net/http
-Paket
Hier ein Beispiel für einen einfachen, aber voll funktionsfähigen Netzdienst [englisch: web server, A.d.Ü.]:
package main import ( "fmt" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hallo! Ich mag %s.", r.URL.Path[1:]) } func main() { http.HandleFunc("/", handler) http.ListenAndServe(":8080", nil) }
Die Funktion main
beginnt mit einem Aufruf von http.HandleFunc
,
der das http
-Paket anweist, auf alle Anfragen an das Wurzelverzeichnis
("/"
) mit dem handler
zu reagieren.
Dann folgt der Aufruf http.ListenAndServe
, der besagt: Horch auf allen
8080-Kanälen (":8080"
). (Und erstmal keine Sorge wegen des zweiten
Arguments, dem nil
.)
Diese Funktion blockiert solange, bis das Programm beendet wird.
Die Funktion handler
ist vom Typ http.HandlerFunc
.
Sie erwartet einen http.ResponseWriter
und ein *http.Request
als Argumente.
Ein http.ResponseWriter
baut die Antwort des HTTP-Servers zusammen;
indem wir ihm schreiben, senden wir Daten zum HTTP-Klienten.
Ein http.Request
ist eine Datenstruktur, die für die HTTP-Anfrage eines
Klienten steht. r.URL.Path
ist die Pfadkomponente der Anfrage-URL.
Das [1:]
am Ende bedeutet "erzeuge ein Teil-Slice aus Path
vom Element 1 bis zum Ende"; damit wird der einleitende Schrägstrich "/
"
vom Pfadnamen entfernt.
Wenn Sie das Programm starten, und dann die folgende URL besuchen:
http://localhost:8080/Äffchen
wird eine Seite präsentiert mit dem Inhalt:
Hallo! Ich mag Äffchen.
Wiki-Seiten servieren mit net/http
Wenn das net/http
-Paket benutzt werden soll, muss es importiert werden.
import ( "fmt" "io/ioutil" "net/http" )
Schreiben wir nun einen viewHandler
, der es Benutzern erlaubt,
eine Wiki-Seite anzuschauen. Er soll auf URLs reagieren, die mit /view/
beginnen.
func viewHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/view/"):] p, _ := loadPage(title) fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body) }
Als erstes extrahiert die Funktion den Seitentitel aus r.URL.Path
,
also der Pfad-Komponenten der Anfrage-URL.
Path
wird dann aufgeschnitten mit [len("/view/"):]
,
um das führende /view/
zu entfernen;
der Pfad beginnt hier nämlich immer mit /view/
, und
das gehört nicht zum Titel.
Danach lädt die Funktion die Daten der Seite, formatiert sie mit einem String
einfachen HTMLs und schreibt sie nach w
, dem http.ResponseWriter
.
Beachten Sie wieder, dass mit _
der Ergebniswert error
von loadPage
ignoriert wird. Das hält den Kode hier erstmal einfach,
wird aber generell als schlechter Stil angesehen. Wir kümmern wir uns später darum.
Um die Funktion zu benutzen, schreiben wir unsere main
-Funktion so um,
dass http
angewiesen wird, auf alle Anfragen zum Pfad /view/
mit einem Aufruf von viewHandler
zu reagieren.
func main() { http.HandleFunc("/view/", viewHandler) http.ListenAndServe(":8080", nil) }
Ein Klick hier zeigt den Kode, den wir bisher geschrieben haben.
Erzeugen wir nun Daten für eine Seite (in test.txt
), kompilieren unseren
Kode und versuchen, eine Wiki-Seite anzuzeigen.
Öffnen Sie eine neue Datei test.txt
mit Ihrem Editor, und speichern darin
den Text "Hallo, Ihr Ziesel." (ohne die Gänsefüßchen). Dann:
$ go build wiki.go $ ./wiki
(Unter Windows müssen Sie den Aufruf "wiki
" ohne "./
" schreiben.)
Während nun der Netzdienst läuft, sollte ein Besuch Ihres Browsers bei
http://localhost:8080/view/test
eine Seite mit der Überschrift "test" und den Worten "Hallo, Ihr Ziesel." anzeigen.
Seiten bearbeiten
Ein Wiki ist keins, wenn man damit nicht Seiten bearbeiten kann. Also kodieren wir
zwei neue Kümmerfunktionen: einen namens editHandler
,
der ein Formular zum Bearbeiten der Seite anzeigt, und einen namens
saveHandler
, der die eingegebenen Formulardaten speichert.
Zunächst fügen wir die Aufrufe in main()
ein:
func main() { http.HandleFunc("/view/", viewHandler) http.HandleFunc("/edit/", editHandler) http.HandleFunc("/save/", saveHandler) http.ListenAndServe(":8080", nil) }
Die Funktion editHandler
lädt die Daten der Seite
(oder erzeugt eine neue Page
-Struktur, wenn die Seite nicht existiert),
und zeigt ein HTML-Formular an.
func editHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/edit/"):] p, err := loadPage(title) if err != nil { p = &Page{Title: title} } fmt.Fprintf(w, "<h1>Bearbeiten %s</h1>"+ "<form action=\"/save/%s\" method=\"POST\">"+ "<textarea name=\"body\">%s</textarea><br>"+ "<input type=\"submit\" value=\"Speichern\">"+ "</form>", p.Title, p.Title, p.Body) }
Funktionieren würde das so schon, nur ist hartkodiertes HTML ziemlich hässlich. Und natürlich geht es auch besser.
Das html/template
-Paket
Das Paket html/template
ist Teil der Go-Standardbibliothek. Wir können
damit den HTML-Teil in einer getrennten Datei halten, was uns erlauben wird, das Layout
unserer Ändern-Seite zu bearbeiten, ohne den Go-Kode ändern zu müssen.
Zunächst müssen wir html/template
zur Liste der Importe hinzufügen.
Dagegen brauchen wir das fmt
-Paket nicht mehr; also entfernen wir es:
import ( "html/template" "io/ioutil" "net/http" )
Dann legen wir eine Schablonen-Datei [englisch: template file, A.d.Ü.] für das
HTML-Formular an. Öffnen Sie also eine neue Datei namens edit.html
und schreiben dort folgendes:
<h1>Bearbeiten {{.Title}}</h1> <form action="/save/{{.Title}}" method="POST"> <div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div> <div><input type="submit" value="Speichern"></div> </form>
Ändern Sie editHandler
jetzt so, dass er statt des hartkodierten HTML
die Schablone benutzt:
func editHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/edit/"):] p, err := loadPage(title) if err != nil { p = &Page{Title: title} } t, _ := template.ParseFiles("edit.html") t.Execute(w, p) }
Die Funktion template.ParseFiles
liest den Inhalt von edit.html
und gibt ein *template.Template
zurück.
Die Methode t.Execute
verarbeitet die Schablone und schreibt das generierte
HTML in den http.ResponseWriter
. Die "Punkt-Bezeichner"
.Title
und .Body
in der Schablone stehen für
p.Title
und p.Body
im Go-Kode.
Schablonenanweisungen werden von doppelten geschweiften Klammern eingeschlossen.
Die Anweisung printf "%s" .Body
ist ein Funktionsaufruf, der
.Body
als String statt als Bytestrom ausgibt (wie woanders ein
Aufruf von fmt.Printf
).
Das html/template
-Paket stellt sicher, dass mit Operationen auf
Schablonen nur sicheres, korrektes HTML generiert wird. Zum Beispiel ersetzt es jedes
"größer als"-Zeichen (>
) durch >
, damit
Benutzerdaten das HTML-Formular nicht korrumpieren können.
Und wo wir schon mit Schablonen arbeiten, erzeugen wir auch noch eine für unseren
viewHandler
und nennen sie view.html
:
<h1>{{.Title}}</h1> <p>[<a href="/edit/{{.Title}}">Bearbeiten</a>]</p> <div>{{printf "%s" .Body}}</div>
Ändern Sie viewHandler
entsprechend:
func viewHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/view/"):] p, _ := loadPage(title) t, _ := template.ParseFiles("view.html") t.Execute(w, p) }
Beachten Sie, dass für die Schablonen fast derselben Kode in beiden Kümmerfunktionen steht. Ziehen wir den doppelten Kode also in eine eigene Funktion:
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) { t, _ := template.ParseFiles(tmpl + ".html") t.Execute(w, p) }
Und dann benutzen wir diese Funktion:
func viewHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/view/"):] p, _ := loadPage(title) renderTemplate(w, "view", p) }
func editHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/edit/"):] p, err := loadPage(title) if err != nil { p = &Page{Title: title} } renderTemplate(w, "edit", p) }
Wenn wir jetzt noch in main
das Registrieren des noch nicht implementierten
saveHandler
auskommentieren, können wir erneut umwandeln und unser Programm
testen.
Ein Klick hier zeigt den Kode, den wir bisher geschrieben haben.
Nicht-existierende Seiten
Was passiert, wenn Sie eine
/view/SeiteDieNichtExistiert
besuchen?
Es erscheint eine Seite, die etwas HTML enthält. Das passiert deshalb, weil der
zurückgegebene Fehler von loadPage
ignoriert und dann
versucht wird, die Schablone ohne Daten zu füllen.
Es sollte aber, wenn es die angefragte Seite nicht gibt, auf die
Ändern-Seite umgeleitet werden, so dass der Klient den Inhalt erzeugen kann:
func viewHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/view/"):] p, err := loadPage(title) if err != nil { http.Redirect(w, r, "/edit/"+title, http.StatusFound) return } renderTemplate(w, "view", p) }
Die Funktion http.Redirect
[deutsch: umleiten, A.d.Ü.] fügt der
HTTP-Antwort den HTTP-Statuskode http.StatusFound
(302) und einen
Location
-Header hinzu.
Seiten speichern
Die Funktion saveHandler
wird sich um die abgeschickten Formulare der
Ändern-Seiten kümmern. Wir wollen die entsprechende Zeile in main
wieder aktivieren und den Kümmerer implementieren:
func saveHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/save/"):] body := r.FormValue("body") p := &Page{Title: title, Body: []byte(body)} p.save() http.Redirect(w, r, "/view/"+title, http.StatusFound) }
Der Seitentitel (Teil der URL) und das einzige Formularfeld Body
werden in eine neue Page
gesichert. Anschließend wird die
save
-Methode aufgerufen, um die Daten in einer Datei zu speichern,
und dann der Klient auf die /view/
-Seite umgeleitet.
Der Ergebniswert von FormValue
ist vom Typ string
.
Wir müssen ihn nach []byte
konvertieren, damit er zur Struktur
Page
passt; für die Konversion benutzen wir []byte(body)
.
Fehlerbehandlung
Noch werden Fehler an verschiedenen Stellen unseres Programms ignoriert. Das ist schlechter Programmierstil, vor allem weil im Fehlerfall das Programm sich undefiniert verhält. Besser ist es, auf Fehler zu reagieren und sie dem Klienten zu melden. So wird, wenn etwas schief geht, der Dienst so arbeiten, wie wir das wollen, und außerdem kann der Benutzer benachrichtigt werden.
Kümmern wir uns zunächst um Fehler in renderTemplate
:
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) { t, err := template.ParseFiles(tmpl + ".html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } err = t.Execute(w, p) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }
Die Funktion http.Error
sendet einen ausgewählten
HTTP-Antwortkode (in diesem Fall "Internal Server Error") sowie die
Fehlermeldung. Und schon hat sich bezahlt gemacht, dass wir
renderTemplate
ausgelagert haben.
Jetzt bringen wir noch Ordnung in den saveHandler
:
func saveHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/save/"):] body := r.FormValue("body") p := &Page{Title: title, Body: []byte(body)} err := p.save() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/view/"+title, http.StatusFound) }
Damit wird jeder Fehler, der während p.save()
auftritt, an den
Klienten gemeldet.
Schablonen zwischenspeichern
Unser Kode ist nicht effizient: renderTemplate
ruft
jedesmal, wenn eine Seite wiedergegeben wird, ParseFiles
auf.
Besser wäre, ParseFiles
nur einmal zu Programmbeginn zu rufen,
um alle Schablonen in einem *Template
zwischenzuspeichern.
Mit der Methode
ExecuteTemplate
können wir dann Schablonen einzeln wiedergeben.
Dafür erzeugen wir eine globale Variable namens templates
,
die wir mit ParseFiles
initialisieren.
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
template.Must
ist eine bequeme Hüllfunktion, die in
Panik verfällt, wenn sie für den error
einen Wert ungleich
nil
erhält. Panik ist hier angemessen: wenn die Schablonen
nicht geladen werden können, ist Programmabbruch das einzig Sinnvolle.
Die Funktion ParseFiles
nimmt beliebig viele String-Argumente
entgegen, welche unsere Schablonendateien benennen. Sie zergliedert die
Dateien und erzeugt gleichnamige Schablonen. Wollten wir
dem Programm weitere Schablonen hinzufügen, würden wir sie als weitere
Argumente an ParseFiles
übergeben.
Dann ändern wir noch renderTemplate
, so dass sie die
templates.ExecuteTemplate
-Methode mit dem jeweiligen
Schablonennamen ruft:
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) { err := templates.ExecuteTemplate(w, tmpl+".html", p) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }
Der Schablonenname entspricht dem Schablonen-Dateinamen, so dass wir nur
".html"
an das Argument tmpl
anhängen müssen.
Gültigkeitsprüfung
Sie haben vielleicht bemerkt, dass unser Programm eine schwere Sicherheitslücke hat: ein Klient kann einen beliebigen Pfad angeben und dann dort auf dem Dienstrechner lesen und schreiben. Um das einzuschränken, schreiben wir eine Funktion, in welcher der Titel mithilfe eines Regulären Ausdrucks geprüft wird.
Ergänzen Sie zunächst "regexp"
in der import
-Liste.
Dann erzeugen Sie eine globale Variable, die das Ergebnis unseres
Prüf-Ausdrucks aufnimmt:
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
Die Funktion regexp.MustCompile
analysiert und wandelt den
Regulären Ausdruck um, und gibt einen Wert vom Typ regexp.Regexp
zurück. MustCompile
wird panisch, wenn die Umwandlung des
Ausdrucks fehlschlägt, Compile
hingegen würde als zweiten Wert einen error
zurückgeben.
Schreiben wir nun eine Funktion, die den validPath
-Ausdruck
benutzt, um den Pfad zu prüfen und den Seitentitel zu extrahieren:
func getTitle(w http.ResponseWriter, r *http.Request) (string, error) {
m := validPath.FindStringSubmatch(r.URL.Path)
if m == nil {
http.NotFound(w, r)
return "", errors.New("Seitentitel ungültig")
}
return m[2], nil // m[2] enthält den Titel.
}
Ist der Titel gültig, so wird er (zusammen mit einem
nil
-Fehlerwert) zurückgegeben. Ist der Titel ungültig, so
schickt die Funktion einen Fehler "404 Not Found" an HTTP,
und gibt einen error
zurück. Um diesen erzeugen zu können,
müssen wir das Paket errors
importieren.
In jeder der Kümmerfunktionen wird jetzt getTitle
gerufen:
func viewHandler(w http.ResponseWriter, r *http.Request) { title, err := getTitle(w, r) if err != nil { return } p, err := loadPage(title) if err != nil { http.Redirect(w, r, "/edit/"+title, http.StatusFound) return } renderTemplate(w, "view", p) }
func editHandler(w http.ResponseWriter, r *http.Request) { title, err := getTitle(w, r) if err != nil { return } p, err := loadPage(title) if err != nil { p = &Page{Title: title} } renderTemplate(w, "edit", p) }
func saveHandler(w http.ResponseWriter, r *http.Request) { title, err := getTitle(w, r) if err != nil { return } body := r.FormValue("body") p := &Page{Title: title, Body: []byte(body)} err = p.save() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/view/"+title, http.StatusFound) }
Einführung in Funktionsliterale und Schließung
Das Abfangen des Fehlers in jeder Kümmerfunktion führte zu Kodewiederholung. Wie wär's, wenn wir sie alle in eine Hüllfunktion einbinden könnten, welche die Gültigkeitsprüfung und Fehlerabfrage übernimmt? Nun, die Funktionsliterale (de) in Go sind ein mächtiges Werkzeug zum Abstrahieren von Funktionalität; sie können uns hier helfen.
Zuerst ergänzen wir die Funktionsdeklarationen der Kümmerer, so dass sie auch einen Titel-String erwarten:
func viewHandler(w http.ResponseWriter, r *http.Request, title string) func editHandler(w http.ResponseWriter, r *http.Request, title string) func saveHandler(w http.ResponseWriter, r *http.Request, title string)
Dann definieren wir eine Hüllfunktion, die eine Funktion des obigen
Typs erwartet und eine Funktion vom Typ http.HandlerFunc
zurückgibt, die wiederum als Argument zum Aufruf von
http.HandleFunc
taugt:
func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Hier werden wir aus Request den Titel extrahieren // und den bereitgestellten Handler 'fn' aufrufen } }
Die zurückgegebene Funktion wird als Schließung bezeichnet, weil sie
Werte mit einschließt, die außerhalb definiert wurden. In diesem Fall
wird die Variable fn
(das Übergabeargument an
makeHandler
) eingeschlossen. Die Variable enthält je eine
unserer Kümmerfunktionen (save|edit|viewHandler).
Wir können jetzt den Kode aus getTitle
nehmen und leicht
verändert hier weiterverwenden:
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { m := validPath.FindStringSubmatch(r.URL.Path) if m == nil { http.NotFound(w, r) return } fn(w, r, m[2]) } }
Die von makeHandler
zurückgegebene Schließung ist eine Funktion,
die einen http.ResponseWriter
und ein *http.Request
erwartet, also eine Funktion vom Typ http.HandlerFunc
.
Die Schließung extrahiert den Titel aus dem Anfrage-Pfad und prüft ihn mit
dem regulären Ausdruck in validPath
. Ist der Titel kein
gültiger, so wird mit der http.NotFound
-Funktion ein Fehler
an ResponseWriter
geschickt. Ist der Titel gültig, so wird die
mitgegebene Funktion fn
gerufen, mit den Argumenten
ResponseWriter
, Request
und title
.
Jetzt können wir in main
alle Kümmerer in die
makeHandler
-Funktion einpacken, bevor sie beim
http
-Paket angemeldet werden:
func main() { http.HandleFunc("/view/", makeHandler(viewHandler)) http.HandleFunc("/edit/", makeHandler(editHandler)) http.HandleFunc("/save/", makeHandler(saveHandler)) http.ListenAndServe(":8080", nil) }
Schließlich entfernen wir bei allen Kümmerern die
getTitle
-Aufrufe, wodurch sie deutlich einfacher werden:
func viewHandler(w http.ResponseWriter, r *http.Request, title string) { p, err := loadPage(title) if err != nil { http.Redirect(w, r, "/edit/"+title, http.StatusFound) return } renderTemplate(w, "view", p) }
func editHandler(w http.ResponseWriter, r *http.Request, title string) { p, err := loadPage(title) if err != nil { p = &Page{Title: title} } renderTemplate(w, "edit", p) }
func saveHandler(w http.ResponseWriter, r *http.Request, title string) { body := r.FormValue("body") p := &Page{Title: title, Body: []byte(body)} err := p.save() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/view/"+title, http.StatusFound) }
Probieren Sie's aus!
Ein Klick hier zeigt den endgültigen Kode.
Wandeln Sie erneut um und starten dann die Anwendung:
$ go build wiki.go $ ./wiki
Ein Besuch bei http://localhost:8080/view/EineNeueSeite sollte Ihnen das Ändern-Formular präsentieren. Sie sollten Text eingeben können, und mit einem Klick auf 'Speichern' zur gerade erstellten Seite umgeleitet werden.
Hausaufgaben
Hier ein paar Aufgaben, die Sie vielleicht selbst angehen möchten:
-
Speichern Sie Schablonen in
tmpl/
und Daten indata/
. -
Ergänzen Sie einen Kümmerer, der eine Anfrage ans Wurzelverzeichnis nach
/view/ErsteSeite
umleitet. - Schmücken Sie die Schablonen mit gültigem HTML und ein paar CSS-Regeln aus.
-
Implementieren Sie Links innerhalb des Wiki durch Umformen jedes Auftretens von
[Seitenname]
in<a href="/view/Seitenname">Seitenname</a>
.
(Tipp: Benutzen Sieregexp.ReplaceAllFunc
.)