Skip to content

Commit 94ac29b

Browse files
committed
Add context google demo
1 parent 6529c14 commit 94ac29b

File tree

4 files changed

+239
-0
lines changed

4 files changed

+239
-0
lines changed
File renamed without changes.

context/google_demo/google/google.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Package google provides a function to do Google searches using the Google Web
2+
// Search API. See https://developers.google.com/web-search/docs/
3+
//
4+
// This package is an example to accompany https://blog.golang.org/context.
5+
// It is not intended for use by others.
6+
//
7+
// Google has since disabled its search API,
8+
// and so this package is no longer useful.
9+
package google
10+
11+
import (
12+
"context"
13+
"encoding/json"
14+
"learn-golang/context/google_demo/userip"
15+
"net/http"
16+
)
17+
18+
// Results is an ordered list of search results.
19+
type Results []Result
20+
21+
// A Result contains the title and URL of a search result.
22+
type Result struct {
23+
Title, URL string
24+
}
25+
26+
// Search sends query to Google search and returns the results.
27+
func Search(ctx context.Context, query string) (Results, error) {
28+
// Prepare the Google Search API request.
29+
req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
30+
if err != nil {
31+
return nil, err
32+
}
33+
q := req.URL.Query()
34+
q.Set("q", query)
35+
36+
// If ctx is carrying the user IP address, forward it to the server.
37+
// Google APIs use the user IP to distinguish server-initiated requests
38+
// from end-user requests.
39+
if userIP, ok := userip.FromContext(ctx); ok {
40+
q.Set("userip", userIP.String())
41+
}
42+
req.URL.RawQuery = q.Encode()
43+
44+
// Issue the HTTP request and handle the response. The httpDo function
45+
// cancels the request if ctx.Done is closed.
46+
var results Results
47+
err = httpDo(ctx, req, func(resp *http.Response, err error) error {
48+
if err != nil {
49+
return err
50+
}
51+
defer resp.Body.Close()
52+
53+
// Parse the JSON search result.
54+
// https://developers.google.com/web-search/docs/#fonje
55+
var data struct {
56+
ResponseData struct {
57+
Results []struct {
58+
TitleNoFormatting string
59+
URL string
60+
}
61+
}
62+
}
63+
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
64+
return err
65+
}
66+
for _, res := range data.ResponseData.Results {
67+
results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
68+
}
69+
return nil
70+
})
71+
// httpDo waits for the closure we provided to return, so it's safe to
72+
// read results here.
73+
return results, err
74+
}
75+
76+
// httpDo issues the HTTP request and calls f with the response. If ctx.Done is
77+
// closed while the request or f is running, httpDo cancels the request, waits
78+
// for f to exit, and returns ctx.Err. Otherwise, httpDo returns f's error.
79+
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
80+
// Run the HTTP request in a goroutine and pass the response to f.
81+
tr := &http.Transport{}
82+
client := &http.Client{Transport: tr}
83+
c := make(chan error, 1)
84+
go func() { c <- f(client.Do(req)) }()
85+
select {
86+
case <-ctx.Done():
87+
tr.CancelRequest(req)
88+
<-c // Wait for f to return.
89+
return ctx.Err()
90+
case err := <-c:
91+
return err
92+
}
93+
}

