Program your next server in Go
Sameer Ajmani
Manager, Go team
Sameer Ajmani
Manager, Go team
This talk was presented at the ACM Applicative conference in New York City on June 1, 2016.
2
1. What is Go, and who uses it?
2. Comparing Go and other languages
3. Code examples
4. Concurrency
5. Getting started
"Go is an open source programming language that makes it easy to build simple, reliable, and efficient software."
4Design began in late 2007.
Open source since 2009 with a very active community.
Language stable as of Go 1, early 2012.
Go 1.7 is coming this August.
5Go is an answer to problems of scale at Google.
Solution: great support for concurrency
In 2011:
Solution: design the language for large code bases
8Hundreds of projects. Thousands of Go programmers. Millions of lines of Go code.
Public examples:
Hundreds of projects. Thousands of Go programmers. Millions of lines of Go code.
Public examples:
The target is networked servers, but Go is a great general-purpose language.
10Aerospike, BBC Worldwide, Bitbucket, Booking.com, Core OS, Datadog, Digital Ocean, Docker, Dropbox, Facebook, Getty Images, GitHub, GOV.UK, Heroku, IBM, Intel, InfluxDB, Iron.io, Kubernetes, Medium, MongoDB, Mozilla services, Netflix, New York Times, pool.ntp.org, Rackspace, Shutterfly, SmugMug, SoundCloud, SpaceX, Square, Stack Exchange, Thomson Reuters Eikon, Tumblr, Twitch, Twitter, Uber, VMWare ...
instanceof
)Fast, efficient for computers:
Fun, fast for humans:
final
Clarity is critical.
When reading code, it should be clear what the program will do.
When writing code, it should be clear how to make the program do what you want.
Sometimes this means writing out a loop instead of invoking an obscure function.
(Don't DRY out.)
For more background on design:
Hello, world!
package main import "fmt" func main() { fmt.Println("Hello, 世界!") }
package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/hello", handleHello) fmt.Println("serving on http://localhost:7777/hello") log.Fatal(http.ListenAndServe("localhost:7777", nil)) } func handleHello(w http.ResponseWriter, req *http.Request) { log.Println("serving", req.URL) fmt.Fprintln(w, "Hello, 世界!") }
Types follow names in declarations.
Exported names are Capitalized. Unexported names are not.
// +build ignore,OMIT
// The server program issues Google search requests. It serves on port 8080.
//
// The /search endpoint accepts these query params:
// q=the Google search query
//
// For example, http://localhost:8080/search?q=golang serves the first
// few Google search results for "golang".
package main
import (
"encoding/json"
"fmt"
"html/template"
"log"
"net/http"
"time"
"golang.org/x/talks/content/2016/applicative/google"
)
func main() { http.HandleFunc("/search", handleSearch) fmt.Println("serving on http://localhost:8080/search") log.Fatal(http.ListenAndServe("localhost:8080", nil)) } // handleSearch handles URLs like "/search?q=golang" by running a // Google search for "golang" and writing the results as HTML to w. // The query parameter "output" selects alternate output formats: // "json" for JSON, "prettyjson" for human-readable JSON. func handleSearch(w http.ResponseWriter, req *http.Request) {
log.Println("serving", req.URL)
// Check the search query.
query := req.FormValue("q") // HL
if query == "" {
http.Error(w, `missing "q" URL parameter`, http.StatusBadRequest)
return
}
// ENDQUERY OMIT
// Run the Google search.
start := time.Now()
results, err := google.Search(query) // HL
elapsed := time.Since(start)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// ENDSEARCH OMIT
// Create the structured response.
type response struct {
Results []google.Result
Elapsed time.Duration
}
resp := response{results, elapsed} // HL
// ENDRESPONSE OMIT
// Render the response.
switch req.FormValue("output") {
case "json":
err = json.NewEncoder(w).Encode(resp) // HL
case "prettyjson":
var b []byte
b, err = json.MarshalIndent(resp, "", " ") // HL
if err == nil {
_, err = w.Write(b)
}
default: // HTML
err = responseTemplate.Execute(w, resp) // HL
}
// ENDRENDER OMIT
if err != nil {
log.Print(err)
return
}
}
var responseTemplate = template.Must(template.New("results").Parse(`
<html>
<head/>
<body>
<ol>
{{range .Results}}
<li>{{.Title}} - <a href="{{.URL}}">{{.URL}}</a></li>
{{end}}
</ol>
<p>{{len .Results}} results in {{.Elapsed}}</p>
</body>
</html>
`))
localhost:8080/search?q=golang
localhost:8080/search?q=golang&output=json
localhost:8080/search?q=golang&output=prettyjson
21func handleSearch(w http.ResponseWriter, req *http.Request) { log.Println("serving", req.URL) // Check the search query. query := req.FormValue("q") if query == "" { http.Error(w, `missing "q" URL parameter`, http.StatusBadRequest) return }
FormValue
is a method on the type *http.Request
:
package http type Request struct {...} func (r *Request) FormValue(key string) string {...}
query := req.FormValue("q")
initializes a new variable query
with
the type of the expression on the right hand side, string
.
import "golang.org/x/talks/content/2016/applicative/google"
// Run the Google search. start := time.Now() results, err := google.Search(query) elapsed := time.Since(start) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
Search
returns two values, a slice of results and an error.
The results are valid only if the error is nil.
type error interface { Error() string // a useful human-readable error message }
Errors may contain additional information, accessed via type assertions.
23// Create the structured response. type response struct { Results []google.Result Elapsed time.Duration } resp := response{results, elapsed}
The response
type is defined locally within handleSearch
.
The google.Result
type is exported from package google
:
package google type Result struct { Title, URL string }
The resp
variable is initialized to a response
value using a composite struct literal.
// Render the response. switch req.FormValue("output") { case "json": err = json.NewEncoder(w).Encode(resp) case "prettyjson": var b []byte b, err = json.MarshalIndent(resp, "", " ") if err == nil { _, err = w.Write(b) } default: // HTML err = responseTemplate.Execute(w, resp) }
Each case writes bytes to an io.Writer
:
type Writer interface { Write(p []byte) (n int, err error) }
http.ResponseWriter
implements the io.Writer
interface.
// +build ignore,OMIT
// The server program issues Google search requests. It serves on port 8080.
//
// The /search endpoint accepts these query params:
// q=the Google search query
//
// For example, http://localhost:8080/search?q=golang serves the first
// few Google search results for "golang".
package main
import (
"encoding/json"
"fmt"
"html/template"
"log"
"net/http"
"time"
"golang.org/x/talks/content/2016/applicative/google"
)
func main() {
http.HandleFunc("/search", handleSearch) // HL
fmt.Println("serving on http://localhost:8080/search")
log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
// handleSearch handles URLs like "/search?q=golang" by running a
// Google search for "golang" and writing the results as HTML to w.
// The query parameter "output" selects alternate output formats:
// "json" for JSON, "prettyjson" for human-readable JSON.
func handleSearch(w http.ResponseWriter, req *http.Request) { // HL
log.Println("serving", req.URL)
// Check the search query.
query := req.FormValue("q") // HL
if query == "" {
http.Error(w, `missing "q" URL parameter`, http.StatusBadRequest)
return
}
// ENDQUERY OMIT
// Run the Google search.
start := time.Now()
results, err := google.Search(query) // HL
elapsed := time.Since(start)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// ENDSEARCH OMIT
// Create the structured response.
type response struct {
Results []google.Result
Elapsed time.Duration
}
resp := response{results, elapsed} // HL
// ENDRESPONSE OMIT
// Render the response.
switch req.FormValue("output") {
case "json":
err = json.NewEncoder(w).Encode(resp) // HL
case "prettyjson":
var b []byte
b, err = json.MarshalIndent(resp, "", " ") // HL
if err == nil {
_, err = w.Write(b)
}
default: // HTML
err = responseTemplate.Execute(w, resp) // HL
}
// ENDRENDER OMIT
if err != nil {
log.Print(err)
return
}
}
var responseTemplate = template.Must(template.New("results").Parse(` <html> <head/> <body> <ol> {{range .Results}} <li>{{.Title}} - <a href="{{.URL}}">{{.URL}}</a></li> {{end}} </ol> <p>{{len .Results}} results in {{.Elapsed}}</p> </body> </html> `))
All the packages are from the standard library:
import ( "encoding/json" "fmt" "html/template" "log" "net/http" "time" )
Go servers scale well: each request runs in its own goroutine.
Let's talk about concurrency.
27
Concurrent programs are structured as independent processes that
execute sequentially and communicate by passing messages.
Sequential execution is easy to understand. Async callbacks are not.
Go primitives: goroutines, channels, and the select
statement.
Goroutines are like lightweight threads.
They start with tiny stacks and resize as needed.
Go programs can have hundreds of thousands of them.
Start a goroutine using the go
statement:
go f(args)
The Go runtime schedules goroutines onto OS threads.
Blocked goroutines don't use a thread.
29Channels provide communication between goroutines.
c := make(chan string) // goroutine 1 c <- "hello!" // goroutine 2 s := <-c fmt.Println(s) // "hello!"
A select
statement blocks until communication can proceed.
select { case x := <-in: fmt.Println("received", x) case out <- v: fmt.Println("sent", v) }
Only the selected case runs.
31Q: What does Google search do?
A: Given a query, return a page of search results (and some ads).
Q: How do we get the search results?
A: Send the query to Web search, Image search, YouTube, Maps, News, etc., then mix the results.
How do we implement this?
32We can simulate a back end search with a random timeout up to 100ms.
var ( Web = FakeSearch("web", "The Go Programming Language", "http://golang.org") Image = FakeSearch("image", "The Go gopher", "https://blog.golang.org/gopher/gopher.png") Video = FakeSearch("video", "Concurrency is not Parallelism", "https://www.youtube.com/watch?v=cN_DpYBzKso") ) type SearchFunc func(query string) Result func FakeSearch(kind, title, url string) SearchFunc { return func(query string) Result { time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) return Result{ Title: fmt.Sprintf("%s(%q): %s", kind, query, title), URL: url, } } }
// +build ignore
package main
import (
"fmt"
"math/rand"
"time"
"golang.org/x/talks/content/2016/applicative/google"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
func main() { start := time.Now() results, err := google.Search("golang") elapsed := time.Since(start) fmt.Println(results) fmt.Println(elapsed, err) }
The Search
function takes a query and returns a slice of Results
.
Search invokes the Web, Image, and Video searches serially, then constructs the results
slice.
func Search(query string) ([]Result, error) { results := []Result{ Web(query), Image(query), Video(query), } return results, nil }
// +build ignore
package main
import (
"fmt"
"math/rand"
"time"
"golang.org/x/talks/content/2016/applicative/google"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
func main() {
start := time.Now()
results, err := google.Search("golang")
elapsed := time.Since(start)
fmt.Println(results)
fmt.Println(elapsed, err)
}
Run the Web, Image, and Video searches concurrently, and wait for all results.
The func
literals are closures over query
and c
.
func SearchParallel(query string) ([]Result, error) { c := make(chan Result) go func() { c <- Web(query) }() go func() { c <- Image(query) }() go func() { c <- Video(query) }() return []Result{<-c, <-c, <-c}, nil }
// +build ignore
package main
import (
"fmt"
"math/rand"
"time"
"golang.org/x/talks/content/2016/applicative/google"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
func main() {
start := time.Now()
results, err := google.SearchParallel("golang")
elapsed := time.Since(start)
fmt.Println(results)
fmt.Println(elapsed, err)
}
Don't wait for slow servers.
func SearchTimeout(query string, timeout time.Duration) ([]Result, error) { timer := time.After(timeout) c := make(chan Result, 3) go func() { c <- Web(query) }() go func() { c <- Image(query) }() go func() { c <- Video(query) }() var results []Result for i := 0; i < 3; i++ { select { case result := <-c: results = append(results, result) case <-timer: return results, errors.New("timed out") } } return results, nil
// +build ignore
package main
import (
"fmt"
"math/rand"
"time"
"golang.org/x/talks/content/2016/applicative/google"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
func main() {
start := time.Now()
results, err := google.SearchTimeout("golang", 80*time.Millisecond)
elapsed := time.Since(start)
fmt.Println(results)
fmt.Println(elapsed, err)
}
Q: How do we avoid discarding results from slow servers?
A: Replicate the servers. Send requests to multiple replicas, and use the first response.
func First(replicas ...SearchFunc) SearchFunc { return func(query string) Result { c := make(chan Result, len(replicas)) searchReplica := func(i int) { c <- replicas[i](query) } for i := range replicas { go searchReplica(i) } return <-c } }
// +build ignore,OMIT
package main
import (
"fmt"
"math/rand"
"time"
"golang.org/x/talks/content/2016/applicative/google"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// START2 OMIT
func main() { start := time.Now() search := google.First( google.FakeSearch("replica 1", "I'm #1!", ""), google.FakeSearch("replica 2", "#2 wins!", "")) result := search("golang") elapsed := time.Since(start) fmt.Println(result) fmt.Println(elapsed) }
// STOP2 OMIT
Reduce tail latency using replicated back ends.
var ( replicatedWeb = First(Web1, Web2) replicatedImage = First(Image1, Image2) replicatedVideo = First(Video1, Video2) ) func SearchReplicated(query string, timeout time.Duration) ([]Result, error) { timer := time.After(timeout) c := make(chan Result, 3) go func() { c <- replicatedWeb(query) }() go func() { c <- replicatedImage(query) }() go func() { c <- replicatedVideo(query) }()
// +build ignore
package main
import (
"fmt"
"math/rand"
"time"
"golang.org/x/talks/content/2016/applicative/google"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
func main() {
start := time.Now()
results, err := google.SearchReplicated("golang", 80*time.Millisecond)
elapsed := time.Since(start)
fmt.Println(results)
fmt.Println(elapsed, err)
}
Go functions have simple, synchronous signatures.
The use of concurrency is encapsulated.
In just a few simple transformations we used Go's concurrency primitives to convert a
program into one that is
No locks. No condition variables. No futures. No callbacks.
41Take the Go Tour online.
Then go deeper ...
Still interested?
Run a pilot project.
43
Reduces the cost & risks of switching to a new technology,
while helping your organization discover the benefits.
1. Choose something small to write in Go (e.g., a microservice)
2. Build a prototype with a friend
3. Compare Go to what you use today
4. Present results to the team
44Go tools meet you where you are. There's no one "Go IDE".
gofmt
: automatic formattinggoimports
: automatic updates of package importsgocode
: automatic completiongo
tool: automatic fetch & buildguru
: static analysis, bug finding, code navigationTake the Go Tour online.
Lots more material.
Great community.
46