go.talks: add talk "a simple programming environment"

R=r, campoy
CC=golang-dev
https://golang.org/cl/6768062
diff --git a/2012/simple.slide b/2012/simple.slide
new file mode 100644
index 0000000..cf6f2ca
--- /dev/null
+++ b/2012/simple.slide
@@ -0,0 +1,376 @@
+Go: a simple programming environment
+
+# Go is a general-purpose language that bridges the gap between efficient
+# statically typed languages and productive dynamic language. But it’s not just
+# the language that makes Go special – Go has broad and consistent standard
+# libraries and powerful but simple tools.
+# 
+# This talk gives an introduction to Go, followed by a tour of some real
+# programs that demonstrate the power, scope, and simplicity of the Go
+# programming environment.
+
+Andrew Gerrand
+Google Inc.
+@enneff
+adg@golang.org
+http://golang.org
+
+* Background
+
+* Why a new language?
+
+Motivated by our needs at Google.
+
+We need:
+
+- Efficiency
+- Safety
+- Concurrency
+- Scalability
+- Fast development cycle
+- No surprises
+- A cute mascot
+
+* Design
+
+"Consensus drove the design. Nothing went into the language until [Ken Thompson, Robert Griesemer, and myself] all agreed that it was right. Some features didn’t get resolved until after a year or more of discussion." - Rob Pike
+
+Go is:
+
+- Lightweight, avoids unnecessary repetition
+- Object Oriented, but not in the usual way
+- Concurrent, in a way that keeps you sane
+- Designed for working programmers
+
+* Go 1
+
+Released in March 2012
+
+A specification of the language and libraries that will be supported for years.
+
+The guarantee: code written for Go 1.0 will build and run with Go 1.x.
+
+Best thing we ever did.
+
+* The gopher
+
+.image simple/gopher.jpg
+
+* Hello, go
+
+.play simple/hello.go
+
+* Standard library
+
+* Packages
+
+Go code lives in packages.
+
+Packages contain type, function, variable, and constant declarations.
+
+Packages can be very small (package `errors` has just one declaration) or very large (package `net/http` has >100 declarations). Most are somewhere in between.
+
+Case determines visiblity: `Foo` is exported, `foo` is not
+
+* io
+
+The `io` package provides fundamental I/O interfaces that are used throughout most Go code.
+
+The most ubiquitous are the `Reader` and `Writer` types, which describe streams of data.
+
+.code simple/io.go
+
+`Reader` and `Writer` implementations include files, sockets, (de)compressors, image and JSON codecs, and many more.
+
+* Chaining io.Readers
+
+.play simple/reader.go
+
+* net/http
+
+The `net/http` package implements an HTTP server and client.
+
+.play simple/hello-web.go
+
+* encoding/json
+
+The `encoding/json` package converts JSON-encoded data to and from native Go data structures.
+
+.play simple/json.go /const/,$
+
+* time
+
+The `time` package provides a representation of time and duration, and other time-related functions.
+
+.play simple/time.go /START/,/END/
+.play simple/time2.go /START/,/END/
+
+`time.Time` values also contain a `time.Location` (for display only):
+
+.play simple/time3.go /START/,/END/
+
+* flag
+
+The `flag` package provides a simple API for parsing command-line flags.
+
+.play simple/flag.go
+
+	$ flag -message 'Hold on...' -delay 5m
+
+* Tools
+
+* The go tool
+
+The `go` tool is the defacto standard for building and installing Go code.
+
+Compile and run a single file program:
+
+	$ go run hello.go
+
+Build and install the package in the current directory (and its dependencies):
+
+	$ go install
+
+Build and install the fmt package (and deps):
+
+	$ go install fmt
+
+It also acts as an interface for most of the Go tools.
+
+* Import paths
+
+The `go` tool is a "zero configuration" tool. No Makefiles or scripts. Just Go code.
+Your build schema and code are always in sync; they are one and the same.
+
+Package import paths mirror the code's location on the file system:
+
+  src/
+    github.com/nf/
+      gosynth/
+        main.go
+        note.go
+        osc.go
+      wav/
+        writer.go
+
+The `gosynth` program imports the `wav` package:
+
+  import "github.com/nf/wav"
+
+Installing `gosynth` will automatically install the `wav` package:
+
+  $ go install github.com/nf/gosynth
+
+* Remote dependencies
+
+The `go` tool also fetches Go code from remote repositories.
+
+Import paths can be URLs:
+
+	import "code.google.com/p/go.net/websocket"
+
+To fetch, build and install a package:
+
+	$ go get code.google.com/p/go.net/websocket
+
+To fetch, build, and install `gosynth` and its dependencies:
+
+	$ go get github.com/nf/gosynth
+
+This simple design leads to other cool tools:
+
+.link http://go.pkgdoc.org
+
+* Godoc
+
+Godoc extracts documentation from Go code and presents it in a variety of forms.
+
+No prescribed format, just regular comments that precede the declaration they document.
+
+	// Split slices s into all substrings separated by sep and returns a slice of
+	// the substrings between those separators.
+	// If sep is empty, Split splits after each UTF-8 sequence.
+	// It is equivalent to SplitN with a count of -1.
+	func Split(s, sep string) []string {
+
+.image simple/split.png
+
+Documentation that lives with code is easy to keep up-to-date.
+
+* Gofmt
+
+The `gofmt` tool is a pretty-printer for Go source code.
+
+All Go code in the core is gofmt'd, as is ~70% of open source Go code.
+
+Ends boring formatting discussions.
+
+Improves readability. Improves writability.
+
+Saves a _huge_ amount of time.
+
+* Tests: writing
+
+The `go` tool and the `testing` package provide a lightweight test framework.
+
+.code simple/string_test.go /func TestIndex/,/^}/
+
+* Tests: running
+
+The go tool runs tests.
+
+	$ go test
+	PASS
+
+	$ go test -v
+	=== RUN TestIndex
+	--- PASS: TestIndex (0.00 seconds)
+	PASS
+
+To run the tests for all my projects:
+
+	$ go test github.com/nf/...
+
+* Tests: benchmarks
+
+The testing package also supports benchmarks.
+	
+A sample benchmark function:
+
+.code simple/string_test.go /func BenchmarkIndex/,/^}/
+ 
+The benchmark package will vary `b.N` until the benchmark function lasts long enough to be timed reliably.
+
+	$ go test -test.bench=Index
+	PASS
+	BenchmarkIndex	50000000	        37.3 ns/op
+
+* Tests: doc examples
+
+The testing package also supports testable examples.
+
+.code simple/string_test.go /func ExampleIndex/,/^}/
+
+Examples and built and run as part of the normal test suite:
+
+	$ go test -v
+	=== RUN: ExampleIndex
+	--- PASS: ExampleIndex (0.00 seconds)
+	PASS
+
+The example is displayed in godoc alongside the thing it demonstrates:
+
+.link http://golang.org/pkg/strings/#Index
+
+* And there's more
+
+- vet: checks code for common programmer mistakes
+- prof: CPU and memory profiling
+- fix: automatically migrate code as APIs change
+- GDB support
+- Editor support: Vim, Emacs, Eclipse, Sublime Text
+
+* An example
+
+* webfront
+
+webfront is an HTTP server and reverse proxy.
+
+It reads a JSON-formatted rule file like this:
+
+.code simple/webfront/main.go /^\[/,/\]/
+
+For all requests to the host `example.com` (or any name ending in `".example.com"`) it serves files from the `/var/www` directory.
+
+For requests to `example.org`, it forwards the request to the HTTP server listening on localhost port 8080.
+
+* The Rule type
+
+A `Rule` value specifies what to do for a request to a specific host.
+
+.code simple/webfront/main.go /Rule represents/,/^}/
+
+It corresponds directly with the entries in the JSON configuration file.
+
+.code simple/webfront/main.go /^\[/,/\]/
+
+* Rule methods
+
+.code simple/webfront/main.go /Match returns/,/^}/
+.code simple/webfront/main.go /Handler returns/,/^}/
+
+* The Server type
+
+The `Server` type is responsible for loading (and refreshing) the rules from the rule file and serving HTTP requests with the appropriate handler.
+
+.code simple/webfront/main.go /Server implements/,/^}/
+.code simple/webfront/main.go /ServeHTTP matches/,/^}/
+
+* The handler method
+
+.code simple/webfront/main.go /handler returns/,/^}/
+
+* Parsing rules
+
+The `parseRules` function uses the `encoding/json` package to read the rule file into a Go data structure.
+
+.code simple/webfront/main.go /parseRules reads/,/^}/
+
+* The loadRules method
+
+.code simple/webfront/main.go /loadRules tests/,/^}/
+
+* Constructing the server
+
+.code simple/webfront/main.go /NewServer constructs/,/^}/
+
+This constructor function launches a goroutine running the `refreshRules` method.
+
+* Refreshing the rules
+
+.code simple/webfront/main.go /refreshRules polls/,/^}/
+
+* Bringing it all together
+
+The main function parses command-line flags, constructs a `Server`, and launches an HTTP server that serves all requests with the `Server`.
+
+.code simple/webfront/main.go /^var/,/^}/
+
+* Demo
+
+* Testing (1/3)
+
+The `Server` integration test uses the `httptest` package to construct a dummy HTTP server, synthesizes a set of rules, and constructs a `Server` instance that use those rules.
+
+.code simple/webfront/server_test.go /^func testHandler/,/STOP/
+
+* Testing (2/3)
+
+Each test cases in the table specifies a request URL and the expected response code and body.
+
+.code simple/webfront/server_test.go /TESTS START/,/STOP/
+
+* Testing (3/3)
+
+For each test case, construct an `http.Request` for the url and an `httptest.ResponseRecorder` to capture the response, and pass them to the `Server.ServeHTTP` method. Then check that the response matches the test case.
+
+.code simple/webfront/server_test.go /RANGE START/,/^}/
+
+* Demo
+
+* Conclusions
+
+* Further reading
+
+All about Go:
+
+.link http://golang.org
+
+The slides for this talk:
+
+.link http://talks.golang.org/2012/simple.slide
+
+webfront:
+
+.link https://github.com/nf/webfront
+
diff --git a/2012/simple/flag.go b/2012/simple/flag.go
new file mode 100644
index 0000000..67ac0bb
--- /dev/null
+++ b/2012/simple/flag.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"time"
+)
+
+var (
+	message = flag.String("message", "Hello!", "what to say")
+	delay   = flag.Duration("delay", 2*time.Second, "how long to wait")
+)
+
+func main() {
+	flag.Parse()
+	fmt.Println(*message)
+	time.Sleep(*delay)
+}
diff --git a/2012/simple/gopher.jpg b/2012/simple/gopher.jpg
new file mode 100644
index 0000000..0e886e4
--- /dev/null
+++ b/2012/simple/gopher.jpg
Binary files differ
diff --git a/2012/simple/hello-web.go b/2012/simple/hello-web.go
new file mode 100644
index 0000000..e9c3992
--- /dev/null
+++ b/2012/simple/hello-web.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+)
+
+type Greeting string
+
+func (g Greeting) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprint(w, g)
+}
+
+func main() {
+	err := http.ListenAndServe("localhost:4000", Greeting("Hello, go"))
+	if err != nil {
+		log.Fatal(err)
+	}
+}
diff --git a/2012/simple/hello.go b/2012/simple/hello.go
new file mode 100644
index 0000000..70366f1
--- /dev/null
+++ b/2012/simple/hello.go
@@ -0,0 +1,7 @@
+package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("Hello, go")
+}
diff --git a/2012/simple/io.go b/2012/simple/io.go
new file mode 100644
index 0000000..0977063
--- /dev/null
+++ b/2012/simple/io.go
@@ -0,0 +1,9 @@
+package io
+
+type Writer interface {
+	Write(p []byte) (n int, err error)
+}
+
+type Reader interface {
+	Read(p []byte) (n int, err error)
+}
diff --git a/2012/simple/json.go b/2012/simple/json.go
new file mode 100644
index 0000000..a716ef2
--- /dev/null
+++ b/2012/simple/json.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+)
+
+const blob = `[
+	{"Title":"Øredev", "URL":"http://oredev.org"},
+	{"Title":"Strange Loop", "URL":"http://thestrangeloop.com"}
+]`
+
+type Item struct {
+	Title string
+	URL   string
+}
+
+func main() {
+	var items []*Item
+	json.NewDecoder(strings.NewReader(blob)).Decode(&items)
+	for _, item := range items {
+		fmt.Printf("Title: %v URL: %v\n", item.Title, item.URL)
+	}
+}
diff --git a/2012/simple/reader.go b/2012/simple/reader.go
new file mode 100644
index 0000000..cd051a2
--- /dev/null
+++ b/2012/simple/reader.go
@@ -0,0 +1,21 @@
+package main
+
+import (
+	"compress/gzip"
+	"encoding/base64"
+	"io"
+	"os"
+	"strings"
+)
+
+func main() {
+	var r io.Reader
+	r = strings.NewReader(data)
+	r = base64.NewDecoder(base64.StdEncoding, r)
+	r, _ = gzip.NewReader(r)
+	io.Copy(os.Stdout, r)
+}
+
+const data = `
+H4sIAAAJbogA/1SOO5KDQAxE8zlFZ5tQXGCjjfYIjoURoPKgcY0E57f4VZlQXf2e+r8yOYbMZJhoZWRxz3wkCVjeReETS0VHz5fBCzpxxg/PbfrT/gacCjbjeiRNOChaVkA9RAdR8eVEw4vxa0Dcs3Fe2ZqowpeqG79L995l3VaMBUV/02OS+B6kMWikwG51c8n5GnEPr11F2/QJAAD//z9IppsHAQAA
+`
diff --git a/2012/simple/split.png b/2012/simple/split.png
new file mode 100644
index 0000000..5140229
--- /dev/null
+++ b/2012/simple/split.png
Binary files differ
diff --git a/2012/simple/string_test.go b/2012/simple/string_test.go
new file mode 100644
index 0000000..688d328
--- /dev/null
+++ b/2012/simple/string_test.go
@@ -0,0 +1,43 @@
+package test
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+)
+
+func TestIndex(t *testing.T) {
+	var tests = []struct {
+		s   string
+		sep string
+		out int
+	}{
+		{"", "", 0},
+		{"", "a", -1},
+		{"fo", "foo", -1},
+		{"foo", "foo", 0},
+		{"oofofoofooo", "f", 2},
+		// etc
+	}
+	for _, test := range tests {
+		actual := strings.Index(test.s, test.sep)
+		if actual != test.out {
+			t.Errorf("Index(%q,%q) = %v; want %v", test.s, test.sep, actual, test.out)
+		}
+	}
+}
+
+func BenchmarkIndex(b *testing.B) {
+	const s = "some_text=some☺value"
+	for i := 0; i < b.N; i++ {
+		strings.Index(s, "v")
+	}
+}
+
+func ExampleIndex() {
+	fmt.Println(strings.Index("chicken", "ken"))
+	fmt.Println(strings.Index("chicken", "dmr"))
+	// Output:
+	// 4
+	// -1
+}
diff --git a/2012/simple/test.go b/2012/simple/test.go
new file mode 100644
index 0000000..66f2de3
--- /dev/null
+++ b/2012/simple/test.go
@@ -0,0 +1,32 @@
+package main
+
+import "strings"
+
+import "testing"
+
+func TestToUpper(t *testing.T) {
+	in := "loud noises"
+	expected := "LOUD NOISES"
+	got := strings.ToUpper(in)
+	if got != want {
+		t.Errorf("ToUpper(%v) = %v, want %v", in, got, expected)
+	}
+}
+
+func TestContains(t *testing.T) {
+	var tests = []struct {
+		str, substr string
+		expected    bool
+	}{
+		{"abc", "bc", true},
+		{"abc", "bcd", false},
+		{"abc", "", true},
+		{"", "a", false},
+	}
+	for _, ct := range tests {
+		if strings.Contains(ct.str, ct.substr) != ct.expected {
+			t.Errorf("Contains(%s, %s) = %v, want %v",
+				ct.str, ct.substr, !ct.expected, ct.expected)
+		}
+	}
+}
diff --git a/2012/simple/time.go b/2012/simple/time.go
new file mode 100644
index 0000000..4e027f7
--- /dev/null
+++ b/2012/simple/time.go
@@ -0,0 +1,16 @@
+package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	// START OMIT
+	if time.Now().Hour() < 12 {
+		fmt.Println("Good morning.")
+	} else {
+		fmt.Println("Good afternoon (or evening).")
+	}
+	// END OMIT
+}
diff --git a/2012/simple/time2.go b/2012/simple/time2.go
new file mode 100644
index 0000000..485e9da
--- /dev/null
+++ b/2012/simple/time2.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	// START OMIT
+	birthday, _ := time.Parse("Jan 2 2006", "Nov 10 2009") // time.Time
+	age := time.Since(birthday)                            // time.Duration
+	fmt.Printf("Go is %d days old\n", age/(time.Hour*24))
+	// END OMIT
+}
diff --git a/2012/simple/time3.go b/2012/simple/time3.go
new file mode 100644
index 0000000..b478a2e
--- /dev/null
+++ b/2012/simple/time3.go
@@ -0,0 +1,15 @@
+package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	// START OMIT
+	t := time.Now()
+	fmt.Println(t.In(time.UTC))
+	home, _ := time.LoadLocation("Australia/Sydney")
+	fmt.Println(t.In(home))
+	// END OMIT
+}
diff --git a/2012/simple/webfront/main.go b/2012/simple/webfront/main.go
new file mode 100644
index 0000000..e177d10
--- /dev/null
+++ b/2012/simple/webfront/main.go
@@ -0,0 +1,194 @@
+/*
+Copyright 2011 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// This is a somewhat cut back version of webfront, available at
+// http://github.com/nf/webfront
+
+/*
+webfront is an HTTP server and reverse proxy.
+
+It reads a JSON-formatted rule file like this:
+
+[
+	{"Host": "example.com", "Serve": "/var/www"},
+	{"Host": "example.org", "Forward": "localhost:8080"}
+]
+
+For all requests to the host example.com (or any name ending in
+".example.com") it serves files from the /var/www directory.
+
+For requests to example.org, it forwards the request to the HTTP
+server listening on localhost port 8080.
+
+Usage of webfront:
+  -http=":80": HTTP listen address
+  -poll=10s: file poll interval
+  -rules="": rule definition file
+
+webfront was written by Andrew Gerrand <adg@golang.org>
+*/
+package main
+
+import (
+	"encoding/json"
+	"flag"
+	"fmt"
+	"log"
+	"net/http"
+	"net/http/httputil"
+	"os"
+	"strings"
+	"sync"
+	"time"
+)
+
+var (
+	httpAddr     = flag.String("http", ":80", "HTTP listen address")
+	ruleFile     = flag.String("rules", "", "rule definition file")
+	pollInterval = flag.Duration("poll", time.Second*10, "file poll interval")
+)
+
+func main() {
+	flag.Parse()
+
+	s, err := NewServer(*ruleFile, *pollInterval)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	err = http.ListenAndServe(*httpAddr, s)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+// Server implements an http.Handler that acts as either a reverse proxy or
+// a simple file server, as determined by a rule set.
+type Server struct {
+	mu    sync.RWMutex // guards the fields below
+	mtime time.Time    // when the rule file was last modified
+	rules []*Rule
+}
+
+// Rule represents a rule in a configuration file.
+type Rule struct {
+	Host    string // to match against request Host header
+	Forward string // non-empty if reverse proxy
+	Serve   string // non-empty if file server
+}
+
+// Match returns true if the Rule matches the given Request.
+func (r *Rule) Match(req *http.Request) bool {
+	return req.Host == r.Host || strings.HasSuffix(req.Host, "."+r.Host)
+}
+
+// Handler returns the appropriate Handler for the Rule.
+func (r *Rule) Handler() http.Handler {
+	if h := r.Forward; h != "" {
+		return &httputil.ReverseProxy{
+			Director: func(req *http.Request) {
+				req.URL.Scheme = "http"
+				req.URL.Host = h
+			},
+		}
+	}
+	if d := r.Serve; d != "" {
+		return http.FileServer(http.Dir(d))
+	}
+	return nil
+}
+
+// NewServer constructs a Server that reads rules from file with a period
+// specified by poll.
+func NewServer(file string, poll time.Duration) (*Server, error) {
+	s := new(Server)
+	if err := s.loadRules(file); err != nil {
+		return nil, err
+	}
+	go s.refreshRules(file, poll)
+	return s, nil
+}
+
+// ServeHTTP matches the Request with a Rule and, if found, serves the
+// request with the Rule's handler.
+func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	if h := s.handler(r); h != nil {
+		h.ServeHTTP(w, r)
+		return
+	}
+	http.Error(w, "Not found.", http.StatusNotFound)
+}
+
+// handler returns the appropriate Handler for the given Request,
+// or nil if none found.
+func (s *Server) handler(req *http.Request) http.Handler {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+	for _, r := range s.rules {
+		if r.Match(req) {
+			return r.Handler()
+		}
+	}
+	return nil
+}
+
+// refreshRules polls file periodically and refreshes the Server's rule
+// set if the file has been modified.
+func (s *Server) refreshRules(file string, poll time.Duration) {
+	for {
+		if err := s.loadRules(file); err != nil {
+			log.Println(err)
+		}
+		time.Sleep(poll)
+	}
+}
+
+// loadRules tests whether file has been modified
+// and, if so, loads the rule set from file.
+func (s *Server) loadRules(file string) error {
+	fi, err := os.Stat(file)
+	if err != nil {
+		return err
+	}
+	mtime := fi.ModTime()
+	if mtime.Before(s.mtime) && s.rules != nil {
+		return nil // no change
+	}
+	rules, err := parseRules(file)
+	if err != nil {
+		return fmt.Errorf("parsing %s: %v", file, err)
+	}
+	s.mu.Lock()
+	s.mtime = mtime
+	s.rules = rules
+	s.mu.Unlock()
+	return nil
+}
+
+// parseRules reads rule definitions from file returns the resultant Rules.
+func parseRules(file string) ([]*Rule, error) {
+	f, err := os.Open(file)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	var rules []*Rule
+	err = json.NewDecoder(f).Decode(&rules)
+	if err != nil {
+		return nil, err
+	}
+	return rules, nil
+}
diff --git a/2012/simple/webfront/server_test.go b/2012/simple/webfront/server_test.go
new file mode 100644
index 0000000..509ea04
--- /dev/null
+++ b/2012/simple/webfront/server_test.go
@@ -0,0 +1,97 @@
+/*
+Copyright 2011 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+	"time"
+)
+
+func testHandler(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("OK"))
+}
+
+func TestServer(t *testing.T) {
+	dummy := httptest.NewServer(http.HandlerFunc(testHandler))
+	defer dummy.Close()
+
+	ruleFile := writeRules([]*Rule{
+		{Host: "example.com", Forward: dummy.Listener.Addr().String()},
+		{Host: "example.org", Serve: "testdata"},
+	})
+	defer os.Remove(ruleFile)
+
+	s, err := NewServer(ruleFile, time.Hour)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// continued next slide
+	// STOP OMIT
+
+	// TESTS START OMIT
+	// continued from previous slide
+
+	var tests = []struct {
+		url  string
+		code int
+		body string
+	}{
+		{"http://example.com/", 200, "OK"},
+		{"http://foo.example.com/", 200, "OK"},
+		{"http://example.org/", 200, "contents of index.html\n"},
+		{"http://example.net/", 404, "Not found.\n"},
+		{"http://fooexample.com/", 404, "Not found.\n"},
+	}
+
+	// continued next slide
+	// STOP OMIT
+
+	// RANGE START OMIT
+	// continued from previous slide
+
+	for _, test := range tests {
+		req, _ := http.NewRequest("GET", test.url, nil)
+		rw := httptest.NewRecorder()
+		rw.Body = new(bytes.Buffer)
+		s.ServeHTTP(rw, req)
+		if g, w := rw.Code, test.code; g != w {
+			t.Errorf("%s: code = %d, want %d", test.url, g, w)
+		}
+		if g, w := rw.Body.String(), test.body; g != w {
+			t.Errorf("%s: body = %q, want %q", test.url, g, w)
+		}
+	}
+}
+
+func writeRules(rules []*Rule) (name string) {
+	f, err := ioutil.TempFile("", "webfront-rules")
+	if err != nil {
+		panic(err)
+	}
+	defer f.Close()
+	err = json.NewEncoder(f).Encode(rules)
+	if err != nil {
+		panic(err)
+	}
+	return f.Name()
+}
diff --git a/2012/simple/webfront/testdata/index.html b/2012/simple/webfront/testdata/index.html
new file mode 100644
index 0000000..583e54d
--- /dev/null
+++ b/2012/simple/webfront/testdata/index.html
@@ -0,0 +1 @@
+contents of index.html