context/google_demo/server.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// The server program issues Google search requests and demonstrates the use of
2+
// the go.net Context API. It serves on port 8080.
3+
//
4+
// The /search endpoint accepts these query params:
5+
// q=the Google search query
6+
// timeout=a timeout for the request, in time.Duration format
7+
//
8+
// For example, http://localhost:8080/search?q=golang&timeout=1s serves the
9+
// first few Google search results for "golang" or a "deadline exceeded" error
10+
// if the timeout expires.
11+
package main
12+
13+
import (
14+
"context"
15+
"html/template"
16+
"learn-golang/context/google_demo/google"
17+
"learn-golang/context/google_demo/userip"
18+
"log"
19+
"net/http"
20+
"time"
21+
)
22+
23+
func main() {
24+
http.HandleFunc("/search", handleSearch)
25+
log.Fatal(http.ListenAndServe(":8080", nil))
26+
}
27+
28+
// handleSearch handles URLs like /search?q=golang&timeout=1s by forwarding the
29+
// query to google.Search. If the query param includes timeout, the search is
30+
// canceled after that duration elapses.
31+
func handleSearch(w http.ResponseWriter, req *http.Request) {
32+
// ctx is the Context for this handler. Calling cancel closes the
33+
// ctx.Done channel, which is the cancellation signal for requests
34+
// started by this handler.
35+
var (
36+
ctx context.Context
37+
cancel context.CancelFunc
38+
)
39+
timeout, err := time.ParseDuration(req.FormValue("timeout"))
40+
if err == nil {
41+
// The request has a timeout, so create a context that is
42+
// canceled automatically when the timeout expires.
43+
ctx, cancel = context.WithTimeout(context.Background(), timeout)
44+
} else {
45+
ctx, cancel = context.WithCancel(context.Background())
46+
}
47+
defer cancel() // Cancel ctx as soon as handleSearch returns.
48+
49+
// Check the search query.
50+
query := req.FormValue("q")
51+
if query == "" {
52+
http.Error(w, "no query", http.StatusBadRequest)
53+
return
54+
}
55+
56+
// Store the user IP in ctx for use by code in other packages.
57+
userIP, err := userip.FromRequest(req)
58+
if err != nil {
59+
http.Error(w, err.Error(), http.StatusBadRequest)
60+
return
61+
}
62+
ctx = userip.NewContext(ctx, userIP)
63+
64+
// Run the Google search and print the results.
65+
start := time.Now()
66+
results, err := google.Search(ctx, query)
67+
elapsed := time.Since(start)
68+
if err != nil {
69+
http.Error(w, err.Error(), http.StatusInternalServerError)
70+
return
71+
}
72+
if err := resultsTemplate.Execute(w, struct {
73+
Results google.Results
74+
Timeout, Elapsed time.Duration
75+
}{
76+
Results: results,
77+
Timeout: timeout,
78+
Elapsed: elapsed,
79+
}); err != nil {
80+
log.Print(err)
81+
return
82+
}
83+
}
84+
85+
var resultsTemplate = template.Must(template.New("results").Parse(`
86+
<html>
87+
<head/>
88+
<body>
89+
<ol>
90+
{{range .Results}}
91+
<li>{{.Title}} - <a href="{{.URL}}">{{.URL}}</a></li>
92+
{{end}}
93+
</ol>
94+
<p>{{len .Results}} results in {{.Elapsed}}; timeout {{.Timeout}}</p>
95+
</body>
96+
</html>
97+
`))

context/google_demo/userip/userip.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Package userip provides functions for extracting a user IP address from a
2+
// request and associating it with a Context.
3+
//
4+
// This package is an example to accompany https://blog.golang.org/context.
5+
// It is not intended for use by others.
6+
package userip
7+
8+
import (
9+
"context"
10+
"fmt"
11+
"net"
12+
"net/http"
13+
)
14+
15+
// FromRequest extracts the user IP address from req, if present.
16+
func FromRequest(req *http.Request) (net.IP, error) {
17+
ip, _, err := net.SplitHostPort(req.RemoteAddr)
18+
if err != nil {
19+
return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
20+
}
21+
22+
userIP := net.ParseIP(ip)
23+
if userIP == nil {
24+
return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
25+
}
26+
return userIP, nil
27+
}
28+
29+
// The key type is unexported to prevent collisions with context keys defined in
30+
// other packages.
31+
type key int
32+
33+
// userIPkey is the context key for the user IP address. Its value of zero is
34+
// arbitrary. If this package defined other context keys, they would have
35+
// different integer values.
36+
const userIPKey key = 0
37+
38+
// NewContext returns a new Context carrying userIP.
39+
func NewContext(ctx context.Context, userIP net.IP) context.Context {
40+
return context.WithValue(ctx, userIPKey, userIP)
41+
}
42+
43+
// FromContext extracts the user IP address from ctx, if present.
44+
func FromContext(ctx context.Context) (net.IP, bool) {
45+
// ctx.Value returns nil if ctx has no value for the key;
46+
// the net.IP type assertion returns ok=false for nil.
47+
userIP, ok := ctx.Value(userIPKey).(net.IP)
48+
return userIP, ok
49+
}

0 commit comments

Comments
 (0)