diff options
| -rw-r--r-- | _content/blog/context/google/google.go | 94 | ||||
| -rw-r--r-- | _content/blog/context/gorilla/gorilla.go | 51 | ||||
| -rw-r--r-- | _content/blog/context/index.adoc | 462 | ||||
| -rw-r--r-- | _content/blog/context/server/server.go | 100 | ||||
| -rw-r--r-- | _content/blog/context/tomb/tomb.go | 24 | ||||
| -rw-r--r-- | _content/blog/context/userip/userip.go | 51 | ||||
| -rw-r--r-- | _content/blog/index.adoc | 3 |
7 files changed, 785 insertions, 0 deletions
diff --git a/_content/blog/context/google/google.go b/_content/blog/context/google/google.go new file mode 100644 index 0000000..e25cfc1 --- /dev/null +++ b/_content/blog/context/google/google.go @@ -0,0 +1,94 @@ +// +build OMIT + +// Package google provides a function to do Google searches using the Google Web +// Search API. See https://developers.google.com/web-search/docs/ +// +// This package is an example to accompany https://blog.golang.org/context. +// It is not intended for use by others. +// +// Google has since disabled its search API, +// and so this package is no longer useful. +package google + +import ( + "context" + "encoding/json" + "net/http" + + "golang.org/x/blog/content/context/userip" +) + +// Results is an ordered list of search results. +type Results []Result + +// A Result contains the title and URL of a search result. +type Result struct { + Title, URL string +} + +// Search sends query to Google search and returns the results. +func Search(ctx context.Context, query string) (Results, error) { + // Prepare the Google Search API request. + req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil) + if err != nil { + return nil, err + } + q := req.URL.Query() + q.Set("q", query) + + // If ctx is carrying the user IP address, forward it to the server. + // Google APIs use the user IP to distinguish server-initiated requests + // from end-user requests. + if userIP, ok := userip.FromContext(ctx); ok { + q.Set("userip", userIP.String()) + } + req.URL.RawQuery = q.Encode() + + // Issue the HTTP request and handle the response. The httpDo function + // cancels the request if ctx.Done is closed. + var results Results + err = httpDo(ctx, req, func(resp *http.Response, err error) error { + if err != nil { + return err + } + defer resp.Body.Close() + + // Parse the JSON search result. + // https://developers.google.com/web-search/docs/#fonje + var data struct { + ResponseData struct { + Results []struct { + TitleNoFormatting string + URL string + } + } + } + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return err + } + for _, res := range data.ResponseData.Results { + results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL}) + } + return nil + }) + // httpDo waits for the closure we provided to return, so it's safe to + // read results here. + return results, err +} + +// httpDo issues the HTTP request and calls f with the response. If ctx.Done is +// closed while the request or f is running, httpDo cancels the request, waits +// for f to exit, and returns ctx.Err. Otherwise, httpDo returns f's error. +func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error { + // Run the HTTP request in a goroutine and pass the response to f. + c := make(chan error, 1) + req = req.WithContext(ctx) + go func() { c <- f(http.DefaultClient.Do(req)) }() + select { + case <-ctx.Done(): + <-c // Wait for f to return. + return ctx.Err() + case err := <-c: + return err + } +} diff --git a/_content/blog/context/gorilla/gorilla.go b/_content/blog/context/gorilla/gorilla.go new file mode 100644 index 0000000..4698dce --- /dev/null +++ b/_content/blog/context/gorilla/gorilla.go @@ -0,0 +1,51 @@ +// +build OMIT + +// Package gorilla provides a go.net/context.Context implementation whose Value +// method returns the values associated with a specific HTTP request in the +// github.com/gorilla/context package. +package gorilla + +import ( + "net/http" + + gcontext "github.com/gorilla/context" + "golang.org/x/net/context" +) + +// NewContext returns a Context whose Value method returns values associated +// with req using the Gorilla context package: +// http://www.gorillatoolkit.org/pkg/context +func NewContext(parent context.Context, req *http.Request) context.Context { + return &wrapper{parent, req} +} + +type wrapper struct { + context.Context + req *http.Request +} + +type key int + +const reqKey key = 0 + +// Value returns Gorilla's context package's value for this Context's request +// and key. It delegates to the parent Context if there is no such value. +func (ctx *wrapper) Value(key interface{}) interface{} { + if key == reqKey { + return ctx.req + } + if val, ok := gcontext.GetOk(ctx.req, key); ok { + return val + } + return ctx.Context.Value(key) +} + +// HTTPRequest returns the *http.Request associated with ctx using NewContext, +// if any. +func HTTPRequest(ctx context.Context) (*http.Request, bool) { + // We cannot use ctx.(*wrapper).req to get the request because ctx may + // be a Context derived from a *wrapper. Instead, we use Value to + // access the request if it is anywhere up the Context tree. + req, ok := ctx.Value(reqKey).(*http.Request) + return req, ok +} diff --git a/_content/blog/context/index.adoc b/_content/blog/context/index.adoc new file mode 100644 index 0000000..ced3e90 --- /dev/null +++ b/_content/blog/context/index.adoc @@ -0,0 +1,462 @@ += Pola Konkurensi Go: Context +Sameer Ajmani +29 Juli 2014 +:toc: +:sectanchors: +:sectlinks: + +Catatan: Artikel ini membutuhkan pengalaman dan pengetahuan tentang membuat +layanan peladen (_server_), seperti peladen HTTP. + +== Pendahuluan + +Dalam peladen (_server_) yang dibuat dengan Go, setiap permintaan yang masuk +ditangani oleh _goroutine_-nya sendiri. +Fungsi yang menangani permintaan (disebut juga _handler_) terkadang +menjalankan _goroutine_ tambahan untuk mengakses _backend_ lainnya seperti +layanan basis-data dan _Remote Procedure Call_ (RPC). +Kumpulan _goroutine_ yang bekerja dalam sebuah _handler_ biasanya membutuhkan +akses ke nilai tertentu seperti identitas pengguna, token otorisasi, dan +tenggat (_deadline_) permintaan. +Saat sebuah permintaan dibatalkan atau kehabisan waktu, semua _goroutine_ yang +sedang bekerja pada permintaan tersebut seharusnya segera berhenti supaya +sistem dapat mengambil alih kembali sumber daya yang mereka gunakan. + +Di Google, kami mengembangkan sebuah paket `context` yang mempermudah +mengirim nilai sesuai skop-permintaan, sinyal pembatalan, dan tenggat ke semua +_goroutine_ yang ikut serta dalam menangani sebuah permintaan. +Paket tersebut tersedia secara publik sebagai +https://golang.org/pkg/context[context^]. +Artikel ini menjelaskan bagaimana menggunakan paket tersebut dan menyediakan +sebuah contoh kerjanya. + +== Context + +Inti dari paket `context` adalah tipe `Context`: + +---- +// Sebuah Context membawa sebuah tenggat (Deadline), sinyal pembatalan (Done), +// dan nilai-nilai (Value) sesuai skop-permintaan. +// Method-method nya aman digunakan secara simultan oleh banyak _goroutine_. +type Context interface { + // Done mengembalikan sebuah kanal yang tertutup saat Context ini + // dibatalkan atau waktunya telah habis. + Done() <-chan struct{} + + // Err mengindikasikan kenapa context dibatalkan, setelah kanal Done + // tertutup. + Err() error + + // Deadline mengembalikan waktu saat Context ini akan dibatalkan, jika + // ada. + Deadline() (deadline time.Time, ok bool) + + // Value mengembalikan nilai yang berasosiasi dengan key, atau nil jika + // key tidak ada. + Value(key interface{}) interface{} +} +---- + +(Deskripsi dari tipe Context di atas telah diringkas; lihat +https://golang.org/pkg/context[godoc^] +untuk lebih lengkapnya.) + +Method `Done` mengembalikan sebuah kanal yang berlaku sebagai sinyal pembatalan +terhadap fungsi-fungsi yang berjalan dengan `Context`: saat kanal tersebut +tertutup, fungsi-fungsi tersebut sebaiknya berhenti bekerja. +Method `Err` mengembalikan sebuah `error` yang mengindikasikan kenapa +`Context` tersebut dibatalkan. +Artikel +link:/blog/pipelines[_pipeline_ dan pembatalan] +mendiskusikan idiom dari kanal `Done` lebih detail. + +Sebuah `Context` _tidak_ memiliki method `Cancel` dengan alasan yang sama +kenapa kanal `Done` hanya menerima-saja: fungsi yang menerima sinyal +pembatalan biasanya bukan yang mengirim sinyal. +Pada khususnya, saat sebuah induk operasi memulai beberapa _goroutine_ untuk +sub-operasi, maka sub-operasi tersebut tidak bisa membatalkan induk. +Namun, fungsi `WithCancel` (yang dijelaskan di bawah) menyediakan cara untuk +membatalkan sebuah nilai `Context` yang baru. + +Sebuah `Context` aman digunakan secara simultan oleh beberapa _goroutine_. +Kode dapat mengirim sebuah `Context` ke sejumlah _goroutine_ dan membatalkan +`Context` tersebut untuk mengirim sinyal ke semua _goroutine_. + +Method `Deadline` membolehkan fungsi menentukan apakah mereka harus mulai +bekerja atau tidak; +jika waktu yang tersedia tinggal sedikit, maka pekerjaan mungkin sebaiknya +tidak dilakukan. +Kode juga bisa menggunakan sebuah tenggat untuk men-set batas waktu untuk +operasi input/output (I/O). + +Method `Value` membolehkan sebuah `Context` membawa data sesuai +skop-permintaan. +Data tersebut haruslah aman untuk digunakan secara simultan oleh beberapa +_goroutine_. + +== Context turunan + +Paket `context` menyediakan fungsi-fungsi untuk _menurunkan_ nilai `Context` +baru dari yang sudah ada. +Nilai-nilai `Context` tersebut membentuk sebuah pohon: saat sebuah `Context` +dibatalkan, semua `Context` turunannya juga akan ikut dibatalkan. + +Fungsi `Background` adalah akar dari semua pohon `Context`; ia tidak pernah +dibatalkan: + +---- +// Background mengembalikan sebuah Context kosong. +// Ia tidak pernah dibatalkan, dan tidak punya tenggat, dan tidak memiliki +// nilai. +// Fungsi Background biasanya digunakan dalam main, init, dan tes, dan sebagai +// Context induk pada penanganan permintaan yang masuk. +func Background() Context +---- + +Fungsi `WithCancel` dan `WithTimeout` mengembalikan turunan dari nilai +`Context` yang dapat dibatalkan lebih awal dari `Context` induk-nya. +`Context` yang berasosiasi dengan permintaan masuk biasanya dibatalkan saat +_handler_ selesai. +Fungsi `WithCancel` berguna untuk membatalkan permintaan yang duplikat saat +menggunakan beberapa replika. +Fungsi `WithTimeout` berguna untuk men-set sebuah tenggat saat mengirim +permintaan ke peladen _backend_ lain. + +---- +// WithCancel mengembalikan sebuah salinan dari Content `parent` dengan kanal +// Done tertutup setelah parent.Done ditutup atau saat cancel dipanggil. +func WithCancel(parent Context) (ctx Context, cancel CancelFunc) + +// CancelFunc membatalkan sebuah Context. +type CancelFunc func() + +// WithTimeout mengembalikan salinan dari Context `parent` dengan kanal Done +// ditutup setelah parent.Done ditutup, atau cancel dipanggil, atau timeout +// telah lewat. +// Tenggat dari Context yang baru yaitu lebih kecil dari now+timeout dan dari +// tenggat `parent`, jika ada. +// Jika timer masih tetap berjalan, fungsi cancel melepaskan sumber daya +// mereka. +func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) +---- + +Fungsi `WithValue` menyediakan sebuah cara untuk mengasosikan nilai-nilai +skop-permintaan dengan sebuah `Context`: + +---- +// WithValue mengembalikan sebuah salinan dari Context parent yang mana method +// `Value`-nya mengembalikan `val` dari `key`. +func WithValue(parent Context, key interface{}, val interface{}) Context +---- + +Cara paling baik untuk melihat bagaimana paket `context` digunakan yaitu lewat +sebuah contoh kode, seperti yang akan kita bahas di bawah. + +== Contoh: Pencarian Web Google + +Contoh ini yaitu sebuah peladen HTTP yang menangani URL seperti +`/search?q=golang&timeout=1s` dengan meneruskan kueri "golang" ke +https://developers.google.com/web-search/docs/[API Google Web Search^] +dan menampilkan hasilnya. +Parameter `timeout` memberitahu peladen untuk membatalkan permintaan tersebut +setelah durasi habis. + +Kode ini dibagi dalam tiga paket: + +* link:/blog/context/server/server.go[server^] menyediakan fungsi main dan + penanganan untuk `/search`. +* link:/blog/context/userip/userip.go[userip^] menyediakan fungsi-fungsi untuk + mengekstraksi alamat IP dari _request_ dan menghubungkan dengan sebuah + `Context`. +* link:/blog/context/google/google.go[google^] menyediakan fungsi `Search` + untuk mengirim sebuah kueri ke Google. + + +=== Program peladen + +Program +link:/blog/context/server/server.go[peladen] +menangani permintaan seperti `/search?q=golang` dengan +mengembalikan beberapa hasil pencarian pertama dari Google untuk kata +`golang`. +Peladen tersebut memiliki fungsi `handleSearch` untuk menangani permintaan +ke `/search`. +Fungsi tersebut membuat sebuah `Context` induk bernama `ctx` yang mengatur +supaya dibatalkan saat fungsi selesai. +Jika permintaan mengikutkan parameter `timeout` pada kueri URL, maka `Context` +akan dibatalkan secara otomatis saat `timeout` telah habis: + +---- +func handleSearch(w http.ResponseWriter, req *http.Request) { + // ctx adalah Context dari fungsi ini. + // Memanggil cancel akan menutup kanal ctx.Done, yang merupakan sinyal + // pembatalan untuk permintaan yang dimulai oleh fungsi ini. + var ( + ctx context.Context + cancel context.CancelFunc + ) + timeout, err := time.ParseDuration(req.FormValue("timeout")) + if err == nil { + // Permintaan memiliki batas waktu, jadi buatlah sebuah context + // yang dibatalkan secara otomatis saat timeout selesai. + ctx, cancel = context.WithTimeout(context.Background(), timeout) + } else { + ctx, cancel = context.WithCancel(context.Background()) + } + defer cancel() // Batalkan ctx saat handleSearch selesai. +---- + +Fungsi `handleSearch` mengekstrak kueri dan alamat IP klien dari HTTP +_request_ dengan memanggil paket `userip`. +Alamat IP dari klien dibutuhkan untuk permintaan ke _backend_, jadi +`handleSearch` memasukkan-nya ke dalam `ctx`: + +---- + // Periksa kueri pencarian. + query := req.FormValue("q") + if query == "" { + http.Error(w, "no query", http.StatusBadRequest) + return + } + + // Simpan alamat pengguna dalam ctx untuk digunakan oleh kode dalam paket + // lain. + userIP, err := userip.FromRequest(req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + ctx = userip.NewContext(ctx, userIP) +---- + +Fungsi `handleSearch` kemudian memanggil `google.Search` dengan mengirim `ctx` +dan `query`: + +---- + // Jalankan pencarian Google dan cetak hasilnya. + start := time.Now() + results, err := google.Search(ctx, query) + elapsed := time.Since(start) +---- + +Jika pencarian sukses, fungsi tersebut menampilkan hasilnya: + +---- + if err := resultsTemplate.Execute(w, struct { + Results google.Results + Timeout, Elapsed time.Duration + }{ + Results: results, + Timeout: timeout, + Elapsed: elapsed, + }); err != nil { + log.Print(err) + return + } +---- + +=== Paket userip + +Paket +link:/blog/context/userip/userip.go[`userip`] +menyediakan fungsi-fungsi untuk mengekstrak alamat IP pengguna +dari sebuah permintaan dan menanamnya dalam sebuah Context. +Sebuah `Context` menyediakan pemetaan kunci-nilai, yang mana kunci dan nilai +bertipe `interface{}`. +Tipe dari kunci haruslah mendukung +link:/ref/spec#Comparison_operators[ekualitas^], +dan tipe dari nilai haruslah aman digunakan secara simultan oleh beberapa +_goroutine_. +Paket seperti `userip` menyembunyikan detail dari pemetaan ini dan menyediakan +akses ke nilai `Context` tertentu. + +Untuk menghindari bentrok dengan kunci yang lain, `userip` mendefinisikan tipe +`key` yang tidak diekspor dan menggunakan nilai dari tipe tersebut sebagai +kunci dari context: + +---- +// Tipe key tidak diekspor untuk mencegah bentrok dengan kunci-kunci dari +// context yang didefinisikan dalam paket yang lain. +type key int + +// userIPkey adalah kunci context untuk alamat IP pengguna. +// Nilainya bisa 0 atau nilai integer lain. +// Jika paket ini mendefinisikan kunci-kunci context lainnya, maka nilai +// setiap kunci tersebut haruslah memiliki nilai integer yang berbeda. +const userIPKey key = 0 +---- + +Fungsi `FromRequest` mengekstrak sebuah nilai `userIP` dari `http.Request`: + +---- +func FromRequest(req *http.Request) (net.IP, error) { + ip, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr) + } +---- + +Fungsi `NewContext` mengembalikan sebuah `Context` baru yang membawa nilai +`userIP`: + +---- +func NewContext(ctx context.Context, userIP net.IP) context.Context { + return context.WithValue(ctx, userIPKey, userIP) +} +---- + +Fungsi `FromContext` mengekstrak sebuah `userIP` dari sebuah `Context`: + +---- +func FromContext(ctx context.Context) (net.IP, bool) { + // ctx.Value mengembalikan nil jika ctx tidak memiliki nilai sesuai key; + // konversi tipe net.IP mengembalikan ok=false jika kunci tidak ada atau + // nilai IP adalah nil. + userIP, ok := ctx.Value(userIPKey).(net.IP) + return userIP, ok +} +---- + + +=== Paket google + +Fungsi +link:/blog/context/google/google.go[`google.Search`^] +membuat permintaan HTTP ke +https://developers.google.com/web-search/docs/[Google Web Search API^] +dan mengurai kembalian dalam bentuk JSON. +Fungsi tersebut menerima sebuah `Context` parameter `ctx` dan selesai bila +`ctx.Done` ditutup walau permintaan masih tetap berjalan. + +Permintaan untuk Google Web Search API mengikutkan `query` pencarian dan +alamat IP pengguna sebagai parameter kueri: + +---- +func Search(ctx context.Context, query string) (Results, error) { + // Persiapkan permintaan untuk Google Search API. + req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil) + if err != nil { + return nil, err + } + q := req.URL.Query() + q.Set("q", query) + + // Jika ctx membawa alamat IP pengguna, teruskan ke peladen. + // Google API menggunakan alamat IP pengguna untuk membedakan permintaan + // yang diinisiasi oleh server dengan permintaan dari user. + if userIP, ok := userip.FromContext(ctx); ok { + q.Set("userip", userIP.String()) + } + req.URL.RawQuery = q.Encode() +---- + +Fungsi `Search` menggunakan fungsi pembantu, `httpDo`, untuk membuat dan +membatalkan permintaan HTTP bila `ctx.Done` ditutup saat permintaan atau +respon masih dalam proses. +Fungsi `Search` mengirim sebuah _closure_ ke `httpDo` untuk menangani respon +HTTP: + +---- + var results Results + err = httpDo(ctx, req, func(resp *http.Response, err error) error { + if err != nil { + return err + } + defer resp.Body.Close() + + // Urai hasil pencarian dalam bentuk JSON. + // https://developers.google.com/web-search/docs/#fonje + var data struct { + ResponseData struct { + Results []struct { + TitleNoFormatting string + URL string + } + } + } + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return err + } + for _, res := range data.ResponseData.Results { + results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL}) + } + return nil + }) + // httpDo menunggu closure yang kita berikan selesai, jadi aman untuk + // membaca hasilnya di sini. + return results, err +---- + +Fungsi `httpDo` menjalankan permintaan HTTP dan memproses respon dalam sebuah +_goroutine_ yang baru. +Ia membatalkan permintaan jika `ctx.Done` ditutup sebelum _goroutine_ selesai: + +---- +func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error { + // Jalankan permintaan HTTP dalam sebuah goroutine dan kirim respon-nya ke + // f. + c := make(chan error, 1) + req = req.WithContext(ctx) + go func() { c <- f(http.DefaultClient.Do(req)) }() + select { + case <-ctx.Done(): + <-c // Tunggu sampai f selesai. + return ctx.Err() + case err := <-c: + return err + } +} +---- + + +== Adaptasi kode untuk `Context` + +Banyak kerangka peladen menyediakan paket dan tipe untuk membawa nilai-nilai +sesuai-nilai sesuai-nilai sesuai-nilai sesuai skop-permintaan. +Kita dapat mendefinisikan implementasi baru dari interface `Context` untuk +menjembatani antara kode yang menggunakan kerangka yang telah ada dan kode +yang mengharapkan sebuah parameter `Context`. + +Misalnya, paket +http://www.gorillatoolkit.org/pkg/context[github.com/gorilla/context^] +pada kerangka peladen HTTP Gorilla membolehkan fungsi-fungsi mengasosiasikan +data dengan permintaan yang masuk dengan menyediakan sebuah pemetaan dari +permintaan HTTP ke pasangan kunci-nilai. +Dalam +https://blog.golang.org/context/gorilla/gorilla.go[gorilla.go^], +kami menyediakan sebuah implementasi `Context` dengan method `Value` +mengembalikan nilai-nilai yang diasosiasikan dengan permintaan HTTP tertentu +dalam paket Gorilla. + +Paket-paket lain telah menyediakan dukungan pembatalan yang mirip dengan +`Context`. +Contohnya, +https://godoc.org/gopkg.in/tomb.v2[Tomb^] +menyediakan sebuah method `Kill` yang mengirim sinyal pembatalan dengan +menutup kanal `Dying`. +`Tomb` juga menyediakan method-method untuk menunggu _goroutine_ selesai, +mirip dengan `sync.WaitGroup`. +Dalam +https://blog.golang.org/context/tomb/tomb.go[tomb.go^], +kami menyediakan sebuah implementasi `Context` yang dibatalkan saat `Context` +induk-nya dibatalkan atau saat `Tomb` dihentikan. + + +== Kesimpulan + +Di Google, kita mengharuskan programmer Go mengirim sebuah parameter `Context` +sebagai argumen pertama pada semua fungsi antara permintaan masuk dan keluar. +Hal ini membolehkan kode Go yang dikembangkan oleh banyak tim yang berbeda +saling terhubung dengan baik. +Ia menyediakan kontrol sederhana terhadap batas waktu dan pembatalan dan +memastikan nilai-nilai penting seperti kredensial keamanan terkirim dalam +program Go dengan benar. + +Kerangka peladen yang ingin dibangun dengan `Context` sebaiknya menyediakan +implementasi `Context` untuk menjembatani antara paket mereka dengan paket +yang mengharapkan sebuah parameter `Context`. +Pustaka klien mereka kemudian menerima sebuah `Context` dari kode yang +dipanggil. +Dengan menjalin sebuah antarmuka umum untuk data dengan skop-permintaan dan +pembatalan, `Context` mempermudah pengembang paket berbagi kode untuk membuat +layanan-layanan yang mudah dikembangkan. diff --git a/_content/blog/context/server/server.go b/_content/blog/context/server/server.go new file mode 100644 index 0000000..ebee9a1 --- /dev/null +++ b/_content/blog/context/server/server.go @@ -0,0 +1,100 @@ +// +build OMIT + +// The server program issues Google search requests and demonstrates the use of +// the go.net Context API. It serves on port 8080. +// +// The /search endpoint accepts these query params: +// q=the Google search query +// timeout=a timeout for the request, in time.Duration format +// +// For example, http://localhost:8080/search?q=golang&timeout=1s serves the +// first few Google search results for "golang" or a "deadline exceeded" error +// if the timeout expires. +package main + +import ( + "context" + "html/template" + "log" + "net/http" + "time" + + "golang.org/x/blog/content/context/google" + "golang.org/x/blog/content/context/userip" +) + +func main() { + http.HandleFunc("/search", handleSearch) + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +// handleSearch handles URLs like /search?q=golang&timeout=1s by forwarding the +// query to google.Search. If the query param includes timeout, the search is +// canceled after that duration elapses. +func handleSearch(w http.ResponseWriter, req *http.Request) { + // ctx is the Context for this handler. Calling cancel closes the + // ctx.Done channel, which is the cancellation signal for requests + // started by this handler. + var ( + ctx context.Context + cancel context.CancelFunc + ) + timeout, err := time.ParseDuration(req.FormValue("timeout")) + if err == nil { + // The request has a timeout, so create a context that is + // canceled automatically when the timeout expires. + ctx, cancel = context.WithTimeout(context.Background(), timeout) + } else { + ctx, cancel = context.WithCancel(context.Background()) + } + defer cancel() // Cancel ctx as soon as handleSearch returns. + + // Check the search query. + query := req.FormValue("q") + if query == "" { + http.Error(w, "no query", http.StatusBadRequest) + return + } + + // Store the user IP in ctx for use by code in other packages. + userIP, err := userip.FromRequest(req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + ctx = userip.NewContext(ctx, userIP) + + // Run the Google search and print the results. + start := time.Now() + results, err := google.Search(ctx, query) + elapsed := time.Since(start) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := resultsTemplate.Execute(w, struct { + Results google.Results + Timeout, Elapsed time.Duration + }{ + Results: results, + Timeout: timeout, + Elapsed: elapsed, + }); err != nil { + log.Print(err) + return + } +} + +var resultsTemplate = 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}}; timeout {{.Timeout}}</p> +</body> +</html> +`)) diff --git a/_content/blog/context/tomb/tomb.go b/_content/blog/context/tomb/tomb.go new file mode 100644 index 0000000..da94dc9 --- /dev/null +++ b/_content/blog/context/tomb/tomb.go @@ -0,0 +1,24 @@ +// +build OMIT + +// Package tomb provides a Context implementation that is canceled when either +// its parent Context is canceled or a provided Tomb is killed. +package tomb + +import ( + "golang.org/x/net/context" + tomb "gopkg.in/tomb.v2" +) + +// NewContext returns a Context that is canceled either when parent is canceled +// or when t is Killed. +func NewContext(parent context.Context, t *tomb.Tomb) context.Context { + ctx, cancel := context.WithCancel(parent) + go func() { + select { + case <-t.Dying(): + cancel() + case <-ctx.Done(): + } + }() + return ctx +} diff --git a/_content/blog/context/userip/userip.go b/_content/blog/context/userip/userip.go new file mode 100644 index 0000000..06d3245 --- /dev/null +++ b/_content/blog/context/userip/userip.go @@ -0,0 +1,51 @@ +// +build OMIT + +// Package userip provides functions for extracting a user IP address from a +// request and associating it with a Context. +// +// This package is an example to accompany https://blog.golang.org/context. +// It is not intended for use by others. +package userip + +import ( + "context" + "fmt" + "net" + "net/http" +) + +// FromRequest extracts the user IP address from req, if present. +func FromRequest(req *http.Request) (net.IP, error) { + ip, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr) + } + + userIP := net.ParseIP(ip) + if userIP == nil { + return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr) + } + return userIP, nil +} + +// The key type is unexported to prevent collisions with context keys defined in +// other packages. +type key int + +// userIPkey is the context key for the user IP address. Its value of zero is +// arbitrary. If this package defined other context keys, they would have +// different integer values. +const userIPKey key = 0 + +// NewContext returns a new Context carrying userIP. +func NewContext(ctx context.Context, userIP net.IP) context.Context { + return context.WithValue(ctx, userIPKey, userIP) +} + +// FromContext extracts the user IP address from ctx, if present. +func FromContext(ctx context.Context) (net.IP, bool) { + // ctx.Value returns nil if ctx has no value for the key; + // the net.IP type assertion returns ok=false for nil. + userIP, ok := ctx.Value(userIPKey).(net.IP) + return userIP, ok +} diff --git a/_content/blog/index.adoc b/_content/blog/index.adoc index 7ee1a5c..9664646 100644 --- a/_content/blog/index.adoc +++ b/_content/blog/index.adoc @@ -69,6 +69,9 @@ === 2014 +* link:/blog/context[Pola konkurensi Go: Context^], + 29 Juli 2014. Sameer Ajmani. + * link:/blog/pipelines[Pola konkurensi Go: _pipeline_ dan pembatalan^], 13 Maret 2014, Sameer Ajmani. |
