cmd/getgo: initial commit

Initial commit of getgo, a "one-line installer".

Example use from bash:

  curl -LO https://get.golang.org/$(uname)/go_installer && chmod +x go_installer && ./go_installer && rm go_installer

It's comprised of two parts:

cmd/getgo/server: an App Engine application that redirects users to an
appropriate installer based on the request path, which identifies the
user's operating system. It's deployed to get.golang.org.

cmd/getgo: a cross-compiled binary that does the heavy lifting of
downloading and installing the latest Go version (including setting up
the environment) to the user's system. The installers are served from
the golang GCS bucket.

Currently supported systems:
  linux, darwin, windows / amd64, 386

Authored by Jess Frazelle, Steve Francia, Chris Broadfoot.

Change-Id: I615de86e198d3bd93e418fa23055d00ddbdd99fb
Reviewed-on: https://go-review.googlesource.com/51115
Reviewed-by: Jaana Burcu Dogan <jbd@google.com>
diff --git a/cmd/getgo/.dockerignore b/cmd/getgo/.dockerignore
new file mode 100644
index 0000000..2b87ad9
--- /dev/null
+++ b/cmd/getgo/.dockerignore
@@ -0,0 +1,5 @@
+.git
+.dockerignore
+LICENSE
+README.md
+.gitignore
diff --git a/cmd/getgo/.gitignore b/cmd/getgo/.gitignore
new file mode 100644
index 0000000..d4984ab
--- /dev/null
+++ b/cmd/getgo/.gitignore
@@ -0,0 +1,3 @@
+build
+testgetgo
+getgo
diff --git a/cmd/getgo/Dockerfile b/cmd/getgo/Dockerfile
new file mode 100644
index 0000000..78fd956
--- /dev/null
+++ b/cmd/getgo/Dockerfile
@@ -0,0 +1,20 @@
+FROM golang:latest
+
+ENV SHELL /bin/bash
+ENV HOME /root
+WORKDIR $HOME
+
+COPY . /go/src/golang.org/x/tools/cmd/getgo
+
+RUN ( \
+		cd /go/src/golang.org/x/tools/cmd/getgo \
+		&& go build \
+		&& mv getgo /usr/local/bin/getgo \
+	)
+
+# undo the adding of GOPATH to env for testing
+ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+ENV GOPATH ""
+
+# delete /go and /usr/local/go for testing
+RUN rm -rf /go /usr/local/go
diff --git a/cmd/getgo/LICENSE b/cmd/getgo/LICENSE
new file mode 100644
index 0000000..32017f8
--- /dev/null
+++ b/cmd/getgo/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2017 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/cmd/getgo/README.md b/cmd/getgo/README.md
new file mode 100644
index 0000000..e62a6c2
--- /dev/null
+++ b/cmd/getgo/README.md
@@ -0,0 +1,71 @@
+# getgo
+
+A proof-of-concept command-line installer for Go.
+
+This installer is designed to both install Go as well as do the initial configuration
+of setting up the right environment variables and paths.
+
+It will install the Go distribution (tools & stdlib) to "/.go" inside your home directory by default.
+
+It will setup "$HOME/go" as your GOPATH.
+This is where third party libraries and apps will be installed as well as where you will write your Go code.
+
+If Go is already installed via this installer it will upgrade it to the latest version of Go.
+
+Currently the installer supports Windows, \*nix and macOS on x86 & x64.
+It supports Bash and Zsh on all of these platforms as well as powershell & cmd.exe on Windows.
+
+## Usage
+
+Windows Powershell/cmd.exe:
+
+`(New-Object System.Net.WebClient).DownloadFile('https://get.golang.org/installer.exe', 'installer.exe'); Start-Process -Wait -NonewWindow installer.exe; Remove-Item installer.exe`
+
+Shell (Linux/macOS/Windows):
+
+`curl -LO https://get.golang.org/$(uname)/go_installer && chmod +x go_installer && ./go_installer && rm go_installer`
+
+## To Do
+
+* Check if Go is already installed (via a different method) and update it in place or at least notify the user
+* Lots of testing. It's only had limited testing so far.
+* Add support for additional shells.
+
+## Development instructions
+
+### Testing
+
+There are integration tests in [`main_test.go`](main_test.go). Please add more
+tests there.
+
+#### On unix/linux with the Dockerfile
+
+The Dockerfile automatically builds the binary, moves it to
+`/usr/local/bin/getgo` and then unsets `$GOPATH` and removes all `$GOPATH` from
+`$PATH`.
+
+```bash
+$ docker build --rm --force-rm -t getgo .
+...
+$ docker run --rm -it getgo bash
+root@78425260fad0:~# getgo -v
+Welcome to the Go installer!
+Downloading Go version go1.8.3 to /usr/local/go
+This may take a bit of time...
+Adding "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin" to /root/.bashrc
+Downloaded!
+Setting up GOPATH
+Adding "export GOPATH=/root/go" to /root/.bashrc
+Adding "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/root/go/bin" to /root/.bashrc
+GOPATH has been setup!
+root@78425260fad0:~# which go
+/usr/local/go/bin/go
+root@78425260fad0:~# echo $GOPATH
+/root/go
+root@78425260fad0:~# echo $PATH
+/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/root/go/bin
+```
+
+## Release instructions
+
+To upload a new release of getgo, run `./make.bash && ./upload.bash`.
diff --git a/cmd/getgo/download.go b/cmd/getgo/download.go
new file mode 100644
index 0000000..2b9ff6a
--- /dev/null
+++ b/cmd/getgo/download.go
@@ -0,0 +1,176 @@
+// Copyright 2017 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 main
+
+import (
+	"archive/tar"
+	"archive/zip"
+	"compress/gzip"
+	"crypto/sha256"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+const (
+	currentVersionURL = "https://golang.org/VERSION?m=text"
+	downloadURLPrefix = "https://storage.googleapis.com/golang"
+)
+
+// downloadGoVersion downloads and upacks the specific go version to dest/go.
+func downloadGoVersion(version, ops, arch, dest string) error {
+	suffix := "tar.gz"
+	if ops == "windows" {
+		suffix = "zip"
+	}
+	uri := fmt.Sprintf("%s/%s.%s-%s.%s", downloadURLPrefix, version, ops, arch, suffix)
+
+	verbosef("Downloading %s", uri)
+
+	resp, err := http.Get(uri)
+	if err != nil {
+		return fmt.Errorf("Downloading Go from %s failed: %v", uri, err)
+	}
+	if resp.StatusCode > 299 {
+		return fmt.Errorf("Downloading Go from %s failed with HTTP status %s", resp.Status)
+	}
+	defer resp.Body.Close()
+
+	tmpf, err := ioutil.TempFile("", "go")
+	if err != nil {
+		return err
+	}
+	defer os.Remove(tmpf.Name())
+
+	h := sha256.New()
+
+	w := io.MultiWriter(tmpf, h)
+	if _, err := io.Copy(w, resp.Body); err != nil {
+		return err
+	}
+
+	verbosef("Downloading SHA %s.sha256", uri)
+
+	sresp, err := http.Get(uri + ".sha256")
+	if err != nil {
+		return fmt.Errorf("Downloading Go sha256 from %s.sha256 failed: %v", uri, err)
+	}
+	defer sresp.Body.Close()
+	if sresp.StatusCode > 299 {
+		return fmt.Errorf("Downloading Go sha256 from %s.sha256 failed with HTTP status %s", sresp.Status)
+	}
+
+	shasum, err := ioutil.ReadAll(sresp.Body)
+	if err != nil {
+		return err
+	}
+
+	// Check the shasum.
+	sum := fmt.Sprintf("%x", h.Sum(nil))
+	if sum != string(shasum) {
+		return fmt.Errorf("Shasum mismatch %s vs. %s", sum, string(shasum))
+	}
+
+	unpackFunc := unpackTar
+	if ops == "windows" {
+		unpackFunc = unpackZip
+	}
+	if err := unpackFunc(tmpf.Name(), dest); err != nil {
+		return fmt.Errorf("Unpacking Go to %s failed: %v", dest, err)
+	}
+	return nil
+}
+
+func unpack(dest, name string, fi os.FileInfo, r io.Reader) error {
+	if strings.HasPrefix(name, "go/") {
+		name = name[len("go/"):]
+	}
+
+	path := filepath.Join(dest, name)
+	if fi.IsDir() {
+		return os.MkdirAll(path, fi.Mode())
+	}
+
+	f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode())
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	_, err = io.Copy(f, r)
+	return err
+}
+
+func unpackTar(src, dest string) error {
+	r, err := os.Open(src)
+	if err != nil {
+		return err
+	}
+	defer r.Close()
+
+	archive, err := gzip.NewReader(r)
+	if err != nil {
+		return err
+	}
+	defer archive.Close()
+
+	tarReader := tar.NewReader(archive)
+
+	for {
+		header, err := tarReader.Next()
+		if err == io.EOF {
+			break
+		} else if err != nil {
+			return err
+		}
+
+		if err := unpack(dest, header.Name, header.FileInfo(), tarReader); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func unpackZip(src, dest string) error {
+	zr, err := zip.OpenReader(src)
+	if err != nil {
+		return err
+	}
+
+	for _, f := range zr.File {
+		fr, err := f.Open()
+		if err != nil {
+			return err
+		}
+		if err := unpack(dest, f.Name, f.FileInfo(), fr); err != nil {
+			return err
+		}
+		fr.Close()
+	}
+
+	return nil
+}
+
+func getLatestGoVersion() (string, error) {
+	resp, err := http.Get(currentVersionURL)
+	if err != nil {
+		return "", fmt.Errorf("Getting current Go version failed: %v", err)
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode > 299 {
+		b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1024))
+		return "", fmt.Errorf("Could not get current Go version: HTTP %d: %q", resp.StatusCode, b)
+	}
+	version, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return "", err
+	}
+	return strings.TrimSpace(string(version)), nil
+}
diff --git a/cmd/getgo/download_test.go b/cmd/getgo/download_test.go
new file mode 100644
index 0000000..1a47823
--- /dev/null
+++ b/cmd/getgo/download_test.go
@@ -0,0 +1,34 @@
+// Copyright 2017 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 main
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+func TestDownloadGoVersion(t *testing.T) {
+	if testing.Short() {
+		t.Skipf("Skipping download in short mode")
+	}
+
+	tmpd, err := ioutil.TempDir("", "go")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmpd)
+
+	if err := downloadGoVersion("go1.8.1", "linux", "amd64", filepath.Join(tmpd, "go")); err != nil {
+		t.Fatal(err)
+	}
+
+	// Ensure the VERSION file exists.
+	vf := filepath.Join(tmpd, "go", "VERSION")
+	if _, err := os.Stat(vf); os.IsNotExist(err) {
+		t.Fatalf("file %s does not exist and should", vf)
+	}
+}
diff --git a/cmd/getgo/main.go b/cmd/getgo/main.go
new file mode 100644
index 0000000..d14f329
--- /dev/null
+++ b/cmd/getgo/main.go
@@ -0,0 +1,114 @@
+// Copyright 2017 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.
+
+// The getgo command installs Go to the user's system.
+package main
+
+import (
+	"bufio"
+	"context"
+	"errors"
+	"flag"
+	"fmt"
+	"os"
+	"os/exec"
+	"strings"
+)
+
+var (
+	interactive = flag.Bool("i", false, "Prompt for inputs.")
+	verbose     = flag.Bool("v", false, "Verbose.")
+	setupOnly   = flag.Bool("skip-dl", false, "Don't download - only set up environment variables")
+	goVersion   = flag.String("version", "", `Version of Go to install (e.g. "1.8.3"). If empty, uses the latest version.`)
+
+	version = "devel"
+)
+
+var exitCleanly error = errors.New("exit cleanly sentinel value")
+
+func main() {
+	flag.Parse()
+	if *goVersion != "" && !strings.HasPrefix(*goVersion, "go") {
+		*goVersion = "go" + *goVersion
+	}
+
+	ctx := context.Background()
+
+	verbosef("version " + version)
+
+	runStep := func(s step) {
+		err := s(ctx)
+		if err == exitCleanly {
+			os.Exit(0)
+		}
+		if err != nil {
+			fmt.Fprintln(os.Stderr, err)
+			os.Exit(2)
+		}
+	}
+
+	if !*setupOnly {
+		runStep(welcome)
+		runStep(chooseVersion)
+		runStep(downloadGo)
+	}
+
+	runStep(setupGOPATH)
+}
+
+func verbosef(format string, v ...interface{}) {
+	if !*verbose {
+		return
+	}
+
+	fmt.Printf(format+"\n", v...)
+}
+
+func prompt(ctx context.Context, query, defaultAnswer string) (string, error) {
+	if !*interactive {
+		return defaultAnswer, nil
+	}
+
+	fmt.Printf("%s [%s]: ", query, defaultAnswer)
+
+	type result struct {
+		answer string
+		err    error
+	}
+	ch := make(chan result, 1)
+	go func() {
+		s := bufio.NewScanner(os.Stdin)
+		if !s.Scan() {
+			ch <- result{"", s.Err()}
+			return
+		}
+		answer := s.Text()
+		if answer == "" {
+			answer = defaultAnswer
+		}
+		ch <- result{answer, nil}
+	}()
+
+	select {
+	case r := <-ch:
+		return r.answer, r.err
+	case <-ctx.Done():
+		return "", ctx.Err()
+	}
+}
+
+func runCommand(ctx context.Context, prog string, args ...string) ([]byte, error) {
+	verbosef("Running command: %s %v", prog, args)
+
+	cmd := exec.CommandContext(ctx, prog, args...)
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return nil, fmt.Errorf("running cmd '%s %s' failed: %s err: %v", prog, strings.Join(args, " "), string(out), err)
+	}
+	if out != nil && err == nil && len(out) != 0 {
+		verbosef("%s", out)
+	}
+
+	return out, nil
+}
diff --git a/cmd/getgo/main_test.go b/cmd/getgo/main_test.go
new file mode 100644
index 0000000..9da726e
--- /dev/null
+++ b/cmd/getgo/main_test.go
@@ -0,0 +1,166 @@
+// Copyright 2017 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 main
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"runtime"
+	"testing"
+)
+
+const (
+	testbin = "testgetgo"
+)
+
+var (
+	exeSuffix string // ".exe" on Windows
+)
+
+func init() {
+	if runtime.GOOS == "windows" {
+		exeSuffix = ".exe"
+	}
+}
+
+// TestMain creates a getgo command for testing purposes and
+// deletes it after the tests have been run.
+func TestMain(m *testing.M) {
+	args := []string{"build", "-tags", testbin, "-o", testbin + exeSuffix}
+	out, err := exec.Command("go", args...).CombinedOutput()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "building %s failed: %v\n%s", testbin, err, out)
+		os.Exit(2)
+	}
+
+	// Don't let these environment variables confuse the test.
+	os.Unsetenv("GOBIN")
+	os.Unsetenv("GOPATH")
+	os.Unsetenv("GIT_ALLOW_PROTOCOL")
+	os.Unsetenv("PATH")
+
+	r := m.Run()
+
+	os.Remove(testbin + exeSuffix)
+
+	os.Exit(r)
+}
+
+func createTmpHome(t *testing.T) string {
+	tmpd, err := ioutil.TempDir("", "testgetgo")
+	if err != nil {
+		t.Fatalf("creating test tempdir failed: %v", err)
+	}
+
+	os.Setenv("HOME", tmpd)
+	return tmpd
+}
+
+// doRun runs the test getgo command, recording stdout and stderr and
+// returning exit status.
+func doRun(t *testing.T, args ...string) error {
+	var stdout, stderr bytes.Buffer
+	t.Logf("running %s %v", testbin, args)
+	cmd := exec.Command("./"+testbin+exeSuffix, args...)
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	cmd.Env = os.Environ()
+	status := cmd.Run()
+	if stdout.Len() > 0 {
+		t.Log("standard output:")
+		t.Log(stdout.String())
+	}
+	if stderr.Len() > 0 {
+		t.Log("standard error:")
+		t.Log(stderr.String())
+	}
+	return status
+}
+
+func TestCommandVerbose(t *testing.T) {
+	tmpd := createTmpHome(t)
+	defer os.RemoveAll(tmpd)
+
+	err := doRun(t, "-v")
+	if err != nil {
+		t.Fatal(err)
+	}
+	// make sure things are in path
+	shellConfig, err := shellConfigFile()
+	if err != nil {
+		t.Fatal(err)
+	}
+	b, err := ioutil.ReadFile(shellConfig)
+	if err != nil {
+		t.Fatal(err)
+	}
+	home, err := getHomeDir()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expected := fmt.Sprintf(`
+export PATH=$PATH:%s/.go/bin
+
+export GOPATH=%s/go
+
+export PATH=$PATH:%s/go/bin
+`, home, home, home)
+
+	if string(b) != expected {
+		t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b))
+	}
+}
+
+func TestCommandPathExists(t *testing.T) {
+	tmpd := createTmpHome(t)
+	defer os.RemoveAll(tmpd)
+
+	// run once
+	err := doRun(t, "-skip-dl")
+	if err != nil {
+		t.Fatal(err)
+	}
+	// make sure things are in path
+	shellConfig, err := shellConfigFile()
+	if err != nil {
+		t.Fatal(err)
+	}
+	b, err := ioutil.ReadFile(shellConfig)
+	if err != nil {
+		t.Fatal(err)
+	}
+	home, err := getHomeDir()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expected := fmt.Sprintf(`
+export GOPATH=%s/go
+
+export PATH=$PATH:%s/go/bin
+`, home, home)
+
+	if string(b) != expected {
+		t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b))
+	}
+
+	// run twice
+	if err := doRun(t, "-skip-dl"); err != nil {
+		t.Fatal(err)
+	}
+
+	b, err = ioutil.ReadFile(shellConfig)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if string(b) != expected {
+		t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b))
+	}
+}
diff --git a/cmd/getgo/make.bash b/cmd/getgo/make.bash
new file mode 100755
index 0000000..cbc3685
--- /dev/null
+++ b/cmd/getgo/make.bash
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+# Copyright 2017 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.
+
+set -e -o -x
+
+LDFLAGS="-X main.version=$(git describe --always --dirty='*')"
+
+GOOS=windows GOARCH=386 go build -o build/installer.exe    -ldflags="$LDFLAGS"
+GOOS=linux GOARCH=386   go build -o build/installer_linux  -ldflags="$LDFLAGS"
+GOOS=darwin GOARCH=386  go build -o build/installer_darwin -ldflags="$LDFLAGS"
diff --git a/cmd/getgo/path.go b/cmd/getgo/path.go
new file mode 100644
index 0000000..551ac42
--- /dev/null
+++ b/cmd/getgo/path.go
@@ -0,0 +1,153 @@
+// Copyright 2017 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 main
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"os"
+	"os/user"
+	"path/filepath"
+	"runtime"
+	"strings"
+)
+
+const (
+	bashConfig = ".bash_profile"
+	zshConfig  = ".zshrc"
+)
+
+// appendToPATH adds the given path to the PATH environment variable and
+// persists it for future sessions.
+func appendToPATH(value string) error {
+	if isInPATH(value) {
+		return nil
+	}
+	return persistEnvVar("PATH", pathVar+envSeparator+value)
+}
+
+func isInPATH(dir string) bool {
+	p := os.Getenv("PATH")
+
+	paths := strings.Split(p, envSeparator)
+	for _, d := range paths {
+		if d == dir {
+			return true
+		}
+	}
+
+	return false
+}
+
+func getHomeDir() (string, error) {
+	home := os.Getenv(homeKey)
+	if home != "" {
+		return home, nil
+	}
+
+	u, err := user.Current()
+	if err != nil {
+		return "", err
+	}
+	return u.HomeDir, nil
+}
+
+func checkStringExistsFile(filename, value string) (bool, error) {
+	file, err := os.OpenFile(filename, os.O_RDONLY, 0600)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return false, nil
+		}
+		return false, err
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		line := scanner.Text()
+		if line == value {
+			return true, nil
+		}
+	}
+
+	return false, scanner.Err()
+}
+
+func appendToFile(filename, value string) error {
+	verbosef("Adding %q to %s", value, filename)
+
+	ok, err := checkStringExistsFile(filename, value)
+	if err != nil {
+		return err
+	}
+	if ok {
+		// Nothing to do.
+		return nil
+	}
+
+	f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	_, err = f.WriteString(lineEnding + value + lineEnding)
+	return err
+}
+
+func isShell(name string) bool {
+	return strings.Contains(currentShell(), name)
+}
+
+// persistEnvVarWindows sets an environment variable in the Windows
+// registry.
+func persistEnvVarWindows(name, value string) error {
+	_, err := runCommand(context.Background(), "powershell", "-command",
+		fmt.Sprintf(`[Environment]::SetEnvironmentVariable("%s", "%s", "User")`, name, value))
+	return err
+}
+
+func persistEnvVar(name, value string) error {
+	if runtime.GOOS == "windows" {
+		if err := persistEnvVarWindows(name, value); err != nil {
+			return err
+		}
+
+		if isShell("cmd.exe") || isShell("powershell.exe") {
+			return os.Setenv(strings.ToUpper(name), value)
+		}
+		// User is in bash, zsh, etc.
+		// Also set the environment variable in their shell config.
+	}
+
+	rc, err := shellConfigFile()
+	if err != nil {
+		return err
+	}
+
+	line := fmt.Sprintf("export %s=%s", strings.ToUpper(name), value)
+	if err := appendToFile(rc, line); err != nil {
+		return err
+	}
+
+	return os.Setenv(strings.ToUpper(name), value)
+}
+
+func shellConfigFile() (string, error) {
+	home, err := getHomeDir()
+	if err != nil {
+		return "", err
+	}
+
+	switch {
+	case isShell("bash"):
+		return filepath.Join(home, bashConfig), nil
+	case isShell("zsh"):
+		return filepath.Join(home, zshConfig), nil
+	default:
+		return "", fmt.Errorf("%q is not a supported shell", currentShell())
+	}
+}
diff --git a/cmd/getgo/path_test.go b/cmd/getgo/path_test.go
new file mode 100644
index 0000000..4cf6647
--- /dev/null
+++ b/cmd/getgo/path_test.go
@@ -0,0 +1,56 @@
+// Copyright 2017 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 main
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func TestAppendPath(t *testing.T) {
+	tmpd, err := ioutil.TempDir("", "go")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmpd)
+
+	if err := os.Setenv("HOME", tmpd); err != nil {
+		t.Fatal(err)
+	}
+
+	GOPATH := os.Getenv("GOPATH")
+	if err := appendToPATH(filepath.Join(GOPATH, "bin")); err != nil {
+		t.Fatal(err)
+	}
+
+	shellConfig, err := shellConfigFile()
+	if err != nil {
+		t.Fatal(err)
+	}
+	b, err := ioutil.ReadFile(shellConfig)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expected := "export PATH=" + pathVar + envSeparator + filepath.Join(GOPATH, "bin")
+	if strings.TrimSpace(string(b)) != expected {
+		t.Fatalf("expected: %q, got %q", expected, strings.TrimSpace(string(b)))
+	}
+
+	// Check that appendToPATH is idempotent.
+	if err := appendToPATH(filepath.Join(GOPATH, "bin")); err != nil {
+		t.Fatal(err)
+	}
+	b, err = ioutil.ReadFile(shellConfig)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if strings.TrimSpace(string(b)) != expected {
+		t.Fatalf("expected: %q, got %q", expected, strings.TrimSpace(string(b)))
+	}
+}
diff --git a/cmd/getgo/server/README.md b/cmd/getgo/server/README.md
new file mode 100644
index 0000000..0cf629d
--- /dev/null
+++ b/cmd/getgo/server/README.md
@@ -0,0 +1,7 @@
+# getgo server
+
+## Deployment
+
+```
+gcloud app deploy --promote --project golang-org
+```
diff --git a/cmd/getgo/server/app.yaml b/cmd/getgo/server/app.yaml
new file mode 100644
index 0000000..0502c4e
--- /dev/null
+++ b/cmd/getgo/server/app.yaml
@@ -0,0 +1,7 @@
+runtime: go
+service: get
+api_version: go1
+
+handlers:
+- url: /.*
+  script: _go_app
diff --git a/cmd/getgo/server/main.go b/cmd/getgo/server/main.go
new file mode 100644
index 0000000..0bd3337
--- /dev/null
+++ b/cmd/getgo/server/main.go
@@ -0,0 +1,64 @@
+// Copyright 2017 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.
+
+// Command server serves get.golang.org, redirecting users to the appropriate
+// getgo installer based on the request path.
+package main
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"google.golang.org/appengine"
+)
+
+const (
+	base             = "https://storage.googleapis.com/golang/getgo/"
+	windowsInstaller = base + "installer.exe"
+	linuxInstaller   = base + "installer_linux"
+	macInstaller     = base + "installer_darwin"
+)
+
+// substring-based redirects.
+var stringMatch = map[string]string{
+	// via uname, from bash
+	"MINGW":  windowsInstaller, // Reported as MINGW64_NT-10.0 in git bash
+	"Linux":  linuxInstaller,
+	"Darwin": macInstaller,
+}
+
+func main() {
+	http.HandleFunc("/", handler)
+	appengine.Main()
+}
+
+func handler(w http.ResponseWriter, r *http.Request) {
+	if containsIgnoreCase(r.URL.Path, "installer.exe") {
+		// cache bust
+		http.Redirect(w, r, windowsInstaller+cacheBust(), http.StatusFound)
+		return
+	}
+
+	for match, redirect := range stringMatch {
+		if containsIgnoreCase(r.URL.Path, match) {
+			http.Redirect(w, r, redirect, http.StatusFound)
+			return
+		}
+	}
+
+	http.NotFound(w, r)
+}
+
+func containsIgnoreCase(s, substr string) bool {
+	return strings.Contains(
+		strings.ToLower(s),
+		strings.ToLower(substr),
+	)
+}
+
+func cacheBust() string {
+	return fmt.Sprintf("?%d", time.Now().Nanosecond())
+}
diff --git a/cmd/getgo/steps.go b/cmd/getgo/steps.go
new file mode 100644
index 0000000..ad65d45
--- /dev/null
+++ b/cmd/getgo/steps.go
@@ -0,0 +1,119 @@
+// Copyright 2017 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 main
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+)
+
+type step func(context.Context) error
+
+func welcome(ctx context.Context) error {
+	fmt.Println("Welcome to the Go installer!")
+	answer, err := prompt(ctx, "Would you like to install Go? Y/n", "Y")
+	if err != nil {
+		return err
+	}
+	if strings.ToLower(answer) != "y" {
+		fmt.Println("Exiting install.")
+		return exitCleanly
+	}
+
+	return nil
+}
+
+func chooseVersion(ctx context.Context) error {
+	// TODO: check if go is currently installed
+	// TODO: if go is currently installed install new version over that
+
+	if *goVersion != "" {
+		return nil
+	}
+
+	var err error
+	*goVersion, err = getLatestGoVersion()
+	if err != nil {
+		return err
+	}
+
+	answer, err := prompt(ctx, fmt.Sprintf("The latest go version is %s, install that? Y/n", *goVersion), "Y")
+	if err != nil {
+		return err
+	}
+
+	if strings.ToLower(answer) != "y" {
+		// TODO: handle passing a version
+		fmt.Println("Aborting install.")
+		return exitCleanly
+	}
+
+	return nil
+}
+
+func downloadGo(ctx context.Context) error {
+	answer, err := prompt(ctx, fmt.Sprintf("Download go version %s to %s? Y/n", *goVersion, installPath), "Y")
+	if err != nil {
+		return err
+	}
+
+	if strings.ToLower(answer) != "y" {
+		fmt.Println("Aborting install.")
+		return exitCleanly
+	}
+
+	fmt.Printf("Downloading Go version %s to %s\n", *goVersion, installPath)
+	fmt.Println("This may take a bit of time...")
+
+	if err := downloadGoVersion(*goVersion, runtime.GOOS, arch, installPath); err != nil {
+		return err
+	}
+
+	if err := appendToPATH(filepath.Join(installPath, "bin")); err != nil {
+		return err
+	}
+
+	fmt.Println("Downloaded!")
+	return nil
+}
+
+func setupGOPATH(ctx context.Context) error {
+	answer, err := prompt(ctx, "Would you like us to setup your GOPATH? Y/n", "Y")
+	if err != nil {
+		return err
+	}
+
+	if strings.ToLower(answer) != "y" {
+		fmt.Println("Exiting and not setting up GOPATH.")
+		return exitCleanly
+	}
+
+	fmt.Println("Setting up GOPATH")
+	home, err := getHomeDir()
+	if err != nil {
+		return err
+	}
+
+	gopath := os.Getenv("GOPATH")
+	if gopath == "" {
+		// set $GOPATH
+		gopath = filepath.Join(home, "go")
+		if err := persistEnvVar("GOPATH", gopath); err != nil {
+			return err
+		}
+		fmt.Println("GOPATH has been set up!")
+	} else {
+		verbosef("GOPATH is already set to %s", gopath)
+	}
+
+	if err := appendToPATH(filepath.Join(gopath, "bin")); err != nil {
+		return err
+	}
+	return persistEnvChangesForSession()
+}
diff --git a/cmd/getgo/system.go b/cmd/getgo/system.go
new file mode 100644
index 0000000..d424dda
--- /dev/null
+++ b/cmd/getgo/system.go
@@ -0,0 +1,29 @@
+// Copyright 2017 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 main
+
+import (
+	"bytes"
+	"os/exec"
+	"runtime"
+)
+
+// arch contains either amd64 or 386.
+var arch = func() string {
+	cmd := exec.Command("uname", "-m") // "x86_64"
+	if runtime.GOOS == "windows" {
+		cmd = exec.Command("powershell", "-command", "(Get-WmiObject -Class Win32_ComputerSystem).SystemType") // "x64-based PC"
+	}
+
+	out, err := cmd.Output()
+	if err != nil {
+		// a sensible default?
+		return "amd64"
+	}
+	if bytes.Contains(out, []byte("64")) {
+		return "amd64"
+	}
+	return "386"
+}()
diff --git a/cmd/getgo/system_unix.go b/cmd/getgo/system_unix.go
new file mode 100644
index 0000000..a3f5957
--- /dev/null
+++ b/cmd/getgo/system_unix.go
@@ -0,0 +1,50 @@
+// Copyright 2017 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 darwin dragonfly freebsd linux nacl netbsd openbsd solaris
+
+package main
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+)
+
+const (
+	envSeparator = ":"
+	homeKey      = "HOME"
+	lineEnding   = "\n"
+	pathVar      = "$PATH"
+)
+
+var installPath = func() string {
+	home, err := getHomeDir()
+	if err != nil {
+		return "/usr/local/go"
+	}
+
+	return filepath.Join(home, ".go")
+}()
+
+func isWindowsXP() bool {
+	return false
+}
+
+func currentShell() string {
+	return os.Getenv("SHELL")
+}
+
+func persistEnvChangesForSession() error {
+	shellConfig, err := shellConfigFile()
+	if err != nil {
+		return err
+	}
+	fmt.Println()
+	fmt.Printf("One more thing! Run `source %s` to persist the\n", shellConfig)
+	fmt.Println("new environment variables to your current session, or open a")
+	fmt.Println("new shell prompt.")
+
+	return nil
+}
diff --git a/cmd/getgo/system_windows.go b/cmd/getgo/system_windows.go
new file mode 100644
index 0000000..45ce92b
--- /dev/null
+++ b/cmd/getgo/system_windows.go
@@ -0,0 +1,81 @@
+// Copyright 2017 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 windows
+
+package main
+
+import (
+	"log"
+	"os"
+	"syscall"
+	"unsafe"
+)
+
+const (
+	envSeparator = ";"
+	homeKey      = "USERPROFILE"
+	lineEnding   = "/r/n"
+	pathVar      = "$env:Path"
+)
+
+var installPath = `c:\go`
+
+func isWindowsXP() bool {
+	v, err := syscall.GetVersion()
+	if err != nil {
+		log.Fatalf("GetVersion failed: %v", err)
+	}
+	major := byte(v)
+	return major < 6
+}
+
+// currentShell reports the current shell.
+// It might be "powershell.exe", "cmd.exe" or any of the *nix shells.
+//
+// Returns empty string if the shell is unknown.
+func currentShell() string {
+	shell := os.Getenv("SHELL")
+	if shell != "" {
+		return shell
+	}
+
+	pid := os.Getppid()
+	pe, err := getProcessEntry(pid)
+	if err != nil {
+		verbosef("getting shell from process entry failed: %v", err)
+		return ""
+	}
+
+	return syscall.UTF16ToString(pe.ExeFile[:])
+}
+
+func getProcessEntry(pid int) (*syscall.ProcessEntry32, error) {
+	// From https://go.googlesource.com/go/+/go1.8.3/src/syscall/syscall_windows.go#941
+	snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
+	if err != nil {
+		return nil, err
+	}
+	defer syscall.CloseHandle(snapshot)
+
+	var procEntry syscall.ProcessEntry32
+	procEntry.Size = uint32(unsafe.Sizeof(procEntry))
+	if err = syscall.Process32First(snapshot, &procEntry); err != nil {
+		return nil, err
+	}
+
+	for {
+		if procEntry.ProcessID == uint32(pid) {
+			return &procEntry, nil
+		}
+
+		if err := syscall.Process32Next(snapshot, &procEntry); err != nil {
+			return nil, err
+		}
+	}
+}
+
+func persistEnvChangesForSession() error {
+	return nil
+}
diff --git a/cmd/getgo/upload.bash b/cmd/getgo/upload.bash
new file mode 100755
index 0000000..f52bb23
--- /dev/null
+++ b/cmd/getgo/upload.bash
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# Copyright 2017 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.
+
+if ! command -v gsutil 2>&1 > /dev/null; then
+  echo "Install gsutil:"
+  echo
+  echo "   https://cloud.google.com/storage/docs/gsutil_install#sdk-install"
+fi
+
+if [ ! -d build ]; then
+  echo "Run make.bash first"
+fi
+
+set -e -o -x
+
+gsutil -m cp -a public-read build/* gs://golang/getgo