From 78d9a6074d0dce10c10246c02567655764d89188 Mon Sep 17 00:00:00 2001 From: Andrew Gerrand Date: Wed, 28 Apr 2010 12:36:39 +1000 Subject: [PATCH] Wiki codelab, complete with tests. R=r, rsc, gri CC=golang-dev https://golang.org/cl/887045 --- doc/codelab/wiki/Makefile | 29 + doc/codelab/wiki/edit.html | 6 + doc/codelab/wiki/final-noclosure.go | 100 +++ doc/codelab/wiki/final-noerror.go | 52 ++ doc/codelab/wiki/final-parsetemplate.go | 90 +++ doc/codelab/wiki/final-template.go | 64 ++ doc/codelab/wiki/final.go | 93 +++ doc/codelab/wiki/htmlify.go | 12 + doc/codelab/wiki/http-sample.go | 15 + doc/codelab/wiki/index.html | 997 ++++++++++++++++++++++++ doc/codelab/wiki/notemplate.go | 55 ++ doc/codelab/wiki/part1-noerror.go | 30 + doc/codelab/wiki/part1.go | 33 + doc/codelab/wiki/part2.go | 40 + doc/codelab/wiki/srcextract.go | 73 ++ doc/codelab/wiki/test.sh | 24 + doc/codelab/wiki/test_Test.txt.good | 1 + doc/codelab/wiki/test_edit.good | 6 + doc/codelab/wiki/test_view.good | 5 + doc/codelab/wiki/view.html | 5 + doc/codelab/wiki/wiki.html | 783 +++++++++++++++++++ 21 files changed, 2513 insertions(+) create mode 100644 doc/codelab/wiki/Makefile create mode 100644 doc/codelab/wiki/edit.html create mode 100644 doc/codelab/wiki/final-noclosure.go create mode 100644 doc/codelab/wiki/final-noerror.go create mode 100644 doc/codelab/wiki/final-parsetemplate.go create mode 100644 doc/codelab/wiki/final-template.go create mode 100644 doc/codelab/wiki/final.go create mode 100644 doc/codelab/wiki/htmlify.go create mode 100644 doc/codelab/wiki/http-sample.go create mode 100644 doc/codelab/wiki/index.html create mode 100644 doc/codelab/wiki/notemplate.go create mode 100644 doc/codelab/wiki/part1-noerror.go create mode 100644 doc/codelab/wiki/part1.go create mode 100644 doc/codelab/wiki/part2.go create mode 100644 doc/codelab/wiki/srcextract.go create mode 100755 doc/codelab/wiki/test.sh create mode 100644 doc/codelab/wiki/test_Test.txt.good create mode 100644 doc/codelab/wiki/test_edit.good create mode 100644 doc/codelab/wiki/test_view.good create mode 100644 doc/codelab/wiki/view.html create mode 100644 doc/codelab/wiki/wiki.html diff --git a/doc/codelab/wiki/Makefile b/doc/codelab/wiki/Makefile new file mode 100644 index 0000000000..76ab5c5bca --- /dev/null +++ b/doc/codelab/wiki/Makefile @@ -0,0 +1,29 @@ +# Copyright 2010 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +include ../../../src/Make.$(GOARCH) + +all: index.html + +# ugly hack to deal with whitespaces in $GOROOT +nullstring := +space := $(nullstring) # a space at the end +QUOTED_GOROOT:=$(subst $(space),\ ,$(GOROOT)) + +include $(QUOTED_GOROOT)/src/Make.common + +CLEANFILES+=index.html srcextract.bin htmlify.bin + +index.html: srcextract.bin htmlify.bin + awk '/^!/{system(substr($$0,2)); next} {print}' "$$@" < wiki.html > index.html + +test: final.bin + ./test.sh + rm -f final.6 final.bin + +%.bin: %.$O + $(QUOTED_GOBIN)/$(LD) -o $@ $< +%.$O: + $(QUOTED_GOBIN)/$(GC) $*.go + diff --git a/doc/codelab/wiki/edit.html b/doc/codelab/wiki/edit.html new file mode 100644 index 0000000000..71a919496f --- /dev/null +++ b/doc/codelab/wiki/edit.html @@ -0,0 +1,6 @@ +

Editing {title}

