func detailHandler(w http.ResponseWriter, r *http.Request) *appError {
	book, err := bookFromRequest(r)
	if err != nil {
		return appErrorf(err, "%v", err)
	}
	return detailTmpl.Execute(w, r, book)
}
detailHandler displays the details of a given book.
package main
import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path"
	"strconv"
	"cloud.google.com/go/pubsub"
	"cloud.google.com/go/storage"
	"golang.org/x/net/context"
	"github.com/gorilla/handlers"
	"github.com/gorilla/mux"
	uuid "github.com/satori/go.uuid"
	"google.golang.org/appengine"
	"github.com/census-ecosystem/opencensus-experiments/go/bookshelf"
	"go.opencensus.io/plugin/ochttp"
	"go.opencensus.io/stats"
	"go.opencensus.io/stats/view"
	"go.opencensus.io/zpages"
)
var (
	listTmpl   = parseTemplate("list.html")
	editTmpl   = parseTemplate("edit.html")
	detailTmpl = parseTemplate("detail.html")
)
var booksPerPage = stats.Int64("books_per_page", "number of books rendered on a page", stats.UnitNone)
func main() {
	registerHandlers()
	view.Register(&view.View{
		Aggregation: view.Distribution(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 100, 200, 300, 500, 1000),
		Measure:     booksPerPage,
	})
	appengine.Main()
}
func registerHandlers() {
	r := mux.NewRouter()
	r.Handle("/", http.RedirectHandler("/books", http.StatusFound))
	r.Methods("GET").Path("/books").
		Handler(appHandler(listHandler))
	r.Methods("GET").Path("/books/mine").
		Handler(appHandler(listMineHandler))
	r.Methods("GET").Path("/books/{id:[0-9]+}").
		Handler(appHandler(detailHandler))
	r.Methods("GET").Path("/books/add").
		Handler(appHandler(addFormHandler))
	r.Methods("GET").Path("/books/{id:[0-9]+}/edit").
		Handler(appHandler(editFormHandler))
	r.Methods("POST").Path("/books").
		Handler(appHandler(createHandler))
	r.Methods("POST", "PUT").Path("/books/{id:[0-9]+}").
		Handler(appHandler(updateHandler))
	r.Methods("POST").Path("/books/{id:[0-9]+}:delete").
		Handler(appHandler(deleteHandler)).Name("delete")
	r.Methods("GET").Path("/login").
		Handler(appHandler(loginHandler))
	r.Methods("POST").Path("/logout").
		Handler(appHandler(logoutHandler))
	r.Methods("GET").Path("/oauth2callback").
		Handler(appHandler(oauthCallbackHandler))
	r.Methods("GET").Path("/_ah/health").HandlerFunc(
		func(w http.ResponseWriter, r *http.Request) {
			w.Write([]byte("ok"))
		})
	http.Handle("/", &ochttp.Handler{Handler: handlers.CombinedLoggingHandler(os.Stderr, r)})
	http.Handle("/debug/zpages/", http.StripPrefix("/debug/zpages", zpages.Handler))
}
func listHandler(w http.ResponseWriter, r *http.Request) *appError {
	books, err := bookshelf.DB.ListBooks(r.Context())
	if err != nil {
		return appErrorf(err, "could not list books: %v", err)
	}
	stats.Record(r.Context(), booksPerPage.M(int64(len(books))))
	return listTmpl.Execute(w, r, books)
}
func listMineHandler(w http.ResponseWriter, r *http.Request) *appError {
	user := profileFromSession(r)
	if user == nil {
		http.Redirect(w, r, "/login?redirect=/books/mine", http.StatusFound)
		return nil
	}
	books, err := bookshelf.DB.ListBooksCreatedBy(r.Context(), user.ID)
	if err != nil {
		return appErrorf(err, "could not list books: %v", err)
	}
	return listTmpl.Execute(w, r, books)
}
func bookFromRequest(r *http.Request) (*bookshelf.Book, error) {
	id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
	if err != nil {
		return nil, fmt.Errorf("bad book id: %v", err)
	}
	book, err := bookshelf.DB.GetBook(r.Context(), id)
	if err != nil {
		return nil, fmt.Errorf("could not find book: %v", err)
	}
	return book, nil
}