summaryrefslogtreecommitdiffstats
path: root/src/io/ioutil
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/io/ioutil/example_test.go132
-rw-r--r--src/io/ioutil/export_test.go7
-rw-r--r--src/io/ioutil/ioutil.go84
-rw-r--r--src/io/ioutil/ioutil_test.go130
-rw-r--r--src/io/ioutil/tempfile.go137
-rw-r--r--src/io/ioutil/tempfile_test.go195
-rw-r--r--src/io/ioutil/testdata/hello1
7 files changed, 686 insertions, 0 deletions
diff --git a/src/io/ioutil/example_test.go b/src/io/ioutil/example_test.go
new file mode 100644
index 0000000..78b0730
--- /dev/null
+++ b/src/io/ioutil/example_test.go
@@ -0,0 +1,132 @@
+// 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.
+
+package ioutil_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+func ExampleReadAll() {
+ r := strings.NewReader("Go is a general-purpose language designed with systems programming in mind.")
+
+ b, err := ioutil.ReadAll(r)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Printf("%s", b)
+
+ // Output:
+ // Go is a general-purpose language designed with systems programming in mind.
+}
+
+func ExampleReadDir() {
+ files, err := ioutil.ReadDir(".")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ for _, file := range files {
+ fmt.Println(file.Name())
+ }
+}
+
+func ExampleTempDir() {
+ content := []byte("temporary file's content")
+ dir, err := ioutil.TempDir("", "example")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ defer os.RemoveAll(dir) // clean up
+
+ tmpfn := filepath.Join(dir, "tmpfile")
+ if err := ioutil.WriteFile(tmpfn, content, 0666); err != nil {
+ log.Fatal(err)
+ }
+}
+
+func ExampleTempDir_suffix() {
+ parentDir := os.TempDir()
+ logsDir, err := ioutil.TempDir(parentDir, "*-logs")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer os.RemoveAll(logsDir) // clean up
+
+ // Logs can be cleaned out earlier if needed by searching
+ // for all directories whose suffix ends in *-logs.
+ globPattern := filepath.Join(parentDir, "*-logs")
+ matches, err := filepath.Glob(globPattern)
+ if err != nil {
+ log.Fatalf("Failed to match %q: %v", globPattern, err)
+ }
+
+ for _, match := range matches {
+ if err := os.RemoveAll(match); err != nil {
+ log.Printf("Failed to remove %q: %v", match, err)
+ }
+ }
+}
+
+func ExampleTempFile() {
+ content := []byte("temporary file's content")
+ tmpfile, err := ioutil.TempFile("", "example")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ defer os.Remove(tmpfile.Name()) // clean up
+
+ if _, err := tmpfile.Write(content); err != nil {
+ log.Fatal(err)
+ }
+ if err := tmpfile.Close(); err != nil {
+ log.Fatal(err)
+ }
+}
+
+func ExampleTempFile_suffix() {
+ content := []byte("temporary file's content")
+ tmpfile, err := ioutil.TempFile("", "example.*.txt")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ defer os.Remove(tmpfile.Name()) // clean up
+
+ if _, err := tmpfile.Write(content); err != nil {
+ tmpfile.Close()
+ log.Fatal(err)
+ }
+ if err := tmpfile.Close(); err != nil {
+ log.Fatal(err)
+ }
+}
+
+func ExampleReadFile() {
+ content, err := ioutil.ReadFile("testdata/hello")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Printf("File contents: %s", content)
+
+ // Output:
+ // File contents: Hello, Gophers!
+}
+
+func ExampleWriteFile() {
+ message := []byte("Hello, Gophers!")
+ err := ioutil.WriteFile("hello", message, 0644)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/src/io/ioutil/export_test.go b/src/io/ioutil/export_test.go
new file mode 100644
index 0000000..dff55f0
--- /dev/null
+++ b/src/io/ioutil/export_test.go
@@ -0,0 +1,7 @@
+// Copyright 2020 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 ioutil
+
+var ErrPatternHasSeparator = errPatternHasSeparator
diff --git a/src/io/ioutil/ioutil.go b/src/io/ioutil/ioutil.go
new file mode 100644
index 0000000..45682b8
--- /dev/null
+++ b/src/io/ioutil/ioutil.go
@@ -0,0 +1,84 @@
+// Copyright 2009 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 ioutil implements some I/O utility functions.
+//
+// As of Go 1.16, the same functionality is now provided
+// by package io or package os, and those implementations
+// should be preferred in new code.
+// See the specific function documentation for details.
+package ioutil
+
+import (
+ "io"
+ "io/fs"
+ "os"
+ "sort"
+)
+
+// ReadAll reads from r until an error or EOF and returns the data it read.
+// A successful call returns err == nil, not err == EOF. Because ReadAll is
+// defined to read from src until EOF, it does not treat an EOF from Read
+// as an error to be reported.
+//
+// As of Go 1.16, this function simply calls io.ReadAll.
+func ReadAll(r io.Reader) ([]byte, error) {
+ return io.ReadAll(r)
+}
+
+// ReadFile reads the file named by filename and returns the contents.
+// A successful call returns err == nil, not err == EOF. Because ReadFile
+// reads the whole file, it does not treat an EOF from Read as an error
+// to be reported.
+//
+// As of Go 1.16, this function simply calls os.ReadFile.
+func ReadFile(filename string) ([]byte, error) {
+ return os.ReadFile(filename)
+}
+
+// WriteFile writes data to a file named by filename.
+// If the file does not exist, WriteFile creates it with permissions perm
+// (before umask); otherwise WriteFile truncates it before writing, without changing permissions.
+//
+// As of Go 1.16, this function simply calls os.WriteFile.
+func WriteFile(filename string, data []byte, perm fs.FileMode) error {
+ return os.WriteFile(filename, data, perm)
+}
+
+// ReadDir reads the directory named by dirname and returns
+// a list of fs.FileInfo for the directory's contents,
+// sorted by filename. If an error occurs reading the directory,
+// ReadDir returns no directory entries along with the error.
+//
+// As of Go 1.16, os.ReadDir is a more efficient and correct choice:
+// it returns a list of fs.DirEntry instead of fs.FileInfo,
+// and it returns partial results in the case of an error
+// midway through reading a directory.
+func ReadDir(dirname string) ([]fs.FileInfo, error) {
+ f, err := os.Open(dirname)
+ if err != nil {
+ return nil, err
+ }
+ list, err := f.Readdir(-1)
+ f.Close()
+ if err != nil {
+ return nil, err
+ }
+ sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() })
+ return list, nil
+}
+
+// NopCloser returns a ReadCloser with a no-op Close method wrapping
+// the provided Reader r.
+//
+// As of Go 1.16, this function simply calls io.NopCloser.
+func NopCloser(r io.Reader) io.ReadCloser {
+ return io.NopCloser(r)
+}
+
+// Discard is an io.Writer on which all Write calls succeed
+// without doing anything.
+//
+// As of Go 1.16, this value is simply io.Discard.
+var Discard io.Writer = io.Discard
diff --git a/src/io/ioutil/ioutil_test.go b/src/io/ioutil/ioutil_test.go
new file mode 100644
index 0000000..db85755
--- /dev/null
+++ b/src/io/ioutil/ioutil_test.go
@@ -0,0 +1,130 @@
+// Copyright 2009 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 ioutil_test
+
+import (
+ "bytes"
+ . "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func checkSize(t *testing.T, path string, size int64) {
+ dir, err := os.Stat(path)
+ if err != nil {
+ t.Fatalf("Stat %q (looking for size %d): %s", path, size, err)
+ }
+ if dir.Size() != size {
+ t.Errorf("Stat %q: size %d want %d", path, dir.Size(), size)
+ }
+}
+
+func TestReadFile(t *testing.T) {
+ filename := "rumpelstilzchen"
+ contents, err := ReadFile(filename)
+ if err == nil {
+ t.Fatalf("ReadFile %s: error expected, none found", filename)
+ }
+
+ filename = "ioutil_test.go"
+ contents, err = ReadFile(filename)
+ if err != nil {
+ t.Fatalf("ReadFile %s: %v", filename, err)
+ }
+
+ checkSize(t, filename, int64(len(contents)))
+}
+
+func TestWriteFile(t *testing.T) {
+ f, err := TempFile("", "ioutil-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ filename := f.Name()
+ data := "Programming today is a race between software engineers striving to " +
+ "build bigger and better idiot-proof programs, and the Universe trying " +
+ "to produce bigger and better idiots. So far, the Universe is winning."
+
+ if err := WriteFile(filename, []byte(data), 0644); err != nil {
+ t.Fatalf("WriteFile %s: %v", filename, err)
+ }
+
+ contents, err := ReadFile(filename)
+ if err != nil {
+ t.Fatalf("ReadFile %s: %v", filename, err)
+ }
+
+ if string(contents) != data {
+ t.Fatalf("contents = %q\nexpected = %q", string(contents), data)
+ }
+
+ // cleanup
+ f.Close()
+ os.Remove(filename) // ignore error
+}
+
+func TestReadOnlyWriteFile(t *testing.T) {
+ if os.Getuid() == 0 {
+ t.Skipf("Root can write to read-only files anyway, so skip the read-only test.")
+ }
+
+ // We don't want to use TempFile directly, since that opens a file for us as 0600.
+ tempDir, err := TempDir("", t.Name())
+ if err != nil {
+ t.Fatalf("TempDir %s: %v", t.Name(), err)
+ }
+ defer os.RemoveAll(tempDir)
+ filename := filepath.Join(tempDir, "blurp.txt")
+
+ shmorp := []byte("shmorp")
+ florp := []byte("florp")
+ err = WriteFile(filename, shmorp, 0444)
+ if err != nil {
+ t.Fatalf("WriteFile %s: %v", filename, err)
+ }
+ err = WriteFile(filename, florp, 0444)
+ if err == nil {
+ t.Fatalf("Expected an error when writing to read-only file %s", filename)
+ }
+ got, err := ReadFile(filename)
+ if err != nil {
+ t.Fatalf("ReadFile %s: %v", filename, err)
+ }
+ if !bytes.Equal(got, shmorp) {
+ t.Fatalf("want %s, got %s", shmorp, got)
+ }
+}
+
+func TestReadDir(t *testing.T) {
+ dirname := "rumpelstilzchen"
+ _, err := ReadDir(dirname)
+ if err == nil {
+ t.Fatalf("ReadDir %s: error expected, none found", dirname)
+ }
+
+ dirname = ".."
+ list, err := ReadDir(dirname)
+ if err != nil {
+ t.Fatalf("ReadDir %s: %v", dirname, err)
+ }
+
+ foundFile := false
+ foundSubDir := false
+ for _, dir := range list {
+ switch {
+ case !dir.IsDir() && dir.Name() == "io_test.go":
+ foundFile = true
+ case dir.IsDir() && dir.Name() == "ioutil":
+ foundSubDir = true
+ }
+ }
+ if !foundFile {
+ t.Fatalf("ReadDir %s: io_test.go file not found", dirname)
+ }
+ if !foundSubDir {
+ t.Fatalf("ReadDir %s: ioutil directory not found", dirname)
+ }
+}
diff --git a/src/io/ioutil/tempfile.go b/src/io/ioutil/tempfile.go
new file mode 100644
index 0000000..af7c6fd
--- /dev/null
+++ b/src/io/ioutil/tempfile.go
@@ -0,0 +1,137 @@
+// Copyright 2010 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 ioutil
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+)
+
+// Random number state.
+// We generate random temporary file names so that there's a good
+// chance the file doesn't exist yet - keeps the number of tries in
+// TempFile to a minimum.
+var rand uint32
+var randmu sync.Mutex
+
+func reseed() uint32 {
+ return uint32(time.Now().UnixNano() + int64(os.Getpid()))
+}
+
+func nextRandom() string {
+ randmu.Lock()
+ r := rand
+ if r == 0 {
+ r = reseed()
+ }
+ r = r*1664525 + 1013904223 // constants from Numerical Recipes
+ rand = r
+ randmu.Unlock()
+ return strconv.Itoa(int(1e9 + r%1e9))[1:]
+}
+
+// TempFile creates a new temporary file in the directory dir,
+// opens the file for reading and writing, and returns the resulting *os.File.
+// The filename is generated by taking pattern and adding a random
+// string to the end. If pattern includes a "*", the random string
+// replaces the last "*".
+// If dir is the empty string, TempFile uses the default directory
+// for temporary files (see os.TempDir).
+// Multiple programs calling TempFile simultaneously
+// will not choose the same file. The caller can use f.Name()
+// to find the pathname of the file. It is the caller's responsibility
+// to remove the file when no longer needed.
+func TempFile(dir, pattern string) (f *os.File, err error) {
+ if dir == "" {
+ dir = os.TempDir()
+ }
+
+ prefix, suffix, err := prefixAndSuffix(pattern)
+ if err != nil {
+ return
+ }
+
+ nconflict := 0
+ for i := 0; i < 10000; i++ {
+ name := filepath.Join(dir, prefix+nextRandom()+suffix)
+ f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
+ if os.IsExist(err) {
+ if nconflict++; nconflict > 10 {
+ randmu.Lock()
+ rand = reseed()
+ randmu.Unlock()
+ }
+ continue
+ }
+ break
+ }
+ return
+}
+
+var errPatternHasSeparator = errors.New("pattern contains path separator")
+
+// prefixAndSuffix splits pattern by the last wildcard "*", if applicable,
+// returning prefix as the part before "*" and suffix as the part after "*".
+func prefixAndSuffix(pattern string) (prefix, suffix string, err error) {
+ if strings.ContainsRune(pattern, os.PathSeparator) {
+ err = errPatternHasSeparator
+ return
+ }
+ if pos := strings.LastIndex(pattern, "*"); pos != -1 {
+ prefix, suffix = pattern[:pos], pattern[pos+1:]
+ } else {
+ prefix = pattern
+ }
+ return
+}
+
+// TempDir creates a new temporary directory in the directory dir.
+// The directory name is generated by taking pattern and applying a
+// random string to the end. If pattern includes a "*", the random string
+// replaces the last "*". TempDir returns the name of the new directory.
+// If dir is the empty string, TempDir uses the
+// default directory for temporary files (see os.TempDir).
+// Multiple programs calling TempDir simultaneously
+// will not choose the same directory. It is the caller's responsibility
+// to remove the directory when no longer needed.
+func TempDir(dir, pattern string) (name string, err error) {
+ if dir == "" {
+ dir = os.TempDir()
+ }
+
+ prefix, suffix, err := prefixAndSuffix(pattern)
+ if err != nil {
+ return
+ }
+
+ nconflict := 0
+ for i := 0; i < 10000; i++ {
+ try := filepath.Join(dir, prefix+nextRandom()+suffix)
+ err = os.Mkdir(try, 0700)
+ if os.IsExist(err) {
+ if nconflict++; nconflict > 10 {
+ randmu.Lock()
+ rand = reseed()
+ randmu.Unlock()
+ }
+ continue
+ }
+ if os.IsNotExist(err) {
+ if _, err := os.Stat(dir); os.IsNotExist(err) {
+ return "", err
+ }
+ }
+ if err == nil {
+ name = try
+ }
+ break
+ }
+ return
+}
diff --git a/src/io/ioutil/tempfile_test.go b/src/io/ioutil/tempfile_test.go
new file mode 100644
index 0000000..440c7cf
--- /dev/null
+++ b/src/io/ioutil/tempfile_test.go
@@ -0,0 +1,195 @@
+// Copyright 2010 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 ioutil_test
+
+import (
+ "io/fs"
+ . "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+)
+
+func TestTempFile(t *testing.T) {
+ dir, err := TempDir("", "TestTempFile_BadDir")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(dir)
+
+ nonexistentDir := filepath.Join(dir, "_not_exists_")
+ f, err := TempFile(nonexistentDir, "foo")
+ if f != nil || err == nil {
+ t.Errorf("TempFile(%q, `foo`) = %v, %v", nonexistentDir, f, err)
+ }
+}
+
+func TestTempFile_pattern(t *testing.T) {
+ tests := []struct{ pattern, prefix, suffix string }{
+ {"ioutil_test", "ioutil_test", ""},
+ {"ioutil_test*", "ioutil_test", ""},
+ {"ioutil_test*xyz", "ioutil_test", "xyz"},
+ }
+ for _, test := range tests {
+ f, err := TempFile("", test.pattern)
+ if err != nil {
+ t.Errorf("TempFile(..., %q) error: %v", test.pattern, err)
+ continue
+ }
+ defer os.Remove(f.Name())
+ base := filepath.Base(f.Name())
+ f.Close()
+ if !(strings.HasPrefix(base, test.prefix) && strings.HasSuffix(base, test.suffix)) {
+ t.Errorf("TempFile pattern %q created bad name %q; want prefix %q & suffix %q",
+ test.pattern, base, test.prefix, test.suffix)
+ }
+ }
+}
+
+func TestTempFile_BadPattern(t *testing.T) {
+ tmpDir, err := TempDir("", t.Name())
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ const sep = string(os.PathSeparator)
+ tests := []struct {
+ pattern string
+ wantErr bool
+ }{
+ {"ioutil*test", false},
+ {"ioutil_test*foo", false},
+ {"ioutil_test" + sep + "foo", true},
+ {"ioutil_test*" + sep + "foo", true},
+ {"ioutil_test" + sep + "*foo", true},
+ {sep + "ioutil_test" + sep + "*foo", true},
+ {"ioutil_test*foo" + sep, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.pattern, func(t *testing.T) {
+ tmpfile, err := TempFile(tmpDir, tt.pattern)
+ defer func() {
+ if tmpfile != nil {
+ tmpfile.Close()
+ }
+ }()
+ if tt.wantErr {
+ if err == nil {
+ t.Errorf("Expected an error for pattern %q", tt.pattern)
+ }
+ if g, w := err, ErrPatternHasSeparator; g != w {
+ t.Errorf("Error mismatch: got %#v, want %#v for pattern %q", g, w, tt.pattern)
+ }
+ } else if err != nil {
+ t.Errorf("Unexpected error %v for pattern %q", err, tt.pattern)
+ }
+ })
+ }
+}
+
+func TestTempDir(t *testing.T) {
+ name, err := TempDir("/_not_exists_", "foo")
+ if name != "" || err == nil {
+ t.Errorf("TempDir(`/_not_exists_`, `foo`) = %v, %v", name, err)
+ }
+
+ tests := []struct {
+ pattern string
+ wantPrefix, wantSuffix string
+ }{
+ {"ioutil_test", "ioutil_test", ""},
+ {"ioutil_test*", "ioutil_test", ""},
+ {"ioutil_test*xyz", "ioutil_test", "xyz"},
+ }
+
+ dir := os.TempDir()
+
+ runTestTempDir := func(t *testing.T, pattern, wantRePat string) {
+ name, err := TempDir(dir, pattern)
+ if name == "" || err != nil {
+ t.Fatalf("TempDir(dir, `ioutil_test`) = %v, %v", name, err)
+ }
+ defer os.Remove(name)
+
+ re := regexp.MustCompile(wantRePat)
+ if !re.MatchString(name) {
+ t.Errorf("TempDir(%q, %q) created bad name\n\t%q\ndid not match pattern\n\t%q", dir, pattern, name, wantRePat)
+ }
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.pattern, func(t *testing.T) {
+ wantRePat := "^" + regexp.QuoteMeta(filepath.Join(dir, tt.wantPrefix)) + "[0-9]+" + regexp.QuoteMeta(tt.wantSuffix) + "$"
+ runTestTempDir(t, tt.pattern, wantRePat)
+ })
+ }
+
+ // Separately testing "*xyz" (which has no prefix). That is when constructing the
+ // pattern to assert on, as in the previous loop, using filepath.Join for an empty
+ // prefix filepath.Join(dir, ""), produces the pattern:
+ // ^<DIR>[0-9]+xyz$
+ // yet we just want to match
+ // "^<DIR>/[0-9]+xyz"
+ t.Run("*xyz", func(t *testing.T) {
+ wantRePat := "^" + regexp.QuoteMeta(filepath.Join(dir)) + regexp.QuoteMeta(string(filepath.Separator)) + "[0-9]+xyz$"
+ runTestTempDir(t, "*xyz", wantRePat)
+ })
+}
+
+// test that we return a nice error message if the dir argument to TempDir doesn't
+// exist (or that it's empty and os.TempDir doesn't exist)
+func TestTempDir_BadDir(t *testing.T) {
+ dir, err := TempDir("", "TestTempDir_BadDir")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(dir)
+
+ badDir := filepath.Join(dir, "not-exist")
+ _, err = TempDir(badDir, "foo")
+ if pe, ok := err.(*fs.PathError); !ok || !os.IsNotExist(err) || pe.Path != badDir {
+ t.Errorf("TempDir error = %#v; want PathError for path %q satisifying os.IsNotExist", err, badDir)
+ }
+}
+
+func TestTempDir_BadPattern(t *testing.T) {
+ tmpDir, err := TempDir("", t.Name())
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ const sep = string(os.PathSeparator)
+ tests := []struct {
+ pattern string
+ wantErr bool
+ }{
+ {"ioutil*test", false},
+ {"ioutil_test*foo", false},
+ {"ioutil_test" + sep + "foo", true},
+ {"ioutil_test*" + sep + "foo", true},
+ {"ioutil_test" + sep + "*foo", true},
+ {sep + "ioutil_test" + sep + "*foo", true},
+ {"ioutil_test*foo" + sep, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.pattern, func(t *testing.T) {
+ _, err := TempDir(tmpDir, tt.pattern)
+ if tt.wantErr {
+ if err == nil {
+ t.Errorf("Expected an error for pattern %q", tt.pattern)
+ }
+ if g, w := err, ErrPatternHasSeparator; g != w {
+ t.Errorf("Error mismatch: got %#v, want %#v for pattern %q", g, w, tt.pattern)
+ }
+ } else if err != nil {
+ t.Errorf("Unexpected error %v for pattern %q", err, tt.pattern)
+ }
+ })
+ }
+}
diff --git a/src/io/ioutil/testdata/hello b/src/io/ioutil/testdata/hello
new file mode 100644
index 0000000..e47c092
--- /dev/null
+++ b/src/io/ioutil/testdata/hello
@@ -0,0 +1 @@
+Hello, Gophers!