+ +
+
+
+
diff --git a/doc/codelab/wiki/final-noclosure.go b/doc/codelab/wiki/final-noclosure.go new file mode 100644 index 0000000000..d4ce715606 --- /dev/null +++ b/doc/codelab/wiki/final-noclosure.go @@ -0,0 +1,100 @@ +package main + +import ( + "http" + "io/ioutil" + "os" + "regexp" + "template" +) + +type page struct { + title string + body []byte +} + +func (p *page) save() os.Error { + filename := p.title + ".txt" + return ioutil.WriteFile(filename, p.body, 0600) +} + +func loadPage(title string) (*page, os.Error) { + filename := title + ".txt" + body, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return &page{title: title, body: body}, nil +} + +func viewHandler(c *http.Conn, r *http.Request) { + title, err := getTitle(c, r) + if err != nil { + return + } + p, err := loadPage(title) + if err != nil { + http.Redirect(c, "/edit/"+title, http.StatusFound) + return + } + renderTemplate(c, "view", p) +} + +func editHandler(c *http.Conn, r *http.Request) { + title, err := getTitle(c, r) + if err != nil { + return + } + p, err := loadPage(title) + if err != nil { + p = &page{title: title} + } + renderTemplate(c, "edit", p) +} + +func saveHandler(c *http.Conn, r *http.Request) { + title, err := getTitle(c, r) + if err != nil { + return + } + body := r.FormValue("body") + p := &page{title: title, body: []byte(body)} + err = p.save() + if err != nil { + http.Error(c, err.String(), http.StatusInternalServerError) + return + } + http.Redirect(c, "/view/"+title, http.StatusFound) +} + +func renderTemplate(c *http.Conn, tmpl string, p *page) { + t, err := template.ParseFile(tmpl+".html", nil) + if err != nil { + http.Error(c, err.String(), http.StatusInternalServerError) + return + } + err = t.Execute(p, c) + if err != nil { + http.Error(c, err.String(), http.StatusInternalServerError) + } +} + +const lenPath = len("/view/") + +var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$") + +func getTitle(c *http.Conn, r *http.Request) (title string, err os.Error) { + title = r.URL.Path[lenPath:] + if !titleValidator.MatchString(title) { + http.NotFound(c, r) + err = os.NewError("Invalid Page Title") + } + return +} + +func main() { + http.HandleFunc("/view/", viewHandler) + http.HandleFunc("/edit/", editHandler) + http.HandleFunc("/save/", saveHandler) + http.ListenAndServe(":8080", nil) +} diff --git a/doc/codelab/wiki/final-noerror.go b/doc/codelab/wiki/final-noerror.go new file mode 100644 index 0000000000..3b699452a9 --- /dev/null +++ b/doc/codelab/wiki/final-noerror.go @@ -0,0 +1,52 @@ +package main + +import ( + "http" + "io/ioutil" + "os" + "template" +) + +type page struct { + title string + body []byte +} + +func (p *page) save() os.Error { + filename := p.title + ".txt" + return ioutil.WriteFile(filename, p.body, 0600) +} + +func loadPage(title string) (*page, os.Error) { + filename := title + ".txt" + body, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return &page{title: title, body: body}, nil +} + +const lenPath = len("/view/") + +func editHandler(c *http.Conn, r *http.Request) { + title := r.URL.Path[lenPath:] + p, err := loadPage(title) + if err != nil { + p = &page{title: title} + } + t, _ := template.ParseFile("edit.html", nil) + t.Execute(p, c) +} + +func viewHandler(c *http.Conn, r *http.Request) { + title := r.URL.Path[lenPath:] + p, _ := loadPage(title) + t, _ := template.ParseFile("view.html", nil) + t.Execute(p, c) +} + +func main() { + http.HandleFunc("/view/", viewHandler) + http.HandleFunc("/edit/", editHandler) + http.ListenAndServe(":8080", nil) +} diff --git a/doc/codelab/wiki/final-parsetemplate.go b/doc/codelab/wiki/final-parsetemplate.go new file mode 100644 index 0000000000..93b956b9d1 --- /dev/null +++ b/doc/codelab/wiki/final-parsetemplate.go @@ -0,0 +1,90 @@ +package main + +import ( + "http" + "io/ioutil" + "os" + "regexp" + "template" +) + +type page struct { + title string + body []byte +} + +func (p *page) save() os.Error { + filename := p.title + ".txt" + return ioutil.WriteFile(filename, p.body, 0600) +} + +func loadPage(title string) (*page, os.Error) { + filename := title + ".txt" + body, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return &page{title: title, body: body}, nil +} + +func viewHandler(c *http.Conn, r *http.Request, title string) { + p, err := loadPage(title) + if err != nil { + http.Redirect(c, "/edit/"+title, http.StatusFound) + return + } + renderTemplate(c, "view", p) +} + +func editHandler(c *http.Conn, r *http.Request, title string) { + p, err := loadPage(title) + if err != nil { + p = &page{title: title} + } + renderTemplate(c, "edit", p) +} + +func saveHandler(c *http.Conn, r *http.Request, title string) { + body := r.FormValue("body") + p := &page{title: title, body: []byte(body)} + err := p.save() + if err != nil { + http.Error(c, err.String(), http.StatusInternalServerError) + return + } + http.Redirect(c, "/view/"+title, http.StatusFound) +} + +func renderTemplate(c *http.Conn, tmpl string, p *page) { + t, err := template.ParseFile(tmpl+".html", nil) + if err != nil { + http.Error(c, err.String(), http.StatusInternalServerError) + return + } + err = t.Execute(p, c) + if err != nil { + http.Error(c, err.String(), http.StatusInternalServerError) + } +} + +const lenPath = len("/view/") + +var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$") + +func makeHandler(fn func(*http.Conn, *http.Request, string)) http.HandlerFunc { + return func(c *http.Conn, r *http.Request) { + title := r.URL.Path[lenPath:] + if !titleValidator.MatchString(title) { + http.NotFound(c, r) + return + } + fn(c, r, title) + } +} + +func main() { + http.HandleFunc("/view/", makeHandler(viewHandler)) + http.HandleFunc("/edit/", makeHandler(editHandler)) + http.HandleFunc("/save/", makeHandler(saveHandler)) + http.ListenAndServe(":8080", nil) +} diff --git a/doc/codelab/wiki/final-template.go b/doc/codelab/wiki/final-template.go new file mode 100644 index 0000000000..481cda1e69 --- /dev/null +++ b/doc/codelab/wiki/final-template.go @@ -0,0 +1,64 @@ +package main + +import ( + "http" + "io/ioutil" + "os" + "template" +) + +type page struct { + title string + body []byte +} + +func (p *page) save() os.Error { + filename := p.title + ".txt" + return ioutil.WriteFile(filename, p.body, 0600) +} + +func loadPage(title string) (*page, os.Error) { + filename := title + ".txt" + body, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return &page{title: title, body: body}, nil +} + +const lenPath = len("/view/") + +func editHandler(c *http.Conn, r *http.Request) { + title := r.URL.Path[lenPath:] + p, err := loadPage(title) + if err != nil { + p = &page{title: title} + } + renderTemplate(c, "view", p) +} + +func viewHandler(c *http.Conn, r *http.Request) { + title := r.URL.Path[lenPath:] + p, _ := loadPage(title) + renderTemplate(c, "edit", p) +} + +func saveHandler(c *http.Conn, r *http.Request) { + title := r.URL.Path[lenPath:] + body := r.FormValue("body") + p := &page{title: title, body: []byte(body)} + p.save() + http.Redirect(c, "/view/"+title, http.StatusFound) +} + +func renderTemplate(c *http.Conn, tmpl string, p *page) { + t, _ := template.ParseFile(tmpl+".html", nil) + t.Execute(p, c) +} + +func main() { + http.HandleFunc("/view/", viewHandler) + http.HandleFunc("/edit/", editHandler) + http.HandleFunc("/save/", saveHandler) + http.ListenAndServe(":8080", nil) +} diff --git a/doc/codelab/wiki/final.go b/doc/codelab/wiki/final.go new file mode 100644 index 0000000000..0186729c29 --- /dev/null +++ b/doc/codelab/wiki/final.go @@ -0,0 +1,93 @@ +package main + +import ( + "http" + "io/ioutil" + "os" + "regexp" + "template" +) + +type page struct { + title string + body []byte +} + +func (p *page) save() os.Error { + filename := p.title + ".txt" + return ioutil.WriteFile(filename, p.body, 0600) +} + +func loadPage(title string) (*page, os.Error) { + filename := title + ".txt" + body, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return &page{title: title, body: body}, nil +} + +func viewHandler(c *http.Conn, r *http.Request, title string) { + p, err := loadPage(title) + if err != nil { + http.Redirect(c, "/edit/"+title, http.StatusFound) + return + } + renderTemplate(c, "view", p) +} + +func editHandler(c *http.Conn, r *http.Request, title string) { + p, err := loadPage(title) + if err != nil { + p = &page{title: title} + } + renderTemplate(c, "edit", p) +} + +func saveHandler(c *http.Conn, r *http.Request, title string) { + body := r.FormValue("body") + p := &page{title: title, body: []byte(body)} + err := p.save() + if err != nil { + http.Error(c, err.String(), http.StatusInternalServerError) + return + } + http.Redirect(c, "/view/"+title, http.StatusFound) +} + +var templates = make(map[string]*template.Template) + +func init() { + for _, tmpl := range []string{"edit", "view"} { + templates[tmpl] = template.MustParseFile(tmpl+".html", nil) + } +} + +func renderTemplate(c *http.Conn, tmpl string, p *page) { + err := templates[tmpl].Execute(p, c) + if err != nil { + http.Error(c, err.String(), http.StatusInternalServerError) + } +} + +const lenPath = len("/view/") + +var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$") + +func makeHandler(fn func(*http.Conn, *http.Request, string)) http.HandlerFunc { + return func(c *http.Conn, r *http.Request) { + title := r.URL.Path[lenPath:] + if !titleValidator.MatchString(title) { + http.NotFound(c, r) + return + } + fn(c, r, title) + } +} + +func main() { + http.HandleFunc("/view/", makeHandler(viewHandler)) + http.HandleFunc("/edit/", makeHandler(editHandler)) + http.HandleFunc("/save/", makeHandler(saveHandler)) + http.ListenAndServe(":8080", nil) +} diff --git a/doc/codelab/wiki/htmlify.go b/doc/codelab/wiki/htmlify.go new file mode 100644 index 0000000000..4a52e077f2 --- /dev/null +++ b/doc/codelab/wiki/htmlify.go @@ -0,0 +1,12 @@ +package main + +import ( + "os" + "template" + "io/ioutil" +) + +func main() { + b, _ := ioutil.ReadAll(os.Stdin) + template.HTMLFormatter(os.Stdout, b, "") +} diff --git a/doc/codelab/wiki/http-sample.go b/doc/codelab/wiki/http-sample.go new file mode 100644 index 0000000000..11d5d78613 --- /dev/null +++ b/doc/codelab/wiki/http-sample.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "http" +) + +func handler(c *http.Conn, r *http.Request) { + fmt.Fprintf(c, "Hi there, I love %s!", r.URL.Path[1:]) +} + +func main() { + http.HandleFunc("/", handler) + http.ListenAndServe(":8080", nil) +} diff --git a/doc/codelab/wiki/index.html b/doc/codelab/wiki/index.html new file mode 100644 index 0000000000..171b9b74f2 --- /dev/null +++ b/doc/codelab/wiki/index.html @@ -0,0 +1,997 @@ +
+ +

