Go: a simple programming environment
9 Nov 2012
Andrew Gerrand
Google Inc.
9 Nov 2012
Andrew Gerrand
Google Inc.
A video of this talk was recorded at Øredev in Malmö, Sweden in November 2012.
2Motivated by our needs at Google.
We need:
"Consensus drove the design. Nothing went into the language until [Ken Thompson, Robert Griesemer, and myself] all agreed that it was right. Some features didn’t get resolved until after a year or more of discussion." - Rob Pike
Go is:
Released in March 2012
A specification of the language and libraries that will be supported for years.
The guarantee: code written for Go 1.0 will build and run with Go 1.x.
Best thing we ever did.
6package main import "fmt" func main() { fmt.Println("Hello, go") }
Go code lives in packages.
Packages contain type, function, variable, and constant declarations.
Packages can be very small (package errors
has just one declaration) or very large (package net/http
has >100 declarations). Most are somewhere in between.
Case determines visibility: Foo
is exported, foo
is not
The io
package provides fundamental I/O interfaces that are used throughout most Go code.
The most ubiquitous are the Reader
and Writer
types, which describe streams of data.
package io type Writer interface { Write(p []byte) (n int, err error) } type Reader interface { Read(p []byte) (n int, err error) }
Reader
and Writer
implementations include files, sockets, (de)compressors, image and JSON codecs, and many more.
package main import ( "compress/gzip" "encoding/base64" "io" "os" "strings" ) func main() { var r io.Reader r = strings.NewReader(data) r = base64.NewDecoder(base64.StdEncoding, r) r, _ = gzip.NewReader(r) io.Copy(os.Stdout, r) } const data = ` H4sIAAAJbogA/1SOO5KDQAxE8zlFZ5tQXGCjjfYIjoURoPKgcY0E57f4VZlQXf2e+r8yOYbMZJhoZWRxz3wkCVjeReETS0VHz5fBCzpxxg/PbfrT/gacCjbjeiRNOChaVkA9RAdR8eVEw4vxa0Dcs3Fe2ZqowpeqG79L995l3VaMBUV/02OS+B6kMWikwG51c8n5GnEPr11F2/QJAAD//z9IppsHAQAA `
The net/http
package implements an HTTP server and client.
package main import ( "fmt" "log" "net/http" ) type Greeting string func (g Greeting) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, g) } func main() { err := http.ListenAndServe("localhost:4000", Greeting("Hello, go")) if err != nil { log.Fatal(err) } }
The encoding/json
package converts JSON-encoded data to and from native Go data structures.
// +build ignore,OMIT
package main
import (
"encoding/json"
"fmt"
"strings"
)
const blob = `[ {"Title":"Øredev", "URL":"http://oredev.org"}, {"Title":"Strange Loop", "URL":"http://thestrangeloop.com"} ]` type Item struct { Title string URL string } func main() { var items []*Item json.NewDecoder(strings.NewReader(blob)).Decode(&items) for _, item := range items { fmt.Printf("Title: %v URL: %v\n", item.Title, item.URL) } }
The time
package provides a representation of time and duration, and other time-related functions.
// +build ignore,OMIT
package main
import (
"fmt"
"time"
)
func main() {
if time.Now().Hour() < 12 { fmt.Println("Good morning.") } else { fmt.Println("Good afternoon (or evening).") }
}
// +build ignore,OMIT
package main
import (
"fmt"
"time"
)
func main() {
birthday, _ := time.Parse("Jan 2 2006", "Nov 10 2009") // time.Time age := time.Since(birthday) // time.Duration fmt.Printf("Go is %d days old\n", age/(time.Hour*24))
}
time.Time
values also contain a time.Location
(for display only):
// +build ignore,OMIT
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now() fmt.Println(t.In(time.UTC)) home, _ := time.LoadLocation("Australia/Sydney") fmt.Println(t.In(home))
}
The flag
package provides a simple API for parsing command-line flags.
package main import ( "flag" "fmt" "time" ) var ( message = flag.String("message", "Hello!", "what to say") delay = flag.Duration("delay", 2*time.Second, "how long to wait") ) func main() { flag.Parse() fmt.Println(*message) time.Sleep(*delay) }
$ flag -message 'Hold on...' -delay 5m
The go
tool is the de facto standard for building and installing Go code.
Compile and run a single-file program:
$ go run hello.go
Build and install the package in the current directory (and its dependencies):
$ go install
Build and install the fmt
package (and its dependencies):
$ go install fmt
This tool also acts as an interface for most of the Go tools.
18
The go
tool is a "zero configuration" tool. No Makefiles or scripts. Just Go code.
Your build schema and code are always in sync; they are one and the same.
Package import paths mirror the code's location in the file system:
src/ github.com/nf/ gosynth/ main.go note.go osc.go wav/ writer.go
The gosynth
program imports the wav
package:
import "github.com/nf/wav"
Installing gosynth
will automatically install the wav
package:
$ go install github.com/nf/gosynth
The go
tool also fetches Go code from remote repositories.
Import paths can be URLs:
import "golang.org/x/net/websocket"
To fetch, build and install a package:
$ go get code.google.com/p/go.net/websocket
To fetch, build, and install gosynth
and its dependencies:
$ go get github.com/nf/gosynth
This simple design leads to other cool tools:
20Godoc extracts documentation from Go code and presents it in a variety of forms.
Comments need no special format, they just need to precede what they document.
// Split slices s into all substrings separated by sep and returns a slice of // the substrings between those separators. // If sep is empty, Split splits after each UTF-8 sequence. // It is equivalent to SplitN with a count of -1. func Split(s, sep string) []string {
Documentation that lives with code is easy to keep up-to-date.
21
The gofmt
tool is a pretty-printer for Go source code.
All Go code in the core is gofmt'd, as is ~70% of open source Go code.
Ends boring formatting discussions.
Improves readability. Improves writability.
Saves a huge amount of time.
22
The go
tool and the testing
package provide a lightweight test framework.
func TestIndex(t *testing.T) { var tests = []struct { s string sep string out int }{ {"", "", 0}, {"", "a", -1}, {"fo", "foo", -1}, {"foo", "foo", 0}, {"oofofoofooo", "f", 2}, // etc } for _, test := range tests { actual := strings.Index(test.s, test.sep) if actual != test.out { t.Errorf("Index(%q,%q) = %v; want %v", test.s, test.sep, actual, test.out) } } }
The go tool runs tests.
$ go test PASS $ go test -v === RUN TestIndex --- PASS: TestIndex (0.00 seconds) PASS
To run the tests for all my projects:
$ go test github.com/nf/...
The testing
package also supports benchmarks.
A sample benchmark function:
func BenchmarkIndex(b *testing.B) { const s = "some_text=some☺value" for i := 0; i < b.N; i++ { strings.Index(s, "v") } }
The benchmark package will vary b.N
until the benchmark function lasts long enough to be timed reliably.
$ go test -test.bench=Index PASS BenchmarkIndex 50000000 37.3 ns/op
The testing
package also supports testable examples.
func ExampleIndex() { fmt.Println(strings.Index("chicken", "ken")) fmt.Println(strings.Index("chicken", "dmr")) // Output: // 4 // -1 }
Examples and built and run as part of the normal test suite:
$ go test -v === RUN: ExampleIndex --- PASS: ExampleIndex (0.00 seconds) PASS
The example is displayed in godoc
alongside the thing it demonstrates:
vet
: checks code for common programmer mistakespprof
: CPU and memory profilingfix
: automatically migrate code as APIs change
Webfront
is an HTTP server and reverse proxy.
It reads a JSON-formatted rule file like this:
[ {"Host": "example.com", "Serve": "/var/www"}, {"Host": "example.org", "Forward": "localhost:8080"} ]
For all requests to the host example.com
(or any name ending in ".example.com"
) it serves files from the /var/www
directory.
For requests to example.org
, it forwards the request to the HTTP server listening on localhost port 8080.
A Rule
value specifies what to do for a request to a specific host.
// Rule represents a rule in a configuration file. type Rule struct { Host string // to match against request Host header Forward string // non-empty if reverse proxy Serve string // non-empty if file server }
It corresponds directly with the entries in the JSON configuration file.
[ {"Host": "example.com", "Serve": "/var/www"}, {"Host": "example.org", "Forward": "localhost:8080"} ]
// Match returns true if the Rule matches the given Request. func (r *Rule) Match(req *http.Request) bool { return req.Host == r.Host || strings.HasSuffix(req.Host, "."+r.Host) }
// Handler returns the appropriate Handler for the Rule. func (r *Rule) Handler() http.Handler { if h := r.Forward; h != "" { return &httputil.ReverseProxy{ Director: func(req *http.Request) { req.URL.Scheme = "http" req.URL.Host = h }, } } if d := r.Serve; d != "" { return http.FileServer(http.Dir(d)) } return nil }
The Server
type is responsible for loading (and refreshing) the rules from the rule file and serving HTTP requests with the appropriate handler.
// Server implements an http.Handler that acts as either a reverse proxy or // a simple file server, as determined by a rule set. type Server struct { mu sync.RWMutex // guards the fields below mtime time.Time // when the rule file was last modified rules []*Rule }
// ServeHTTP matches the Request with a Rule and, if found, serves the // request with the Rule's handler. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if h := s.handler(r); h != nil { h.ServeHTTP(w, r) return } http.Error(w, "Not found.", http.StatusNotFound) }
// handler returns the appropriate Handler for the given Request, // or nil if none found. func (s *Server) handler(req *http.Request) http.Handler { s.mu.RLock() defer s.mu.RUnlock() for _, r := range s.rules { if r.Match(req) { return r.Handler() } } return nil }
The parseRules
function uses the encoding/json
package to read the rule file into a Go data structure.
// parseRules reads rule definitions from file returns the resultant Rules. func parseRules(file string) ([]*Rule, error) { f, err := os.Open(file) if err != nil { return nil, err } defer f.Close() var rules []*Rule err = json.NewDecoder(f).Decode(&rules) if err != nil { return nil, err } return rules, nil }
// loadRules tests whether file has been modified // and, if so, loads the rule set from file. func (s *Server) loadRules(file string) error { fi, err := os.Stat(file) if err != nil { return err } mtime := fi.ModTime() if mtime.Before(s.mtime) && s.rules != nil { return nil // no change } rules, err := parseRules(file) if err != nil { return fmt.Errorf("parsing %s: %v", file, err) } s.mu.Lock() s.mtime = mtime s.rules = rules s.mu.Unlock() return nil }
// NewServer constructs a Server that reads rules from file with a period // specified by poll. func NewServer(file string, poll time.Duration) (*Server, error) { s := new(Server) if err := s.loadRules(file); err != nil { return nil, err } go s.refreshRules(file, poll) return s, nil }
This constructor function launches a goroutine running the refreshRules
method.
// refreshRules polls file periodically and refreshes the Server's rule // set if the file has been modified. func (s *Server) refreshRules(file string, poll time.Duration) { for { if err := s.loadRules(file); err != nil { log.Println(err) } time.Sleep(poll) } }
The main function parses command-line flags, constructs a Server
, and launches an HTTP server that serves all requests with the Server
.
var ( httpAddr = flag.String("http", ":80", "HTTP listen address") ruleFile = flag.String("rules", "", "rule definition file") pollInterval = flag.Duration("poll", time.Second*10, "file poll interval") ) func main() { flag.Parse() s, err := NewServer(*ruleFile, *pollInterval) if err != nil { log.Fatal(err) } err = http.ListenAndServe(*httpAddr, s) if err != nil { log.Fatal(err) } }
The Server
integration test uses the httptest
package to construct a dummy HTTP server, synthesizes a set of rules, and constructs a Server
instance that uses those rules.
func testHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) } func TestServer(t *testing.T) { dummy := httptest.NewServer(http.HandlerFunc(testHandler)) defer dummy.Close() ruleFile := writeRules([]*Rule{ {Host: "example.com", Forward: dummy.Listener.Addr().String()}, {Host: "example.org", Serve: "testdata"}, }) defer os.Remove(ruleFile) s, err := NewServer(ruleFile, time.Hour) if err != nil { t.Fatal(err) } // continued next slide
Each test case in the table specifies a request URL and the expected response code and body.
// continued from previous slide var tests = []struct { url string code int body string }{ {"http://example.com/", 200, "OK"}, {"http://foo.example.com/", 200, "OK"}, {"http://example.org/", 200, "contents of index.html\n"}, {"http://example.net/", 404, "Not found.\n"}, {"http://fooexample.com/", 404, "Not found.\n"}, } // continued next slide
For each test case, construct an http.Request
for the url and an httptest.ResponseRecorder
to capture the response, and pass them to the Server.ServeHTTP
method. Then check that the response matches the test case.
// continued from previous slide for _, test := range tests { req, _ := http.NewRequest("GET", test.url, nil) rw := httptest.NewRecorder() rw.Body = new(bytes.Buffer) s.ServeHTTP(rw, req) if g, w := rw.Code, test.code; g != w { t.Errorf("%s: code = %d, want %d", test.url, g, w) } if g, w := rw.Body.String(), test.body; g != w { t.Errorf("%s: body = %q, want %q", test.url, g, w) } } }
All about Go:
The slides for this talk:
go.dev/talks/2012/simple.slide
webfront:
459 Nov 2012