diff options
author | Andrew Gerrand <adg@golang.org> | 2010-04-28 12:36:39 +1000 |
---|---|---|
committer | Andrew Gerrand <adg@golang.org> | 2010-04-28 12:36:39 +1000 |
commit | 681cfebb0b23a188df4b79f6a83cec985dcbd019 (patch) | |
tree | d8eb642a4004568eac7fa8eccc1671568eb43301 /doc | |
parent | 7363ca610df6bb07c755cda57111d7ffad374e97 (diff) | |
download | golang-681cfebb0b23a188df4b79f6a83cec985dcbd019.tar.gz |
Wiki codelab, complete with tests.
R=r, rsc, gri
CC=golang-dev
http://codereview.appspot.com/887045
Diffstat (limited to 'doc')
-rw-r--r-- | doc/codelab/wiki/Makefile | 29 | ||||
-rw-r--r-- | doc/codelab/wiki/edit.html | 6 | ||||
-rw-r--r-- | doc/codelab/wiki/final-noclosure.go | 100 | ||||
-rw-r--r-- | doc/codelab/wiki/final-noerror.go | 52 | ||||
-rw-r--r-- | doc/codelab/wiki/final-parsetemplate.go | 90 | ||||
-rw-r--r-- | doc/codelab/wiki/final-template.go | 64 | ||||
-rw-r--r-- | doc/codelab/wiki/final.go | 93 | ||||
-rw-r--r-- | doc/codelab/wiki/htmlify.go | 12 | ||||
-rw-r--r-- | doc/codelab/wiki/http-sample.go | 15 | ||||
-rw-r--r-- | doc/codelab/wiki/index.html | 997 | ||||
-rw-r--r-- | doc/codelab/wiki/notemplate.go | 55 | ||||
-rw-r--r-- | doc/codelab/wiki/part1-noerror.go | 30 | ||||
-rw-r--r-- | doc/codelab/wiki/part1.go | 33 | ||||
-rw-r--r-- | doc/codelab/wiki/part2.go | 40 | ||||
-rw-r--r-- | doc/codelab/wiki/srcextract.go | 73 | ||||
-rwxr-xr-x | doc/codelab/wiki/test.sh | 24 | ||||
-rw-r--r-- | doc/codelab/wiki/test_Test.txt.good | 1 | ||||
-rw-r--r-- | doc/codelab/wiki/test_edit.good | 6 | ||||
-rw-r--r-- | doc/codelab/wiki/test_view.good | 5 | ||||
-rw-r--r-- | doc/codelab/wiki/view.html | 5 | ||||
-rw-r--r-- | doc/codelab/wiki/wiki.html | 783 |
21 files changed, 2513 insertions, 0 deletions
diff --git a/doc/codelab/wiki/Makefile b/doc/codelab/wiki/Makefile new file mode 100644 index 000000000..76ab5c5bc --- /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 000000000..71a919496 --- /dev/null +++ b/doc/codelab/wiki/edit.html @@ -0,0 +1,6 @@ +<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> diff --git a/doc/codelab/wiki/final-noclosure.go b/doc/codelab/wiki/final-noclosure.go new file mode 100644 index 000000000..d4ce71560 --- /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 000000000..3b699452a --- /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 000000000..93b956b9d --- /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 000000000..481cda1e6 --- /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 000000000..0186729c2 --- /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 000000000..4a52e077f --- /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 000000000..11d5d7861 --- /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 000000000..171b9b74f --- /dev/null +++ b/doc/codelab/wiki/index.html @@ -0,0 +1,997 @@ +<div class="content"> + +<h1>Writing Web Applications</h1> + +<h2>Introduction</h2> + +<p> +Covered in this codelab: +</p> +<ul> +<li>Creating a data structure with load and save methods</li> +<li>Using the <code>http</code> package to build web applications +<li>Using the <code>template</code> package to process HTML templates</li> +<li>Using the <code>regexp</code> package to validate user input</li> +<li>Using closures</li> +</ul> + +<p> +Assumed knowledge: +</p> +<ul> +<li>Programming experience</li> +<li>Understanding of basic web technologies (HTTP, HTML)</li> +<li>Some UNIX command-line knowledge</li> +</ul> + +<h2>Getting Started</h2> + +<p> +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 +<a href="http://www.virtualbox.org/">VirtualBox</a> or similar) or a +<a href="http://www.google.com/search?q=virtual+private+server">Virtual +Private Server</a>. +</p> + +<p> +Install Go (see the <a href="http://golang.org/doc/install.html">Installation Instructions</a>). +</p> + +<p> +Make a new directory for this codelab and cd to it: +</p> + +<pre> +$ mkdir ~/gowiki +$ cd ~/gowiki +</pre> + +<p> +Create a file named <code>wiki.go</code>, open it in your favorite editor, and +add the following lines: +</p> + +<pre> +package main + +import ( + "fmt" + "io/ioutil" +) +</pre> + +<p> +Both <code>fmt</code> and <code>ioutil</code> are built-in packages that +we'll be using. Later, as we implement additional functionality, we will add +more packages to this <code>import</code> declaration. +</p> + +<h2>Data Structures</h2> + +<p> +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 <code>page</code> as a struct with two fields representing +the title and body. +</p> + +<pre> +type page struct { + title string + body []byte +} +</pre> + +<p> +The type <code>[]byte</code> means "a <code>byte</code> slice". +(See <a href="http://golang.org/doc/effective_go.html#slices">Effective Go</a> +for more on slices.) +The <code>body</code> element is a <code>[]byte</code> rather than +<code>string</code> because that is the type expected by the <code>io</code> +libraries we will use, as you'll see below. +</p> + +<p> +The <code>page</code> struct describes how page data will be stored in memory. +But what about persistent storage? We can address that by creating a +<code>save</code> method on <code>page</code>: +</p> + +<pre> +func (p *page) save() os.Error { + filename := p.title + ".txt" + return ioutil.WriteFile(filename, p.body, 0600) +} +</pre> + +<p> +This method's signature reads: "This is a method named <code>save</code> that +takes as its receiver <code>p</code>, a pointer to <code>page</code> . It takes +no parameters, and returns a value of type <code>os.Error</code>." +</p> + +<p> +This method will save the <code>page</code>'s <code>body</code> to a text +file. For simplicity, we will use the <code>title</code> as the file name. +</p> + +<p> +The <code>save</code> method returns an <code>os.Error</code> value because +that is the return type of <code>WriteFile</code> (a standard library function +that writes a byte slice to a file). The <code>save</code> method returns the +error value, to let the application handle it should anything go wrong while +writing the file. If all goes well, <code>page.save()</code> will return +<code>nil</code> (the zero-value for pointers, interfaces, and some other +types). +</p> + +<p> +The octal integer constant <code>0600</code>, passed as the third parameter to +<code>WriteFile</code>, indicates that the file should be created with +read-write permissions for the current user only. (See the Unix man page +<code>open(2)</code> for details.) +</p> + +<p> +We will want to load pages, too: +</p> + +<pre> +func loadPage(title string) *page { + filename := title + ".txt" + body, _ := ioutil.ReadFile(filename) + return &page{title: title, body: body} +} +</pre> + +<p> +The function <code>loadPage</code> constructs the file name from +<code>title</code>, reads the file's contents into a new +<code>page</code>, and returns a pointer to that new <code>page</code>. +</p> + +<p> +Functions can return multiple values. The standard library function +<code>io.ReadFile</code> returns <code>[]byte</code> and <code>os.Error</code>. +In <code>loadPage</code>, error isn't being handled yet; the "blank identifier" +represented by the underscore (<code>_</code>) symbol is used to throw away the +error return value (in essence, assigning the value to nothing). +</p> + +<p> +But what happens if <code>ReadFile</code> encounters an error? For example, +the file might not exist. We should not ignore such errors. Let's modify the +function to return <code>*page</code> and <code>os.Error</code>. +</p> + +<pre> +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 +} +</pre> + +<p> +Callers of this function can now check the second parameter; if it is +<code>nil</code> then it has succesfully loaded a page. If not, it will be an +<code>os.Error</code> that can be handled by the caller (see the <a +href="http://golang.org/pkg/os/#Error">os package documentation</a> for +details). +</p> + +<p> +At this point we have a simple data structure and the ability to save to and +load from a file. Let's write a <code>main</code> function to test what we've +written: +</p> + +<pre> +func main() { + p1 := &page{title: "TestPage", body: []byte("This is a sample page.")} + p1.save() + p2, _ := loadPage("TestPage") + fmt.Println(string(p2.body)) +} +</pre> + +<p> +After compiling and executing this code, a file named <code>TestPage.txt</code> +would be created, containing the contents of <code>p1</code>. The file would +then be read into the struct <code>p2</code>, and its <code>body</code> element +printed to the screen. +</p> + +<p> +You can compile and run the program like this: +</p> + +<pre> +$ 8g wiki.go +$ 8l wiki.8 +$ ./8.out +This is a sample page. +</pre> + +<p> +(The <code>8g</code> and <code>8l</code> commands are applicable to +<code>GOARCH=386</code>. If you're on an <code>amd64</code> system, +subtitute 6's for the 8's.) +</p> + +<p> +<a href="part1.go">Click here to view the code we've written so far.</a> +</p> + +<h2>Introducing the <code>http</code> package (an interlude)</h2> + +<p> +Here's a full working example of a simple web server: +</p> + +<pre> +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) +} +</pre> + +<p> +The <code>main</code> function begins with a call to +<code>http.HandleFunc</code>, which tells the <code>http</code> package to +handle all requests to the web root (<code>"/"</code>) with +<code>handler</code>. +</p> + +<p> +It then calls <code>http.ListenAndServe</code>, specifying that it should +listen on port 8080 on any interface (<code>":8080"</code>). (Don't +worry about its second parameter, <code>nil</code>, for now.) +This function will block until the program is terminated. +</p> + +<p> +The function <code>handler</code> is of the type <code>http.HandlerFunc</code>. +It takes an <code>http.Conn</code> and <code>http.Request</code> as its +arguments. +</p> + +<p> +An <code>http.Conn</code> is the server end of an HTTP connection; by writing +to it, we send data to the HTTP client. +</p> + +<p> +An <code>http.Request</code> is a data structure that represents the client +HTTP request. The string <code>r.URL.Path</code> is the path component +of the request URL. The trailing <code>[1:]</code> means +"create a sub-slice of <code>Path</code> from the 1st character to the end." +This drops the leading "/" from the path name. +</p> + +<p> +If you run this program and access the URL: +</p> +<pre>http://localhost:8080/monkeys</pre> +<p> +the program would present a page containing: +</p> +<pre>Hi there, I love monkeys!</pre> + +<h2>Using <code>http</code> to serve wiki pages</h2> + +<p> +To use the <code>http</code> package, it must be imported: +</p> + +<pre> +import ( + "fmt" + <b>"http"</b> + "io/ioutil" +) +</pre> + +<p> +Let's create a handler to view a wiki page: +</p> + +<pre> +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) +} +</pre> + +<p> +First, this function extracts the page title from <code>r.URL.Path</code>, +the path component of the request URL. The global constant +<code>lenPath</code> is the length of the leading <code>"/view/"</code> +component of the request path. +The <code>Path</code> is re-sliced with <code>[lenPath:]</code> to drop the +first 6 characters of the string. This is because the path will invariably +begin with <code>"/view/"</code>, which is not part of the page title. +</p> + +<p> +The function then loads the page data, formats the page with a string of simple +HTML, and writes it to <code>c</code>, the <code>http.Conn</code>. +</p> + +<p> +Again, note the use of <code>_</code> to ignore the <code>os.Error</code> +return value from <code>loadPage</code>. This is done here for simplicity +and generally considered bad practice. We will attend to this later. +</p> + +<p> +To use this handler, we create a <code>main</code> function that +initializes <code>http</code> using the <code>viewHandler</code> to handle +any requests under the path <code>/view/</code>. +</p> + +<pre> +func main() { + http.HandleFunc("/view/", viewHandler) + http.ListenAndServe(":8080", nil) +} +</pre> + +<p> +<a href="part2.go">Click here to view the code we've written so far.</a> +</p> + +<p> +Let's create some page data (as <code>test.txt</code>), compile our code, and +try serving a wiki page: +</p> + +<pre> +$ echo "Hello world" > test.txt +$ 8g wiki.go +$ 8l wiki.8 +$ ./8.out +</pre> + +<p> +With this web server running, a visit to <code><a +href="http://localhost:8080/view/test">http://localhost:8080/view/test</a></code> +should show a page titled "test" containing the words "Hello world". +</p> + +<h2>Editing pages</h2> + +<p> +A wiki is not a wiki without the ability to edit pages. Let's create two new +handlers: one named <code>editHandler</code> to display an 'edit page' form, +and the other named <code>saveHandler</code> to save the data entered via the +form. +</p> + +<p> +First, we add them to <code>main()</code>: +</p> + +<pre> +func main() { + http.HandleFunc("/view/", viewHandler) + http.HandleFunc("/edit/", editHandler) + http.HandleFunc("/save/", saveHandler) + http.ListenAndServe(":8080", nil) +} +</pre> + +<p> +The function <code>editHandler</code> loads the page +(or, if it doesn't exist, create an empty <code>page</code> struct), +and displays an HTML form. +</p> + +<pre> +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) +} +</pre> + +<p> +This function will work fine, but all that hard-coded HTML is ugly. +Of course, there is a better way. +</p> + +<h2>The <code>template</code> package</h2> + +<p> +The <code>template</code> package is part of the Go standard library. We can +use <code>template</code> to keep the HTML in a separate file, allowing +us to change the layout of our edit page without modifying the underlying Go +code. +</p> + +<p> +First, we must add <code>template</code> to the list of imports: +</p> + +<pre> +import ( + "http" + "io/ioutil" + "os" + <b>"template"</b> +) +</pre> + +<p> +Let's create a template file containg the HTML form. +Open a new file named <code>edit.html</code>, and add the following lines: +</p> + +<pre> +<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> +</pre> + +<p> +Modify <code>editHandler</code> to use the template, instead of the hard-coded +HTML: +</p> + +<pre> +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) +} +</pre> + +<p> +The function <code>template.ParseFile</code> will read the contents of +<code>edit.html</code> and return a <code>*template.Template</code>. +</p> + +<p> +The method <code>t.Execute</code> replaces all occurrences of +<code>{title}</code> and <code>{body}</code> with the values of +<code>p.title</code> and <code>p.body</code>, and writes the resultant +HTML to the <code>http.Conn</code>. +</p> + +<p> +Note that we've used <code>{body|html}</code> in the above template. +The <code>|html</code> part asks the template engine to pass the value +<code>body</code> through the <code>html</code> formatter before outputting it, +which escapes HTML characters (such as replacing <code>></code> with +<code>&gt;</code>). +This will prevent user data from corrupting the form HTML. +</p> + +<p> +Now that we've removed the <code>fmt.Sprintf</code> statement, we can remove +<code>"fmt"</code> from the <code>import</code> list. +</p> + +<p> +While we're working with templates, let's create a template for our +<code>viewHandler</code> called <code>view.html</code>: +</p> + +<pre> +<h1>{title}</h1> + +<p>[<a href="/edit/{title}">edit</a>]</p> + +<div>{body}</div> +</pre> + +<p> +Modify <code>viewHandler</code> accordingly: +</p> + +<pre> +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) +} +</pre> + +<p> +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: +</p> + +<pre> +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) +} +</pre> + +<p> +The handlers are now shorter and simpler. +</p> + +<h2>Handling non-existent pages</h2> + +<p> +What if you visit <code>/view/APageThatDoesntExist</code>? The program will +crash. This is because it ignores the error return value from +<code>loadPage</code>. Instead, if the requested page doesn't exist, it should +redirect the client to the edit page so the content may be created: +</p> + +<pre> +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) +} +</pre> + +<p> +The <code>http.Redirect</code> function adds an HTTP status code of +<code>http.StatusFound</code> (302) and a <code>Location</code> +header to the HTTP response. +</p> + +<h2>Saving pages</h2> + +<p> +The function <code>saveHandler</code> will handle the form submission. +</p> + +<pre> +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) +} +</pre> + +<p> +The page title (provided in the URL) and the form's only field, +<code>body</code>, are stored in a new <code>page</code>. +The <code>save()</code> method is then called to write the data to a file, +and the client is redirected to the <code>/view/</code> page. +</p> + +<p> +The value returned by <code>FormValue</code> is of type <code>string</code>. +We must convert that value to <code>[]byte</code> before it will fit into +the <code>page</code> struct. We use <code>[]byte(body)</code> to perform +the conversion. +</p> + +<h2>Error handling</h2> + +<p> +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. +</p> + +<p> +First, let's handle the errors in <code>renderTemplate</code>: +</p> + +<pre> +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) + } +} +</pre> + +<p> +The <code>http.Error</code> 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. +</p> + +<p> +Now let's fix up <code>saveHandler</code>: +</p> + +<pre> +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) +} +</pre> + +<p> +Any errors that occur during <code>p.save()</code> will be reported +to the user. +</p> + +<h2>Template caching</h2> + +<p> +There is an inefficiency in this code: <code>renderTemplate</code> calls +<code>ParseFile</code> every time a page is rendered. +A better approach would be to call <code>ParseFile</code> once for each +template at program initialization, and store the resultant +<code>*Template</code> values in a data structure for later use. +</p> + +<p> +First we create a global map named <code>templates</code> in which to store +our <code>*Template</code> values, keyed by <code>string</code> +(the template name): +</p> + +<pre> +var templates = make(map[string]*template.Template) +</pre> + +<p> +Then we create an <code>init</code> function, which will be called before +<code>main</code> at program initialization. The function +<code>template.MustParseFile</code> is a convenience wrapper around +<code>ParseFile</code> 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. +</p + +<pre> +func init() { + for _, tmpl := range []string{"edit", "view"} { + templates[tmpl] = template.MustParseFile(tmpl+".html", nil) + } +} +</pre> + +<p> +A <code>for</code> loop is used with a <code>range</code> 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. +</p> + +<p> +We then modify our <code>renderTemplate</code> function to call +the <code>Execute</code> method on the appropriate <code>Template</code> from +<code>templates</code>: + +<pre> +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) + } +} +</pre> + +<h2>Validation</h2> + +<p> +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. +</p> + +<p> +First, add <code>"regexp"</code> to the <code>import</code> list. +Then we can create a global variable to store our validation regexp: +</p> + +<pre> +var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$") +</pre> + +<p> +The function <code>regexp.MustCompile</code> will parse and compile the +regular expression, and return a <code>regexp.Regexp</code>. +<code>MustCompile</code>, like <code>template.MustParseFile</code>, +is distinct from <code>Compile</code> in that it will panic if +the expression compilation fails, while <code>Compile</code> returns an +<code>os.Error</code> as a second parameter. +</p> + +<p> +Now, let's write a function that extracts the title string from the request +URL, and tests it against our <code>titleValidator</code> expression: +</p> + +<pre> +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 +} +</pre> + +<p> +If the title is valid, it will be returned along with a <code>nil</code> +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. +</p> + +<p> +Let's put a call to <code>getTitle</code> in each of the handlers: +</p> + +<pre> +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) +} +</pre> + +<h2>Introducing Function Literals and Closures</h2> + +<p> +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 +<a href="http://golang.org/doc/go_spec.html#Function_declarations">function +literals</a> provide a powerful means of abstracting functionality +that can help us here. +</p> + +<p> +First, we re-write the function definition of each of the handlers to accept +a title string: +</p> + +<pre> +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) +</pre> + +<p> +Now let's define a wrapper function that <i>takes a function of the above +type</i>, and returns a function of type <code>http.HandlerFunc</code> +(suitable to be passed to the function <code>http.HandleFunc</code>): +</p> + +<pre> +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' + } +} +</pre> + +<p> +The returned function is called a closure because it encloses values defined +outside of it. In this case, the variable <code>fn</code> (the single argument +to <code>makeHandler</code>) is enclosed by the closure. The variable +<code>fn</code> will be one of our save, edit, or view handlers. +</p> + +<p> +Now we can take the code from <code>getTitle</code> and use it here +(with some minor modifications): +</p> + +<pre> +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) + } +} +</pre> + +<p> +The closure returned by <code>makeHandler</code> is a function that takes +an <code>http.Conn</code> and <code>http.Request</code> (in other words, +an <code>http.HandlerFunc</code>). +The closure extracts the <code>title</code> from the request path, and +validates it with the <code>titleValidator</code> regexp. If the +<code>title</code> is invalid, an error will be written to the +<code>Conn</code> using the <code>http.NotFound</code> function. +If the <code>title</code> is valid, the enclosed handler function +<code>fn</code> will be called with the <code>Conn</code>, +<code>Request</code>, and <code>title</code> as arguments. +</p> + +<p> +Now we can wwrap the handler functions with <code>makeHandler</code> in +<code>main</code>, before they are registered with the <code>http</code> +package: +</p> + +<pre> +func main() { + http.HandleFunc("/view/", makeHandler(viewHandler)) + http.HandleFunc("/edit/", makeHandler(editHandler)) + http.HandleFunc("/save/", makeHandler(saveHandler)) + http.ListenAndServe(":8080", nil) +} +</pre> + +<p> +Finally we remove the calls to <code>getTitle</code> from the handler functions, +making them much simpler: +</p> + +<pre> +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) +} +</pre> + +<h2>Try it out!</h2> + +<p> +<a href="final.go">Click here to view the final code listing.</a> +</p> + +<p> +Recompile the code, and run the app: +</p> + +<pre> +$ 8g wiki.go +$ 8l wiki.8 +$ ./8.out +</pre> + +<p> +Visiting <a href="http://localhost:8080/ANewPage">http://localhost:8080/ANewPage</a> +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. +</p> + +<h2>Other tasks</h2> + +<p> +Here are some simple tasks you might want to tackle on your own: +</p> + +<ul> +<li>Store templates in <code>tmpl/</code> and page data in <code>data/</code>. +<li>Add a handler to make the web root redirect to + <code>/view/FrontPage</code>.</li> +<li>Spruce up the page templates by making them valid HTML and adding some + CSS rules.</li> +<li>Implement inter-page linking by converting instances of + <code>[PageName]</code> to <br> + <code><a href="/view/PageName">PageName</a></code>. + (hint: you could use <code>regexp.ReplaceAllFunc</code> to do this) + </li> +</ul> + +</div> diff --git a/doc/codelab/wiki/notemplate.go b/doc/codelab/wiki/notemplate.go new file mode 100644 index 000000000..a61d905e3 --- /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, "<h1>%s</h1><div>%s</div>", 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, "<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) +} + +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 000000000..39e8331e3 --- /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 000000000..f3678baa5 --- /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 000000000..c2c29dc3b --- /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, "<h1>%s</h1><div>%s</div>", 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 000000000..607375183 --- /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 000000000..5b752fe3c --- /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 000000000..f0eec86f6 --- /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 000000000..36c6dbb73 --- /dev/null +++ b/doc/codelab/wiki/test_edit.good @@ -0,0 +1,6 @@ +<h1>Editing Test</h1> + +<form action="/save/Test" method="POST"> +<div><textarea name="body" rows="20" cols="80"></textarea></div> +<div><input type="submit" value="Save"></div> +</form> diff --git a/doc/codelab/wiki/test_view.good b/doc/codelab/wiki/test_view.good new file mode 100644 index 000000000..07e8edb22 --- /dev/null +++ b/doc/codelab/wiki/test_view.good @@ -0,0 +1,5 @@ +<h1>Test</h1> + +<p>[<a href="/edit/Test">edit</a>]</p> + +<div>some content</div> diff --git a/doc/codelab/wiki/view.html b/doc/codelab/wiki/view.html new file mode 100644 index 000000000..a46622d01 --- /dev/null +++ b/doc/codelab/wiki/view.html @@ -0,0 +1,5 @@ +<h1>{title}</h1> + +<p>[<a href="/edit/{title}">edit</a>]</p> + +<div>{body}</div> diff --git a/doc/codelab/wiki/wiki.html b/doc/codelab/wiki/wiki.html new file mode 100644 index 000000000..b3525f73b --- /dev/null +++ b/doc/codelab/wiki/wiki.html @@ -0,0 +1,783 @@ +<div class="content"> + +<h1>Writing Web Applications</h1> + +<h2>Introduction</h2> + +<p> +Covered in this codelab: +</p> +<ul> +<li>Creating a data structure with load and save methods</li> +<li>Using the <code>http</code> package to build web applications +<li>Using the <code>template</code> package to process HTML templates</li> +<li>Using the <code>regexp</code> package to validate user input</li> +<li>Using closures</li> +</ul> + +<p> +Assumed knowledge: +</p> +<ul> +<li>Programming experience</li> +<li>Understanding of basic web technologies (HTTP, HTML)</li> +<li>Some UNIX command-line knowledge</li> +</ul> + +<h2>Getting Started</h2> + +<p> +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 +<a href="http://www.virtualbox.org/">VirtualBox</a> or similar) or a +<a href="http://www.google.com/search?q=virtual+private+server">Virtual +Private Server</a>. +</p> + +<p> +Install Go (see the <a href="http://golang.org/doc/install.html">Installation Instructions</a>). +</p> + +<p> +Make a new directory for this codelab and cd to it: +</p> + +<pre> +$ mkdir ~/gowiki +$ cd ~/gowiki +</pre> + +<p> +Create a file named <code>wiki.go</code>, open it in your favorite editor, and +add the following lines: +</p> + +<pre> +package main + +import ( + "fmt" + "io/ioutil" +) +</pre> + +<p> +Both <code>fmt</code> and <code>ioutil</code> are built-in packages that +we'll be using. Later, as we implement additional functionality, we will add +more packages to this <code>import</code> declaration. +</p> + +<h2>Data Structures</h2> + +<p> +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 <code>page</code> as a struct with two fields representing +the title and body. +</p> + +<pre> +!./srcextract.bin -src=part1.go -name=page +</pre> + +<p> +The type <code>[]byte</code> means "a <code>byte</code> slice". +(See <a href="http://golang.org/doc/effective_go.html#slices">Effective Go</a> +for more on slices.) +The <code>body</code> element is a <code>[]byte</code> rather than +<code>string</code> because that is the type expected by the <code>io</code> +libraries we will use, as you'll see below. +</p> + +<p> +The <code>page</code> struct describes how page data will be stored in memory. +But what about persistent storage? We can address that by creating a +<code>save</code> method on <code>page</code>: +</p> + +<pre> +!./srcextract.bin -src=part1.go -name=save +</pre> + +<p> +This method's signature reads: "This is a method named <code>save</code> that +takes as its receiver <code>p</code>, a pointer to <code>page</code> . It takes +no parameters, and returns a value of type <code>os.Error</code>." +</p> + +<p> +This method will save the <code>page</code>'s <code>body</code> to a text +file. For simplicity, we will use the <code>title</code> as the file name. +</p> + +<p> +The <code>save</code> method returns an <code>os.Error</code> value because +that is the return type of <code>WriteFile</code> (a standard library function +that writes a byte slice to a file). The <code>save</code> method returns the +error value, to let the application handle it should anything go wrong while +writing the file. If all goes well, <code>page.save()</code> will return +<code>nil</code> (the zero-value for pointers, interfaces, and some other +types). +</p> + +<p> +The octal integer constant <code>0600</code>, passed as the third parameter to +<code>WriteFile</code>, indicates that the file should be created with +read-write permissions for the current user only. (See the Unix man page +<code>open(2)</code> for details.) +</p> + +<p> +We will want to load pages, too: +</p> + +<pre> +!./srcextract.bin -src=part1-noerror.go -name=loadPage +</pre> + +<p> +The function <code>loadPage</code> constructs the file name from +<code>title</code>, reads the file's contents into a new +<code>page</code>, and returns a pointer to that new <code>page</code>. +</p> + +<p> +Functions can return multiple values. The standard library function +<code>io.ReadFile</code> returns <code>[]byte</code> and <code>os.Error</code>. +In <code>loadPage</code>, error isn't being handled yet; the "blank identifier" +represented by the underscore (<code>_</code>) symbol is used to throw away the +error return value (in essence, assigning the value to nothing). +</p> + +<p> +But what happens if <code>ReadFile</code> encounters an error? For example, +the file might not exist. We should not ignore such errors. Let's modify the +function to return <code>*page</code> and <code>os.Error</code>. +</p> + +<pre> +!./srcextract.bin -src=part1.go -name=loadPage +</pre> + +<p> +Callers of this function can now check the second parameter; if it is +<code>nil</code> then it has succesfully loaded a page. If not, it will be an +<code>os.Error</code> that can be handled by the caller (see the <a +href="http://golang.org/pkg/os/#Error">os package documentation</a> for +details). +</p> + +<p> +At this point we have a simple data structure and the ability to save to and +load from a file. Let's write a <code>main</code> function to test what we've +written: +</p> + +<pre> +!./srcextract.bin -src=part1.go -name=main +</pre> + +<p> +After compiling and executing this code, a file named <code>TestPage.txt</code> +would be created, containing the contents of <code>p1</code>. The file would +then be read into the struct <code>p2</code>, and its <code>body</code> element +printed to the screen. +</p> + +<p> +You can compile and run the program like this: +</p> + +<pre> +$ 8g wiki.go +$ 8l wiki.8 +$ ./8.out +This is a sample page. +</pre> + +<p> +(The <code>8g</code> and <code>8l</code> commands are applicable to +<code>GOARCH=386</code>. If you're on an <code>amd64</code> system, +subtitute 6's for the 8's.) +</p> + +<p> +<a href="part1.go">Click here to view the code we've written so far.</a> +</p> + +<h2>Introducing the <code>http</code> package (an interlude)</h2> + +<p> +Here's a full working example of a simple web server: +</p> + +<pre> +!./htmlify.bin < http-sample.go +</pre> + +<p> +The <code>main</code> function begins with a call to +<code>http.HandleFunc</code>, which tells the <code>http</code> package to +handle all requests to the web root (<code>"/"</code>) with +<code>handler</code>. +</p> + +<p> +It then calls <code>http.ListenAndServe</code>, specifying that it should +listen on port 8080 on any interface (<code>":8080"</code>). (Don't +worry about its second parameter, <code>nil</code>, for now.) +This function will block until the program is terminated. +</p> + +<p> +The function <code>handler</code> is of the type <code>http.HandlerFunc</code>. +It takes an <code>http.Conn</code> and <code>http.Request</code> as its +arguments. +</p> + +<p> +An <code>http.Conn</code> is the server end of an HTTP connection; by writing +to it, we send data to the HTTP client. +</p> + +<p> +An <code>http.Request</code> is a data structure that represents the client +HTTP request. The string <code>r.URL.Path</code> is the path component +of the request URL. The trailing <code>[1:]</code> means +"create a sub-slice of <code>Path</code> from the 1st character to the end." +This drops the leading "/" from the path name. +</p> + +<p> +If you run this program and access the URL: +</p> +<pre>http://localhost:8080/monkeys</pre> +<p> +the program would present a page containing: +</p> +<pre>Hi there, I love monkeys!</pre> + +<h2>Using <code>http</code> to serve wiki pages</h2> + +<p> +To use the <code>http</code> package, it must be imported: +</p> + +<pre> +import ( + "fmt" + <b>"http"</b> + "io/ioutil" +) +</pre> + +<p> +Let's create a handler to view a wiki page: +</p> + +<pre> +!./srcextract.bin -src=part2.go -name=lenPath + +!./srcextract.bin -src=part2.go -name=viewHandler +</pre> + +<p> +First, this function extracts the page title from <code>r.URL.Path</code>, +the path component of the request URL. The global constant +<code>lenPath</code> is the length of the leading <code>"/view/"</code> +component of the request path. +The <code>Path</code> is re-sliced with <code>[lenPath:]</code> to drop the +first 6 characters of the string. This is because the path will invariably +begin with <code>"/view/"</code>, which is not part of the page title. +</p> + +<p> +The function then loads the page data, formats the page with a string of simple +HTML, and writes it to <code>c</code>, the <code>http.Conn</code>. +</p> + +<p> +Again, note the use of <code>_</code> to ignore the <code>os.Error</code> +return value from <code>loadPage</code>. This is done here for simplicity +and generally considered bad practice. We will attend to this later. +</p> + +<p> +To use this handler, we create a <code>main</code> function that +initializes <code>http</code> using the <code>viewHandler</code> to handle +any requests under the path <code>/view/</code>. +</p> + +<pre> +!./srcextract.bin -src=part2.go -name=main +</pre> + +<p> +<a href="part2.go">Click here to view the code we've written so far.</a> +</p> + +<p> +Let's create some page data (as <code>test.txt</code>), compile our code, and +try serving a wiki page: +</p> + +<pre> +$ echo "Hello world" > test.txt +$ 8g wiki.go +$ 8l wiki.8 +$ ./8.out +</pre> + +<p> +With this web server running, a visit to <code><a +href="http://localhost:8080/view/test">http://localhost:8080/view/test</a></code> +should show a page titled "test" containing the words "Hello world". +</p> + +<h2>Editing pages</h2> + +<p> +A wiki is not a wiki without the ability to edit pages. Let's create two new +handlers: one named <code>editHandler</code> to display an 'edit page' form, +and the other named <code>saveHandler</code> to save the data entered via the +form. +</p> + +<p> +First, we add them to <code>main()</code>: +</p> + +<pre> +!./srcextract.bin -src=final-noclosure.go -name=main +</pre> + +<p> +The function <code>editHandler</code> loads the page +(or, if it doesn't exist, create an empty <code>page</code> struct), +and displays an HTML form. +</p> + +<pre> +!./srcextract.bin -src=notemplate.go -name=editHandler +</pre> + +<p> +This function will work fine, but all that hard-coded HTML is ugly. +Of course, there is a better way. +</p> + +<h2>The <code>template</code> package</h2> + +<p> +The <code>template</code> package is part of the Go standard library. We can +use <code>template</code> to keep the HTML in a separate file, allowing +us to change the layout of our edit page without modifying the underlying Go +code. +</p> + +<p> +First, we must add <code>template</code> to the list of imports: +</p> + +<pre> +import ( + "http" + "io/ioutil" + "os" + <b>"template"</b> +) +</pre> + +<p> +Let's create a template file containg the HTML form. +Open a new file named <code>edit.html</code>, and add the following lines: +</p> + +<pre> +!./htmlify.bin < edit.html +</pre> + +<p> +Modify <code>editHandler</code> to use the template, instead of the hard-coded +HTML: +</p> + +<pre> +!./srcextract.bin -src=final-noerror.go -name=editHandler +</pre> + +<p> +The function <code>template.ParseFile</code> will read the contents of +<code>edit.html</code> and return a <code>*template.Template</code>. +</p> + +<p> +The method <code>t.Execute</code> replaces all occurrences of +<code>{title}</code> and <code>{body}</code> with the values of +<code>p.title</code> and <code>p.body</code>, and writes the resultant +HTML to the <code>http.Conn</code>. +</p> + +<p> +Note that we've used <code>{body|html}</code> in the above template. +The <code>|html</code> part asks the template engine to pass the value +<code>body</code> through the <code>html</code> formatter before outputting it, +which escapes HTML characters (such as replacing <code>></code> with +<code>&gt;</code>). +This will prevent user data from corrupting the form HTML. +</p> + +<p> +Now that we've removed the <code>fmt.Sprintf</code> statement, we can remove +<code>"fmt"</code> from the <code>import</code> list. +</p> + +<p> +While we're working with templates, let's create a template for our +<code>viewHandler</code> called <code>view.html</code>: +</p> + +<pre> +!./htmlify.bin < view.html +</pre> + +<p> +Modify <code>viewHandler</code> accordingly: +</p> + +<pre> +!./srcextract.bin -src=final-noerror.go -name=viewHandler +</pre> + +<p> +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: +</p> + +<pre> +!./srcextract.bin -src=final-template.go -name=viewHandler + +!./srcextract.bin -src=final-template.go -name=editHandler + +!./srcextract.bin -src=final-template.go -name=renderTemplate +</pre> + +<p> +The handlers are now shorter and simpler. +</p> + +<h2>Handling non-existent pages</h2> + +<p> +What if you visit <code>/view/APageThatDoesntExist</code>? The program will +crash. This is because it ignores the error return value from +<code>loadPage</code>. Instead, if the requested page doesn't exist, it should +redirect the client to the edit page so the content may be created: +</p> + +<pre> +!./srcextract.bin -src=final.go -name=viewHandler +</pre> + +<p> +The <code>http.Redirect</code> function adds an HTTP status code of +<code>http.StatusFound</code> (302) and a <code>Location</code> +header to the HTTP response. +</p> + +<h2>Saving pages</h2> + +<p> +The function <code>saveHandler</code> will handle the form submission. +</p> + +<pre> +!./srcextract.bin -src=final-template.go -name=saveHandler +</pre> + +<p> +The page title (provided in the URL) and the form's only field, +<code>body</code>, are stored in a new <code>page</code>. +The <code>save()</code> method is then called to write the data to a file, +and the client is redirected to the <code>/view/</code> page. +</p> + +<p> +The value returned by <code>FormValue</code> is of type <code>string</code>. +We must convert that value to <code>[]byte</code> before it will fit into +the <code>page</code> struct. We use <code>[]byte(body)</code> to perform +the conversion. +</p> + +<h2>Error handling</h2> + +<p> +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. +</p> + +<p> +First, let's handle the errors in <code>renderTemplate</code>: +</p> + +<pre> +!./srcextract.bin -src=final-parsetemplate.go -name=renderTemplate +</pre> + +<p> +The <code>http.Error</code> 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. +</p> + +<p> +Now let's fix up <code>saveHandler</code>: +</p> + +<pre> +!./srcextract.bin -src=final.go -name=saveHandler +</pre> + +<p> +Any errors that occur during <code>p.save()</code> will be reported +to the user. +</p> + +<h2>Template caching</h2> + +<p> +There is an inefficiency in this code: <code>renderTemplate</code> calls +<code>ParseFile</code> every time a page is rendered. +A better approach would be to call <code>ParseFile</code> once for each +template at program initialization, and store the resultant +<code>*Template</code> values in a data structure for later use. +</p> + +<p> +First we create a global map named <code>templates</code> in which to store +our <code>*Template</code> values, keyed by <code>string</code> +(the template name): +</p> + +<pre> +!./srcextract.bin -src=final.go -name=templates +</pre> + +<p> +Then we create an <code>init</code> function, which will be called before +<code>main</code> at program initialization. The function +<code>template.MustParseFile</code> is a convenience wrapper around +<code>ParseFile</code> 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. +</p + +<pre> +!./srcextract.bin -src=final.go -name=init +</pre> + +<p> +A <code>for</code> loop is used with a <code>range</code> 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. +</p> + +<p> +We then modify our <code>renderTemplate</code> function to call +the <code>Execute</code> method on the appropriate <code>Template</code> from +<code>templates</code>: + +<pre> +!./srcextract.bin -src=final.go -name=renderTemplate +</pre> + +<h2>Validation</h2> + +<p> +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. +</p> + +<p> +First, add <code>"regexp"</code> to the <code>import</code> list. +Then we can create a global variable to store our validation regexp: +</p> + +<pre> +!./srcextract.bin -src=final-noclosure.go -name=titleValidator +</pre> + +<p> +The function <code>regexp.MustCompile</code> will parse and compile the +regular expression, and return a <code>regexp.Regexp</code>. +<code>MustCompile</code>, like <code>template.MustParseFile</code>, +is distinct from <code>Compile</code> in that it will panic if +the expression compilation fails, while <code>Compile</code> returns an +<code>os.Error</code> as a second parameter. +</p> + +<p> +Now, let's write a function that extracts the title string from the request +URL, and tests it against our <code>titleValidator</code> expression: +</p> + +<pre> +!./srcextract.bin -src=final-noclosure.go -name=getTitle +</pre> + +<p> +If the title is valid, it will be returned along with a <code>nil</code> +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. +</p> + +<p> +Let's put a call to <code>getTitle</code> in each of the handlers: +</p> + +<pre> +!./srcextract.bin -src=final-noclosure.go -name=viewHandler + +!./srcextract.bin -src=final-noclosure.go -name=editHandler + +!./srcextract.bin -src=final-noclosure.go -name=saveHandler +</pre> + +<h2>Introducing Function Literals and Closures</h2> + +<p> +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 +<a href="http://golang.org/doc/go_spec.html#Function_declarations">function +literals</a> provide a powerful means of abstracting functionality +that can help us here. +</p> + +<p> +First, we re-write the function definition of each of the handlers to accept +a title string: +</p> + +<pre> +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) +</pre> + +<p> +Now let's define a wrapper function that <i>takes a function of the above +type</i>, and returns a function of type <code>http.HandlerFunc</code> +(suitable to be passed to the function <code>http.HandleFunc</code>): +</p> + +<pre> +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' + } +} +</pre> + +<p> +The returned function is called a closure because it encloses values defined +outside of it. In this case, the variable <code>fn</code> (the single argument +to <code>makeHandler</code>) is enclosed by the closure. The variable +<code>fn</code> will be one of our save, edit, or view handlers. +</p> + +<p> +Now we can take the code from <code>getTitle</code> and use it here +(with some minor modifications): +</p> + +<pre> +!./srcextract.bin -src=final.go -name=makeHandler +</pre> + +<p> +The closure returned by <code>makeHandler</code> is a function that takes +an <code>http.Conn</code> and <code>http.Request</code> (in other words, +an <code>http.HandlerFunc</code>). +The closure extracts the <code>title</code> from the request path, and +validates it with the <code>titleValidator</code> regexp. If the +<code>title</code> is invalid, an error will be written to the +<code>Conn</code> using the <code>http.NotFound</code> function. +If the <code>title</code> is valid, the enclosed handler function +<code>fn</code> will be called with the <code>Conn</code>, +<code>Request</code>, and <code>title</code> as arguments. +</p> + +<p> +Now we can wwrap the handler functions with <code>makeHandler</code> in +<code>main</code>, before they are registered with the <code>http</code> +package: +</p> + +<pre> +!./srcextract.bin -src=final.go -name=main +</pre> + +<p> +Finally we remove the calls to <code>getTitle</code> from the handler functions, +making them much simpler: +</p> + +<pre> +!./srcextract.bin -src=final.go -name=viewHandler + +!./srcextract.bin -src=final.go -name=editHandler + +!./srcextract.bin -src=final.go -name=saveHandler +</pre> + +<h2>Try it out!</h2> + +<p> +<a href="final.go">Click here to view the final code listing.</a> +</p> + +<p> +Recompile the code, and run the app: +</p> + +<pre> +$ 8g wiki.go +$ 8l wiki.8 +$ ./8.out +</pre> + +<p> +Visiting <a href="http://localhost:8080/ANewPage">http://localhost:8080/ANewPage</a> +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. +</p> + +<h2>Other tasks</h2> + +<p> +Here are some simple tasks you might want to tackle on your own: +</p> + +<ul> +<li>Store templates in <code>tmpl/</code> and page data in <code>data/</code>. +<li>Add a handler to make the web root redirect to + <code>/view/FrontPage</code>.</li> +<li>Spruce up the page templates by making them valid HTML and adding some + CSS rules.</li> +<li>Implement inter-page linking by converting instances of + <code>[PageName]</code> to <br> + <code><a href="/view/PageName">PageName</a></code>. + (hint: you could use <code>regexp.ReplaceAllFunc</code> to do this) + </li> +</ul> + +</div> |