Writing Web Applications

+ +

Introduction

+ +

+Covered in this codelab: +

+ + +

+Assumed knowledge: +

+ + +

Getting Started

+ +

+At present, you need to have a Linux, OS X, or FreeBSD machine to run Go. If +you don't have access to one, you could set up a Linux Virtual Machine (using +VirtualBox or similar) or a +Virtual +Private Server. +

+ +

+Install Go (see the Installation Instructions). +

+ +

+Make a new directory for this codelab and cd to it: +

+ +
+$ mkdir ~/gowiki
+$ cd ~/gowiki
+
+ +

+Create a file named wiki.go, open it in your favorite editor, and +add the following lines: +

+ +
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+)
+
+ +

+Both fmt and ioutil are built-in packages that +we'll be using. Later, as we implement additional functionality, we will add +more packages to this import declaration. +

+ +

Data Structures

+ +

+Let's start by defining the data structures. A wiki consists of a series of +interconnected pages, each of which has a title and a body (the page content). +Here, we define page as a struct with two fields representing +the title and body. +

+ +
+type page struct {
+	title	string
+	body	[]byte
+}
+
+ +

+The type []byte means "a byte slice". +(See Effective Go +for more on slices.) +The body element is a []byte rather than +string because that is the type expected by the io +libraries we will use, as you'll see below. +

+ +

+The page struct describes how page data will be stored in memory. +But what about persistent storage? We can address that by creating a +save method on page: +

+ +
+func (p *page) save() os.Error {
+	filename := p.title + ".txt"
+	return ioutil.WriteFile(filename, p.body, 0600)
+}
+
+ +

+This method's signature reads: "This is a method named save that +takes as its receiver p, a pointer to page . It takes +no parameters, and returns a value of type os.Error." +

+ +

+This method will save the page's body to a text +file. For simplicity, we will use the title as the file name. +

+ +

+The save method returns an os.Error value because +that is the return type of WriteFile (a standard library function +that writes a byte slice to a file). The save method returns the +error value, to let the application handle it should anything go wrong while +writing the file. If all goes well, page.save() will return +nil (the zero-value for pointers, interfaces, and some other +types). +

+ +

+The octal integer constant 0600, passed as the third parameter to +WriteFile, indicates that the file should be created with +read-write permissions for the current user only. (See the Unix man page +open(2) for details.) +

+ +

+We will want to load pages, too: +

+ +
+func loadPage(title string) *page {
+	filename := title + ".txt"
+	body, _ := ioutil.ReadFile(filename)
+	return &page{title: title, body: body}
+}
+
+ +

+The function loadPage constructs the file name from +title, reads the file's contents into a new +page, and returns a pointer to that new page. +

+ +

+Functions can return multiple values. The standard library function +io.ReadFile returns []byte and os.Error. +In loadPage, error isn't being handled yet; the "blank identifier" +represented by the underscore (_) symbol is used to throw away the +error return value (in essence, assigning the value to nothing). +

+ +

+But what happens if ReadFile encounters an error? For example, +the file might not exist. We should not ignore such errors. Let's modify the +function to return *page and os.Error. +

+ +
+func loadPage(title string) (*page, os.Error) {
+	filename := title + ".txt"
+	body, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+	return &page{title: title, body: body}, nil
+}
+
+ +

+Callers of this function can now check the second parameter; if it is +nil then it has succesfully loaded a page. If not, it will be an +os.Error that can be handled by the caller (see the os package documentation for +details). +

+ +

+At this point we have a simple data structure and the ability to save to and +load from a file. Let's write a main function to test what we've +written: +

+ +
+func main() {
+	p1 := &page{title: "TestPage", body: []byte("This is a sample page.")}
+	p1.save()
+	p2, _ := loadPage("TestPage")
+	fmt.Println(string(p2.body))
+}
+
+ +

+After compiling and executing this code, a file named TestPage.txt +would be created, containing the contents of p1. The file would +then be read into the struct p2, and its body element +printed to the screen. +

+ +

+You can compile and run the program like this: +

+ +
+$ 8g wiki.go
+$ 8l wiki.8
+$ ./8.out
+This is a sample page.
+
+ +

