godoc: migrate to App Engine flexible
See bug for more details on exactly what was migrated.
Notably:
* No more Google-internal deployment scripts; see README.godoc-app and
the Makefile for details.
* Build tag "golangorg" is used for the godoc configuration used for
golang.org.
* Use of App Engine libraries replaced with GCP client libraries.
* Redis is used to replace App Engine memcache.
* Google analytics is controlled by an environment variable.
* Regression tests have been migrated from Google-internal.
* hg -> git hash map is moved from Google-internal.
Updates golang/go#27205.
Change-Id: Ia0a983f239c50eda8be2363494c8b784f60c2c6d
Reviewed-on: https://go-review.googlesource.com/133355
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/godoc/Dockerfile.prod b/cmd/godoc/Dockerfile.prod
new file mode 100644
index 0000000..21fb92d
--- /dev/null
+++ b/cmd/godoc/Dockerfile.prod
@@ -0,0 +1,42 @@
+# Builder
+#########
+
+FROM golang:1.11 AS build
+
+RUN apt-get update && apt-get install -y \
+ zip # required for generate-index.bash
+
+ENV GODOC_REF release-branch.go1.11
+
+RUN go get -v -d \
+ golang.org/x/net/context \
+ google.golang.org/appengine \
+ cloud.google.com/go/datastore \
+ golang.org/x/build \
+ github.com/gomodule/redigo/redis
+
+COPY . /go/src/golang.org/x/tools
+
+WORKDIR /go/src/golang.org/x/tools/cmd/godoc
+RUN git clone --single-branch --depth=1 -b $GODOC_REF https://go.googlesource.com/go /docset
+RUN GODOC_DOCSET=/docset ./generate-index.bash
+
+RUN go build -o /godoc -tags=golangorg golang.org/x/tools/cmd/godoc
+
+
+# Final image
+#############
+
+FROM gcr.io/distroless/base
+
+WORKDIR /app
+COPY --from=build /godoc /app/
+COPY --from=build /go/src/golang.org/x/tools/cmd/godoc/hg-git-mapping.bin /app/
+
+COPY --from=build /docset /goroot
+ENV GOROOT /goroot
+
+COPY --from=build /go/src/golang.org/x/tools/cmd/godoc/index.split.* /app/
+ENV GODOC_INDEX_GLOB index.split.*
+
+CMD ["/app/godoc"]
diff --git a/cmd/godoc/Makefile b/cmd/godoc/Makefile
new file mode 100644
index 0000000..217515b
--- /dev/null
+++ b/cmd/godoc/Makefile
@@ -0,0 +1,24 @@
+# Copyright 2018 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.
+
+.PHONY: usage
+
+usage:
+ echo "See Makefile"
+ exit 1
+
+docker-prod: Dockerfile.prod
+ cd ../..; docker build -f cmd/godoc/Dockerfile.prod --tag=gcr.io/golang-org/godoc:$(VERSION) .
+
+push-prod: docker-prod
+ docker push gcr.io/golang-org/godoc:$(VERSION)
+
+deploy-prod: push-prod
+ gcloud -q app deploy app.prod.yaml --project golang-org --no-promote --image-url gcr.io/golang-org/godoc:$(VERSION)
+
+get-latest-url:
+ @gcloud app versions list -s default --project golang-org --sort-by=~version.createTime --format='value(version.versionUrl)' --limit 1 | cut -f1
+
+regtest:
+ ./regtest.bash $(shell make get-latest-url)
diff --git a/cmd/godoc/README.godoc-app b/cmd/godoc/README.godoc-app
index 94abf0c..0bb8112 100644
--- a/cmd/godoc/README.godoc-app
+++ b/cmd/godoc/README.godoc-app
@@ -7,31 +7,78 @@
* Google Cloud SDK
https://cloud.google.com/sdk/
+* Redis
+
* Go sources under $GOROOT
* Godoc sources inside $GOPATH
(go get -d golang.org/x/tools/cmd/godoc)
-Running in dev_appserver.py
----------------------------
+Running locally, in production mode
+-----------------------------------
-Use dev_appserver.py to run the server in development mode:
+Build the app:
- dev_appserver.py app.dev.yaml
+ go build -tags golangorg
-To run the server with generated zip file and search index:
+Run the app:
- ./generate-index.bash
- dev_appserver.py app.prod.yaml
+ ./godoc
godoc should come up at http://localhost:8080
-Use the --host and --port flags to listen on a different address.
-To clean up the index files, use git:
+Use the PORT environment variable to change the port:
- git clean -xn # n is dry run, replace with f
+ PORT=8081 ./godoc
+Running locally, in production mode, using Docker
+-------------------------------------------------
+
+Build the app's Docker container:
+
+ VERSION=$(git rev-parse HEAD) make docker-prod
+
+Make sure redis is running on port 6379:
+
+ $ echo PING | nc localhost 6379
+ +PONG
+ ^C
+
+Run the datastore emulator:
+
+ gcloud beta emulators datastore start --project golang-org
+
+In another terminal window, run the container:
+
+ $(gcloud beta emulators datastore env-init)
+
+ docker run --rm \
+ --net host \
+ --env GODOC_REDIS_ADDR=localhost:6379 \
+ --env DATASTORE_EMULATOR_HOST=$DATASTORE_EMULATOR_HOST \
+ --env DATASTORE_PROJECT_ID=$DATASTORE_PROJECT_ID \
+ gcr.io/golang-org/godoc
+
+godoc should come up at http://localhost:8080
+
+
+Deploying to golang.org
+-----------------------
+
+Build the image, push it to gcr.io, and deploy to Flex:
+
+ VERSION=$(git rev-parse HEAD) make deploy-prod
+
+Run regression tests:
+
+ make regtest
+
+Go to the console to migrate traffic to the newly deployed version:
+
+ https://console.cloud.google.com/appengine/versions?project=golang-org&serviceId=default&versionssize=50
+
+Shut down any very old versions (keep at least one to roll back to, just in case).
Troubleshooting
---------------
diff --git a/cmd/godoc/app.prod.yaml b/cmd/godoc/app.prod.yaml
index 6a18a64..832db09 100644
--- a/cmd/godoc/app.prod.yaml
+++ b/cmd/godoc/app.prod.yaml
@@ -1,18 +1,16 @@
-runtime: go
-api_version: go1
-instance_class: F4_1G
-
-handlers:
-- url: /s
- script: _go_app
- login: admin
-- url: /dl/init
- script: _go_app
- login: admin
-- url: /.*
- script: _go_app
+runtime: custom
+env: flex
env_variables:
- GODOC_ZIP: godoc.zip
- GODOC_ZIP_PREFIX: goroot
- GODOC_INDEX_GLOB: 'index.split.*'
+ GODOC_PROD: true
+ # GODOC_ENFORCE_HOSTS: true # TODO(cbro): modify host filter to allow version-specific URLs (see issue 27205).
+ GODOC_REDIS_ADDR: 10.0.0.4:6379 # instance "gophercache"
+ GODOC_ANALYTICS: UA-11222381-2
+ DATASTORE_PROJECT_ID: golang-org
+
+network:
+ name: golang
+
+resources:
+ cpu: 4
+ memory_gb: 7.50
diff --git a/cmd/godoc/appinit.go b/cmd/godoc/appinit.go
index e422d9c..293ad52 100644
--- a/cmd/godoc/appinit.go
+++ b/cmd/godoc/appinit.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// +build appengine
+// +build golangorg
package main
@@ -11,26 +11,34 @@
import (
"archive/zip"
+ "context"
+ "io"
"log"
"net/http"
"os"
"path"
"regexp"
"runtime"
+ "strings"
"golang.org/x/tools/godoc"
"golang.org/x/tools/godoc/dl"
"golang.org/x/tools/godoc/proxy"
+ "golang.org/x/tools/godoc/redirect"
"golang.org/x/tools/godoc/short"
"golang.org/x/tools/godoc/static"
"golang.org/x/tools/godoc/vfs"
"golang.org/x/tools/godoc/vfs/gatefs"
"golang.org/x/tools/godoc/vfs/mapfs"
"golang.org/x/tools/godoc/vfs/zipfs"
- "google.golang.org/appengine"
+
+ "cloud.google.com/go/datastore"
+ "golang.org/x/tools/internal/memcache"
)
-func init() {
+func main() {
+ log.SetFlags(log.Lshortfile | log.LstdFlags)
+
var (
// .zip filename
zipFilename = os.Getenv("GODOC_ZIP")
@@ -43,7 +51,6 @@
indexFilenames = os.Getenv("GODOC_INDEX_GLOB")
)
- enforceHosts = !appengine.IsDevAppServer()
playEnabled = true
log.Println("initializing godoc ...")
@@ -84,17 +91,61 @@
pres.ShowExamples = true
pres.DeclLinks = true
pres.NotesRx = regexp.MustCompile("BUG")
+ pres.GoogleAnalytics = os.Getenv("GODOC_ANALYTICS")
readTemplates(pres, true)
+ datastoreClient, memcacheClient := getClients()
+
+ // NOTE(cbro): registerHandlers registers itself against DefaultServeMux.
+ // The mux returned has host enforcement, so it's important to register
+ // against this mux and not DefaultServeMux.
mux := registerHandlers(pres)
- dl.RegisterHandlers(mux)
- short.RegisterHandlers(mux)
+ dl.RegisterHandlers(mux, datastoreClient, memcacheClient)
+ short.RegisterHandlers(mux, datastoreClient, memcacheClient)
// Register /compile and /share handlers against the default serve mux
// so that other app modules can make plain HTTP requests to those
// hosts. (For reasons, HTTPS communication between modules is broken.)
proxy.RegisterHandlers(http.DefaultServeMux)
+ http.HandleFunc("/_ah/health", func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, "ok")
+ })
+
+ http.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, "User-agent: *\nDisallow: /search\n")
+ })
+
+ if err := redirect.LoadChangeMap("hg-git-mapping.bin"); err != nil {
+ log.Fatalf("LoadChangeMap: %v", err)
+ }
+
log.Println("godoc initialization complete")
+
+ // TODO(cbro): add instrumentation via opencensus.
+ port := "8080"
+ if p := os.Getenv("PORT"); p != "" { // PORT is set by GAE flex.
+ port = p
+ }
+ log.Fatal(http.ListenAndServe(":"+port, nil))
+}
+
+func getClients() (*datastore.Client, *memcache.Client) {
+ ctx := context.Background()
+
+ datastoreClient, err := datastore.NewClient(ctx, "")
+ if err != nil {
+ if strings.Contains(err.Error(), "missing project") {
+ log.Fatalf("Missing datastore project. Set the DATASTORE_PROJECT_ID env variable. Use `gcloud beta emulators datastore` to start a local datastore.")
+ }
+ log.Fatalf("datastore.NewClient: %v.", err)
+ }
+
+ redisAddr := os.Getenv("GODOC_REDIS_ADDR")
+ if redisAddr == "" {
+ log.Fatalf("Missing redis server for godoc in production mode. set GODOC_REDIS_ADDR environment variable.")
+ }
+ memcacheClient := memcache.New(redisAddr)
+ return datastoreClient, memcacheClient
}
diff --git a/cmd/godoc/dl.go b/cmd/godoc/dl.go
index 40e6658..edeecb8 100644
--- a/cmd/godoc/dl.go
+++ b/cmd/godoc/dl.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// +build !appengine
+// +build !golangorg
package main
diff --git a/cmd/godoc/generate-index.bash b/cmd/godoc/generate-index.bash
index 38ac79a..21b567a 100755
--- a/cmd/godoc/generate-index.bash
+++ b/cmd/godoc/generate-index.bash
@@ -25,24 +25,24 @@
}
getArgs() {
- if [ ! -v GOROOT ]; then
- GOROOT="$(go env GOROOT)"
- echo "GOROOT not set explicitly, using go env value instead"
+ if [ ! -v GODOC_DOCSET ]; then
+ GODOC_DOCSET="$(go env GOROOT)"
+ echo "GODOC_DOCSET not set explicitly, using GOROOT instead"
fi
# safety checks
- if [ ! -d "$GOROOT" ]; then
- error "$GOROOT is not a directory"
+ if [ ! -d "$GODOC_DOCSET" ]; then
+ error "$GODOC_DOCSET is not a directory"
fi
# reporting
- echo "GOROOT = $GOROOT"
+ echo "GODOC_DOCSET = $GODOC_DOCSET"
}
makeZipfile() {
echo "*** make $ZIPFILE"
rm -f $ZIPFILE goroot
- ln -s "$GOROOT" goroot
+ ln -s "$GODOC_DOCSET" goroot
zip -q -r $ZIPFILE goroot/* # glob to ignore dotfiles (like .git)
rm goroot
}
diff --git a/cmd/godoc/handlers.go b/cmd/godoc/handlers.go
index a8447b3..4152a3e 100644
--- a/cmd/godoc/handlers.go
+++ b/cmd/godoc/handlers.go
@@ -21,6 +21,7 @@
"text/template"
"golang.org/x/tools/godoc"
+ "golang.org/x/tools/godoc/env"
"golang.org/x/tools/godoc/redirect"
"golang.org/x/tools/godoc/vfs"
)
@@ -30,8 +31,6 @@
fs = vfs.NameSpace{}
)
-var enforceHosts = false // set true in production on app engine
-
// hostEnforcerHandler redirects requests to "http://foo.golang.org/bar"
// to "https://golang.org/bar".
// It permits requests to the host "godoc-test.golang.org" for testing and
@@ -41,7 +40,7 @@
}
func (h hostEnforcerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if !enforceHosts {
+ if !env.EnforceHosts() {
h.h.ServeHTTP(w, r)
return
}
diff --git a/cmd/godoc/hg-git-mapping.bin b/cmd/godoc/hg-git-mapping.bin
new file mode 100644
index 0000000..3f6ca77
--- /dev/null
+++ b/cmd/godoc/hg-git-mapping.bin
Binary files differ
diff --git a/cmd/godoc/main.go b/cmd/godoc/main.go
index 89e9bba..7f0ac0c 100644
--- a/cmd/godoc/main.go
+++ b/cmd/godoc/main.go
@@ -23,7 +23,7 @@
// godoc crypto/block Cipher NewCMAC
// - prints doc for Cipher and NewCMAC in package crypto/block
-// +build !appengine
+// +build !golangorg
package main
diff --git a/cmd/godoc/play.go b/cmd/godoc/play.go
index 02f477d..f44a3cc 100644
--- a/cmd/godoc/play.go
+++ b/cmd/godoc/play.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// +build !appengine
+// +build !golangorg
package main
diff --git a/cmd/godoc/regtest.bash b/cmd/godoc/regtest.bash
new file mode 100755
index 0000000..9b596fa
--- /dev/null
+++ b/cmd/godoc/regtest.bash
@@ -0,0 +1,110 @@
+#!/usr/bin/env bash
+
+# Copyright 2018 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.
+
+# Regression tests for golang.org.
+# Usage: ./regtest.bash https://golang.org/
+
+#TODO: turn this into a Go program. maybe behind a build tag and "go run regtest.go <url>"
+
+set -e
+
+addr="$(echo $1 | sed -e 's/\/$//')"
+if [ -z "$addr" ]; then
+ echo "usage: $0 <addr>" 1>&2
+ echo "example: $0 https://20180928t023837-dot-golang-org.appspot.com/" 1>&2
+ exit 1
+fi
+
+set -u
+
+# fetch url, check the response with a regexp.
+fetch() {
+ curl -s "${addr}$1" | grep "$2" > /dev/null
+}
+fatal() {
+ log "$1"
+ exit 1
+}
+log() {
+ echo "$1" 1>&2
+}
+logn() {
+ echo -n "$1" 1>&2
+}
+
+log "Checking FAQ..."
+fetch /doc/faq 'What is the purpose of the project' || {
+ fatal "FAQ did not match."
+}
+
+log "Checking package listing..."
+fetch /pkg/ 'Package tar' || {
+ fatal "package listing page did not match."
+}
+
+log "Checking os package..."
+fetch /pkg/os/ 'func Open' || {
+ fatal "os package page did not match."
+}
+
+log "Checking robots.txt..."
+fetch /robots.txt 'Disallow: /search' || {
+ fatal "robots.txt did not match."
+}
+
+log "Checking /change/ redirect..."
+fetch /change/75944e2e3a63 'bdb10cf' || {
+ fatal "/change/ direct did not match."
+}
+
+log "Checking /dl/ page has data..."
+fetch /dl/ 'go1.11.windows-amd64.msi' || {
+ fatal "/dl/ did not match."
+}
+
+log "Checking /dl/?mode=json page has data..."
+fetch /dl/?mode=json 'go1.11.windows-amd64.msi' || {
+ fatal "/dl/?mode=json did not match."
+}
+
+log "Checking shortlinks (/s/go2design)..."
+fetch /s/go2design 'proposal.*Found' || {
+ fatal "/s/go2design did not match."
+}
+
+log "Checking analytics on pages..."
+ga_id="UA-11222381-2"
+fetch / $ga_id || fatal "/ missing GA."
+fetch /dl/ $ga_id || fatal "/dl/ missing GA."
+fetch /project/ $ga_id || fatal "/project missing GA."
+fetch /pkg/context/ $ga_id || fatal "/pkg/context missing GA."
+
+log "Checking search..."
+fetch /search?q=IsDir 'src/os/types.go' || {
+ fatal "search result did not match."
+}
+
+log "Checking compile service..."
+compile="curl -s ${addr}/compile"
+
+p="package main; func main() { print(6*7); }"
+$compile --data-urlencode "body=$p" | tee /tmp/compile.out | grep '^{"compile_errors":"","output":"42"}$' > /dev/null || {
+ cat /tmp/compile.out
+ fatal "compile service output did not match."
+}
+
+$compile --data-urlencode "body=//empty" | tee /tmp/compile.out | grep "expected 'package', found 'EOF'" > /dev/null || {
+ cat /tmp/compile.out
+ fatal "compile service error output did not match."
+}
+
+# Check API version 2
+d="version=2&body=package+main%3Bimport+(%22fmt%22%3B%22time%22)%3Bfunc+main()%7Bfmt.Print(%22A%22)%3Btime.Sleep(time.Second)%3Bfmt.Print(%22B%22)%7D"
+$compile --data "$d" | grep '^{"Errors":"","Events":\[{"Message":"A","Kind":"stdout","Delay":0},{"Message":"B","Kind":"stdout","Delay":1000000000}\]}$' > /dev/null || {
+ fatal "compile service v2 output did not match."
+}
+
+log "All OK"
diff --git a/cmd/godoc/remotesearch.go b/cmd/godoc/remotesearch.go
index f01d5c7..6f27d0b 100644
--- a/cmd/godoc/remotesearch.go
+++ b/cmd/godoc/remotesearch.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// +build !appengine
+// +build !golangorg
package main
diff --git a/godoc/appengine.go b/godoc/appengine.go
deleted file mode 100644
index fe5e687..0000000
--- a/godoc/appengine.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright 2015 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.
-
-// +build appengine
-
-package godoc
-
-import "google.golang.org/appengine"
-
-func init() {
- onAppengine = !appengine.IsDevAppServer()
-}
diff --git a/godoc/dl/dl.go b/godoc/dl/dl.go
index 83bb21e..edc09aa 100644
--- a/godoc/dl/dl.go
+++ b/godoc/dl/dl.go
@@ -2,8 +2,6 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
-// +build appengine
-
// Package dl implements a simple downloads frontend server.
//
// It accepts HTTP POST requests to create a new download metadata entity, and
@@ -19,6 +17,7 @@
"html"
"html/template"
"io"
+ "log"
"net/http"
"regexp"
"sort"
@@ -27,11 +26,10 @@
"sync"
"time"
+ "cloud.google.com/go/datastore"
"golang.org/x/net/context"
- "google.golang.org/appengine"
- "google.golang.org/appengine/datastore"
- "google.golang.org/appengine/log"
- "google.golang.org/appengine/memcache"
+ "golang.org/x/tools/godoc/env"
+ "golang.org/x/tools/internal/memcache"
)
const (
@@ -40,11 +38,21 @@
cacheDuration = time.Hour
)
-func RegisterHandlers(mux *http.ServeMux) {
- mux.HandleFunc("/dl", getHandler)
- mux.HandleFunc("/dl/", getHandler) // also serves listHandler
- mux.HandleFunc("/dl/upload", uploadHandler)
- mux.HandleFunc("/dl/init", initHandler)
+type server struct {
+ datastore *datastore.Client
+ memcache *memcache.CodecClient
+}
+
+func RegisterHandlers(mux *http.ServeMux, dc *datastore.Client, mc *memcache.Client) {
+ s := server{dc, mc.WithCodec(memcache.Gob)}
+ mux.HandleFunc("/dl", s.getHandler)
+ mux.HandleFunc("/dl/", s.getHandler) // also serves listHandler
+ mux.HandleFunc("/dl/upload", s.uploadHandler)
+
+ // NOTE(cbro): this only needs to be run once per project,
+ // and should be behind an admin login.
+ // TODO(cbro): move into a locally-run program? or remove?
+ // mux.HandleFunc("/dl/init", initHandler)
}
// File represents a file on the golang.org downloads page.
@@ -191,26 +199,25 @@
templateFuncs = template.FuncMap{"pretty": pretty}
)
-func listHandler(w http.ResponseWriter, r *http.Request) {
+func (h server) listHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
- var (
- c = appengine.NewContext(r)
- d listTemplateData
- )
- if _, err := memcache.Gob.Get(c, cacheKey, &d); err != nil {
- if err == memcache.ErrCacheMiss {
- log.Debugf(c, "cache miss")
- } else {
- log.Errorf(c, "cache get error: %v", err)
+ ctx := r.Context()
+ var d listTemplateData
+
+ if err := h.memcache.Get(ctx, cacheKey, &d); err != nil {
+ if err != memcache.ErrCacheMiss {
+ log.Printf("ERROR cache get error: %v", err)
+ // NOTE(cbro): continue to hit datastore if the memcache is down.
}
var fs []File
- _, err := datastore.NewQuery("File").Ancestor(rootKey(c)).GetAll(c, &fs)
- if err != nil {
- log.Errorf(c, "error listing: %v", err)
+ q := datastore.NewQuery("File").Ancestor(rootKey)
+ if _, err := h.datastore.GetAll(ctx, q, &fs); err != nil {
+ log.Printf("ERROR error listing: %v", err)
+ http.Error(w, "Could not get download page. Try again in a few minutes.", 500)
return
}
d.Stable, d.Unstable, d.Archive = filesToReleases(fs)
@@ -219,8 +226,8 @@
}
item := &memcache.Item{Key: cacheKey, Object: &d, Expiration: cacheDuration}
- if err := memcache.Gob.Set(c, item); err != nil {
- log.Errorf(c, "cache set error: %v", err)
+ if err := h.memcache.Set(ctx, item); err != nil {
+ log.Printf("ERROR cache set error: %v", err)
}
}
@@ -229,13 +236,13 @@
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err := enc.Encode(d.Stable); err != nil {
- log.Errorf(c, "failed rendering JSON for releases: %v", err)
+ log.Printf("ERROR rendering JSON for releases: %v", err)
}
return
}
if err := listTemplate.ExecuteTemplate(w, "root", d); err != nil {
- log.Errorf(c, "error executing template: %v", err)
+ log.Printf("ERROR executing template: %v", err)
}
}
@@ -383,12 +390,12 @@
return
}
-func uploadHandler(w http.ResponseWriter, r *http.Request) {
+func (h server) uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
- c := appengine.NewContext(r)
+ ctx := r.Context()
// Authenticate using a user token (same as gomote).
user := r.FormValue("user")
@@ -396,7 +403,7 @@
http.Error(w, "bad user", http.StatusForbidden)
return
}
- if r.FormValue("key") != userKey(c, user) {
+ if r.FormValue("key") != h.userKey(ctx, user) {
http.Error(w, "bad key", http.StatusForbidden)
return
}
@@ -404,7 +411,7 @@
var f File
defer r.Body.Close()
if err := json.NewDecoder(r.Body).Decode(&f); err != nil {
- log.Errorf(c, "error decoding upload JSON: %v", err)
+ log.Printf("ERROR decoding upload JSON: %v", err)
http.Error(w, "Something broke", http.StatusInternalServerError)
return
}
@@ -415,19 +422,19 @@
if f.Uploaded.IsZero() {
f.Uploaded = time.Now()
}
- k := datastore.NewKey(c, "File", f.Filename, 0, rootKey(c))
- if _, err := datastore.Put(c, k, &f); err != nil {
- log.Errorf(c, "putting File entity: %v", err)
+ k := datastore.NameKey("File", f.Filename, rootKey)
+ if _, err := h.datastore.Put(ctx, k, &f); err != nil {
+ log.Printf("ERROR File entity: %v", err)
http.Error(w, "could not put File entity", http.StatusInternalServerError)
return
}
- if err := memcache.Delete(c, cacheKey); err != nil {
- log.Errorf(c, "cache delete error: %v", err)
+ if err := h.memcache.Delete(ctx, cacheKey); err != nil {
+ log.Printf("ERROR delete error: %v", err)
}
io.WriteString(w, "OK")
}
-func getHandler(w http.ResponseWriter, r *http.Request) {
+func (h server) getHandler(w http.ResponseWriter, r *http.Request) {
// For go get golang.org/dl/go1.x.y, we need to serve the
// same meta tags at /dl for cmd/go to validate against /dl/go1.x.y:
if r.URL.Path == "/dl" && (r.Method == "GET" || r.Method == "HEAD") && r.FormValue("go-get") == "1" {
@@ -444,7 +451,7 @@
name := strings.TrimPrefix(r.URL.Path, "/dl/")
if name == "" {
- listHandler(w, r)
+ h.listHandler(w, r)
return
}
if fileRe.MatchString(name) {
@@ -486,10 +493,10 @@
return false
}
-func userKey(c context.Context, user string) string {
- h := hmac.New(md5.New, []byte(secret(c)))
- h.Write([]byte("user-" + user))
- return fmt.Sprintf("%x", h.Sum(nil))
+func (h server) userKey(c context.Context, user string) string {
+ hash := hmac.New(md5.New, []byte(h.secret(c)))
+ hash.Write([]byte("user-" + user))
+ return fmt.Sprintf("%x", hash.Sum(nil))
}
var (
@@ -497,18 +504,18 @@
goGetRe = regexp.MustCompile(`^go[0-9a-z.]+\.[0-9a-z.-]+$`)
)
-func initHandler(w http.ResponseWriter, r *http.Request) {
+func (h server) initHandler(w http.ResponseWriter, r *http.Request) {
var fileRoot struct {
Root string
}
- c := appengine.NewContext(r)
- k := rootKey(c)
- err := datastore.RunInTransaction(c, func(c context.Context) error {
- err := datastore.Get(c, k, &fileRoot)
+ ctx := r.Context()
+ k := rootKey
+ _, err := h.datastore.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
+ err := tx.Get(k, &fileRoot)
if err != nil && err != datastore.ErrNoSuchEntity {
return err
}
- _, err = datastore.Put(c, k, &fileRoot)
+ _, err = tx.Put(k, &fileRoot)
return err
}, nil)
if err != nil {
@@ -519,9 +526,7 @@
}
// rootKey is the ancestor of all File entities.
-func rootKey(c context.Context) *datastore.Key {
- return datastore.NewKey(c, "FileRoot", "root", 0, nil)
-}
+var rootKey = datastore.NameKey("FileRoot", "root", nil)
// pretty returns a human-readable version of the given OS, Arch, or Kind.
func pretty(s string) string {
@@ -559,11 +564,11 @@
Secret string
}
-func (k *builderKey) Key(c context.Context) *datastore.Key {
- return datastore.NewKey(c, "BuilderKey", "root", 0, nil)
+func (k *builderKey) Key() *datastore.Key {
+ return datastore.NameKey("BuilderKey", "root", nil)
}
-func secret(c context.Context) string {
+func (h server) secret(ctx context.Context) string {
// check with rlock
theKey.RLock()
k := theKey.Secret
@@ -580,18 +585,18 @@
}
// fill
- if err := datastore.Get(c, theKey.Key(c), &theKey.builderKey); err != nil {
+ if err := h.datastore.Get(ctx, theKey.Key(), &theKey.builderKey); err != nil {
if err == datastore.ErrNoSuchEntity {
// If the key is not stored in datastore, write it.
// This only happens at the beginning of a new deployment.
// The code is left here for SDK use and in case a fresh
// deployment is ever needed. "gophers rule" is not the
// real key.
- if !appengine.IsDevAppServer() {
+ if env.IsProd() {
panic("lost key from datastore")
}
theKey.Secret = "gophers rule"
- datastore.Put(c, theKey.Key(c), &theKey.builderKey)
+ h.datastore.Put(ctx, theKey.Key(), &theKey.builderKey)
return theKey.Secret
}
panic("cannot load builder key: " + err.Error())
diff --git a/godoc/dl/dl_test.go b/godoc/dl/dl_test.go
index 3f61fe9..2cdc1aa 100644
--- a/godoc/dl/dl_test.go
+++ b/godoc/dl/dl_test.go
@@ -2,8 +2,6 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
-// +build appengine
-
package dl
import (
diff --git a/godoc/dl/tmpl.go b/godoc/dl/tmpl.go
index 47ef9f4..d086b69 100644
--- a/godoc/dl/tmpl.go
+++ b/godoc/dl/tmpl.go
@@ -2,8 +2,6 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
-// +build appengine
-
package dl
// TODO(adg): refactor this to use the tools/godoc/static template.
diff --git a/godoc/env/env.go b/godoc/env/env.go
new file mode 100644
index 0000000..e1f55cd
--- /dev/null
+++ b/godoc/env/env.go
@@ -0,0 +1,41 @@
+// Copyright 2018 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.
+
+// Package env provides environment information for the godoc server running on
+// golang.org.
+package env
+
+import (
+ "log"
+ "os"
+ "strconv"
+)
+
+var (
+ isProd = boolEnv("GODOC_PROD")
+ enforceHosts = boolEnv("GODOC_ENFORCE_HOSTS")
+)
+
+// IsProd reports whether the server is running in its production configuration
+// on golang.org.
+func IsProd() bool {
+ return isProd
+}
+
+// EnforceHosts reports whether host filtering should be enforced.
+func EnforceHosts() bool {
+ return enforceHosts
+}
+
+func boolEnv(key string) bool {
+ v := os.Getenv(key)
+ if v == "" {
+ return false
+ }
+ b, err := strconv.ParseBool(v)
+ if err != nil {
+ log.Fatalf("environment variable %s (%q) must be a boolean", key, v)
+ }
+ return b
+}
diff --git a/godoc/page.go b/godoc/page.go
index 10e86e5..819af55 100644
--- a/godoc/page.go
+++ b/godoc/page.go
@@ -10,6 +10,8 @@
"path/filepath"
"runtime"
"strings"
+
+ "golang.org/x/tools/godoc/env"
)
// Page describes the contents of the top-level godoc webpage.
@@ -22,10 +24,11 @@
Body []byte
GoogleCN bool // page is being served from golang.google.cn
- // filled in by servePage
- SearchBox bool
- Playground bool
- Version string
+ // filled in by ServePage
+ SearchBox bool
+ Playground bool
+ Version string
+ GoogleAnalytics string
}
func (p *Presentation) ServePage(w http.ResponseWriter, page Page) {
@@ -35,6 +38,7 @@
page.SearchBox = p.Corpus.IndexEnabled
page.Playground = p.ShowPlayground
page.Version = runtime.Version()
+ page.GoogleAnalytics = p.GoogleAnalytics
applyTemplateToResponseWriter(w, p.GodocHTML, page)
}
@@ -49,20 +53,19 @@
}
}
p.ServePage(w, Page{
- Title: "File " + relpath,
- Subtitle: relpath,
- Body: applyTemplate(p.ErrorHTML, "errorHTML", err),
- GoogleCN: googleCN(r),
+ Title: "File " + relpath,
+ Subtitle: relpath,
+ Body: applyTemplate(p.ErrorHTML, "errorHTML", err),
+ GoogleCN: googleCN(r),
+ GoogleAnalytics: p.GoogleAnalytics,
})
}
-var onAppengine = false // overridden in appengine.go when on app engine
-
func googleCN(r *http.Request) bool {
if r.FormValue("googlecn") != "" {
return true
}
- if !onAppengine {
+ if !env.IsProd() {
return false
}
if strings.HasSuffix(r.Host, ".cn") {
diff --git a/godoc/pres.go b/godoc/pres.go
index de23c75..b0077fd 100644
--- a/godoc/pres.go
+++ b/godoc/pres.go
@@ -92,6 +92,10 @@
// body for displaying search results.
SearchResults []SearchResultFunc
+ // GoogleAnalytics optionally adds Google Analytics via the provided
+ // tracking ID to each page.
+ GoogleAnalytics string
+
initFuncMapOnce sync.Once
funcMap template.FuncMap
templateFuncs template.FuncMap
diff --git a/godoc/proxy/proxy.go b/godoc/proxy/proxy.go
index cdac3bf..f302372 100644
--- a/godoc/proxy/proxy.go
+++ b/godoc/proxy/proxy.go
@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// +build appengine
-
// Package proxy proxies requests to the playground's compile and share handlers.
// It is designed to run only on the instance of godoc that serves golang.org.
package proxy
@@ -13,6 +11,7 @@
"encoding/json"
"fmt"
"io/ioutil"
+ "log"
"net/http"
"net/http/httputil"
"net/url"
@@ -20,12 +19,18 @@
"time"
"golang.org/x/net/context"
-
- "google.golang.org/appengine"
- "google.golang.org/appengine/log"
- "google.golang.org/appengine/urlfetch"
+ "golang.org/x/tools/godoc/env"
)
+const playgroundURL = "https://play.golang.org"
+
+var proxy *httputil.ReverseProxy
+
+func init() {
+ target, _ := url.Parse(playgroundURL)
+ proxy = httputil.NewSingleHostReverseProxy(target)
+}
+
type Request struct {
Body string
}
@@ -41,8 +46,6 @@
Delay time.Duration // time to wait before printing Message
}
-const playgroundURL = "https://play.golang.org"
-
const expires = 7 * 24 * time.Hour // 1 week
var cacheControlHeader = fmt.Sprintf("public, max-age=%d", int(expires.Seconds()))
@@ -57,21 +60,17 @@
return
}
- ctx := appengine.NewContext(r)
+ ctx := r.Context()
body := r.FormValue("body")
res := &Response{}
req := &Request{Body: body}
if err := makeCompileRequest(ctx, req, res); err != nil {
- log.Errorf(ctx, "compile error: %v", err)
+ log.Printf("ERROR compile error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
- expiresTime := time.Now().Add(expires).UTC()
- w.Header().Set("Expires", expiresTime.Format(time.RFC1123))
- w.Header().Set("Cache-Control", cacheControlHeader)
-
var out interface{}
switch r.FormValue("version") {
case "2":
@@ -82,9 +81,17 @@
Output string `json:"output"`
}{res.Errors, flatten(res.Events)}
}
- if err := json.NewEncoder(w).Encode(out); err != nil {
- log.Errorf(ctx, "encoding response: %v", err)
+ b, err := json.Marshal(out)
+ if err != nil {
+ log.Printf("ERROR encoding response: %v", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
}
+
+ expiresTime := time.Now().Add(expires).UTC()
+ w.Header().Set("Expires", expiresTime.Format(time.RFC1123))
+ w.Header().Set("Cache-Control", cacheControlHeader)
+ w.Write(b)
}
// makePlaygroundRequest sends the given Request to the playground compile
@@ -94,17 +101,22 @@
if err != nil {
return fmt.Errorf("marshalling request: %v", err)
}
- r, err := urlfetch.Client(ctx).Post(playgroundURL+"/compile", "application/json", bytes.NewReader(reqJ))
+ hReq, _ := http.NewRequest("POST", playgroundURL+"/compile", bytes.NewReader(reqJ))
+ hReq.Header.Set("Content-Type", "application/json")
+ hReq = hReq.WithContext(ctx)
+
+ r, err := http.DefaultClient.Do(hReq)
if err != nil {
return fmt.Errorf("making request: %v", err)
}
defer r.Body.Close()
+
if r.StatusCode != http.StatusOK {
b, _ := ioutil.ReadAll(r.Body)
return fmt.Errorf("bad status: %v body:\n%s", r.Status, b)
}
- err = json.NewDecoder(r.Body).Decode(res)
- if err != nil {
+
+ if err := json.NewDecoder(r.Body).Decode(res); err != nil {
return fmt.Errorf("unmarshalling response: %v", err)
}
return nil
@@ -124,17 +136,14 @@
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
- target, _ := url.Parse(playgroundURL)
- p := httputil.NewSingleHostReverseProxy(target)
- p.Transport = &urlfetch.Transport{Context: appengine.NewContext(r)}
- p.ServeHTTP(w, r)
+ proxy.ServeHTTP(w, r)
}
func googleCN(r *http.Request) bool {
if r.FormValue("googlecn") != "" {
return true
}
- if appengine.IsDevAppServer() {
+ if !env.IsProd() {
return false
}
if strings.HasSuffix(r.Host, ".cn") {
diff --git a/godoc/short/short.go b/godoc/short/short.go
index 44d3c93..da710eb 100644
--- a/godoc/short/short.go
+++ b/godoc/short/short.go
@@ -2,8 +2,6 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
-// +build appengine
-
// Package short implements a simple URL shortener, serving an administrative
// interface at /s and shortened urls from /s/key.
// It is designed to run only on the instance of godoc that serves golang.org.
@@ -15,16 +13,15 @@
"errors"
"fmt"
"html/template"
+ "io"
+ "log"
"net/http"
"net/url"
"regexp"
+ "cloud.google.com/go/datastore"
"golang.org/x/net/context"
-
- "google.golang.org/appengine"
- "google.golang.org/appengine/datastore"
- "google.golang.org/appengine/log"
- "google.golang.org/appengine/memcache"
+ "golang.org/x/tools/internal/memcache"
"google.golang.org/appengine/user"
)
@@ -41,17 +38,32 @@
var validKey = regexp.MustCompile(`^[a-zA-Z0-9-_.]+$`)
-func RegisterHandlers(mux *http.ServeMux) {
- mux.HandleFunc(prefix, adminHandler)
- mux.HandleFunc(prefix+"/", linkHandler)
+type server struct {
+ datastore *datastore.Client
+ memcache *memcache.CodecClient
+}
+
+func RegisterHandlers(mux *http.ServeMux, dc *datastore.Client, mc *memcache.Client) {
+ s := server{dc, mc.WithCodec(memcache.JSON)}
+ mux.HandleFunc(prefix+"/", s.linkHandler)
+
+ // TODO(cbro): move storage of the links to a text file in Gerrit.
+ // Disable the admin handler until that happens, since GAE Flex doesn't support
+ // the "google.golang.org/appengine/user" package.
+ // See golang.org/issue/27205#issuecomment-418673218
+ // mux.HandleFunc(prefix, adminHandler)
+ mux.HandleFunc(prefix, func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusForbidden)
+ io.WriteString(w, "Link creation temporarily unavailable. See golang.org/issue/27205.")
+ })
}
// linkHandler services requests to short URLs.
// http://golang.org/s/key
// It consults memcache and datastore for the Link for key.
// It then sends a redirects or an error message.
-func linkHandler(w http.ResponseWriter, r *http.Request) {
- c := appengine.NewContext(r)
+func (h server) linkHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
key := r.URL.Path[len(prefix)+1:]
if !validKey.MatchString(key) {
@@ -60,16 +72,15 @@
}
var link Link
- _, err := memcache.JSON.Get(c, cacheKey(key), &link)
- if err != nil {
- k := datastore.NewKey(c, kind, key, 0, nil)
- err = datastore.Get(c, k, &link)
+ if err := h.memcache.Get(ctx, cacheKey(key), &link); err != nil {
+ k := datastore.NameKey(kind, key, nil)
+ err = h.datastore.Get(ctx, k, &link)
switch err {
case datastore.ErrNoSuchEntity:
http.Error(w, "not found", http.StatusNotFound)
return
default: // != nil
- log.Errorf(c, "%q: %v", key, err)
+ log.Printf("ERROR %q: %v", key, err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
case nil:
@@ -77,8 +88,8 @@
Key: cacheKey(key),
Object: &link,
}
- if err := memcache.JSON.Set(c, item); err != nil {
- log.Warningf(c, "%q: %v", key, err)
+ if err := h.memcache.Set(ctx, item); err != nil {
+ log.Printf("WARNING %q: %v", key, err)
}
}
}
@@ -89,10 +100,10 @@
var adminTemplate = template.Must(template.New("admin").Parse(templateHTML))
// adminHandler serves an administrative interface.
-func adminHandler(w http.ResponseWriter, r *http.Request) {
- c := appengine.NewContext(r)
+func (h server) adminHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
- if !user.IsAdmin(c) {
+ if !user.IsAdmin(ctx) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
@@ -104,24 +115,24 @@
switch r.FormValue("do") {
case "Add":
newLink = &Link{key, r.FormValue("target")}
- doErr = putLink(c, newLink)
+ doErr = h.putLink(ctx, newLink)
case "Delete":
- k := datastore.NewKey(c, kind, key, 0, nil)
- doErr = datastore.Delete(c, k)
+ k := datastore.NameKey(kind, key, nil)
+ doErr = h.datastore.Delete(ctx, k)
default:
http.Error(w, "unknown action", http.StatusBadRequest)
}
- err := memcache.Delete(c, cacheKey(key))
+ err := h.memcache.Delete(ctx, cacheKey(key))
if err != nil && err != memcache.ErrCacheMiss {
- log.Warningf(c, "%q: %v", key, err)
+ log.Printf("WARNING %q: %v", key, err)
}
}
var links []*Link
- _, err := datastore.NewQuery(kind).Order("Key").GetAll(c, &links)
- if err != nil {
+ q := datastore.NewQuery(kind).Order("Key")
+ if _, err := h.datastore.GetAll(ctx, q, &links); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
- log.Errorf(c, "%v", err)
+ log.Printf("ERROR %v", err)
return
}
@@ -150,20 +161,20 @@
Error error
}{baseURL, prefix, links, newLink, doErr}
if err := adminTemplate.Execute(w, &data); err != nil {
- log.Criticalf(c, "adminTemplate: %v", err)
+ log.Printf("ERROR adminTemplate: %v", err)
}
}
// putLink validates the provided link and puts it into the datastore.
-func putLink(c context.Context, link *Link) error {
+func (h server) putLink(ctx context.Context, link *Link) error {
if !validKey.MatchString(link.Key) {
return errors.New("invalid key; must match " + validKey.String())
}
if _, err := url.Parse(link.Target); err != nil {
return fmt.Errorf("bad target: %v", err)
}
- k := datastore.NewKey(c, kind, link.Key, 0, nil)
- _, err := datastore.Put(c, k, link)
+ k := datastore.NameKey(kind, link.Key, nil)
+ _, err := h.datastore.Put(ctx, k, link)
return err
}
diff --git a/godoc/short/tmpl.go b/godoc/short/tmpl.go
index 95e4c2a..66f5401 100644
--- a/godoc/short/tmpl.go
+++ b/godoc/short/tmpl.go
@@ -2,8 +2,6 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
-// +build appengine
-
package short
const templateHTML = `
diff --git a/godoc/static/godoc.html b/godoc/static/godoc.html
index 6c7889f..2688b16 100644
--- a/godoc/static/godoc.html
+++ b/godoc/static/godoc.html
@@ -15,6 +15,19 @@
{{end}}
<link rel="stylesheet" href="/lib/godoc/jquery.treeview.css">
<script>window.initFuncs = [];</script>
+{{with .GoogleAnalytics}}
+<script type="text/javascript">
+var _gaq = _gaq || [];
+_gaq.push(["_setAccount", "{{.}}"]);
+window.trackPageview = function() {
+ _gaq.push(["_trackPageview", location.pathname+location.hash]);
+};
+window.trackPageview();
+window.trackEvent = function(category, action, opt_label, opt_value, opt_noninteraction) {
+ _gaq.push(["_trackEvent", category, action, opt_label, opt_value, opt_noninteraction]);
+};
+</script>
+{{end}}
<script src="/lib/godoc/jquery.js" defer></script>
<script src="/lib/godoc/jquery.treeview.js" defer></script>
<script src="/lib/godoc/jquery.treeview.edit.js" defer></script>
@@ -112,6 +125,15 @@
</div><!-- .container -->
</div><!-- #page -->
+{{if .GoogleAnalytics}}
+<script type="text/javascript">
+(function() {
+ var ga = document.createElement("script"); ga.type = "text/javascript"; ga.async = true;
+ ga.src = ("https:" == document.location.protocol ? "https://ssl" : "http://www") + ".google-analytics.com/ga.js";
+ var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ga, s);
+})();
+</script>
+{{end}}
</body>
</html>
diff --git a/godoc/static/static.go b/godoc/static/static.go
index eacd495..97b84c6 100644
--- a/godoc/static/static.go
+++ b/godoc/static/static.go
@@ -51,7 +51,7 @@
"example.html": "<div\x20id=\"example_{{.Name}}\"\x20class=\"toggle\">\x0a\x09<div\x20class=\"collapsed\">\x0a\x09\x09<p\x20class=\"exampleHeading\x20toggleButton\">\xe2\x96\xb9\x20<span\x20class=\"text\">Example{{example_suffix\x20.Name}}</span></p>\x0a\x09</div>\x0a\x09<div\x20class=\"expanded\">\x0a\x09\x09<p\x20class=\"exampleHeading\x20toggleButton\">\xe2\x96\xbe\x20<span\x20class=\"text\">Example{{example_suffix\x20.Name}}</span></p>\x0a\x09\x09{{with\x20.Doc}}<p>{{html\x20.}}</p>{{end}}\x0a\x09\x09{{$output\x20:=\x20.Output}}\x0a\x09\x09{{with\x20.Play}}\x0a\x09\x09\x09<div\x20class=\"play\">\x0a\x09\x09\x09\x09<div\x20class=\"input\"><textarea\x20class=\"code\"\x20spellcheck=\"false\">{{html\x20.}}</textarea></div>\x0a\x09\x09\x09\x09<div\x20class=\"output\"><pre>{{html\x20$output}}</pre></div>\x0a\x09\x09\x09\x09<div\x20class=\"buttons\">\x0a\x09\x09\x09\x09\x09<a\x20class=\"run\"\x20title=\"Run\x20this\x20code\x20[shift-enter]\">Run</a>\x0a\x09\x09\x09\x09\x09<a\x20class=\"fmt\"\x20title=\"Format\x20this\x20code\">Format</a>\x0a\x09\x09\x09\x09\x09{{if\x20not\x20$.GoogleCN}}\x0a\x09\x09\x09\x09\x09<a\x20class=\"share\"\x20title=\"Share\x20this\x20code\">Share</a>\x0a\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09</div>\x0a\x09\x09\x09</div>\x0a\x09\x09{{else}}\x0a\x09\x09\x09<p>Code:</p>\x0a\x09\x09\x09<pre\x20class=\"code\">{{.Code}}</pre>\x0a\x09\x09\x09{{with\x20.Output}}\x0a\x09\x09\x09<p>Output:</p>\x0a\x09\x09\x09<pre\x20class=\"output\">{{html\x20.}}</pre>\x0a\x09\x09\x09{{end}}\x0a\x09\x09{{end}}\x0a\x09</div>\x0a</div>\x0a",
- "godoc.html": "<!DOCTYPE\x20html>\x0a<html>\x0a<head>\x0a<meta\x20http-equiv=\"Content-Type\"\x20content=\"text/html;\x20charset=utf-8\">\x0a<meta\x20name=\"viewport\"\x20content=\"width=device-width,\x20initial-scale=1\">\x0a<meta\x20name=\"theme-color\"\x20content=\"#375EAB\">\x0a{{with\x20.Tabtitle}}\x0a\x20\x20<title>{{html\x20.}}\x20-\x20The\x20Go\x20Programming\x20Language</title>\x0a{{else}}\x0a\x20\x20<title>The\x20Go\x20Programming\x20Language</title>\x0a{{end}}\x0a<link\x20type=\"text/css\"\x20rel=\"stylesheet\"\x20href=\"/lib/godoc/style.css\">\x0a{{if\x20.SearchBox}}\x0a<link\x20rel=\"search\"\x20type=\"application/opensearchdescription+xml\"\x20title=\"godoc\"\x20href=\"/opensearch.xml\"\x20/>\x0a{{end}}\x0a<link\x20rel=\"stylesheet\"\x20href=\"/lib/godoc/jquery.treeview.css\">\x0a<script>window.initFuncs\x20=\x20[];</script>\x0a<script\x20src=\"/lib/godoc/jquery.js\"\x20defer></script>\x0a<script\x20src=\"/lib/godoc/jquery.treeview.js\"\x20defer></script>\x0a<script\x20src=\"/lib/godoc/jquery.treeview.edit.js\"\x20defer></script>\x0a\x0a{{if\x20.Playground}}\x0a<script\x20src=\"/lib/godoc/playground.js\"\x20defer></script>\x0a{{end}}\x0a{{with\x20.Version}}<script>var\x20goVersion\x20=\x20{{printf\x20\"%q\"\x20.}};</script>{{end}}\x0a<script\x20src=\"/lib/godoc/godocs.js\"\x20defer></script>\x0a</head>\x0a<body>\x0a\x0a<div\x20id='lowframe'\x20style=\"position:\x20fixed;\x20bottom:\x200;\x20left:\x200;\x20height:\x200;\x20width:\x20100%;\x20border-top:\x20thin\x20solid\x20grey;\x20background-color:\x20white;\x20overflow:\x20auto;\">\x0a...\x0a</div><!--\x20#lowframe\x20-->\x0a\x0a<div\x20id=\"topbar\"{{if\x20.Title}}\x20class=\"wide\"{{end}}><div\x20class=\"container\">\x0a<div\x20class=\"top-heading\"\x20id=\"heading-wide\"><a\x20href=\"/\">The\x20Go\x20Programming\x20Language</a></div>\x0a<div\x20class=\"top-heading\"\x20id=\"heading-narrow\"><a\x20href=\"/\">Go</a></div>\x0a<a\x20href=\"#\"\x20id=\"menu-button\"><span\x20id=\"menu-button-arrow\">▽</span></a>\x0a<form\x20method=\"GET\"\x20action=\"/search\">\x0a<div\x20id=\"menu\">\x0a<a\x20href=\"/doc/\">Documents</a>\x0a<a\x20href=\"/pkg/\">Packages</a>\x0a<a\x20href=\"/project/\">The\x20Project</a>\x0a<a\x20href=\"/help/\">Help</a>\x0a{{if\x20not\x20.GoogleCN}}\x0a<a\x20href=\"/blog/\">Blog</a>\x0a{{end}}\x0a{{if\x20.Playground}}\x0a<a\x20id=\"playgroundButton\"\x20href=\"http://play.golang.org/\"\x20title=\"Show\x20Go\x20Playground\">Play</a>\x0a{{end}}\x0a<span\x20class=\"search-box\"><input\x20type=\"search\"\x20id=\"search\"\x20name=\"q\"\x20placeholder=\"Search\"\x20aria-label=\"Search\"\x20required><button\x20type=\"submit\"><span><!--\x20magnifying\x20glass:\x20--><svg\x20width=\"24\"\x20height=\"24\"\x20viewBox=\"0\x200\x2024\x2024\"><title>submit\x20search</title><path\x20d=\"M15.5\x2014h-.79l-.28-.27C15.41\x2012.59\x2016\x2011.11\x2016\x209.5\x2016\x205.91\x2013.09\x203\x209.5\x203S3\x205.91\x203\x209.5\x205.91\x2016\x209.5\x2016c1.61\x200\x203.09-.59\x204.23-1.57l.27.28v.79l5\x204.99L20.49\x2019l-4.99-5zm-6\x200C7.01\x2014\x205\x2011.99\x205\x209.5S7.01\x205\x209.5\x205\x2014\x207.01\x2014\x209.5\x2011.99\x2014\x209.5\x2014z\"/><path\x20d=\"M0\x200h24v24H0z\"\x20fill=\"none\"/></svg></span></button></span>\x0a</div>\x0a</form>\x0a\x0a</div></div>\x0a\x0a{{if\x20.Playground}}\x0a<div\x20id=\"playground\"\x20class=\"play\">\x0a\x09<div\x20class=\"input\"><textarea\x20class=\"code\"\x20spellcheck=\"false\">package\x20main\x0a\x0aimport\x20\"fmt\"\x0a\x0afunc\x20main()\x20{\x0a\x09fmt.Println(\"Hello,\x20\xe4\xb8\x96\xe7\x95\x8c\")\x0a}</textarea></div>\x0a\x09<div\x20class=\"output\"></div>\x0a\x09<div\x20class=\"buttons\">\x0a\x09\x09<a\x20class=\"run\"\x20title=\"Run\x20this\x20code\x20[shift-enter]\">Run</a>\x0a\x09\x09<a\x20class=\"fmt\"\x20title=\"Format\x20this\x20code\">Format</a>\x0a\x09\x09{{if\x20not\x20$.GoogleCN}}\x0a\x09\x09<a\x20class=\"share\"\x20title=\"Share\x20this\x20code\">Share</a>\x0a\x09\x09{{end}}\x0a\x09</div>\x0a</div>\x0a{{end}}\x0a\x0a<div\x20id=\"page\"{{if\x20.Title}}\x20class=\"wide\"{{end}}>\x0a<div\x20class=\"container\">\x0a\x0a{{if\x20or\x20.Title\x20.SrcPath}}\x0a\x20\x20<h1>\x0a\x20\x20\x20\x20{{html\x20.Title}}\x0a\x20\x20\x20\x20{{html\x20.SrcPath\x20|\x20srcBreadcrumb}}\x0a\x20\x20</h1>\x0a{{end}}\x0a\x0a{{with\x20.Subtitle}}\x0a\x20\x20<h2>{{html\x20.}}</h2>\x0a{{end}}\x0a\x0a{{with\x20.SrcPath}}\x0a\x20\x20<h2>\x0a\x20\x20\x20\x20Documentation:\x20{{html\x20.\x20|\x20srcToPkgLink}}\x0a\x20\x20</h2>\x0a{{end}}\x0a\x0a{{/*\x20The\x20Table\x20of\x20Contents\x20is\x20automatically\x20inserted\x20in\x20this\x20<div>.\x0a\x20\x20\x20\x20\x20Do\x20not\x20delete\x20this\x20<div>.\x20*/}}\x0a<div\x20id=\"nav\"></div>\x0a\x0a{{/*\x20Body\x20is\x20HTML-escaped\x20elsewhere\x20*/}}\x0a{{printf\x20\"%s\"\x20.Body}}\x0a\x0a<div\x20id=\"footer\">\x0aBuild\x20version\x20{{html\x20.Version}}.<br>\x0aExcept\x20as\x20<a\x20href=\"https://developers.google.com/site-policies#restrictions\">noted</a>,\x0athe\x20content\x20of\x20this\x20page\x20is\x20licensed\x20under\x20the\x0aCreative\x20Commons\x20Attribution\x203.0\x20License,\x0aand\x20code\x20is\x20licensed\x20under\x20a\x20<a\x20href=\"/LICENSE\">BSD\x20license</a>.<br>\x0a<a\x20href=\"/doc/tos.html\">Terms\x20of\x20Service</a>\x20|\x0a<a\x20href=\"http://www.google.com/intl/en/policies/privacy/\">Privacy\x20Policy</a>\x0a</div>\x0a\x0a</div><!--\x20.container\x20-->\x0a</div><!--\x20#page\x20-->\x0a</body>\x0a</html>\x0a\x0a",
+ "godoc.html": "<!DOCTYPE\x20html>\x0a<html>\x0a<head>\x0a<meta\x20http-equiv=\"Content-Type\"\x20content=\"text/html;\x20charset=utf-8\">\x0a<meta\x20name=\"viewport\"\x20content=\"width=device-width,\x20initial-scale=1\">\x0a<meta\x20name=\"theme-color\"\x20content=\"#375EAB\">\x0a{{with\x20.Tabtitle}}\x0a\x20\x20<title>{{html\x20.}}\x20-\x20The\x20Go\x20Programming\x20Language</title>\x0a{{else}}\x0a\x20\x20<title>The\x20Go\x20Programming\x20Language</title>\x0a{{end}}\x0a<link\x20type=\"text/css\"\x20rel=\"stylesheet\"\x20href=\"/lib/godoc/style.css\">\x0a{{if\x20.SearchBox}}\x0a<link\x20rel=\"search\"\x20type=\"application/opensearchdescription+xml\"\x20title=\"godoc\"\x20href=\"/opensearch.xml\"\x20/>\x0a{{end}}\x0a<link\x20rel=\"stylesheet\"\x20href=\"/lib/godoc/jquery.treeview.css\">\x0a<script>window.initFuncs\x20=\x20[];</script>\x0a{{with\x20.GoogleAnalytics}}\x0a<script\x20type=\"text/javascript\">\x0avar\x20_gaq\x20=\x20_gaq\x20||\x20[];\x0a_gaq.push([\"_setAccount\",\x20\"{{.}}\"]);\x0awindow.trackPageview\x20=\x20function()\x20{\x0a\x20\x20_gaq.push([\"_trackPageview\",\x20location.pathname+location.hash]);\x0a};\x0awindow.trackPageview();\x0awindow.trackEvent\x20=\x20function(category,\x20action,\x20opt_label,\x20opt_value,\x20opt_noninteraction)\x20{\x0a\x20\x20_gaq.push([\"_trackEvent\",\x20category,\x20action,\x20opt_label,\x20opt_value,\x20opt_noninteraction]);\x0a};\x0a</script>\x0a{{end}}\x0a<script\x20src=\"/lib/godoc/jquery.js\"\x20defer></script>\x0a<script\x20src=\"/lib/godoc/jquery.treeview.js\"\x20defer></script>\x0a<script\x20src=\"/lib/godoc/jquery.treeview.edit.js\"\x20defer></script>\x0a\x0a{{if\x20.Playground}}\x0a<script\x20src=\"/lib/godoc/playground.js\"\x20defer></script>\x0a{{end}}\x0a{{with\x20.Version}}<script>var\x20goVersion\x20=\x20{{printf\x20\"%q\"\x20.}};</script>{{end}}\x0a<script\x20src=\"/lib/godoc/godocs.js\"\x20defer></script>\x0a</head>\x0a<body>\x0a\x0a<div\x20id='lowframe'\x20style=\"position:\x20fixed;\x20bottom:\x200;\x20left:\x200;\x20height:\x200;\x20width:\x20100%;\x20border-top:\x20thin\x20solid\x20grey;\x20background-color:\x20white;\x20overflow:\x20auto;\">\x0a...\x0a</div><!--\x20#lowframe\x20-->\x0a\x0a<div\x20id=\"topbar\"{{if\x20.Title}}\x20class=\"wide\"{{end}}><div\x20class=\"container\">\x0a<div\x20class=\"top-heading\"\x20id=\"heading-wide\"><a\x20href=\"/\">The\x20Go\x20Programming\x20Language</a></div>\x0a<div\x20class=\"top-heading\"\x20id=\"heading-narrow\"><a\x20href=\"/\">Go</a></div>\x0a<a\x20href=\"#\"\x20id=\"menu-button\"><span\x20id=\"menu-button-arrow\">▽</span></a>\x0a<form\x20method=\"GET\"\x20action=\"/search\">\x0a<div\x20id=\"menu\">\x0a<a\x20href=\"/doc/\">Documents</a>\x0a<a\x20href=\"/pkg/\">Packages</a>\x0a<a\x20href=\"/project/\">The\x20Project</a>\x0a<a\x20href=\"/help/\">Help</a>\x0a{{if\x20not\x20.GoogleCN}}\x0a<a\x20href=\"/blog/\">Blog</a>\x0a{{end}}\x0a{{if\x20.Playground}}\x0a<a\x20id=\"playgroundButton\"\x20href=\"http://play.golang.org/\"\x20title=\"Show\x20Go\x20Playground\">Play</a>\x0a{{end}}\x0a<span\x20class=\"search-box\"><input\x20type=\"search\"\x20id=\"search\"\x20name=\"q\"\x20placeholder=\"Search\"\x20aria-label=\"Search\"\x20required><button\x20type=\"submit\"><span><!--\x20magnifying\x20glass:\x20--><svg\x20width=\"24\"\x20height=\"24\"\x20viewBox=\"0\x200\x2024\x2024\"><title>submit\x20search</title><path\x20d=\"M15.5\x2014h-.79l-.28-.27C15.41\x2012.59\x2016\x2011.11\x2016\x209.5\x2016\x205.91\x2013.09\x203\x209.5\x203S3\x205.91\x203\x209.5\x205.91\x2016\x209.5\x2016c1.61\x200\x203.09-.59\x204.23-1.57l.27.28v.79l5\x204.99L20.49\x2019l-4.99-5zm-6\x200C7.01\x2014\x205\x2011.99\x205\x209.5S7.01\x205\x209.5\x205\x2014\x207.01\x2014\x209.5\x2011.99\x2014\x209.5\x2014z\"/><path\x20d=\"M0\x200h24v24H0z\"\x20fill=\"none\"/></svg></span></button></span>\x0a</div>\x0a</form>\x0a\x0a</div></div>\x0a\x0a{{if\x20.Playground}}\x0a<div\x20id=\"playground\"\x20class=\"play\">\x0a\x09<div\x20class=\"input\"><textarea\x20class=\"code\"\x20spellcheck=\"false\">package\x20main\x0a\x0aimport\x20\"fmt\"\x0a\x0afunc\x20main()\x20{\x0a\x09fmt.Println(\"Hello,\x20\xe4\xb8\x96\xe7\x95\x8c\")\x0a}</textarea></div>\x0a\x09<div\x20class=\"output\"></div>\x0a\x09<div\x20class=\"buttons\">\x0a\x09\x09<a\x20class=\"run\"\x20title=\"Run\x20this\x20code\x20[shift-enter]\">Run</a>\x0a\x09\x09<a\x20class=\"fmt\"\x20title=\"Format\x20this\x20code\">Format</a>\x0a\x09\x09{{if\x20not\x20$.GoogleCN}}\x0a\x09\x09<a\x20class=\"share\"\x20title=\"Share\x20this\x20code\">Share</a>\x0a\x09\x09{{end}}\x0a\x09</div>\x0a</div>\x0a{{end}}\x0a\x0a<div\x20id=\"page\"{{if\x20.Title}}\x20class=\"wide\"{{end}}>\x0a<div\x20class=\"container\">\x0a\x0a{{if\x20or\x20.Title\x20.SrcPath}}\x0a\x20\x20<h1>\x0a\x20\x20\x20\x20{{html\x20.Title}}\x0a\x20\x20\x20\x20{{html\x20.SrcPath\x20|\x20srcBreadcrumb}}\x0a\x20\x20</h1>\x0a{{end}}\x0a\x0a{{with\x20.Subtitle}}\x0a\x20\x20<h2>{{html\x20.}}</h2>\x0a{{end}}\x0a\x0a{{with\x20.SrcPath}}\x0a\x20\x20<h2>\x0a\x20\x20\x20\x20Documentation:\x20{{html\x20.\x20|\x20srcToPkgLink}}\x0a\x20\x20</h2>\x0a{{end}}\x0a\x0a{{/*\x20The\x20Table\x20of\x20Contents\x20is\x20automatically\x20inserted\x20in\x20this\x20<div>.\x0a\x20\x20\x20\x20\x20Do\x20not\x20delete\x20this\x20<div>.\x20*/}}\x0a<div\x20id=\"nav\"></div>\x0a\x0a{{/*\x20Body\x20is\x20HTML-escaped\x20elsewhere\x20*/}}\x0a{{printf\x20\"%s\"\x20.Body}}\x0a\x0a<div\x20id=\"footer\">\x0aBuild\x20version\x20{{html\x20.Version}}.<br>\x0aExcept\x20as\x20<a\x20href=\"https://developers.google.com/site-policies#restrictions\">noted</a>,\x0athe\x20content\x20of\x20this\x20page\x20is\x20licensed\x20under\x20the\x0aCreative\x20Commons\x20Attribution\x203.0\x20License,\x0aand\x20code\x20is\x20licensed\x20under\x20a\x20<a\x20href=\"/LICENSE\">BSD\x20license</a>.<br>\x0a<a\x20href=\"/doc/tos.html\">Terms\x20of\x20Service</a>\x20|\x0a<a\x20href=\"http://www.google.com/intl/en/policies/privacy/\">Privacy\x20Policy</a>\x0a</div>\x0a\x0a</div><!--\x20.container\x20-->\x0a</div><!--\x20#page\x20-->\x0a{{with\x20.GoogleAnalytics}}\x0a<script\x20type=\"text/javascript\">\x0a(function()\x20{\x0a\x20\x20var\x20ga\x20=\x20document.createElement(\"script\");\x20ga.type\x20=\x20\"text/javascript\";\x20ga.async\x20=\x20true;\x0a\x20\x20ga.src\x20=\x20(\"https:\"\x20==\x20document.location.protocol\x20?\x20\"https://ssl\"\x20:\x20\"http://www\")\x20+\x20\".google-analytics.com/ga.js\";\x0a\x20\x20var\x20s\x20=\x20document.getElementsByTagName(\"script\")[0];\x20s.parentNode.insertBefore(ga,\x20s);\x0a})();\x0a</script>\x0a{{end}}\x0a</body>\x0a</html>\x0a\x0a",
"godocs.js": "//\x20Copyright\x202012\x20The\x20Go\x20Authors.\x20All\x20rights\x20reserved.\x0a//\x20Use\x20of\x20this\x20source\x20code\x20is\x20governed\x20by\x20a\x20BSD-style\x0a//\x20license\x20that\x20can\x20be\x20found\x20in\x20the\x20LICENSE\x20file.\x0a\x0a/*\x20A\x20little\x20code\x20to\x20ease\x20navigation\x20of\x20these\x20documents.\x0a\x20*\x0a\x20*\x20On\x20window\x20load\x20we:\x0a\x20*\x20\x20+\x20Generate\x20a\x20table\x20of\x20contents\x20(generateTOC)\x0a\x20*\x20\x20+\x20Bind\x20foldable\x20sections\x20(bindToggles)\x0a\x20*\x20\x20+\x20Bind\x20links\x20to\x20foldable\x20sections\x20(bindToggleLinks)\x0a\x20*/\x0a\x0a(function()\x20{\x0a'use\x20strict';\x0a\x0a//\x20Mobile-friendly\x20topbar\x20menu\x0a$(function()\x20{\x0a\x20\x20var\x20menu\x20=\x20$('#menu');\x0a\x20\x20var\x20menuButton\x20=\x20$('#menu-button');\x0a\x20\x20var\x20menuButtonArrow\x20=\x20$('#menu-button-arrow');\x0a\x20\x20menuButton.click(function(event)\x20{\x0a\x20\x20\x20\x20menu.toggleClass('menu-visible');\x0a\x20\x20\x20\x20menuButtonArrow.toggleClass('vertical-flip');\x0a\x20\x20\x20\x20event.preventDefault();\x0a\x20\x20\x20\x20return\x20false;\x0a\x20\x20});\x0a});\x0a\x0a/*\x20Generates\x20a\x20table\x20of\x20contents:\x20looks\x20for\x20h2\x20and\x20h3\x20elements\x20and\x20generates\x0a\x20*\x20links.\x20\"Decorates\"\x20the\x20element\x20with\x20id==\"nav\"\x20with\x20this\x20table\x20of\x20contents.\x0a\x20*/\x0afunction\x20generateTOC()\x20{\x0a\x20\x20if\x20($('#manual-nav').length\x20>\x200)\x20{\x0a\x20\x20\x20\x20return;\x0a\x20\x20}\x0a\x0a\x20\x20var\x20nav\x20=\x20$('#nav');\x0a\x20\x20if\x20(nav.length\x20===\x200)\x20{\x0a\x20\x20\x20\x20return;\x0a\x20\x20}\x0a\x0a\x20\x20var\x20toc_items\x20=\x20[];\x0a\x20\x20$(nav).nextAll('h2,\x20h3').each(function()\x20{\x0a\x20\x20\x20\x20var\x20node\x20=\x20this;\x0a\x20\x20\x20\x20if\x20(node.id\x20==\x20'')\x0a\x20\x20\x20\x20\x20\x20node.id\x20=\x20'tmp_'\x20+\x20toc_items.length;\x0a\x20\x20\x20\x20var\x20link\x20=\x20$('<a/>').attr('href',\x20'#'\x20+\x20node.id).text($(node).text());\x0a\x20\x20\x20\x20var\x20item;\x0a\x20\x20\x20\x20if\x20($(node).is('h2'))\x20{\x0a\x20\x20\x20\x20\x20\x20item\x20=\x20$('<dt/>');\x0a\x20\x20\x20\x20}\x20else\x20{\x20//\x20h3\x0a\x20\x20\x20\x20\x20\x20item\x20=\x20$('<dd\x20class=\"indent\"/>');\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20item.append(link);\x0a\x20\x20\x20\x20toc_items.push(item);\x0a\x20\x20});\x0a\x20\x20if\x20(toc_items.length\x20<=\x201)\x20{\x0a\x20\x20\x20\x20return;\x0a\x20\x20}\x0a\x0a\x20\x20var\x20dl1\x20=\x20$('<dl/>');\x0a\x20\x20var\x20dl2\x20=\x20$('<dl/>');\x0a\x0a\x20\x20var\x20split_index\x20=\x20(toc_items.length\x20/\x202)\x20+\x201;\x0a\x20\x20if\x20(split_index\x20<\x208)\x20{\x0a\x20\x20\x20\x20split_index\x20=\x20toc_items.length;\x0a\x20\x20}\x0a\x20\x20for\x20(var\x20i\x20=\x200;\x20i\x20<\x20split_index;\x20i++)\x20{\x0a\x20\x20\x20\x20dl1.append(toc_items[i]);\x0a\x20\x20}\x0a\x20\x20for\x20(/*\x20keep\x20using\x20i\x20*/;\x20i\x20<\x20toc_items.length;\x20i++)\x20{\x0a\x20\x20\x20\x20dl2.append(toc_items[i]);\x0a\x20\x20}\x0a\x0a\x20\x20var\x20tocTable\x20=\x20$('<table\x20class=\"unruled\"/>').appendTo(nav);\x0a\x20\x20var\x20tocBody\x20=\x20$('<tbody/>').appendTo(tocTable);\x0a\x20\x20var\x20tocRow\x20=\x20$('<tr/>').appendTo(tocBody);\x0a\x0a\x20\x20//\x201st\x20column\x0a\x20\x20$('<td\x20class=\"first\"/>').appendTo(tocRow).append(dl1);\x0a\x20\x20//\x202nd\x20column\x0a\x20\x20$('<td/>').appendTo(tocRow).append(dl2);\x0a}\x0a\x0afunction\x20bindToggle(el)\x20{\x0a\x20\x20$('.toggleButton',\x20el).click(function()\x20{\x0a\x20\x20\x20\x20if\x20($(this).closest(\".toggle,\x20.toggleVisible\")[0]\x20!=\x20el)\x20{\x0a\x20\x20\x20\x20\x20\x20//\x20Only\x20trigger\x20the\x20closest\x20toggle\x20header.\x0a\x20\x20\x20\x20\x20\x20return;\x0a\x20\x20\x20\x20}\x0a\x0a\x20\x20\x20\x20if\x20($(el).is('.toggle'))\x20{\x0a\x20\x20\x20\x20\x20\x20$(el).addClass('toggleVisible').removeClass('toggle');\x0a\x20\x20\x20\x20}\x20else\x20{\x0a\x20\x20\x20\x20\x20\x20$(el).addClass('toggle').removeClass('toggleVisible');\x0a\x20\x20\x20\x20}\x0a\x20\x20});\x0a}\x0a\x0afunction\x20bindToggles(selector)\x20{\x0a\x20\x20$(selector).each(function(i,\x20el)\x20{\x0a\x20\x20\x20\x20bindToggle(el);\x0a\x20\x20});\x0a}\x0a\x0afunction\x20bindToggleLink(el,\x20prefix)\x20{\x0a\x20\x20$(el).click(function()\x20{\x0a\x20\x20\x20\x20var\x20href\x20=\x20$(el).attr('href');\x0a\x20\x20\x20\x20var\x20i\x20=\x20href.indexOf('#'+prefix);\x0a\x20\x20\x20\x20if\x20(i\x20<\x200)\x20{\x0a\x20\x20\x20\x20\x20\x20return;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20var\x20id\x20=\x20'#'\x20+\x20prefix\x20+\x20href.slice(i+1+prefix.length);\x0a\x20\x20\x20\x20if\x20($(id).is('.toggle'))\x20{\x0a\x20\x20\x20\x20\x20\x20$(id).find('.toggleButton').first().click();\x0a\x20\x20\x20\x20}\x0a\x20\x20});\x0a}\x0afunction\x20bindToggleLinks(selector,\x20prefix)\x20{\x0a\x20\x20$(selector).each(function(i,\x20el)\x20{\x0a\x20\x20\x20\x20bindToggleLink(el,\x20prefix);\x0a\x20\x20});\x0a}\x0a\x0afunction\x20setupDropdownPlayground()\x20{\x0a\x20\x20if\x20(!$('#page').is('.wide'))\x20{\x0a\x20\x20\x20\x20return;\x20//\x20don't\x20show\x20on\x20front\x20page\x0a\x20\x20}\x0a\x20\x20var\x20button\x20=\x20$('#playgroundButton');\x0a\x20\x20var\x20div\x20=\x20$('#playground');\x0a\x20\x20var\x20setup\x20=\x20false;\x0a\x20\x20button.toggle(function()\x20{\x0a\x20\x20\x20\x20button.addClass('active');\x0a\x20\x20\x20\x20div.show();\x0a\x20\x20\x20\x20if\x20(setup)\x20{\x0a\x20\x20\x20\x20\x20\x20return;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20setup\x20=\x20true;\x0a\x20\x20\x20\x20playground({\x0a\x20\x20\x20\x20\x20\x20'codeEl':\x20$('.code',\x20div),\x0a\x20\x20\x20\x20\x20\x20'outputEl':\x20$('.output',\x20div),\x0a\x20\x20\x20\x20\x20\x20'runEl':\x20$('.run',\x20div),\x0a\x20\x20\x20\x20\x20\x20'fmtEl':\x20$('.fmt',\x20div),\x0a\x20\x20\x20\x20\x20\x20'shareEl':\x20$('.share',\x20div),\x0a\x20\x20\x20\x20\x20\x20'shareRedirect':\x20'//play.golang.org/p/'\x0a\x20\x20\x20\x20});\x0a\x20\x20},\x0a\x20\x20function()\x20{\x0a\x20\x20\x20\x20button.removeClass('active');\x0a\x20\x20\x20\x20div.hide();\x0a\x20\x20});\x0a\x20\x20button.show();\x0a\x20\x20$('#menu').css('min-width',\x20'+=60');\x0a}\x0a\x0afunction\x20setupInlinePlayground()\x20{\x0a\x09'use\x20strict';\x0a\x09//\x20Set\x20up\x20playground\x20when\x20each\x20element\x20is\x20toggled.\x0a\x09$('div.play').each(function\x20(i,\x20el)\x20{\x0a\x09\x09//\x20Set\x20up\x20playground\x20for\x20this\x20example.\x0a\x09\x09var\x20setup\x20=\x20function()\x20{\x0a\x09\x09\x09var\x20code\x20=\x20$('.code',\x20el);\x0a\x09\x09\x09playground({\x0a\x09\x09\x09\x09'codeEl':\x20\x20\x20code,\x0a\x09\x09\x09\x09'outputEl':\x20$('.output',\x20el),\x0a\x09\x09\x09\x09'runEl':\x20\x20\x20\x20$('.run',\x20el),\x0a\x09\x09\x09\x09'fmtEl':\x20\x20\x20\x20$('.fmt',\x20el),\x0a\x09\x09\x09\x09'shareEl':\x20\x20$('.share',\x20el),\x0a\x09\x09\x09\x09'shareRedirect':\x20'//play.golang.org/p/'\x0a\x09\x09\x09});\x0a\x0a\x09\x09\x09//\x20Make\x20the\x20code\x20textarea\x20resize\x20to\x20fit\x20content.\x0a\x09\x09\x09var\x20resize\x20=\x20function()\x20{\x0a\x09\x09\x09\x09code.height(0);\x0a\x09\x09\x09\x09var\x20h\x20=\x20code[0].scrollHeight;\x0a\x09\x09\x09\x09code.height(h+20);\x20//\x20minimize\x20bouncing.\x0a\x09\x09\x09\x09code.closest('.input').height(h);\x0a\x09\x09\x09};\x0a\x09\x09\x09code.on('keydown',\x20resize);\x0a\x09\x09\x09code.on('keyup',\x20resize);\x0a\x09\x09\x09code.keyup();\x20//\x20resize\x20now.\x0a\x09\x09};\x0a\x0a\x09\x09//\x20If\x20example\x20already\x20visible,\x20set\x20up\x20playground\x20now.\x0a\x09\x09if\x20($(el).is(':visible'))\x20{\x0a\x09\x09\x09setup();\x0a\x09\x09\x09return;\x0a\x09\x09}\x0a\x0a\x09\x09//\x20Otherwise,\x20set\x20up\x20playground\x20when\x20example\x20is\x20expanded.\x0a\x09\x09var\x20built\x20=\x20false;\x0a\x09\x09$(el).closest('.toggle').click(function()\x20{\x0a\x09\x09\x09//\x20Only\x20set\x20up\x20once.\x0a\x09\x09\x09if\x20(!built)\x20{\x0a\x09\x09\x09\x09setup();\x0a\x09\x09\x09\x09built\x20=\x20true;\x0a\x09\x09\x09}\x0a\x09\x09});\x0a\x09});\x0a}\x0a\x0a//\x20fixFocus\x20tries\x20to\x20put\x20focus\x20to\x20div#page\x20so\x20that\x20keyboard\x20navigation\x20works.\x0afunction\x20fixFocus()\x20{\x0a\x20\x20var\x20page\x20=\x20$('div#page');\x0a\x20\x20var\x20topbar\x20=\x20$('div#topbar');\x0a\x20\x20page.css('outline',\x200);\x20//\x20disable\x20outline\x20when\x20focused\x0a\x20\x20page.attr('tabindex',\x20-1);\x20//\x20and\x20set\x20tabindex\x20so\x20that\x20it\x20is\x20focusable\x0a\x20\x20$(window).resize(function\x20(evt)\x20{\x0a\x20\x20\x20\x20//\x20only\x20focus\x20page\x20when\x20the\x20topbar\x20is\x20at\x20fixed\x20position\x20(that\x20is,\x20it's\x20in\x0a\x20\x20\x20\x20//\x20front\x20of\x20page,\x20and\x20keyboard\x20event\x20will\x20go\x20to\x20the\x20former\x20by\x20default.)\x0a\x20\x20\x20\x20//\x20by\x20focusing\x20page,\x20keyboard\x20event\x20will\x20go\x20to\x20page\x20so\x20that\x20up/down\x20arrow,\x0a\x20\x20\x20\x20//\x20space,\x20etc.\x20will\x20work\x20as\x20expected.\x0a\x20\x20\x20\x20if\x20(topbar.css('position')\x20==\x20\"fixed\")\x0a\x20\x20\x20\x20\x20\x20page.focus();\x0a\x20\x20}).resize();\x0a}\x0a\x0afunction\x20toggleHash()\x20{\x0a\x20\x20var\x20id\x20=\x20window.location.hash.substring(1);\x0a\x20\x20//\x20Open\x20all\x20of\x20the\x20toggles\x20for\x20a\x20particular\x20hash.\x0a\x20\x20var\x20els\x20=\x20$(\x0a\x20\x20\x20\x20document.getElementById(id),\x0a\x20\x20\x20\x20$('a[name]').filter(function()\x20{\x0a\x20\x20\x20\x20\x20\x20return\x20$(this).attr('name')\x20==\x20id;\x0a\x20\x20\x20\x20})\x0a\x20\x20);\x0a\x0a\x20\x20while\x20(els.length)\x20{\x0a\x20\x20\x20\x20for\x20(var\x20i\x20=\x200;\x20i\x20<\x20els.length;\x20i++)\x20{\x0a\x20\x20\x20\x20\x20\x20var\x20el\x20=\x20$(els[i]);\x0a\x20\x20\x20\x20\x20\x20if\x20(el.is('.toggle'))\x20{\x0a\x20\x20\x20\x20\x20\x20\x20\x20el.find('.toggleButton').first().click();\x0a\x20\x20\x20\x20\x20\x20}\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20els\x20=\x20el.parent();\x0a\x20\x20}\x0a}\x0a\x0afunction\x20personalizeInstallInstructions()\x20{\x0a\x20\x20var\x20prefix\x20=\x20'?download=';\x0a\x20\x20var\x20s\x20=\x20window.location.search;\x0a\x20\x20if\x20(s.indexOf(prefix)\x20!=\x200)\x20{\x0a\x20\x20\x20\x20//\x20No\x20'download'\x20query\x20string;\x20detect\x20\"test\"\x20instructions\x20from\x20User\x20Agent.\x0a\x20\x20\x20\x20if\x20(navigator.platform.indexOf('Win')\x20!=\x20-1)\x20{\x0a\x20\x20\x20\x20\x20\x20$('.testUnix').hide();\x0a\x20\x20\x20\x20\x20\x20$('.testWindows').show();\x0a\x20\x20\x20\x20}\x20else\x20{\x0a\x20\x20\x20\x20\x20\x20$('.testUnix').show();\x0a\x20\x20\x20\x20\x20\x20$('.testWindows').hide();\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20return;\x0a\x20\x20}\x0a\x0a\x20\x20var\x20filename\x20=\x20s.substr(prefix.length);\x0a\x20\x20var\x20filenameRE\x20=\x20/^go1\\.\\d+(\\.\\d+)?([a-z0-9]+)?\\.([a-z0-9]+)(-[a-z0-9]+)?(-osx10\\.[68])?\\.([a-z.]+)$/;\x0a\x20\x20var\x20m\x20=\x20filenameRE.exec(filename);\x0a\x20\x20if\x20(!m)\x20{\x0a\x20\x20\x20\x20//\x20Can't\x20interpret\x20file\x20name;\x20bail.\x0a\x20\x20\x20\x20return;\x0a\x20\x20}\x0a\x20\x20$('.downloadFilename').text(filename);\x0a\x20\x20$('.hideFromDownload').hide();\x0a\x0a\x20\x20var\x20os\x20=\x20m[3];\x0a\x20\x20var\x20ext\x20=\x20m[6];\x0a\x20\x20if\x20(ext\x20!=\x20'tar.gz')\x20{\x0a\x20\x20\x20\x20$('#tarballInstructions').hide();\x0a\x20\x20}\x0a\x20\x20if\x20(os\x20!=\x20'darwin'\x20||\x20ext\x20!=\x20'pkg')\x20{\x0a\x20\x20\x20\x20$('#darwinPackageInstructions').hide();\x0a\x20\x20}\x0a\x20\x20if\x20(os\x20!=\x20'windows')\x20{\x0a\x20\x20\x20\x20$('#windowsInstructions').hide();\x0a\x20\x20\x20\x20$('.testUnix').show();\x0a\x20\x20\x20\x20$('.testWindows').hide();\x0a\x20\x20}\x20else\x20{\x0a\x20\x20\x20\x20if\x20(ext\x20!=\x20'msi')\x20{\x0a\x20\x20\x20\x20\x20\x20$('#windowsInstallerInstructions').hide();\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20if\x20(ext\x20!=\x20'zip')\x20{\x0a\x20\x20\x20\x20\x20\x20$('#windowsZipInstructions').hide();\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20$('.testUnix').hide();\x0a\x20\x20\x20\x20$('.testWindows').show();\x0a\x20\x20}\x0a\x0a\x20\x20var\x20download\x20=\x20\"https://dl.google.com/go/\"\x20+\x20filename;\x0a\x0a\x20\x20var\x20message\x20=\x20$('<p\x20class=\"downloading\">'+\x0a\x20\x20\x20\x20'Your\x20download\x20should\x20begin\x20shortly.\x20'+\x0a\x20\x20\x20\x20'If\x20it\x20does\x20not,\x20click\x20<a>this\x20link</a>.</p>');\x0a\x20\x20message.find('a').attr('href',\x20download);\x0a\x20\x20message.insertAfter('#nav');\x0a\x0a\x20\x20window.location\x20=\x20download;\x0a}\x0a\x0afunction\x20updateVersionTags()\x20{\x0a\x20\x20var\x20v\x20=\x20window.goVersion;\x0a\x20\x20if\x20(/^go[0-9.]+$/.test(v))\x20{\x0a\x20\x20\x20\x20$(\".versionTag\").empty().text(v);\x0a\x20\x20\x20\x20$(\".whereTag\").hide();\x0a\x20\x20}\x0a}\x0a\x0afunction\x20addPermalinks()\x20{\x0a\x20\x20function\x20addPermalink(source,\x20parent)\x20{\x0a\x20\x20\x20\x20var\x20id\x20=\x20source.attr(\"id\");\x0a\x20\x20\x20\x20if\x20(id\x20==\x20\"\"\x20||\x20id.indexOf(\"tmp_\")\x20===\x200)\x20{\x0a\x20\x20\x20\x20\x20\x20//\x20Auto-generated\x20permalink.\x0a\x20\x20\x20\x20\x20\x20return;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20if\x20(parent.find(\">\x20.permalink\").length)\x20{\x0a\x20\x20\x20\x20\x20\x20//\x20Already\x20attached.\x0a\x20\x20\x20\x20\x20\x20return;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20parent.append(\"\x20\").append($(\"<a\x20class='permalink'>¶</a>\").attr(\"href\",\x20\"#\"\x20+\x20id));\x0a\x20\x20}\x0a\x0a\x20\x20$(\"#page\x20.container\").find(\"h2[id],\x20h3[id]\").each(function()\x20{\x0a\x20\x20\x20\x20var\x20el\x20=\x20$(this);\x0a\x20\x20\x20\x20addPermalink(el,\x20el);\x0a\x20\x20});\x0a\x0a\x20\x20$(\"#page\x20.container\").find(\"dl[id]\").each(function()\x20{\x0a\x20\x20\x20\x20var\x20el\x20=\x20$(this);\x0a\x20\x20\x20\x20//\x20Add\x20the\x20anchor\x20to\x20the\x20\"dt\"\x20element.\x0a\x20\x20\x20\x20addPermalink(el,\x20el.find(\">\x20dt\").first());\x0a\x20\x20});\x0a}\x0a\x0a$(\".js-expandAll\").click(function()\x20{\x0a\x20\x20if\x20($(this).hasClass(\"collapsed\"))\x20{\x0a\x20\x20\x20\x20toggleExamples('toggle');\x0a\x20\x20\x20\x20$(this).text(\"(Collapse\x20All)\");\x0a\x20\x20}\x20else\x20{\x0a\x20\x20\x20\x20toggleExamples('toggleVisible');\x0a\x20\x20\x20\x20$(this).text(\"(Expand\x20All)\");\x0a\x20\x20}\x0a\x20\x20$(this).toggleClass(\"collapsed\")\x0a});\x0a\x0afunction\x20toggleExamples(className)\x20{\x0a\x20\x20//\x20We\x20need\x20to\x20explicitly\x20iterate\x20through\x20divs\x20starting\x20with\x20\"example_\"\x0a\x20\x20//\x20to\x20avoid\x20toggling\x20Overview\x20and\x20Index\x20collapsibles.\x0a\x20\x20$(\"[id^='example_']\").each(function()\x20{\x0a\x20\x20\x20\x20//\x20Check\x20for\x20state\x20and\x20click\x20it\x20only\x20if\x20required.\x0a\x20\x20\x20\x20if\x20($(this).hasClass(className))\x20{\x0a\x20\x20\x20\x20\x20\x20$(this).find('.toggleButton').first().click();\x0a\x20\x20\x20\x20}\x0a\x20\x20});\x0a}\x0a\x0a$(document).ready(function()\x20{\x0a\x20\x20generateTOC();\x0a\x20\x20addPermalinks();\x0a\x20\x20bindToggles(\".toggle\");\x0a\x20\x20bindToggles(\".toggleVisible\");\x0a\x20\x20bindToggleLinks(\".exampleLink\",\x20\"example_\");\x0a\x20\x20bindToggleLinks(\".overviewLink\",\x20\"\");\x0a\x20\x20bindToggleLinks(\".examplesLink\",\x20\"\");\x0a\x20\x20bindToggleLinks(\".indexLink\",\x20\"\");\x0a\x20\x20setupDropdownPlayground();\x0a\x20\x20setupInlinePlayground();\x0a\x20\x20fixFocus();\x0a\x20\x20setupTypeInfo();\x0a\x20\x20setupCallgraphs();\x0a\x20\x20toggleHash();\x0a\x20\x20personalizeInstallInstructions();\x0a\x20\x20updateVersionTags();\x0a\x0a\x20\x20//\x20godoc.html\x20defines\x20window.initFuncs\x20in\x20the\x20<head>\x20tag,\x20and\x20root.html\x20and\x0a\x20\x20//\x20codewalk.js\x20push\x20their\x20on-page-ready\x20functions\x20to\x20the\x20list.\x0a\x20\x20//\x20We\x20execute\x20those\x20functions\x20here,\x20to\x20avoid\x20loading\x20jQuery\x20until\x20the\x20page\x0a\x20\x20//\x20content\x20is\x20loaded.\x0a\x20\x20for\x20(var\x20i\x20=\x200;\x20i\x20<\x20window.initFuncs.length;\x20i++)\x20window.initFuncs[i]();\x0a});\x0a\x0a//\x20--\x20analysis\x20---------------------------------------------------------\x0a\x0a//\x20escapeHTML\x20returns\x20HTML\x20for\x20s,\x20with\x20metacharacters\x20quoted.\x0a//\x20It\x20is\x20safe\x20for\x20use\x20in\x20both\x20elements\x20and\x20attributes\x0a//\x20(unlike\x20the\x20\"set\x20innerText,\x20read\x20innerHTML\"\x20trick).\x0afunction\x20escapeHTML(s)\x20{\x0a\x20\x20\x20\x20return\x20s.replace(/&/g,\x20'&').\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20replace(/\\\"/g,\x20'"').\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20replace(/\\'/g,\x20''').\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20replace(/</g,\x20'<').\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20replace(/>/g,\x20'>');\x0a}\x0a\x0a//\x20makeAnchor\x20returns\x20HTML\x20for\x20an\x20<a>\x20element,\x20given\x20an\x20anchorJSON\x20object.\x0afunction\x20makeAnchor(json)\x20{\x0a\x20\x20var\x20html\x20=\x20escapeHTML(json.Text);\x0a\x20\x20if\x20(json.Href\x20!=\x20\"\")\x20{\x0a\x20\x20\x20\x20\x20\x20html\x20=\x20\"<a\x20href='\"\x20+\x20escapeHTML(json.Href)\x20+\x20\"'>\"\x20+\x20html\x20+\x20\"</a>\";\x0a\x20\x20}\x0a\x20\x20return\x20html;\x0a}\x0a\x0afunction\x20showLowFrame(html)\x20{\x0a\x20\x20var\x20lowframe\x20=\x20document.getElementById('lowframe');\x0a\x20\x20lowframe.style.height\x20=\x20\"200px\";\x0a\x20\x20lowframe.innerHTML\x20=\x20\"<p\x20style='text-align:\x20left;'>\"\x20+\x20html\x20+\x20\"</p>\\n\"\x20+\x0a\x20\x20\x20\x20\x20\x20\"<div\x20onclick='hideLowFrame()'\x20style='position:\x20absolute;\x20top:\x200;\x20right:\x200;\x20cursor:\x20pointer;'>\xe2\x9c\x98</div>\"\x0a};\x0a\x0adocument.hideLowFrame\x20=\x20function()\x20{\x0a\x20\x20var\x20lowframe\x20=\x20document.getElementById('lowframe');\x0a\x20\x20lowframe.style.height\x20=\x20\"0px\";\x0a}\x0a\x0a//\x20onClickCallers\x20is\x20the\x20onclick\x20action\x20for\x20the\x20'func'\x20tokens\x20of\x20a\x0a//\x20function\x20declaration.\x0adocument.onClickCallers\x20=\x20function(index)\x20{\x0a\x20\x20var\x20data\x20=\x20document.ANALYSIS_DATA[index]\x0a\x20\x20if\x20(data.Callers.length\x20==\x201\x20&&\x20data.Callers[0].Sites.length\x20==\x201)\x20{\x0a\x20\x20\x20\x20document.location\x20=\x20data.Callers[0].Sites[0].Href;\x20//\x20jump\x20to\x20sole\x20caller\x0a\x20\x20\x20\x20return;\x0a\x20\x20}\x0a\x0a\x20\x20var\x20html\x20=\x20\"Callers\x20of\x20<code>\"\x20+\x20escapeHTML(data.Callee)\x20+\x20\"</code>:<br/>\\n\";\x0a\x20\x20for\x20(var\x20i\x20=\x200;\x20i\x20<\x20data.Callers.length;\x20i++)\x20{\x0a\x20\x20\x20\x20var\x20caller\x20=\x20data.Callers[i];\x0a\x20\x20\x20\x20html\x20+=\x20\"<code>\"\x20+\x20escapeHTML(caller.Func)\x20+\x20\"</code>\";\x0a\x20\x20\x20\x20var\x20sites\x20=\x20caller.Sites;\x0a\x20\x20\x20\x20if\x20(sites\x20!=\x20null\x20&&\x20sites.length\x20>\x200)\x20{\x0a\x20\x20\x20\x20\x20\x20html\x20+=\x20\"\x20at\x20line\x20\";\x0a\x20\x20\x20\x20\x20\x20for\x20(var\x20j\x20=\x200;\x20j\x20<\x20sites.length;\x20j++)\x20{\x0a\x20\x20\x20\x20\x20\x20\x20\x20if\x20(j\x20>\x200)\x20{\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20html\x20+=\x20\",\x20\";\x0a\x20\x20\x20\x20\x20\x20\x20\x20}\x0a\x20\x20\x20\x20\x20\x20\x20\x20html\x20+=\x20\"<code>\"\x20+\x20makeAnchor(sites[j])\x20+\x20\"</code>\";\x0a\x20\x20\x20\x20\x20\x20}\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20html\x20+=\x20\"<br/>\\n\";\x0a\x20\x20}\x0a\x20\x20showLowFrame(html);\x0a};\x0a\x0a//\x20onClickCallees\x20is\x20the\x20onclick\x20action\x20for\x20the\x20'('\x20token\x20of\x20a\x20function\x20call.\x0adocument.onClickCallees\x20=\x20function(index)\x20{\x0a\x20\x20var\x20data\x20=\x20document.ANALYSIS_DATA[index]\x0a\x20\x20if\x20(data.Callees.length\x20==\x201)\x20{\x0a\x20\x20\x20\x20document.location\x20=\x20data.Callees[0].Href;\x20//\x20jump\x20to\x20sole\x20callee\x0a\x20\x20\x20\x20return;\x0a\x20\x20}\x0a\x0a\x20\x20var\x20html\x20=\x20\"Callees\x20of\x20this\x20\"\x20+\x20escapeHTML(data.Descr)\x20+\x20\":<br/>\\n\";\x0a\x20\x20for\x20(var\x20i\x20=\x200;\x20i\x20<\x20data.Callees.length;\x20i++)\x20{\x0a\x20\x20\x20\x20html\x20+=\x20\"<code>\"\x20+\x20makeAnchor(data.Callees[i])\x20+\x20\"</code><br/>\\n\";\x0a\x20\x20}\x0a\x20\x20showLowFrame(html);\x0a};\x0a\x0a//\x20onClickTypeInfo\x20is\x20the\x20onclick\x20action\x20for\x20identifiers\x20declaring\x20a\x20named\x20type.\x0adocument.onClickTypeInfo\x20=\x20function(index)\x20{\x0a\x20\x20var\x20data\x20=\x20document.ANALYSIS_DATA[index];\x0a\x20\x20var\x20html\x20=\x20\"Type\x20<code>\"\x20+\x20data.Name\x20+\x20\"</code>:\x20\"\x20+\x0a\x20\x20\" <small>(size=\"\x20+\x20data.Size\x20+\x20\",\x20align=\"\x20+\x20data.Align\x20+\x20\")</small><br/>\\n\";\x0a\x20\x20html\x20+=\x20implementsHTML(data);\x0a\x20\x20html\x20+=\x20methodsetHTML(data);\x0a\x20\x20showLowFrame(html);\x0a};\x0a\x0a//\x20implementsHTML\x20returns\x20HTML\x20for\x20the\x20implements\x20relation\x20of\x20the\x0a//\x20specified\x20TypeInfoJSON\x20value.\x0afunction\x20implementsHTML(info)\x20{\x0a\x20\x20var\x20html\x20=\x20\"\";\x0a\x20\x20if\x20(info.ImplGroups\x20!=\x20null)\x20{\x0a\x20\x20\x20\x20for\x20(var\x20i\x20=\x200;\x20i\x20<\x20info.ImplGroups.length;\x20i++)\x20{\x0a\x20\x20\x20\x20\x20\x20var\x20group\x20=\x20info.ImplGroups[i];\x0a\x20\x20\x20\x20\x20\x20var\x20x\x20=\x20\"<code>\"\x20+\x20escapeHTML(group.Descr)\x20+\x20\"</code>\x20\";\x0a\x20\x20\x20\x20\x20\x20for\x20(var\x20j\x20=\x200;\x20j\x20<\x20group.Facts.length;\x20j++)\x20{\x0a\x20\x20\x20\x20\x20\x20\x20\x20var\x20fact\x20=\x20group.Facts[j];\x0a\x20\x20\x20\x20\x20\x20\x20\x20var\x20y\x20=\x20\"<code>\"\x20+\x20makeAnchor(fact.Other)\x20+\x20\"</code>\";\x0a\x20\x20\x20\x20\x20\x20\x20\x20if\x20(fact.ByKind\x20!=\x20null)\x20{\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20html\x20+=\x20escapeHTML(fact.ByKind)\x20+\x20\"\x20type\x20\"\x20+\x20y\x20+\x20\"\x20implements\x20\"\x20+\x20x;\x0a\x20\x20\x20\x20\x20\x20\x20\x20}\x20else\x20{\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20html\x20+=\x20x\x20+\x20\"\x20implements\x20\"\x20+\x20y;\x0a\x20\x20\x20\x20\x20\x20\x20\x20}\x0a\x20\x20\x20\x20\x20\x20\x20\x20html\x20+=\x20\"<br/>\\n\";\x0a\x20\x20\x20\x20\x20\x20}\x0a\x20\x20\x20\x20}\x0a\x20\x20}\x0a\x20\x20return\x20html;\x0a}\x0a\x0a\x0a//\x20methodsetHTML\x20returns\x20HTML\x20for\x20the\x20methodset\x20of\x20the\x20specified\x0a//\x20TypeInfoJSON\x20value.\x0afunction\x20methodsetHTML(info)\x20{\x0a\x20\x20var\x20html\x20=\x20\"\";\x0a\x20\x20if\x20(info.Methods\x20!=\x20null)\x20{\x0a\x20\x20\x20\x20for\x20(var\x20i\x20=\x200;\x20i\x20<\x20info.Methods.length;\x20i++)\x20{\x0a\x20\x20\x20\x20\x20\x20html\x20+=\x20\"<code>\"\x20+\x20makeAnchor(info.Methods[i])\x20+\x20\"</code><br/>\\n\";\x0a\x20\x20\x20\x20}\x0a\x20\x20}\x0a\x20\x20return\x20html;\x0a}\x0a\x0a//\x20onClickComm\x20is\x20the\x20onclick\x20action\x20for\x20channel\x20\"make\"\x20and\x20\"<-\"\x0a//\x20send/receive\x20tokens.\x0adocument.onClickComm\x20=\x20function(index)\x20{\x0a\x20\x20var\x20ops\x20=\x20document.ANALYSIS_DATA[index].Ops\x0a\x20\x20if\x20(ops.length\x20==\x201)\x20{\x0a\x20\x20\x20\x20document.location\x20=\x20ops[0].Op.Href;\x20//\x20jump\x20to\x20sole\x20element\x0a\x20\x20\x20\x20return;\x0a\x20\x20}\x0a\x0a\x20\x20var\x20html\x20=\x20\"Operations\x20on\x20this\x20channel:<br/>\\n\";\x0a\x20\x20for\x20(var\x20i\x20=\x200;\x20i\x20<\x20ops.length;\x20i++)\x20{\x0a\x20\x20\x20\x20html\x20+=\x20makeAnchor(ops[i].Op)\x20+\x20\"\x20by\x20<code>\"\x20+\x20escapeHTML(ops[i].Fn)\x20+\x20\"</code><br/>\\n\";\x0a\x20\x20}\x0a\x20\x20if\x20(ops.length\x20==\x200)\x20{\x0a\x20\x20\x20\x20html\x20+=\x20\"(none)<br/>\\n\";\x0a\x20\x20}\x0a\x20\x20showLowFrame(html);\x0a};\x0a\x0a$(window).load(function()\x20{\x0a\x20\x20\x20\x20//\x20Scroll\x20window\x20so\x20that\x20first\x20selection\x20is\x20visible.\x0a\x20\x20\x20\x20//\x20(This\x20means\x20we\x20don't\x20need\x20to\x20emit\x20id='L%d'\x20spans\x20for\x20each\x20line.)\x0a\x20\x20\x20\x20//\x20TODO(adonovan):\x20ideally,\x20scroll\x20it\x20so\x20that\x20it's\x20under\x20the\x20pointer,\x0a\x20\x20\x20\x20//\x20but\x20I\x20don't\x20know\x20how\x20to\x20get\x20the\x20pointer\x20y\x20coordinate.\x0a\x20\x20\x20\x20var\x20elts\x20=\x20document.getElementsByClassName(\"selection\");\x0a\x20\x20\x20\x20if\x20(elts.length\x20>\x200)\x20{\x0a\x09elts[0].scrollIntoView()\x0a\x20\x20\x20\x20}\x0a});\x0a\x0a//\x20setupTypeInfo\x20populates\x20the\x20\"Implements\"\x20and\x20\"Method\x20set\"\x20toggle\x20for\x0a//\x20each\x20type\x20in\x20the\x20package\x20doc.\x0afunction\x20setupTypeInfo()\x20{\x0a\x20\x20for\x20(var\x20i\x20in\x20document.ANALYSIS_DATA)\x20{\x0a\x20\x20\x20\x20var\x20data\x20=\x20document.ANALYSIS_DATA[i];\x0a\x0a\x20\x20\x20\x20var\x20el\x20=\x20document.getElementById(\"implements-\"\x20+\x20i);\x0a\x20\x20\x20\x20if\x20(el\x20!=\x20null)\x20{\x0a\x20\x20\x20\x20\x20\x20//\x20el\x20!=\x20null\x20=>\x20data\x20is\x20TypeInfoJSON.\x0a\x20\x20\x20\x20\x20\x20if\x20(data.ImplGroups\x20!=\x20null)\x20{\x0a\x20\x20\x20\x20\x20\x20\x20\x20el.innerHTML\x20=\x20implementsHTML(data);\x0a\x20\x20\x20\x20\x20\x20\x20\x20el.parentNode.parentNode.style.display\x20=\x20\"block\";\x0a\x20\x20\x20\x20\x20\x20}\x0a\x20\x20\x20\x20}\x0a\x0a\x20\x20\x20\x20var\x20el\x20=\x20document.getElementById(\"methodset-\"\x20+\x20i);\x0a\x20\x20\x20\x20if\x20(el\x20!=\x20null)\x20{\x0a\x20\x20\x20\x20\x20\x20//\x20el\x20!=\x20null\x20=>\x20data\x20is\x20TypeInfoJSON.\x0a\x20\x20\x20\x20\x20\x20if\x20(data.Methods\x20!=\x20null)\x20{\x0a\x20\x20\x20\x20\x20\x20\x20\x20el.innerHTML\x20=\x20methodsetHTML(data);\x0a\x20\x20\x20\x20\x20\x20\x20\x20el.parentNode.parentNode.style.display\x20=\x20\"block\";\x0a\x20\x20\x20\x20\x20\x20}\x0a\x20\x20\x20\x20}\x0a\x20\x20}\x0a}\x0a\x0afunction\x20setupCallgraphs()\x20{\x0a\x20\x20if\x20(document.CALLGRAPH\x20==\x20null)\x20{\x0a\x20\x20\x20\x20return\x0a\x20\x20}\x0a\x20\x20document.getElementById(\"pkg-callgraph\").style.display\x20=\x20\"block\";\x0a\x0a\x20\x20var\x20treeviews\x20=\x20document.getElementsByClassName(\"treeview\");\x0a\x20\x20for\x20(var\x20i\x20=\x200;\x20i\x20<\x20treeviews.length;\x20i++)\x20{\x0a\x20\x20\x20\x20var\x20tree\x20=\x20treeviews[i];\x0a\x20\x20\x20\x20if\x20(tree.id\x20==\x20null\x20||\x20tree.id.indexOf(\"callgraph-\")\x20!=\x200)\x20{\x0a\x20\x20\x20\x20\x20\x20continue;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20var\x20id\x20=\x20tree.id.substring(\"callgraph-\".length);\x0a\x20\x20\x20\x20$(tree).treeview({collapsed:\x20true,\x20animated:\x20\"fast\"});\x0a\x20\x20\x20\x20document.cgAddChildren(tree,\x20tree,\x20[id]);\x0a\x20\x20\x20\x20tree.parentNode.parentNode.style.display\x20=\x20\"block\";\x0a\x20\x20}\x0a}\x0a\x0adocument.cgAddChildren\x20=\x20function(tree,\x20ul,\x20indices)\x20{\x0a\x20\x20if\x20(indices\x20!=\x20null)\x20{\x0a\x20\x20\x20\x20for\x20(var\x20i\x20=\x200;\x20i\x20<\x20indices.length;\x20i++)\x20{\x0a\x20\x20\x20\x20\x20\x20var\x20li\x20=\x20cgAddChild(tree,\x20ul,\x20document.CALLGRAPH[indices[i]]);\x0a\x20\x20\x20\x20\x20\x20if\x20(i\x20==\x20indices.length\x20-\x201)\x20{\x0a\x20\x20\x20\x20\x20\x20\x20\x20$(li).addClass(\"last\");\x0a\x20\x20\x20\x20\x20\x20}\x0a\x20\x20\x20\x20}\x0a\x20\x20}\x0a\x20\x20$(tree).treeview({animated:\x20\"fast\",\x20add:\x20ul});\x0a}\x0a\x0a//\x20cgAddChild\x20adds\x20an\x20<li>\x20element\x20for\x20document.CALLGRAPH\x20node\x20cgn\x20to\x0a//\x20the\x20parent\x20<ul>\x20element\x20ul.\x20tree\x20is\x20the\x20tree's\x20root\x20<ul>\x20element.\x0afunction\x20cgAddChild(tree,\x20ul,\x20cgn)\x20{\x0a\x20\x20\x20var\x20li\x20=\x20document.createElement(\"li\");\x0a\x20\x20\x20ul.appendChild(li);\x0a\x20\x20\x20li.className\x20=\x20\"closed\";\x0a\x0a\x20\x20\x20var\x20code\x20=\x20document.createElement(\"code\");\x0a\x0a\x20\x20\x20if\x20(cgn.Callees\x20!=\x20null)\x20{\x0a\x20\x20\x20\x20\x20$(li).addClass(\"expandable\");\x0a\x0a\x20\x20\x20\x20\x20//\x20Event\x20handlers\x20and\x20innerHTML\x20updates\x20don't\x20play\x20nicely\x20together,\x0a\x20\x20\x20\x20\x20//\x20hence\x20all\x20this\x20explicit\x20DOM\x20manipulation.\x0a\x20\x20\x20\x20\x20var\x20hitarea\x20=\x20document.createElement(\"div\");\x0a\x20\x20\x20\x20\x20hitarea.className\x20=\x20\"hitarea\x20expandable-hitarea\";\x0a\x20\x20\x20\x20\x20li.appendChild(hitarea);\x0a\x0a\x20\x20\x20\x20\x20li.appendChild(code);\x0a\x0a\x20\x20\x20\x20\x20var\x20childUL\x20=\x20document.createElement(\"ul\");\x0a\x20\x20\x20\x20\x20li.appendChild(childUL);\x0a\x20\x20\x20\x20\x20childUL.setAttribute('style',\x20\"display:\x20none;\");\x0a\x0a\x20\x20\x20\x20\x20var\x20onClick\x20=\x20function()\x20{\x0a\x20\x20\x20\x20\x20\x20\x20document.cgAddChildren(tree,\x20childUL,\x20cgn.Callees);\x0a\x20\x20\x20\x20\x20\x20\x20hitarea.removeEventListener('click',\x20onClick)\x0a\x20\x20\x20\x20\x20};\x0a\x20\x20\x20\x20\x20hitarea.addEventListener('click',\x20onClick);\x0a\x0a\x20\x20\x20}\x20else\x20{\x0a\x20\x20\x20\x20\x20li.appendChild(code);\x0a\x20\x20\x20}\x0a\x20\x20\x20code.innerHTML\x20+=\x20\" \"\x20+\x20makeAnchor(cgn.Func);\x0a\x20\x20\x20return\x20li\x0a}\x0a\x0a})();\x0a",
diff --git a/internal/memcache/memcache.go b/internal/memcache/memcache.go
new file mode 100644
index 0000000..25d5a62
--- /dev/null
+++ b/internal/memcache/memcache.go
@@ -0,0 +1,157 @@
+// Copyright 2018 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.
+
+// Package memcache provides a minimally compatible interface for
+// google.golang.org/appengine/memcache
+// and stores the data in Redis (e.g., via Cloud Memorystore).
+package memcache
+
+import (
+ "bytes"
+ "context"
+ "encoding/gob"
+ "encoding/json"
+ "errors"
+ "time"
+
+ "github.com/gomodule/redigo/redis"
+)
+
+var ErrCacheMiss = errors.New("memcache: cache miss")
+
+func New(addr string) *Client {
+ const maxConns = 20
+
+ pool := redis.NewPool(func() (redis.Conn, error) {
+ return redis.Dial("tcp", addr)
+ }, maxConns)
+
+ return &Client{
+ pool: pool,
+ }
+}
+
+type Client struct {
+ pool *redis.Pool
+}
+
+type CodecClient struct {
+ client *Client
+ codec Codec
+}
+
+type Item struct {
+ Key string
+ Value []byte
+ Object interface{} // Used with Codec.
+ Expiration time.Duration // Read-only.
+}
+
+func (c *Client) WithCodec(codec Codec) *CodecClient {
+ return &CodecClient{
+ c, codec,
+ }
+}
+
+func (c *Client) Delete(ctx context.Context, key string) error {
+ conn, err := c.pool.GetContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+
+ _, err = conn.Do("DEL", key)
+ return err
+}
+
+func (c *CodecClient) Delete(ctx context.Context, key string) error {
+ return c.client.Delete(ctx, key)
+}
+
+func (c *Client) Set(ctx context.Context, item *Item) error {
+ if item.Value == nil {
+ return errors.New("nil item value")
+ }
+ return c.set(ctx, item.Key, item.Value, item.Expiration)
+}
+
+func (c *CodecClient) Set(ctx context.Context, item *Item) error {
+ if item.Object == nil {
+ return errors.New("nil object value")
+ }
+ b, err := c.codec.Marshal(item.Object)
+ if err != nil {
+ return err
+ }
+ return c.client.set(ctx, item.Key, b, item.Expiration)
+}
+
+func (c *Client) set(ctx context.Context, key string, value []byte, expiration time.Duration) error {
+ conn, err := c.pool.GetContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+
+ if expiration == 0 {
+ _, err := conn.Do("SET", key, value)
+ return err
+ }
+
+ // NOTE(cbro): redis does not support expiry in units more granular than a second.
+ exp := int64(expiration.Seconds())
+ if exp == 0 {
+ // Redis doesn't allow a zero expiration, delete the key instead.
+ _, err := conn.Do("DEL", key)
+ return err
+ }
+
+ _, err = conn.Do("SETEX", key, exp, value)
+ return err
+}
+
+// Get gets the item.
+func (c *Client) Get(ctx context.Context, key string) ([]byte, error) {
+ conn, err := c.pool.GetContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer conn.Close()
+
+ b, err := redis.Bytes(conn.Do("GET", key))
+ if err == redis.ErrNil {
+ err = ErrCacheMiss
+ }
+ return b, err
+}
+
+func (c *CodecClient) Get(ctx context.Context, key string, v interface{}) error {
+ b, err := c.client.Get(ctx, key)
+ if err != nil {
+ return err
+ }
+ return c.codec.Unmarshal(b, v)
+}
+
+var (
+ Gob = Codec{gobMarshal, gobUnmarshal}
+ JSON = Codec{json.Marshal, json.Unmarshal}
+)
+
+type Codec struct {
+ Marshal func(interface{}) ([]byte, error)
+ Unmarshal func([]byte, interface{}) error
+}
+
+func gobMarshal(v interface{}) ([]byte, error) {
+ var buf bytes.Buffer
+ if err := gob.NewEncoder(&buf).Encode(v); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+func gobUnmarshal(data []byte, v interface{}) error {
+ return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v)
+}
diff --git a/internal/memcache/memcache_test.go b/internal/memcache/memcache_test.go
new file mode 100644
index 0000000..74f6ade
--- /dev/null
+++ b/internal/memcache/memcache_test.go
@@ -0,0 +1,83 @@
+// Copyright 2018 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.
+
+package memcache
+
+import (
+ "context"
+ "os"
+ "testing"
+ "time"
+)
+
+func getClient(t *testing.T) *Client {
+ t.Helper()
+
+ addr := os.Getenv("GOLANG_REDIS_ADDR")
+ if addr == "" {
+ t.Skip("skipping because GOLANG_REDIS_ADDR is unset")
+ }
+
+ return New(addr)
+}
+
+func TestCacheMiss(t *testing.T) {
+ c := getClient(t)
+ ctx := context.Background()
+
+ if _, err := c.Get(ctx, "doesnotexist"); err != ErrCacheMiss {
+ t.Errorf("got %v; want ErrCacheMiss", err)
+ }
+}
+
+func TestExpiry(t *testing.T) {
+ c := getClient(t).WithCodec(Gob)
+ ctx := context.Background()
+
+ key := "testexpiry"
+
+ firstTime := time.Now()
+ err := c.Set(ctx, &Item{
+ Key: key,
+ Object: firstTime,
+ Expiration: 3500 * time.Millisecond, // NOTE: check that non-rounded expiries work.
+ })
+ if err != nil {
+ t.Fatalf("Set: %v", err)
+ }
+
+ var newTime time.Time
+ if err := c.Get(ctx, key, &newTime); err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ if !firstTime.Equal(newTime) {
+ t.Errorf("Get: got value %v, want %v", newTime, firstTime)
+ }
+
+ time.Sleep(4 * time.Second)
+
+ if err := c.Get(ctx, key, &newTime); err != ErrCacheMiss {
+ t.Errorf("Get: got %v, want ErrCacheMiss", err)
+ }
+}
+
+func TestShortExpiry(t *testing.T) {
+ c := getClient(t).WithCodec(Gob)
+ ctx := context.Background()
+
+ key := "testshortexpiry"
+
+ err := c.Set(ctx, &Item{
+ Key: key,
+ Value: []byte("ok"),
+ Expiration: time.Millisecond,
+ })
+ if err != nil {
+ t.Fatalf("Set: %v", err)
+ }
+
+ if err := c.Get(ctx, key, nil); err != ErrCacheMiss {
+ t.Errorf("GetBytes: got %v, want ErrCacheMiss", err)
+ }
+}