diff --git a/cmd/godoc/Dockerfile.prod b/cmd/godoc/Dockerfile.prod new file mode 100644 index 00000000..21fb92d8 --- /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 00000000..217515bb --- /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 94abf0cd..0bb81120 100644 --- a/cmd/godoc/README.godoc-app +++ b/cmd/godoc/README.godoc-app @@ -7,31 +7,78 @@ Prerequisites * 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 6a18a647..832db097 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 eb1d9483..293ad52a 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,16 +11,20 @@ package main 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" @@ -28,10 +32,13 @@ import ( "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") @@ -44,7 +51,6 @@ func init() { indexFilenames = os.Getenv("GODOC_INDEX_GLOB") ) - enforceHosts = !appengine.IsDevAppServer() playEnabled = true log.Println("initializing godoc ...") @@ -85,17 +91,61 @@ func init() { 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 40e66584..edeecb8a 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 38ac79ae..21b567a9 100755 --- a/cmd/godoc/generate-index.bash +++ b/cmd/godoc/generate-index.bash @@ -25,24 +25,24 @@ install() { } 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 a8447b37..4152a3ee 100644 --- a/cmd/godoc/handlers.go +++ b/cmd/godoc/handlers.go @@ -21,6 +21,7 @@ import ( "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 @@ var ( 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 @@ type hostEnforcerHandler struct { } 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 00000000..3f6ca77b Binary files /dev/null and b/cmd/godoc/hg-git-mapping.bin differ diff --git a/cmd/godoc/main.go b/cmd/godoc/main.go index 89e9bba8..7f0ac0c9 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 02f477d3..f44a3ccc 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 00000000..9b596fae --- /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 " + +set -e + +addr="$(echo $1 | sed -e 's/\/$//')" +if [ -z "$addr" ]; then + echo "usage: $0 " 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 f01d5c7a..6f27d0b3 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 fe5e6875..00000000 --- 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 83bb21ec..edc09aa3 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 @@ import ( "html" "html/template" "io" + "log" "net/http" "regexp" "sort" @@ -27,11 +26,10 @@ import ( "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 @@ const ( 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 @@ var ( 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 @@ func listHandler(w http.ResponseWriter, r *http.Request) { } 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 @@ func listHandler(w http.ResponseWriter, r *http.Request) { 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 @@ func parseVersion(v string) (maj, min int, tail string) { 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 @@ func uploadHandler(w http.ResponseWriter, r *http.Request) { 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 @@ func uploadHandler(w http.ResponseWriter, r *http.Request) { 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 @@ func uploadHandler(w http.ResponseWriter, r *http.Request) { 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 @@ func getHandler(w http.ResponseWriter, r *http.Request) { 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 @@ func validUser(user string) bool { 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 @@ var ( 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 @@ func initHandler(w http.ResponseWriter, r *http.Request) { } // 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 @@ type builderKey struct { 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 @@ func secret(c context.Context) string { } // 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 3f61fe9e..2cdc1aa9 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 47ef9f49..d086b696 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 00000000..e1f55cd3 --- /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 10e86e5c..819af557 100644 --- a/godoc/page.go +++ b/godoc/page.go @@ -10,6 +10,8 @@ import ( "path/filepath" "runtime" "strings" + + "golang.org/x/tools/godoc/env" ) // Page describes the contents of the top-level godoc webpage. @@ -22,10 +24,11 @@ type Page struct { 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 @@ func (p *Presentation) ServePage(w http.ResponseWriter, page Page) { 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 @@ func (p *Presentation) ServeError(w http.ResponseWriter, r *http.Request, relpat } } 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 de23c756..b0077fd5 100644 --- a/godoc/pres.go +++ b/godoc/pres.go @@ -92,6 +92,10 @@ type Presentation struct { // 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 cdac3bf5..f3023727 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 @@ import ( "encoding/json" "fmt" "io/ioutil" + "log" "net/http" "net/http/httputil" "net/url" @@ -20,12 +19,18 @@ import ( "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 @@ type Event struct { 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 @@ func compile(w http.ResponseWriter, r *http.Request) { 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 @@ func compile(w http.ResponseWriter, r *http.Request) { 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 @@ func makeCompileRequest(ctx context.Context, req *Request, res *Response) error 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 @@ func share(w http.ResponseWriter, r *http.Request) { 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 44d3c936..da710eb3 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 @@ import ( "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 @@ type Link struct { 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 @@ func linkHandler(w http.ResponseWriter, r *http.Request) { } 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 @@ func linkHandler(w http.ResponseWriter, r *http.Request) { 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 @@ func linkHandler(w http.ResponseWriter, r *http.Request) { 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 @@ func adminHandler(w http.ResponseWriter, r *http.Request) { 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 @@ func adminHandler(w http.ResponseWriter, r *http.Request) { 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 95e4c2a8..66f5401e 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 6c7889f0..2688b166 100644 --- a/godoc/static/godoc.html +++ b/godoc/static/godoc.html @@ -15,6 +15,19 @@ {{end}} +{{with .GoogleAnalytics}} + +{{end}} @@ -112,6 +125,15 @@ and code is licensed under a BSD license.
+{{if .GoogleAnalytics}} + +{{end}} diff --git a/godoc/static/static.go b/godoc/static/static.go index 33f59931..eb9493cd 100644 --- a/godoc/static/static.go +++ b/godoc/static/static.go @@ -51,7 +51,7 @@ var Files = map[string]string{ "example.html": "\x0a\x09\x0a\x09\x09\xe2\x96\xb9\x20Example{{example_suffix\x20.Name}}

\x0a\x09\x0a\x09\x0a\x09\x09\xe2\x96\xbe\x20Example{{example_suffix\x20.Name}}

\x0a\x09\x09{{with\x20.Doc}}

{{html\x20.}}

{{end}}\x0a\x09\x09{{$output\x20:=\x20.Output}}\x0a\x09\x09{{with\x20.Play}}\x0a\x09\x09\x09\x0a\x09\x09\x09\x09{{html\x20.}}\x0a\x09\x09\x09\x09
{{html\x20$output}}
\x0a\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09Run\x0a\x09\x09\x09\x09\x09Format\x0a\x09\x09\x09\x09\x09{{if\x20not\x20$.GoogleCN}}\x0a\x09\x09\x09\x09\x09Share\x0a\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09\x0a\x09\x09\x09\x0a\x09\x09{{else}}\x0a\x09\x09\x09

Code:

\x0a\x09\x09\x09{{.Code}}\x0a\x09\x09\x09{{with\x20.Output}}\x0a\x09\x09\x09

Output:

\x0a\x09\x09\x09{{html\x20.}}\x0a\x09\x09\x09{{end}}\x0a\x09\x09{{end}}\x0a\x09\x0a\x0a", - "godoc.html": "\x0a\x0a\x0a\x0a\x0a\x0a{{with\x20.Tabtitle}}\x0a\x20\x20{{html\x20.}}\x20-\x20The\x20Go\x20Programming\x20Language\x0a{{else}}\x0a\x20\x20The\x20Go\x20Programming\x20Language\x0a{{end}}\x0a\x0a{{if\x20.SearchBox}}\x0a\x0a{{end}}\x0a\x0a\x0a\x0a\x0a\x0a\x0a{{if\x20.Playground}}\x0a\x0a{{end}}\x0a{{with\x20.Version}}{{end}}\x0a\x0a\x0a\x0a\x0a\x0a...\x0a\x0a\x0a\x0aThe\x20Go\x20Programming\x20Language\x0aGo\x0a▽\x0a\x0a\x0aDocuments\x0aPackages\x0aThe\x20Project\x0aHelp\x0a{{if\x20not\x20.GoogleCN}}\x0aBlog\x0a{{end}}\x0a{{if\x20.Playground}}\x0aPlay\x0a{{end}}\x0asubmit\x20search\x0a\x0a\x0a\x0a\x0a\x0a{{if\x20.Playground}}\x0a\x0a\x09package\x20main\x0a\x0aimport\x20\"fmt\"\x0a\x0afunc\x20main()\x20{\x0a\x09fmt.Println(\"Hello,\x20\xe4\xb8\x96\xe7\x95\x8c\")\x0a}\x0a\x09\x0a\x09\x0a\x09\x09Run\x0a\x09\x09Format\x0a\x09\x09{{if\x20not\x20$.GoogleCN}}\x0a\x09\x09Share\x0a\x09\x09{{end}}\x0a\x09\x0a\x0a{{end}}\x0a\x0a\x0a\x0a\x0a{{if\x20or\x20.Title\x20.SrcPath}}\x0a\x20\x20

\x0a\x20\x20\x20\x20{{html\x20.Title}}\x0a\x20\x20\x20\x20{{html\x20.SrcPath\x20|\x20srcBreadcrumb}}\x0a\x20\x20

\x0a{{end}}\x0a\x0a{{with\x20.Subtitle}}\x0a\x20\x20

{{html\x20.}}

\x0a{{end}}\x0a\x0a{{with\x20.SrcPath}}\x0a\x20\x20

\x0a\x20\x20\x20\x20Documentation:\x20{{html\x20.\x20|\x20srcToPkgLink}}\x0a\x20\x20

\x0a{{end}}\x0a\x0a{{/*\x20The\x20Table\x20of\x20Contents\x20is\x20automatically\x20inserted\x20in\x20this\x20
.\x0a\x20\x20\x20\x20\x20Do\x20not\x20delete\x20this\x20
.\x20*/}}\x0a
\x0a\x0a{{/*\x20Body\x20is\x20HTML-escaped\x20elsewhere\x20*/}}\x0a{{printf\x20\"%s\"\x20.Body}}\x0a\x0a\x0aBuild\x20version\x20{{html\x20.Version}}.
\x0aExcept\x20as\x20noted,\x0athe\x20content\x20of\x20this\x20page\x20is\x20licensed\x20under\x20the\x0aCreative\x20Commons\x20Attribution\x203.0\x20License,\x0aand\x20code\x20is\x20licensed\x20under\x20a\x20BSD\x20license.
\x0aTerms\x20of\x20Service\x20|\x0aPrivacy\x20Policy\x0a
\x0a\x0a\x0a\x0a\x0a\x0a\x0a", + "godoc.html": "\x0a\x0a\x0a\x0a\x0a\x0a{{with\x20.Tabtitle}}\x0a\x20\x20{{html\x20.}}\x20-\x20The\x20Go\x20Programming\x20Language\x0a{{else}}\x0a\x20\x20The\x20Go\x20Programming\x20Language\x0a{{end}}\x0a\x0a{{if\x20.SearchBox}}\x0a\x0a{{end}}\x0a\x0a\x0a{{with\x20.GoogleAnalytics}}\x0a\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\x0a{{end}}\x0a\x0a\x0a\x0a\x0a{{if\x20.Playground}}\x0a\x0a{{end}}\x0a{{with\x20.Version}}{{end}}\x0a\x0a\x0a\x0a\x0a\x0a...\x0a\x0a\x0a\x0aThe\x20Go\x20Programming\x20Language\x0aGo\x0a▽\x0a\x0a\x0aDocuments\x0aPackages\x0aThe\x20Project\x0aHelp\x0a{{if\x20not\x20.GoogleCN}}\x0aBlog\x0a{{end}}\x0a{{if\x20.Playground}}\x0aPlay\x0a{{end}}\x0asubmit\x20search\x0a\x0a\x0a\x0a\x0a\x0a{{if\x20.Playground}}\x0a\x0a\x09package\x20main\x0a\x0aimport\x20\"fmt\"\x0a\x0afunc\x20main()\x20{\x0a\x09fmt.Println(\"Hello,\x20\xe4\xb8\x96\xe7\x95\x8c\")\x0a}\x0a\x09\x0a\x09\x0a\x09\x09Run\x0a\x09\x09Format\x0a\x09\x09{{if\x20not\x20$.GoogleCN}}\x0a\x09\x09Share\x0a\x09\x09{{end}}\x0a\x09\x0a\x0a{{end}}\x0a\x0a\x0a\x0a\x0a{{if\x20or\x20.Title\x20.SrcPath}}\x0a\x20\x20

\x0a\x20\x20\x20\x20{{html\x20.Title}}\x0a\x20\x20\x20\x20{{html\x20.SrcPath\x20|\x20srcBreadcrumb}}\x0a\x20\x20

\x0a{{end}}\x0a\x0a{{with\x20.Subtitle}}\x0a\x20\x20

{{html\x20.}}

\x0a{{end}}\x0a\x0a{{with\x20.SrcPath}}\x0a\x20\x20

\x0a\x20\x20\x20\x20Documentation:\x20{{html\x20.\x20|\x20srcToPkgLink}}\x0a\x20\x20

\x0a{{end}}\x0a\x0a{{/*\x20The\x20Table\x20of\x20Contents\x20is\x20automatically\x20inserted\x20in\x20this\x20
.\x0a\x20\x20\x20\x20\x20Do\x20not\x20delete\x20this\x20
.\x20*/}}\x0a
\x0a\x0a{{/*\x20Body\x20is\x20HTML-escaped\x20elsewhere\x20*/}}\x0a{{printf\x20\"%s\"\x20.Body}}\x0a\x0a\x0aBuild\x20version\x20{{html\x20.Version}}.
\x0aExcept\x20as\x20noted,\x0athe\x20content\x20of\x20this\x20page\x20is\x20licensed\x20under\x20the\x0aCreative\x20Commons\x20Attribution\x203.0\x20License,\x0aand\x20code\x20is\x20licensed\x20under\x20a\x20BSD\x20license.
\x0aTerms\x20of\x20Service\x20|\x0aPrivacy\x20Policy\x0a
\x0a\x0a\x0a\x0a{{with\x20.GoogleAnalytics}}\x0a\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\x0a{{end}}\x0a\x0a\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$('').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$('
');\x0a\x20\x20\x20\x20}\x20else\x20{\x20//\x20h3\x0a\x20\x20\x20\x20\x20\x20item\x20=\x20$('');\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$('
');\x0a\x20\x20var\x20dl2\x20=\x20$('
');\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$('').appendTo(nav);\x0a\x20\x20var\x20tocBody\x20=\x20$('').appendTo(tocTable);\x0a\x20\x20var\x20tocRow\x20=\x20$('').appendTo(tocBody);\x0a\x0a\x20\x20//\x201st\x20column\x0a\x20\x20$('').appendTo(tocRow).append(dl1);\x0a\x20\x20//\x202nd\x20column\x0a\x20\x20$('').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$(''+\x0a\x20\x20\x20\x20'Your\x20download\x20should\x20begin\x20shortly.\x20'+\x0a\x20\x20\x20\x20'If\x20it\x20does\x20not,\x20click\x20this\x20link.

');\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($(\"¶\").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\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}\x0a\x0a//\x20makeAnchor\x20returns\x20HTML\x20for\x20an\x20\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\"\"\x20+\x20html\x20+\x20\"\";\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\"\"\x20+\x20html\x20+\x20\"

\\n\"\x20+\x0a\x20\x20\x20\x20\x20\x20\"\xe2\x9c\x98\"\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\"\x20+\x20escapeHTML(data.Callee)\x20+\x20\":
\\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\"\"\x20+\x20escapeHTML(caller.Func)\x20+\x20\"\";\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\"\"\x20+\x20makeAnchor(sites[j])\x20+\x20\"\";\x0a\x20\x20\x20\x20\x20\x20}\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20html\x20+=\x20\"
\\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\":
\\n\";\x0a\x20\x20for\x20(var\x20i\x20=\x200;\x20i\x20<\x20data.Callees.length;\x20i++)\x20{\x0a\x20\x20\x20\x20html\x20+=\x20\"\"\x20+\x20makeAnchor(data.Callees[i])\x20+\x20\"
\\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\"\x20+\x20data.Name\x20+\x20\":\x20\"\x20+\x0a\x20\x20\"      (size=\"\x20+\x20data.Size\x20+\x20\",\x20align=\"\x20+\x20data.Align\x20+\x20\")
\\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\"\"\x20+\x20escapeHTML(group.Descr)\x20+\x20\"\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\"\"\x20+\x20makeAnchor(fact.Other)\x20+\x20\"\";\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\"
\\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\"\"\x20+\x20makeAnchor(info.Methods[i])\x20+\x20\"
\\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:
\\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\"\x20+\x20escapeHTML(ops[i].Fn)\x20+\x20\"
\\n\";\x0a\x20\x20}\x0a\x20\x20if\x20(ops.length\x20==\x200)\x20{\x0a\x20\x20\x20\x20html\x20+=\x20\"(none)
\\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
  • \x20element\x20for\x20document.CALLGRAPH\x20node\x20cgn\x20to\x0a//\x20the\x20parent\x20
      \x20element\x20ul.\x20tree\x20is\x20the\x20tree's\x20root\x20
        \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 00000000..25d5a623 --- /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 00000000..74f6ade6 --- /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) + } +}