+(The 8g and 8l commands are applicable to +GOARCH=386. If you're on an amd64 system, +subtitute 6's for the 8's.) +

+ +

+Click here to view the code we've written so far. +

+ +

Introducing the http package (an interlude)

+ +

+Here's a full working example of a simple web server: +

+ +
+package main
+
+import (
+	"fmt"
+	"http"
+)
+
+func handler(c *http.Conn, r *http.Request) {
+	fmt.Fprintf(c, "Hi there, I love %s!", r.URL.Path[1:])
+}
+
+func main() {
+	http.HandleFunc("/", handler)
+	http.ListenAndServe(":8080", nil)
+}
+
+ +

+The main function begins with a call to +http.HandleFunc, which tells the http package to +handle all requests to the web root ("/") with +handler. +

+ +

+It then calls http.ListenAndServe, specifying that it should +listen on port 8080 on any interface (":8080"). (Don't +worry about its second parameter, nil, for now.) +This function will block until the program is terminated. +

+ +

+The function handler is of the type http.HandlerFunc. +It takes an http.Conn and http.Request as its +arguments. +

+ +

+An http.Conn is the server end of an HTTP connection; by writing +to it, we send data to the HTTP client. +

+ +

+An http.Request is a data structure that represents the client +HTTP request. The string r.URL.Path is the path component +of the request URL. The trailing [1:] means +"create a sub-slice of Path from the 1st character to the end." +This drops the leading "/" from the path name. +

+ +

+If you run this program and access the URL: +

+
http://localhost:8080/monkeys
+

+the program would present a page containing: +

+
Hi there, I love monkeys!
+ +

Using http to serve wiki pages

+ +

+To use the http package, it must be imported: +

+ +
+import (
+	"fmt"
+	"http"
+	"io/ioutil"
+)
+
+ +

+Let's create a handler to view a wiki page: +

+ +
+const lenPath = len("/view/")
+
+func viewHandler(c *http.Conn, r *http.Request) {
+	title := r.URL.Path[lenPath:]
+	p, _ := loadPage(title)
+	fmt.Fprintf(c, "<h1>%s</h1><div>%s</div>", p.title, p.body)
+}
+
+ +

+First, this function extracts the page title from r.URL.Path, +the path component of the request URL. The global constant +lenPath is the length of the leading "/view/" +component of the request path. +The Path is re-sliced with [lenPath:] to drop the +first 6 characters of the string. This is because the path will invariably +begin with "/view/", which is not part of the page title. +

+ +

+The function then loads the page data, formats the page with a string of simple +HTML, and writes it to c, the http.Conn. +

+ +

+Again, note the use of _ to ignore the os.Error +return value from loadPage. This is done here for simplicity +and generally considered bad practice. We will attend to this later. +

+ +

+To use this handler, we create a main function that +initializes http using the viewHandler to handle +any requests under the path /view/. +

+ +
+func main() {
+	http.HandleFunc("/view/", viewHandler)
+	http.ListenAndServe(":8080", nil)
+}
+
+ +

+Click here to view the code we've written so far. +

+ +

+Let's create some page data (as test.txt), compile our code, and +try serving a wiki page: +

+ +
+$ echo "Hello world" > test.txt
+$ 8g wiki.go
+$ 8l wiki.8
+$ ./8.out
+
+ +

+With this web server running, a visit to http://localhost:8080/view/test +should show a page titled "test" containing the words "Hello world". +

+ +

Editing pages

+ +

+A wiki is not a wiki without the ability to edit pages. Let's create two new +handlers: one named editHandler to display an 'edit page' form, +and the other named saveHandler to save the data entered via the +form. +

+ +

+First, we add them to main(): +

+ +
+func main() {
+	http.HandleFunc("/view/", viewHandler)
+	http.HandleFunc("/edit/", editHandler)
+	http.HandleFunc("/save/", saveHandler)
+	http.ListenAndServe(":8080", nil)
+}
+
+ +

+The function editHandler loads the page +(or, if it doesn't exist, create an empty page struct), +and displays an HTML form. +

+ +
+func editHandler(c *http.Conn, r *http.Request) {
+	title := r.URL.Path[lenPath:]
+	p, err := loadPage(title)
+	if err != nil {
+		p = &page{title: title}
+	}
+	fmt.Fprintf(c, "<h1>Editing %s</h1>"+
+		"<form action=\"/save/%s\" method=\"POST\">"+
+		"<textarea name=\"body\">%s</textarea><br>"+
+		"<input type=\"submit\" value=\"Save\">"+
+		"</form>",
+		p.title, p.title, p.body)
+}
+
+ +

+This function will work fine, but all that hard-coded HTML is ugly. +Of course, there is a better way. +

+ +

The template package

+ +

+The template package is part of the Go standard library. We can +use template to keep the HTML in a separate file, allowing +us to change the layout of our edit page without modifying the underlying Go +code. +

+ +

+First, we must add template to the list of imports: +

+ +
+import (
+	"http"
+	"io/ioutil"
+	"os"
+	"template"
+)
+
+ +

+Let's create a template file containg the HTML form. +Open a new file named edit.html, and add the following lines: +

+ +
+<h1>Editing {title}</h1>
+
+<form action="/save/{title}" method="POST">
+<div><textarea name="body" rows="20" cols="80">{body|html}</textarea></div>
+<div><input type="submit" value="Save"></div>
+</form>
+
+ +

+Modify editHandler to use the template, instead of the hard-coded +HTML: +

+ +
+func editHandler(c *http.Conn, r *http.Request) {
+	title := r.URL.Path[lenPath:]
+	p, err := loadPage(title)
+	if err != nil {
+		p = &page{title: title}
+	}
+	t, _ := template.ParseFile("edit.html", nil)
+	t.Execute(p, c)
+}
+
+ +

+The function template.ParseFile will read the contents of +edit.html and return a *template.Template. +

+ +

+The method t.Execute replaces all occurrences of +{title} and {body} with the values of +p.title and p.body, and writes the resultant +HTML to the http.Conn. +

+ +

+Note that we've used {body|html} in the above template. +The |html part asks the template engine to pass the value +body through the html formatter before outputting it, +which escapes HTML characters (such as replacing > with +&gt;). +This will prevent user data from corrupting the form HTML. +

+ +

+Now that we've removed the fmt.Sprintf statement, we can remove +"fmt" from the import list. +

+ +

+While we're working with templates, let's create a template for our +viewHandler called view.html: +

+ +
+<h1>{title}</h1>
+
+<p>[<a href="/edit/{title}">edit</a>]</p>
+
+<div>{body}</div>
+
+ +

+Modify viewHandler accordingly: +

+ +
+func viewHandler(c *http.Conn, r *http.Request) {
+	title := r.URL.Path[lenPath:]
+	p, _ := loadPage(title)
+	t, _ := template.ParseFile("view.html", nil)
+	t.Execute(p, c)
+}
+
+ +

+Notice that we've used almost exactly the same templating code in both +handlers. Let's remove this duplication by moving the templating code +to its own function: +

+ +
+func viewHandler(c *http.Conn, r *http.Request) {
+	title := r.URL.Path[lenPath:]
+	p, _ := loadPage(title)
+	renderTemplate(c, "edit", p)
+}
+
+func editHandler(c *http.Conn, r *http.Request) {
+	title := r.URL.Path[lenPath:]
+	p, err := loadPage(title)
+	if err != nil {
+		p = &page{title: title}
+	}
+	renderTemplate(c, "view", p)
+}
+
+func renderTemplate(c *http.Conn, tmpl string, p *page) {
+	t, _ := template.ParseFile(tmpl+".html", nil)
+	t.Execute(p, c)
+}
+
+ +

+The handlers are now shorter and simpler. +

+ +

Handling non-existent pages

+ +

+What if you visit /view/APageThatDoesntExist? The program will +crash. This is because it ignores the error return value from +loadPage. Instead, if the requested page doesn't exist, it should +redirect the client to the edit page so the content may be created: +

+ +
+func viewHandler(c *http.Conn, r *http.Request, title string) {
+	p, err := loadPage(title)
+	if err != nil {
+		http.Redirect(c, "/edit/"+title, http.StatusFound)
+		return
+	}
+	renderTemplate(c, "view", p)
+}
+
+ +

+The http.Redirect function adds an HTTP status code of +http.StatusFound (302) and a Location +header to the HTTP response. +

+ +

Saving pages

+ +

+The function saveHandler will handle the form submission. +

+ +
+func saveHandler(c *http.Conn, r *http.Request) {
+	title := r.URL.Path[lenPath:]
+	body := r.FormValue("body")
+	p := &page{title: title, body: []byte(body)}
+	p.save()
+	http.Redirect(c, "/view/"+title, http.StatusFound)
+}
+
+ +

+The page title (provided in the URL) and the form's only field, +body, are stored in a new page. +The save() method is then called to write the data to a file, +and the client is redirected to the /view/ page. +

+ +

+The value returned by FormValue is of type string. +We must convert that value to []byte before it will fit into +the page struct. We use []byte(body) to perform +the conversion. +

+ +

Error handling

+ +

+There are several places in our program where errors are being ignored. This +is bad practice, not least because when an error does occur the program will +crash. A better solution is to handle the errors and return an error message +to the user. That way if something does go wrong, the server will continue to +function and the user will be notified. +

+ +

+First, let's handle the errors in renderTemplate: +

+ +
+func renderTemplate(c *http.Conn, tmpl string, p *page) {
+	t, err := template.ParseFile(tmpl+".html", nil)
+	if err != nil {
+		http.Error(c, err.String(), http.StatusInternalServerError)
+		return
+	}
+	err = t.Execute(p, c)
+	if err != nil {
+		http.Error(c, err.String(), http.StatusInternalServerError)
+	}
+}
+
+ +

+The http.Error function sends a specified HTTP response code +(in this case "Internal Server Error") and error message. +Already the decision to put this in a separate function is paying off. +

+ +

+Now let's fix up saveHandler: +

+ +
+func saveHandler(c *http.Conn, r *http.Request, title string) {
+	body := r.FormValue("body")
+	p := &page{title: title, body: []byte(body)}
+	err := p.save()
+	if err != nil {
+		http.Error(c, err.String(), http.StatusInternalServerError)
+		return
+	}
+	http.Redirect(c, "/view/"+title, http.StatusFound)
+}
+
+ +

+Any errors that occur during p.save() will be reported +to the user. +

+ +

Template caching

+ +

+There is an inefficiency in this code: renderTemplate calls +ParseFile every time a page is rendered. +A better approach would be to call ParseFile once for each +template at program initialization, and store the resultant +*Template values in a data structure for later use. +

+ +

+First we create a global map named templates in which to store +our *Template values, keyed by string +(the template name): +

+ +
+var templates = make(map[string]*template.Template)
+
+ +

+Then we create an init function, which will be called before +main at program initialization. The function +template.MustParseFile is a convenience wrapper around +ParseFile that does not return an error code; instead, it panics +if an error is encountered. A panic is appropriate here; if the templates can't +be loaded the only sensible thing to do is exit the program. +

+func init() { + for _, tmpl := range []string{"edit", "view"} { + templates[tmpl] = template.MustParseFile(tmpl+".html", nil) + } +} + + +

+A for loop is used with a range statement to iterate +over an array constant containing the names of the templates we want parsed. +If we were to add more templates to our program, we would add their names to +that array. +

+ +

+We then modify our renderTemplate function to call +the Execute method on the appropriate Template from +templates: + +

+func renderTemplate(c *http.Conn, tmpl string, p *page) {
+	err := templates[tmpl].Execute(p, c)
+	if err != nil {
+		http.Error(c, err.String(), http.StatusInternalServerError)
+	}
+}
+
+ +

Validation

+ +

+As you may have observed, this program has a serious security flaw: a user +can supply an arbitrary path to be read/written on the server. To mitigate +this, we can write a function to validate the title with a regular expression. +

+ +

+First, add "regexp" to the import list. +Then we can create a global variable to store our validation regexp: +

+ +
+var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")
+
+ +

+The function regexp.MustCompile will parse and compile the +regular expression, and return a regexp.Regexp. +MustCompile, like template.MustParseFile, +is distinct from Compile in that it will panic if +the expression compilation fails, while Compile returns an +os.Error as a second parameter. +

+ +

+Now, let's write a function that extracts the title string from the request +URL, and tests it against our titleValidator expression: +

+ +
+func getTitle(c *http.Conn, r *http.Request) (title string, err os.Error) {
+	title = r.URL.Path[lenPath:]
+	if !titleValidator.MatchString(title) {
+		http.NotFound(c, r)
+		err = os.NewError("Invalid Page Title")
+	}
+	return
+}
+
+ +

+If the title is valid, it will be returned along with a nil +error value. If the title is invalid, the function will write a +"404 Not Found" error to the HTTP connection, and return an error to the +handler. +

+ +

+Let's put a call to getTitle in each of the handlers: +

+ +
+func viewHandler(c *http.Conn, r *http.Request) {
+	title, err := getTitle(c, r)
+	if err != nil {
+		return
+	}
+	p, err := loadPage(title)
+	if err != nil {
+		http.Redirect(c, "/edit/"+title, http.StatusFound)
+		return
+	}
+	renderTemplate(c, "view", p)
+}
+
+func editHandler(c *http.Conn, r *http.Request) {
+	title, err := getTitle(c, r)
+	if err != nil {
+		return
+	}
+	p, err := loadPage(title)
+	if err != nil {
+		p = &page{title: title}
+	}
+	renderTemplate(c, "edit", p)
+}
+
+func saveHandler(c *http.Conn, r *http.Request) {
+	title, err := getTitle(c, r)
+	if err != nil {
+		return
+	}
+	body := r.FormValue("body")
+	p := &page{title: title, body: []byte(body)}
+	err = p.save()
+	if err != nil {
+		http.Error(c, err.String(), http.StatusInternalServerError)
+		return
+	}
+	http.Redirect(c, "/view/"+title, http.StatusFound)
+}
+
+ +

Introducing Function Literals and Closures

+ +

+Catching the error condition in each handler introduces a lot of repeated code. +What if we could wrap each of the handlers in a function that does this +validation and error checking? Go's +function +literals provide a powerful means of abstracting functionality +that can help us here. +

+ +

+First, we re-write the function definition of each of the handlers to accept +a title string: +

+ +
+func viewHandler(c, *http.Conn, r *http.Request, title string)
+func editHandler(c, *http.Conn, r *http.Request, title string)
+func saveHandler(c, *http.Conn, r *http.Request, title string)
+
+ +

+Now let's define a wrapper function that takes a function of the above +type, and returns a function of type http.HandlerFunc +(suitable to be passed to the function http.HandleFunc): +

+ +
+func makeHandler(fn func (*http.Conn, *http.Request, string)) http.HandlerFunc {
+	return func(c *http.Conn, r *http.Request) {
+		// Here we will extract the page title from the Request,
+		// and call the provided handler 'fn'
+	}
+}
+
+ +

+The returned function is called a closure because it encloses values defined +outside of it. In this case, the variable fn (the single argument +to makeHandler) is enclosed by the closure. The variable +fn will be one of our save, edit, or view handlers. +

+ +

+Now we can take the code from getTitle and use it here +(with some minor modifications): +

+ +
+func makeHandler(fn func(*http.Conn, *http.Request, string)) http.HandlerFunc {
+	return func(c *http.Conn, r *http.Request) {
+		title := r.URL.Path[lenPath:]
+		if !titleValidator.MatchString(title) {
+			http.NotFound(c, r)
+			return
+		}
+		fn(c, r, title)
+	}
+}
+
+ +

+The closure returned by makeHandler is a function that takes +an http.Conn and http.Request (in other words, +an http.HandlerFunc). +The closure extracts the title from the request path, and +validates it with the titleValidator regexp. If the +title is invalid, an error will be written to the +Conn using the http.NotFound function. +If the title is valid, the enclosed handler function +fn will be called with the Conn, +Request, and title as arguments. +

+ +

+Now we can wwrap the handler functions with makeHandler in +main, before they are registered with the http +package: +

+ +
+func main() {
+	http.HandleFunc("/view/", makeHandler(viewHandler))
+	http.HandleFunc("/edit/", makeHandler(editHandler))
+	http.HandleFunc("/save/", makeHandler(saveHandler))
+	http.ListenAndServe(":8080", nil)
+}
+
+ +

+Finally we remove the calls to getTitle from the handler functions, +making them much simpler: +

+ +
+func viewHandler(c *http.Conn, r *http.Request, title string) {
+	p, err := loadPage(title)
+	if err != nil {
+		http.Redirect(c, "/edit/"+title, http.StatusFound)
+		return
+	}
+	renderTemplate(c, "view", p)
+}
+
+func editHandler(c *http.Conn, r *http.Request, title string) {
+	p, err := loadPage(title)
+	if err != nil {
+		p = &page{title: title}
+	}
+	renderTemplate(c, "edit", p)
+}
+
+func saveHandler(c *http.Conn, r *http.Request, title string) {
+	body := r.FormValue("body")
+	p := &page{title: title, body: []byte(body)}
+	err := p.save()
+	if err != nil {
+		http.Error(c, err.String(), http.StatusInternalServerError)
+		return
+	}
+	http.Redirect(c, "/view/"+title, http.StatusFound)
+}
+
+ +

Try it out!

+ +

+Click here to view the final code listing. +

+ +

+Recompile the code, and run the app: +

+ +
+$ 8g wiki.go
+$ 8l wiki.8
+$ ./8.out
+
+ +

+Visiting http://localhost:8080/ANewPage +should present you with the page edit form. You should then be able to +enter some text, click 'Save', and be redirected to the newly created page. +

+ +

Other tasks

+ +

+Here are some simple tasks you might want to tackle on your own: +

+ + + +
diff --git a/doc/codelab/wiki/notemplate.go b/doc/codelab/wiki/notemplate.go new file mode 100644 index 0000000000..a61d905e39 --- /dev/null +++ b/doc/codelab/wiki/notemplate.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "http" + "io/ioutil" + "os" +) + +type page struct { + title string + body []byte +} + +func (p *page) save() os.Error { + filename := p.title + ".txt" + return ioutil.WriteFile(filename, p.body, 0600) +} + +func loadPage(title string) (*page, os.Error) { + filename := title + ".txt" + body, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return &page{title: title, body: body}, nil +} + +const lenPath = len("/view/") + +func viewHandler(c *http.Conn, r *http.Request) { + title := r.URL.Path[lenPath:] + p, _ := loadPage(title) + fmt.Fprintf(c, "

%s

%s
", p.title, p.body) +} + +func editHandler(c *http.Conn, r *http.Request) { + title := r.URL.Path[lenPath:] + p, err := loadPage(title) + if err != nil { + p = &page{title: title} + } + fmt.Fprintf(c, "

Editing %s

"+ + "
"+ + "
"+ + ""+ + "
", + p.title, p.title, p.body) +} + +func main() { + http.HandleFunc("/view/", viewHandler) + http.HandleFunc("/edit/", editHandler) + http.ListenAndServe(":8080", nil) +} diff --git a/doc/codelab/wiki/part1-noerror.go b/doc/codelab/wiki/part1-noerror.go new file mode 100644 index 0000000000..39e8331e39 --- /dev/null +++ b/doc/codelab/wiki/part1-noerror.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" +) + +type page struct { + title string + body []byte +} + +func (p *page) save() os.Error { + filename := p.title + ".txt" + return ioutil.WriteFile(filename, p.body, 0600) +} + +func loadPage(title string) *page { + filename := title + ".txt" + body, _ := ioutil.ReadFile(filename) + return &page{title: title, body: body} +} + +func main() { + p1 := &page{title: "TestPage", body: []byte("This is a sample page.")} + p1.save() + p2 := loadPage("TestPage") + fmt.Println(string(p2.body)) +} diff --git a/doc/codelab/wiki/part1.go b/doc/codelab/wiki/part1.go new file mode 100644 index 0000000000..f3678baa51 --- /dev/null +++ b/doc/codelab/wiki/part1.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" +) + +type page struct { + title string + body []byte +} + +func (p *page) save() os.Error { + filename := p.title + ".txt" + return ioutil.WriteFile(filename, p.body, 0600) +} + +func loadPage(title string) (*page, os.Error) { + filename := title + ".txt" + body, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return &page{title: title, body: body}, nil +} + +func main() { + p1 := &page{title: "TestPage", body: []byte("This is a sample page.")} + p1.save() + p2, _ := loadPage("TestPage") + fmt.Println(string(p2.body)) +} diff --git a/doc/codelab/wiki/part2.go b/doc/codelab/wiki/part2.go new file mode 100644 index 0000000000..c2c29dc3bc --- /dev/null +++ b/doc/codelab/wiki/part2.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "http" + "io/ioutil" + "os" +) + +type page struct { + title string + body []byte +} + +func (p *page) save() os.Error { + filename := p.title + ".txt" + return ioutil.WriteFile(filename, p.body, 0600) +} + +func loadPage(title string) (*page, os.Error) { + filename := title + ".txt" + body, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return &page{title: title, body: body}, nil +} + +const lenPath = len("/view/") + +func viewHandler(c *http.Conn, r *http.Request) { + title := r.URL.Path[lenPath:] + p, _ := loadPage(title) + fmt.Fprintf(c, "

%s

%s
", p.title, p.body) +} + +func main() { + http.HandleFunc("/view/", viewHandler) + http.ListenAndServe(":8080", nil) +} diff --git a/doc/codelab/wiki/srcextract.go b/doc/codelab/wiki/srcextract.go new file mode 100644 index 0000000000..607375183c --- /dev/null +++ b/doc/codelab/wiki/srcextract.go @@ -0,0 +1,73 @@ +package main + +import ( + "bytes" + "flag" + "go/parser" + "go/printer" + "go/ast" + "log" + "os" +) + +var ( + srcFn = flag.String("src", "", "source filename") + getName = flag.String("name", "", "func/type name to output") + html = flag.Bool("html", true, "output HTML") + showPkg = flag.Bool("pkg", false, "show package in output") +) + +func main() { + // handle input + flag.Parse() + if *srcFn == "" || *getName == "" { + flag.Usage() + os.Exit(2) + } + // load file + file, err := parser.ParseFile(*srcFn, nil, nil, 0) + if err != nil { + log.Exit(err) + } + // create printer + p := &printer.Config{ + Mode: 0, + Tabwidth: 8, + Styler: nil, + } + if *html { + p.Mode = printer.GenHTML + } + // create filter + filter := func(name string) bool { + return name == *getName + } + // filter + if !ast.FilterFile(file, filter) { + os.Exit(1) + } + b := new(bytes.Buffer) + p.Fprint(b, file) + // drop package declaration + if !*showPkg { + for { + c, err := b.ReadByte() + if c == '\n' || err != nil { + break + } + } + } + // drop leading newlines + for { + b, err := b.ReadByte() + if err != nil { + break + } + if b != '\n' { + os.Stdout.Write([]byte{b}) + break + } + } + // output + b.WriteTo(os.Stdout) +} diff --git a/doc/codelab/wiki/test.sh b/doc/codelab/wiki/test.sh new file mode 100755 index 0000000000..5b752fe3c2 --- /dev/null +++ b/doc/codelab/wiki/test.sh @@ -0,0 +1,24 @@ +#1/bin/bash + +./final.bin & +wiki_pid=$! + +cleanup() { + kill $wiki_pid + rm -f test_*.out Test.txt + exit ${1:-1} +} +trap cleanup INT + +sleep 1 + +curl -s -o test_edit.out http://localhost:8080/edit/Test +cmp test_edit.out test_edit.good || cleanup 1 +curl -s -o /dev/null -d body=some%20content http://localhost:8080/save/Test +cmp Test.txt test_Test.txt.good || cleanup 1 +curl -s -o test_view.out http://localhost:8080/view/Test +cmp test_view.out test_view.good || cleanup 1 + +echo "Passed" +cleanup 0 + diff --git a/doc/codelab/wiki/test_Test.txt.good b/doc/codelab/wiki/test_Test.txt.good new file mode 100644 index 0000000000..f0eec86f61 --- /dev/null +++ b/doc/codelab/wiki/test_Test.txt.good @@ -0,0 +1 @@ +some content \ No newline at end of file diff --git a/doc/codelab/wiki/test_edit.good b/doc/codelab/wiki/test_edit.good new file mode 100644 index 0000000000..36c6dbb732 --- /dev/null +++ b/doc/codelab/wiki/test_edit.good @@ -0,0 +1,6 @@ +

Editing Test

+ +
+
+
+
diff --git a/doc/codelab/wiki/test_view.good b/doc/codelab/wiki/test_view.good new file mode 100644 index 0000000000..07e8edb22e --- /dev/null +++ b/doc/codelab/wiki/test_view.good @@ -0,0 +1,5 @@ +

Test

+ +

[edit]

+ +
some content
diff --git a/doc/codelab/wiki/view.html b/doc/codelab/wiki/view.html new file mode 100644 index 0000000000..a46622d01f --- /dev/null +++ b/doc/codelab/wiki/view.html @@ -0,0 +1,5 @@ +

{title}

+ +

[edit]

+ +
{body}
diff --git a/doc/codelab/wiki/wiki.html b/doc/codelab/wiki/wiki.html new file mode 100644 index 0000000000..b3525f73b6 --- /dev/null +++ b/doc/codelab/wiki/wiki.html @@ -0,0 +1,783 @@ +
+ +

Writing Web Applications

+ +

Introduction

+ +

+Covered in this codelab: +

+ + +

+Assumed knowledge: +

+ + +

Getting Started

+ +

+At present, you need to have a Linux, OS X, or FreeBSD machine to run Go. If +you don't have access to one, you could set up a Linux Virtual Machine (using +VirtualBox or similar) or a +Virtual +Private Server. +

+ +

+Install Go (see the Installation Instructions). +

+ +

+Make a new directory for this codelab and cd to it: +

+ +
+$ mkdir ~/gowiki
+$ cd ~/gowiki
+
+ +

+Create a file named wiki.go, open it in your favorite editor, and +add the following lines: +

+ +
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+)
+
+ +

+Both fmt and ioutil are built-in packages that +we'll be using. Later, as we implement additional functionality, we will add +more packages to this import declaration. +

+ +

Data Structures

+ +

+Let's start by defining the data structures. A wiki consists of a series of +interconnected pages, each of which has a title and a body (the page content). +Here, we define page as a struct with two fields representing +the title and body. +

+ +
+!./srcextract.bin -src=part1.go -name=page
+
+ +

+The type []byte means "a byte slice". +(See Effective Go +for more on slices.) +The body element is a []byte rather than +string because that is the type expected by the io +libraries we will use, as you'll see below. +

+ +

+The page struct describes how page data will be stored in memory. +But what about persistent storage? We can address that by creating a +save method on page: +

+ +
+!./srcextract.bin -src=part1.go -name=save
+
+ +

+This method's signature reads: "This is a method named save that +takes as its receiver p, a pointer to page . It takes +no parameters, and returns a value of type os.Error." +

+ +

+This method will save the page's body to a text +file. For simplicity, we will use the title as the file name. +

+ +

+The save method returns an os.Error value because +that is the return type of WriteFile (a standard library function +that writes a byte slice to a file). The save method returns the +error value, to let the application handle it should anything go wrong while +writing the file. If all goes well, page.save() will return +nil (the zero-value for pointers, interfaces, and some other +types). +

+ +

+The octal integer constant 0600, passed as the third parameter to +WriteFile, indicates that the file should be created with +read-write permissions for the current user only. (See the Unix man page +open(2) for details.) +

+ +

+We will want to load pages, too: +

+ +
+!./srcextract.bin -src=part1-noerror.go -name=loadPage
+
+ +

+The function loadPage constructs the file name from +title, reads the file's contents into a new +page, and returns a pointer to that new page. +

+ +

+Functions can return multiple values. The standard library function +io.ReadFile returns []byte and os.Error. +In loadPage, error isn't being handled yet; the "blank identifier" +represented by the underscore (_) symbol is used to throw away the +error return value (in essence, assigning the value to nothing). +

+ +

+But what happens if ReadFile encounters an error? For example, +the file might not exist. We should not ignore such errors. Let's modify the +function to return *page and os.Error. +

+ +
+!./srcextract.bin -src=part1.go -name=loadPage
+
+ +

+Callers of this function can now check the second parameter; if it is +nil then it has succesfully loaded a page. If not, it will be an +os.Error that can be handled by the caller (see the os package documentation for +details). +

+ +

+At this point we have a simple data structure and the ability to save to and +load from a file. Let's write a main function to test what we've +written: +

+ +
+!./srcextract.bin -src=part1.go -name=main
+
+ +

+After compiling and executing this code, a file named TestPage.txt +would be created, containing the contents of p1. The file would +then be read into the struct p2, and its body element +printed to the screen. +

+ +

+You can compile and run the program like this: +

+ +
+$ 8g wiki.go
+$ 8l wiki.8
+$ ./8.out
+This is a sample page.
+
+ +

+(The 8g and 8l commands are applicable to +GOARCH=386. If you're on an amd64 system, +subtitute 6's for the 8's.) +

+ +

+Click here to view the code we've written so far. +

+ +

Introducing the http package (an interlude)

+ +

+Here's a full working example of a simple web server: +

+ +
+!./htmlify.bin < http-sample.go
+
+ +

+The main function begins with a call to +http.HandleFunc, which tells the http package to +handle all requests to the web root ("/") with +handler. +

+ +

+It then calls http.ListenAndServe, specifying that it should +listen on port 8080 on any interface (":8080"). (Don't +worry about its second parameter, nil, for now.) +This function will block until the program is terminated. +

+ +

+The function handler is of the type http.HandlerFunc. +It takes an http.Conn and http.Request as its +arguments. +

+ +

+An http.Conn is the server end of an HTTP connection; by writing +to it, we send data to the HTTP client. +

+ +

+An http.Request is a data structure that represents the client +HTTP request. The string r.URL.Path is the path component +of the request URL. The trailing [1:] means +"create a sub-slice of Path from the 1st character to the end." +This drops the leading "/" from the path name. +

+ +

+If you run this program and access the URL: +

+
http://localhost:8080/monkeys
+

+the program would present a page containing: +

+
Hi there, I love monkeys!
+ +

Using http to serve wiki pages

+ +

+To use the http package, it must be imported: +

+ +
+import (
+	"fmt"
+	"http"
+	"io/ioutil"
+)
+
+ +

+Let's create a handler to view a wiki page: +

+ +
+!./srcextract.bin -src=part2.go -name=lenPath
+
+!./srcextract.bin -src=part2.go -name=viewHandler
+
+ +

+First, this function extracts the page title from r.URL.Path, +the path component of the request URL. The global constant +lenPath is the length of the leading "/view/" +component of the request path. +The Path is re-sliced with [lenPath:] to drop the +first 6 characters of the string. This is because the path will invariably +begin with "/view/", which is not part of the page title. +

+ +

+The function then loads the page data, formats the page with a string of simple +HTML, and writes it to c, the http.Conn. +

+ +

+Again, note the use of _ to ignore the os.Error +return value from loadPage. This is done here for simplicity +and generally considered bad practice. We will attend to this later. +

+ +

+To use this handler, we create a main function that +initializes http using the viewHandler to handle +any requests under the path /view/. +

+ +
+!./srcextract.bin -src=part2.go -name=main
+
+ +

+Click here to view the code we've written so far. +

+ +

+Let's create some page data (as test.txt), compile our code, and +try serving a wiki page: +

+ +
+$ echo "Hello world" > test.txt
+$ 8g wiki.go
+$ 8l wiki.8
+$ ./8.out
+
+ +

+With this web server running, a visit to http://localhost:8080/view/test +should show a page titled "test" containing the words "Hello world". +

+ +

Editing pages

+ +

+A wiki is not a wiki without the ability to edit pages. Let's create two new +handlers: one named editHandler to display an 'edit page' form, +and the other named saveHandler to save the data entered via the +form. +

+ +

+First, we add them to main(): +

+ +
+!./srcextract.bin -src=final-noclosure.go -name=main
+
+ +

+The function editHandler loads the page +(or, if it doesn't exist, create an empty page struct), +and displays an HTML form. +

+ +
+!./srcextract.bin -src=notemplate.go -name=editHandler
+
+ +

+This function will work fine, but all that hard-coded HTML is ugly. +Of course, there is a better way. +

+ +

The template package

+ +

+The template package is part of the Go standard library. We can +use template to keep the HTML in a separate file, allowing +us to change the layout of our edit page without modifying the underlying Go +code. +

+ +

+First, we must add template to the list of imports: +

+ +
+import (
+	"http"
+	"io/ioutil"
+	"os"
+	"template"
+)
+
+ +

+Let's create a template file containg the HTML form. +Open a new file named edit.html, and add the following lines: +

+ +
+!./htmlify.bin < edit.html
+
+ +

+Modify editHandler to use the template, instead of the hard-coded +HTML: +

+ +
+!./srcextract.bin -src=final-noerror.go -name=editHandler
+
+ +

+The function template.ParseFile will read the contents of +edit.html and return a *template.Template. +

+ +

+The method t.Execute replaces all occurrences of +{title} and {body} with the values of +p.title and p.body, and writes the resultant +HTML to the http.Conn. +

+ +

+Note that we've used {body|html} in the above template. +The |html part asks the template engine to pass the value +body through the html formatter before outputting it, +which escapes HTML characters (such as replacing > with +&gt;). +This will prevent user data from corrupting the form HTML. +

+ +

+Now that we've removed the fmt.Sprintf statement, we can remove +"fmt" from the import list. +

+ +

+While we're working with templates, let's create a template for our +viewHandler called view.html: +

+ +
+!./htmlify.bin < view.html
+
+ +

+Modify viewHandler accordingly: +

+ +
+!./srcextract.bin -src=final-noerror.go -name=viewHandler
+
+ +

+Notice that we've used almost exactly the same templating code in both +handlers. Let's remove this duplication by moving the templating code +to its own function: +

+ +
+!./srcextract.bin -src=final-template.go -name=viewHandler
+
+!./srcextract.bin -src=final-template.go -name=editHandler
+
+!./srcextract.bin -src=final-template.go -name=renderTemplate
+
+ +

+The handlers are now shorter and simpler. +

+ +

Handling non-existent pages

+ +

+What if you visit /view/APageThatDoesntExist? The program will +crash. This is because it ignores the error return value from +loadPage. Instead, if the requested page doesn't exist, it should +redirect the client to the edit page so the content may be created: +

+ +
+!./srcextract.bin -src=final.go -name=viewHandler
+
+ +

+The http.Redirect function adds an HTTP status code of +http.StatusFound (302) and a Location +header to the HTTP response. +

+ +

Saving pages

+ +

+The function saveHandler will handle the form submission. +

+ +
+!./srcextract.bin -src=final-template.go -name=saveHandler
+
+ +

+The page title (provided in the URL) and the form's only field, +body, are stored in a new page. +The save() method is then called to write the data to a file, +and the client is redirected to the /view/ page. +

+ +

+The value returned by FormValue is of type string. +We must convert that value to []byte before it will fit into +the page struct. We use []byte(body) to perform +the conversion. +

+ +

Error handling

+ +

+There are several places in our program where errors are being ignored. This +is bad practice, not least because when an error does occur the program will +crash. A better solution is to handle the errors and return an error message +to the user. That way if something does go wrong, the server will continue to +function and the user will be notified. +

+ +

+First, let's handle the errors in renderTemplate: +

+ +
+!./srcextract.bin -src=final-parsetemplate.go -name=renderTemplate
+
+ +

+The http.Error function sends a specified HTTP response code +(in this case "Internal Server Error") and error message. +Already the decision to put this in a separate function is paying off. +

+ +

+Now let's fix up saveHandler: +

+ +
+!./srcextract.bin -src=final.go -name=saveHandler
+
+ +

+Any errors that occur during p.save() will be reported +to the user. +

+ +

Template caching

+ +

+There is an inefficiency in this code: renderTemplate calls +ParseFile every time a page is rendered. +A better approach would be to call ParseFile once for each +template at program initialization, and store the resultant +*Template values in a data structure for later use. +

+ +

+First we create a global map named templates in which to store +our *Template values, keyed by string +(the template name): +

+ +
+!./srcextract.bin -src=final.go -name=templates
+
+ +

+Then we create an init function, which will be called before +main at program initialization. The function +template.MustParseFile is a convenience wrapper around +ParseFile that does not return an error code; instead, it panics +if an error is encountered. A panic is appropriate here; if the templates can't +be loaded the only sensible thing to do is exit the program. +

+!./srcextract.bin -src=final.go -name=init + + +

+A for loop is used with a range statement to iterate +over an array constant containing the names of the templates we want parsed. +If we were to add more templates to our program, we would add their names to +that array. +

+ +

+We then modify our renderTemplate function to call +the Execute method on the appropriate Template from +templates: + +

+!./srcextract.bin -src=final.go -name=renderTemplate
+
+ +

Validation

+ +

+As you may have observed, this program has a serious security flaw: a user +can supply an arbitrary path to be read/written on the server. To mitigate +this, we can write a function to validate the title with a regular expression. +

+ +

+First, add "regexp" to the import list. +Then we can create a global variable to store our validation regexp: +

+ +
+!./srcextract.bin -src=final-noclosure.go -name=titleValidator
+
+ +

+The function regexp.MustCompile will parse and compile the +regular expression, and return a regexp.Regexp. +MustCompile, like template.MustParseFile, +is distinct from Compile in that it will panic if +the expression compilation fails, while Compile returns an +os.Error as a second parameter. +

+ +

+Now, let's write a function that extracts the title string from the request +URL, and tests it against our titleValidator expression: +

+ +
+!./srcextract.bin -src=final-noclosure.go -name=getTitle
+
+ +

+If the title is valid, it will be returned along with a nil +error value. If the title is invalid, the function will write a +"404 Not Found" error to the HTTP connection, and return an error to the +handler. +

+ +

+Let's put a call to getTitle in each of the handlers: +

+ +
+!./srcextract.bin -src=final-noclosure.go -name=viewHandler
+
+!./srcextract.bin -src=final-noclosure.go -name=editHandler
+
+!./srcextract.bin -src=final-noclosure.go -name=saveHandler
+
+ +

Introducing Function Literals and Closures

+ +

+Catching the error condition in each handler introduces a lot of repeated code. +What if we could wrap each of the handlers in a function that does this +validation and error checking? Go's +function +literals provide a powerful means of abstracting functionality +that can help us here. +

+ +

+First, we re-write the function definition of each of the handlers to accept +a title string: +

+ +
+func viewHandler(c, *http.Conn, r *http.Request, title string)
+func editHandler(c, *http.Conn, r *http.Request, title string)
+func saveHandler(c, *http.Conn, r *http.Request, title string)
+
+ +

+Now let's define a wrapper function that takes a function of the above +type, and returns a function of type http.HandlerFunc +(suitable to be passed to the function http.HandleFunc): +

+ +
+func makeHandler(fn func (*http.Conn, *http.Request, string)) http.HandlerFunc {
+	return func(c *http.Conn, r *http.Request) {
+		// Here we will extract the page title from the Request,
+		// and call the provided handler 'fn'
+	}
+}
+
+ +

+The returned function is called a closure because it encloses values defined +outside of it. In this case, the variable fn (the single argument +to makeHandler) is enclosed by the closure. The variable +fn will be one of our save, edit, or view handlers. +

+ +

+Now we can take the code from getTitle and use it here +(with some minor modifications): +

+ +
+!./srcextract.bin -src=final.go -name=makeHandler
+
+ +

+The closure returned by makeHandler is a function that takes +an http.Conn and http.Request (in other words, +an http.HandlerFunc). +The closure extracts the title from the request path, and +validates it with the titleValidator regexp. If the +title is invalid, an error will be written to the +Conn using the http.NotFound function. +If the title is valid, the enclosed handler function +fn will be called with the Conn, +Request, and title as arguments. +

+ +

+Now we can wwrap the handler functions with makeHandler in +main, before they are registered with the http +package: +

+ +
+!./srcextract.bin -src=final.go -name=main
+
+ +

+Finally we remove the calls to getTitle from the handler functions, +making them much simpler: +

+ +
+!./srcextract.bin -src=final.go -name=viewHandler
+
+!./srcextract.bin -src=final.go -name=editHandler
+
+!./srcextract.bin -src=final.go -name=saveHandler
+
+ +

Try it out!

+ +

+Click here to view the final code listing. +

+ +

+Recompile the code, and run the app: +

+ +
+$ 8g wiki.go
+$ 8l wiki.8
+$ ./8.out
+
+ +

+Visiting http://localhost:8080/ANewPage +should present you with the page edit form. You should then be able to +enter some text, click 'Save', and be redirected to the newly created page. +

+ +

Other tasks

+ +

+Here are some simple tasks you might want to tackle on your own: +

+ + + +