Go Home Page
Die Programmiersprache Go

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:

Es wird vorausgesetzt:

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.

Ich möchte "handler" nicht unübersetzt lassen, und im folgenden mit "Kümmerer" oder "Kümmerfunktion" wiedergeben. A.d.Ü.

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 &gt;, 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: